shopping export dialog

This commit is contained in:
vabene1111
2025-05-28 19:12:19 +02:00
parent fa8af5596f
commit 81180207ba
36 changed files with 244 additions and 29 deletions

View File

@@ -1,11 +1,13 @@
<template>
<v-btn ref="copyBtn" :color="color" :size="size" :density="density" @click="clickCopy()" :variant="variant">
<slot name="default">
<v-icon icon="$copy"></v-icon>
<v-tooltip v-model="showToolip" :target="btn" location="top">
<v-icon icon="$copy"></v-icon>
Copied!
{{$t('Copied')}}!
</v-tooltip>
</slot>
</v-btn>
</template>

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

View File

@@ -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,7 +101,7 @@
</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>
@@ -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

View File

@@ -61,6 +61,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",

View File

@@ -61,6 +61,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Копиране",
"Copy_template_reference": "Копирайте препратка към шаблона",
"CountMore": "...+{count} още",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopírovat",
"Copy Link": "Kopírovat odkaz",
"Copy Token": "Kopírovat token",

View File

@@ -73,6 +73,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopier",
"Copy Link": "Kopier link",
"Copy Token": "Kopier token",

View File

@@ -76,6 +76,7 @@
"ConversionsHelp": "Mit Umrechnungen kann die Menge einens Lebensmittels in verschiedenen Einheiten ausgerechnet werden. Aktuell wird dies nur zur berechnung von Eigenschaften verwendet, später jedoch sollen auch andere Funktionen von Tandoor davon profitieren.",
"CookLog": "Kochprotokoll",
"Cooked": "Gekocht",
"Copied": "Kopiert",
"Copy": "Kopieren",
"Copy Link": "Link Kopieren",
"Copy Token": "Kopiere Token",

View File

@@ -72,6 +72,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Αντιγραφή",
"Copy Link": "Αντιγραφή συνδέσμου",
"Copy Token": "Αντιγραφή token",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "With conversions you can calculate the amount of a food in different units. Currently this is only used for property calculation, later it might also be used in other parts of tandoor. ",
"CookLog": "Cook Log",
"Cooked": "Cooked",
"Copied": "Copied",
"Copy": "Copy",
"Copy Link": "Copy Link",
"Copy Token": "Copy Token",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Copiar",
"Copy Link": "Copiar Enlace",
"Copy Token": "Copiar Token",

View File

@@ -47,6 +47,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopioi",
"Copy_template_reference": "Kopioi malliviittaus",
"Create": "Luo",

View File

@@ -73,6 +73,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Copier",
"Copy Link": "Copier le lien",
"Copy Token": "Copier le jeton",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "העתקה",
"Copy Link": "העתק קישור",
"Copy Token": "העתק טוקן",

View File

@@ -73,6 +73,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Másolás",
"Copy Link": "Link másolása",
"Copy Token": "Token másolása",

View File

@@ -39,6 +39,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "",
"Create": "Ստեղծել",
"Create_New_Food": "Ավելացնել նոր սննդամթերք",

View File

@@ -65,6 +65,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Salin",
"Copy Link": "Salin Tautan",
"Copy Token": "Salin Token",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",

View File

@@ -69,6 +69,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Copia",
"Copy Link": "Copia link",
"Copy Token": "Copia token",

View File

@@ -73,6 +73,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",

View File

@@ -71,6 +71,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopier",
"Copy Link": "Kopier lenke",
"Copy Token": "Kopier Token",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopie",
"Copy Link": "Kopieer Link",
"Copy Token": "Kopieer Token",

View File

@@ -75,6 +75,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopiuj",
"Copy Link": "Skopiuj link",
"Copy Token": "Kopiuj Token",

View File

@@ -60,6 +60,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Copiar",
"Copy Link": "Copiar Ligação",
"Copy Token": "Copiar Chave",

View File

@@ -73,6 +73,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Copiar",
"Copy Link": "Copiar Link",
"Copy Token": "Copiar Token",

View File

@@ -70,6 +70,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Copie",
"Copy Link": "Copiere link",
"Copy Token": "Copiere token",

View File

@@ -55,6 +55,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Копировать",
"Copy_template_reference": "Скопировать ссылку на шаблон",
"CountMore": "...+{count} больше",

View File

@@ -56,6 +56,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopiraj",
"Copy_template_reference": "Kopiraj referenco vzorca",
"CountMore": "...+{count} več",

View File

@@ -75,6 +75,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopiera",
"Copy Link": "Kopiera Länk",
"Copy Token": "Kopiera token",

View File

@@ -74,6 +74,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"Copy Token": "Anahtarı Kopyala",

View File

@@ -64,6 +64,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "Копіювати",
"Copy Link": "Скопіювати Посилання",
"Copy Token": "Скопіювати Токен",

View File

@@ -73,6 +73,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "复制",
"Copy Link": "复制链接",
"Copy Token": "复制令牌",

View File

@@ -36,6 +36,7 @@
"ConversionsHelp": "",
"CookLog": "",
"Cooked": "",
"Copied": "",
"Copy": "",
"Copy_template_reference": "複製參考模板",
"Create": "",

View File

@@ -294,7 +294,7 @@
</v-col>
<v-col cols="12" md="6">
<v-list>
<vue-draggable v-model="s.ingredients" group="ingredients" handle=".drag-handle" empty-insert-threshold="25">
<vue-draggable v-model="s.ingredients" group="ingredients" handle=".drag-handle" :empty-insert-threshold="25">
<v-list-item v-for="(i, ingredientIndex) in s.ingredients" border>
<v-icon size="small" class="drag-handle cursor-grab mr-2" icon="$dragHandle"></v-icon>
<v-chip density="compact" label class="mr-1">{{ i.amount }}</v-chip>

View File

@@ -1,6 +1,7 @@
import {ShoppingListEntry, Space} from "@/openapi";
import {IShoppingListFood} from "@/types/Shopping";
import {IShoppingListCategory, IShoppingListFood} from "@/types/Shopping";
import {DeviceSettings} from "@/types/settings";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
// -------------- SHOPPING RELATED ----------------------
@@ -54,6 +55,23 @@ export function isShoppingListFoodDelayed(slf: IShoppingListFood) {
return hasDelayedEntry
}
/**
* determines if a category has entries that should be visible
* @param category
*/
export function isShoppingCategoryVisible(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
}
// -------------- SPACE RELATED ----------------------
/**