mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-04 13:48:32 -05:00
shopping export dialog
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
|
||||
<v-btn ref="copyBtn" :color="color" :size="size" :density="density" @click="clickCopy()" :variant="variant">
|
||||
<v-icon icon="$copy"></v-icon>
|
||||
<v-tooltip v-model="showToolip" :target="btn" location="top">
|
||||
<slot name="default">
|
||||
<v-icon icon="$copy"></v-icon>
|
||||
Copied!
|
||||
</v-tooltip>
|
||||
<v-tooltip v-model="showToolip" :target="btn" location="top">
|
||||
<v-icon icon="$copy"></v-icon>
|
||||
{{$t('Copied')}}!
|
||||
</v-tooltip>
|
||||
</slot>
|
||||
</v-btn>
|
||||
|
||||
</template>
|
||||
|
||||
173
vue3/src/components/dialogs/ShoppingExportDialog.vue
Normal file
173
vue3/src/components/dialogs/ShoppingExportDialog.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
|
||||
<v-dialog v-model="dialog" activator="parent" style="max-width: 75vw;">
|
||||
<v-card>
|
||||
|
||||
<v-closable-card-title :title="$t('Export')" v-model="dialog"></v-closable-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
|
||||
<v-btn-toggle border divided v-model="mode">
|
||||
<v-btn value="md_list"><i class="fa-solid fa-list-check"></i></v-btn>
|
||||
<v-btn value="md_table"><i class="fa-solid fa-table-cells"></i></v-btn>
|
||||
<v-btn value="csv"><i class="fa-solid fa-file-csv"></i></v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field :label="$t('csv_delim_label')" :hint="$t('csv_delim_help')" persistent-hint
|
||||
v-model="useUserPreferenceStore().userSettings.csvDelim"></v-text-field>
|
||||
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field :label="$t('csv_prefix_label')" :hint="$t('csv_prefix_help')" persistent-hint
|
||||
v-model="useUserPreferenceStore().userSettings.csvPrefix"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea :model-value="exportText" auto-grow max-rows="25" readonly>
|
||||
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<btn-copy class="float-right" :copy-value="exportText"></btn-copy>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn @click="downloadExport()" prepend-icon="fa-solid fa-download">{{ $t('Download') }}</v-btn>
|
||||
<v-btn @click="copy(exportText)" prepend-icon="$copy">{{ $t('Copy') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {useShoppingStore} from "@/stores/ShoppingStore.ts";
|
||||
import {isEntryVisible, isShoppingCategoryVisible, isShoppingListFoodVisible} from "@/utils/logic_utils.ts";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
|
||||
import {ShoppingListEntry} from "@/openapi";
|
||||
import BtnCopy from "@/components/buttons/BtnCopy.vue";
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
|
||||
const {t} = useI18n()
|
||||
const {copy} = useClipboard()
|
||||
|
||||
const dialog = defineModel<boolean>()
|
||||
const mode = ref<'md_list' | 'md_table' | 'csv'>('md_list')
|
||||
|
||||
/**
|
||||
* shorthand for csvDelim user preference
|
||||
*/
|
||||
const csvDelim = computed(() => {
|
||||
return useUserPreferenceStore().userSettings.csvDelim
|
||||
})
|
||||
|
||||
/**
|
||||
* compute the export text
|
||||
*/
|
||||
const exportText = computed(() => {
|
||||
let textArray: string[] = []
|
||||
|
||||
textArray.push(formatHeader())
|
||||
|
||||
useShoppingStore().getEntriesByGroup.forEach(category => {
|
||||
if (isShoppingCategoryVisible(category)) {
|
||||
if (category.name === useShoppingStore().UNDEFINED_CATEGORY) {
|
||||
textArray.push(formatCategory(t('NoCategory')))
|
||||
} else {
|
||||
textArray.push(formatCategory(category.name))
|
||||
}
|
||||
|
||||
category.foods.forEach(food => {
|
||||
if (isShoppingListFoodVisible(food, useUserPreferenceStore().deviceSettings)) {
|
||||
food.entries.forEach(entry => {
|
||||
if (isEntryVisible(entry, useUserPreferenceStore().deviceSettings)) {
|
||||
textArray.push(formatEntry(entry))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// delete first two empty lines in md list
|
||||
if (mode.value == 'md_list') {
|
||||
textArray.splice(0, 2)
|
||||
}
|
||||
|
||||
return textArray.join('\n')
|
||||
})
|
||||
|
||||
/**
|
||||
* create the header for the exported list depending on the mode
|
||||
*/
|
||||
function formatHeader() {
|
||||
if (mode.value == 'md_list') {
|
||||
return ''
|
||||
} else if (mode.value == 'md_table') {
|
||||
return `|${t('Amount')}|${t('Unit')}|${t('Food')}|\n|-|-|-|`
|
||||
} else if (mode.value == 'csv') {
|
||||
return `${t('Amount')} ${csvDelim.value} ${t('Unit')} ${csvDelim.value} ${t('Food')}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* format category lines depending on the mode
|
||||
* @param categoryName name of the category
|
||||
*/
|
||||
function formatCategory(categoryName: string) {
|
||||
if (mode.value == 'md_list') {
|
||||
return `\n${categoryName}`
|
||||
} else if (mode.value == 'md_table') {
|
||||
return `|${categoryName}|`
|
||||
} else if (mode.value == 'csv') {
|
||||
return `${categoryName}${csvDelim.value}${csvDelim.value}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* format the shopping list entry according to the selected mode
|
||||
* @param entry
|
||||
*/
|
||||
function formatEntry(entry: ShoppingListEntry) {
|
||||
if (mode.value == 'md_list') {
|
||||
return `${useUserPreferenceStore().userSettings.csvPrefix} ${entry.amount} ${entry.unit?.name} ${entry.food?.name}`
|
||||
} else if (mode.value == 'md_table') {
|
||||
return `|${entry.amount}|${entry.unit?.name}|${entry.food?.name}|`
|
||||
} else if (mode.value == 'csv') {
|
||||
return `${entry.amount}${csvDelim.value}${entry.unit?.name}${csvDelim.value}${entry.food?.name}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* encode the exportText into a URI and trigger browser to download the export as a file
|
||||
*/
|
||||
function downloadExport() {
|
||||
let data = encodeURI("data:text/text;charset=utf-8," + exportText.value)
|
||||
let link = document.createElement("a")
|
||||
link.setAttribute("href", data)
|
||||
|
||||
if (mode.value == 'md_list' || mode.value == 'md_table') {
|
||||
link.setAttribute("download", `${t('Shopping_list')}.md`)
|
||||
} else if (mode.value == 'csv') {
|
||||
link.setAttribute("download", `${t('Shopping_list')}.csv`)
|
||||
}
|
||||
link.click()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -55,7 +55,7 @@
|
||||
<v-list-item>
|
||||
<v-switch color="primary" hide-details :label="$t('CreatedBy')" v-model="useUserPreferenceStore().deviceSettings.shopping_item_info_created_by"></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item >
|
||||
<v-list-item>
|
||||
<v-switch color="primary" hide-details label="New Input" v-model="useUserPreferenceStore().deviceSettings.shopping_input_autocomplete"></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="useUserPreferenceStore().serverSettings.debug">
|
||||
@@ -65,6 +65,11 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-btn height="100%" rounded="0" variant="plain">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
<shopping-export-dialog></shopping-export-dialog>
|
||||
</v-btn>
|
||||
|
||||
<v-btn height="100%" rounded="0" variant="plain" @click="useShoppingStore().undoChange()">
|
||||
<i class="fa-solid fa-arrow-rotate-left"></i>
|
||||
</v-btn>
|
||||
@@ -96,14 +101,14 @@
|
||||
</v-list>
|
||||
<v-list class="mt-3" density="compact" v-else>
|
||||
<template v-for="category in useShoppingStore().getEntriesByGroup" :key="category.name">
|
||||
<template v-if="isCategoryVisible(category)">
|
||||
<template v-if="isShoppingCategoryVisible(category)">
|
||||
|
||||
<v-list-subheader v-if="category.name === useShoppingStore().UNDEFINED_CATEGORY"><i>{{ $t('NoCategory') }}</i></v-list-subheader>
|
||||
<v-list-subheader v-else>{{ category.name }}</v-list-subheader>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<template v-for="[i, value] in category.foods" :key="value.food.id">
|
||||
<shopping-line-item :shopping-list-food="value" ></shopping-line-item>
|
||||
<shopping-line-item :shopping-list-food="value"></shopping-line-item>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
@@ -166,14 +171,14 @@
|
||||
<v-list>
|
||||
<v-list-item v-for="r in useShoppingStore().getAssociatedRecipes()">
|
||||
<template #prepend>
|
||||
<v-btn color="edit" icon >
|
||||
<v-btn color="edit" icon>
|
||||
{{ r.servings }}
|
||||
<number-scaler-dialog
|
||||
v-if="r.mealplan == undefined"
|
||||
:number="r.servings"
|
||||
@confirm="(servings: number) => {updateRecipeServings(r, servings)}"
|
||||
></number-scaler-dialog>
|
||||
<model-edit-dialog model="MealPlan" :item-id="r.mealplan" v-if="r.mealplan != undefined" activator="parent"> </model-edit-dialog>
|
||||
<model-edit-dialog model="MealPlan" :item-id="r.mealplan" v-if="r.mealplan != undefined" activator="parent"></model-edit-dialog>
|
||||
</v-btn>
|
||||
|
||||
</template>
|
||||
@@ -207,7 +212,7 @@
|
||||
<v-row>
|
||||
<v-col>
|
||||
<supermarket-editor :item="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket"
|
||||
@save="(args: Supermarket) => (useUserPreferenceStore().deviceSettings.shopping_selected_supermarket = args)"></supermarket-editor>
|
||||
@save="(args: Supermarket) => (useUserPreferenceStore().deviceSettings.shopping_selected_supermarket = args)"></supermarket-editor>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
@@ -237,6 +242,8 @@ import {DateTime} from "luxon";
|
||||
import MealPlanEditor from "@/components/model_editors/MealPlanEditor.vue";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
import {onBeforeRouteLeave} from "vue-router";
|
||||
import {isShoppingCategoryVisible} from "@/utils/logic_utils.ts";
|
||||
import ShoppingExportDialog from "@/components/dialogs/ShoppingExportDialog.vue";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
@@ -274,22 +281,6 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* determines if a category as entries that should be visible
|
||||
* @param category
|
||||
*/
|
||||
function isCategoryVisible(category: IShoppingListCategory) {
|
||||
let entryCount = category.stats.countUnchecked
|
||||
|
||||
if (useUserPreferenceStore().deviceSettings.shopping_show_checked_entries) {
|
||||
entryCount += category.stats.countChecked
|
||||
}
|
||||
if (useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries) {
|
||||
entryCount += category.stats.countUncheckedDelayed
|
||||
}
|
||||
return entryCount > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* update the number of servings for an embedded recipe and with it the ShoppingListEntry amounts
|
||||
* @param recipe ShoppingListRecipe to update
|
||||
|
||||
Reference in New Issue
Block a user