basic swiping working

This commit is contained in:
vabene1111 2024-01-14 11:54:10 +08:00
parent 37c7a62853
commit 5a0ca3f4e5
2 changed files with 116 additions and 44 deletions

View File

@ -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>

View File

@ -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