basic swiping working
This commit is contained in:
parent
37c7a62853
commit
5a0ca3f4e5
@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="shopping_line_item">
|
<div id="shopping_line_item" class="swipe-container" @touchend="handleSwipe()"
|
||||||
|
v-if="(useUserPreferenceStore().device_settings.shopping_show_checked_entries || !is_checked) && (useUserPreferenceStore().device_settings.shopping_show_delayed_entries || !is_delayed)"
|
||||||
|
>
|
||||||
|
<div class="swipe-action" :class="{'bg-success': !is_checked , 'bg-warning': is_checked }">
|
||||||
|
<i class="swipe-icon fa-fw fas" :class="{'fa-check': !is_checked , 'fa-cart-plus': is_checked }"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<b-button-group class="w-100" v-if="(useUserPreferenceStore().device_settings.shopping_show_checked_entries || !is_checked) && (useUserPreferenceStore().device_settings.shopping_show_delayed_entries || !is_delayed)">
|
<b-button-group class="swipe-element">
|
||||||
<b-button variant="primary" v-if="is_delayed">
|
<b-button variant="primary" v-if="is_delayed">
|
||||||
<i class="fa-fw fas fa-hourglass-half"></i>
|
<i class="fa-fw fas fa-hourglass-half"></i>
|
||||||
</b-button>
|
</b-button>
|
||||||
@ -17,11 +22,14 @@
|
|||||||
|
|
||||||
<span v-if="info_row"><small class="text-muted">{{ info_row }}</small></span>
|
<span v-if="info_row"><small class="text-muted">{{ info_row }}</small></span>
|
||||||
</div>
|
</div>
|
||||||
<b-button variant="success" @click="useShoppingListStore().setEntriesCheckedState(entries, !is_checked)" :class="{'btn-success': !is_checked, 'btn-warning': is_checked}">
|
<b-button variant="success" @click="useShoppingListStore().setEntriesCheckedState(entries, !is_checked)"
|
||||||
|
:class="{'btn-success': !is_checked, 'btn-warning': is_checked}">
|
||||||
<i class="fa-fw fas" :class="{'fa-check': !is_checked , 'fa-cart-plus': is_checked }"></i>
|
<i class="fa-fw fas" :class="{'fa-check': !is_checked , 'fa-cart-plus': is_checked }"></i>
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
|
<div class="swipe-action bg-primary justify-content-end">
|
||||||
|
<i class="fa-fw fas fa-hourglass-half swipe-icon"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
<b-modal v-model="detail_modal_visible" @hidden="detail_modal_visible = false">
|
<b-modal v-model="detail_modal_visible" @hidden="detail_modal_visible = false">
|
||||||
<template #modal-title>
|
<template #modal-title>
|
||||||
@ -45,21 +53,29 @@
|
|||||||
<!-- TODO implement -->
|
<!-- TODO implement -->
|
||||||
<!-- <b-button variant="success" block @click="detail_modal_visible = false;"> {{ $t("Edit_Food") }}</b-button> -->
|
<!-- <b-button variant="success" block @click="detail_modal_visible = false;"> {{ $t("Edit_Food") }}</b-button> -->
|
||||||
|
|
||||||
<b-button variant="info" block @click="detail_modal_visible = false;useShoppingListStore().delayEntries(entries)">{{ $t('Postpone') }}</b-button>
|
<b-button variant="info" block
|
||||||
|
@click="detail_modal_visible = false;useShoppingListStore().delayEntries(entries,!this.is_delayed, true)">
|
||||||
|
{{ $t('Postpone') }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
|
||||||
<h6 class="mt-2">{{ $t('Entries') }}</h6>
|
<h6 class="mt-2">{{ $t('Entries') }}</h6>
|
||||||
|
|
||||||
<b-button variant="danger" block @click="detail_modal_visible = false;useShoppingListStore().deleteEntries(entries)">{{ $t('Delete_All') }}</b-button>
|
<b-button variant="danger" block
|
||||||
|
@click="detail_modal_visible = false;useShoppingListStore().deleteEntries(entries)">
|
||||||
|
{{ $t('Delete_All') }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
<b-row v-for="e in entries" v-bind:key="e.id">
|
<b-row v-for="e in entries" v-bind:key="e.id">
|
||||||
<b-col cold="12">
|
<b-col cold="12">
|
||||||
<b-button-group class="mt-1 w-100">
|
<b-button-group class="mt-1 w-100">
|
||||||
<b-button variant="dark" block class="btn btn-block text-left">
|
<b-button variant="dark" block class="btn btn-block text-left">
|
||||||
<span><span v-if="e.amount > 0">{{e.amount}}</span> {{e.unit?.name}} {{ food.name }}</span>
|
<span><span v-if="e.amount > 0">{{ e.amount }}</span> {{ e.unit?.name }} {{ food.name }}</span>
|
||||||
<span><br/><small class="text-muted">
|
<span><br/><small class="text-muted">
|
||||||
<span v-if="e.recipe_mealplan && e.recipe_mealplan.recipe_name !== ''">
|
<span v-if="e.recipe_mealplan && e.recipe_mealplan.recipe_name !== ''">
|
||||||
<a :href="resolveDjangoUrl('view_recipe', e.recipe_mealplan.recipe)"> {{ e.recipe_mealplan.recipe_name }} </a>({{
|
<a :href="resolveDjangoUrl('view_recipe', e.recipe_mealplan.recipe)"> {{
|
||||||
|
e.recipe_mealplan.recipe_name
|
||||||
|
}} </a>({{
|
||||||
e.recipe_mealplan.servings
|
e.recipe_mealplan.servings
|
||||||
}} {{ $t('Servings') }})<br/>
|
}} {{ $t('Servings') }})<br/>
|
||||||
</span>
|
</span>
|
||||||
@ -72,10 +88,14 @@
|
|||||||
</small></span>
|
</small></span>
|
||||||
|
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="warning" @click="detail_modal_visible = false; useShoppingListStore().deleteObject(e)"><i class="fas fa-trash"></i></b-button> <!-- TODO implement -->
|
<b-button variant="warning"
|
||||||
|
@click="detail_modal_visible = false; useShoppingListStore().deleteObject(e)"><i
|
||||||
|
class="fas fa-trash"></i></b-button> <!-- TODO implement -->
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
|
|
||||||
<number-scaler-component :number="e.amount" @change="e.amount = $event; useShoppingListStore().updateObject(e)" v-if="e.recipe_mealplan === null"></number-scaler-component>
|
<number-scaler-component :number="e.amount"
|
||||||
|
@change="e.amount = $event; useShoppingListStore().updateObject(e)"
|
||||||
|
v-if="e.recipe_mealplan === null"></number-scaler-component>
|
||||||
|
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
@ -86,7 +106,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
|
||||||
<generic-modal-form :model="Models.FOOD" :show="editing_food !== null" @hidden="editing_food = null; useShoppingListStore().refreshFromAPI()"></generic-modal-form>
|
<generic-modal-form :model="Models.FOOD" :show="editing_food !== null"
|
||||||
|
@hidden="editing_food = null; useShoppingListStore().refreshFromAPI()"></generic-modal-form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -186,7 +207,7 @@ export default {
|
|||||||
if (e.recipe_mealplan !== null) {
|
if (e.recipe_mealplan !== null) {
|
||||||
let recipe_name = e.recipe_mealplan.recipe_name
|
let recipe_name = e.recipe_mealplan.recipe_name
|
||||||
if (recipes.indexOf(recipe_name) === -1) {
|
if (recipes.indexOf(recipe_name) === -1) {
|
||||||
recipes.push(recipe_name)
|
recipes.push(recipe_name.substring(0, 14) + (recipe_name.length > 14 ? '..' : ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('mealplan_from_date' in e.recipe_mealplan) {
|
if ('mealplan_from_date' in e.recipe_mealplan) {
|
||||||
@ -196,14 +217,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipes.length > 1) {
|
|
||||||
let short_recipes = []
|
|
||||||
recipes.forEach(r => {
|
|
||||||
short_recipes.push(r.substring(0, 14) + (r.length > 14 ? '..' : ''))
|
|
||||||
})
|
|
||||||
recipes = short_recipes
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useUserPreferenceStore().device_settings.shopping_item_info_created_by && authors.length > 0) {
|
if (useUserPreferenceStore().device_settings.shopping_item_info_created_by && authors.length > 0) {
|
||||||
@ -236,9 +249,12 @@ export default {
|
|||||||
dateStyle: "short",
|
dateStyle: "short",
|
||||||
}).format(Date.parse(datetime))
|
}).format(Date.parse(datetime))
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* update the food after the category was changed
|
||||||
|
* handle changing category to category ID as a workaround
|
||||||
|
* @param food
|
||||||
|
*/
|
||||||
updateFoodCategory: function (food) {
|
updateFoodCategory: function (food) {
|
||||||
|
|
||||||
if (typeof food.supermarket_category === "number") { // not the best solution, but as long as generic multiselect does not support caching, I don't want to use a proper model
|
if (typeof food.supermarket_category === "number") { // not the best solution, but as long as generic multiselect does not support caching, I don't want to use a proper model
|
||||||
food.supermarket_category = this.useShoppingListStore().supermarket_categories.filter(sc => sc.id === food.supermarket_category)[0]
|
food.supermarket_category = this.useShoppingListStore().supermarket_categories.filter(sc => sc.id === food.supermarket_category)[0]
|
||||||
}
|
}
|
||||||
@ -249,14 +265,66 @@ export default {
|
|||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* function triggered by touchend event of swipe container
|
||||||
|
* check if min distance is reached and execute desired action
|
||||||
|
*/
|
||||||
|
handleSwipe: function () {
|
||||||
|
const minDistance = 80;
|
||||||
|
const container = document.querySelector('.swipe-container');
|
||||||
|
// get the distance the user swiped
|
||||||
|
const swipeDistance = container.scrollLeft - container.clientWidth;
|
||||||
|
if (swipeDistance < minDistance * -1) {
|
||||||
|
useShoppingListStore().setEntriesCheckedState(this.entries, !this.is_checked)
|
||||||
|
} else if (swipeDistance > minDistance) {
|
||||||
|
useShoppingListStore().delayEntries(this.entries, !this.is_delayed, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* scroll snap takes care of restoring scroll position */
|
||||||
|
.swipe-container {
|
||||||
|
display: flex;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* scrollbar should be hidden */
|
||||||
|
.swipe-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* main element should always snap into view */
|
||||||
|
.swipe-element {
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-icon {
|
||||||
|
color: white;
|
||||||
|
position: sticky;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* swipe-actions and element should be 100% wide */
|
||||||
|
.swipe-action,
|
||||||
|
.swipe-element {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -291,27 +291,34 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
|
|||||||
* function to handle user checking or unchecking a set of entries
|
* function to handle user checking or unchecking a set of entries
|
||||||
* @param {{}} entries set of entries
|
* @param {{}} entries set of entries
|
||||||
* @param checked boolean to set checked state of entry to
|
* @param checked boolean to set checked state of entry to
|
||||||
|
* @param undo if the user should be able to undo the change or not
|
||||||
*/
|
*/
|
||||||
setEntriesCheckedState(entries, checked) {
|
setEntriesCheckedState(entries, checked, undo) {
|
||||||
this.registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
|
if (undo) {
|
||||||
|
this.registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
|
||||||
|
}
|
||||||
|
|
||||||
for (let i in entries) {
|
for (let i in entries) {
|
||||||
this.entries[i].checked = checked
|
this.entries[i].checked = checked
|
||||||
this.updateObject(this.entries[i])
|
this.updateObject(this.entries[i])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* function to handle user "delaying" shopping entries
|
* function to handle user "delaying" and "undelaying" shopping entries
|
||||||
* @param {{}} entries set of entries
|
* @param {{}} entries set of entries
|
||||||
|
* @param delay if entries should be delayed or if delay should be removed
|
||||||
|
* @param undo if the user should be able to undo the change or not
|
||||||
*/
|
*/
|
||||||
delayEntries(entries) {
|
delayEntries(entries, delay, undo) {
|
||||||
let delay = 4 //TODO get delay from settings in an offline friendly way
|
let delay_hours = 4 //TODO get delay from settings in an offline friendly way
|
||||||
let delay_date = new Date(Date.now() + delay * (60 * 60 * 1000))
|
let delay_date = new Date(Date.now() + delay_hours * (60 * 60 * 1000))
|
||||||
|
|
||||||
this.registerChange('DELAY', entries)
|
if (undo) {
|
||||||
|
this.registerChange((delay ? 'DELAY' : 'UNDELAY'), entries)
|
||||||
|
}
|
||||||
|
|
||||||
for (let i in entries) {
|
for (let i in entries) {
|
||||||
console.log('DELAYING ', i, ' until ', delay_date)
|
this.entries[i].delay_until = (delay ? delay_date : null)
|
||||||
this.entries[i].delay_until = delay_date
|
|
||||||
this.updateObject(this.entries[i])
|
this.updateObject(this.entries[i])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -332,7 +339,7 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
|
|||||||
* @param {{}} entries set of entries
|
* @param {{}} entries set of entries
|
||||||
*/
|
*/
|
||||||
registerChange(type, entries) {
|
registerChange(type, entries) {
|
||||||
if (!['CREATED', 'CHECKED', 'UNCHECKED', 'DELAY'].includes(type)) {
|
if (!['CREATED', 'CHECKED', 'UNCHECKED', 'DELAY', 'UNDELAY'].includes(type)) {
|
||||||
throw Error('Tried to register unknown change type')
|
throw Error('Tried to register unknown change type')
|
||||||
}
|
}
|
||||||
this.undo_stack.push({'type': type, 'entries': entries})
|
this.undo_stack.push({'type': type, 'entries': entries})
|
||||||
@ -346,18 +353,15 @@ export const useShoppingListStore = defineStore(_STORE_ID, {
|
|||||||
let type = last_item['type']
|
let type = last_item['type']
|
||||||
let entries = last_item['entries']
|
let entries = last_item['entries']
|
||||||
|
|
||||||
for (let i in entries) {
|
if (type === 'CHECKED' || type === 'UNCHECKED') {
|
||||||
let e = entries[i]
|
this.setEntriesCheckedState(entries, (type === 'UNCHECKED'), false)
|
||||||
if (type === 'CREATED') {
|
} else if (type === 'DELAY' || type === 'UNDELAY') {
|
||||||
|
this.delayEntries(entries, (type === 'UNDELAY'), false)
|
||||||
|
} else if (type === 'CREATED') {
|
||||||
|
for (let i in entries) {
|
||||||
|
let e = entries[i]
|
||||||
this.deleteObject(e)
|
this.deleteObject(e)
|
||||||
} else if (type === 'CHECKED' || type === 'UNCHECKED') {
|
|
||||||
e.checked = (type === 'UNCHECKED')
|
|
||||||
this.updateObject(e)
|
|
||||||
} else if (type === 'DELAY') {
|
|
||||||
e.delay_until = null
|
|
||||||
this.updateObject(e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// can use localization in store
|
// can use localization in store
|
||||||
|
Loading…
Reference in New Issue
Block a user