usable books page

This commit is contained in:
vabene1111
2025-02-15 09:50:40 +01:00
parent 09a76a2057
commit 0461c57cf3
5 changed files with 170 additions and 31 deletions

View File

@@ -10,7 +10,7 @@ from treebeard.forms import movenodeform_factory
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink, from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType, Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
@@ -103,6 +103,13 @@ class ConnectorConfigAdmin(admin.ModelAdmin):
admin.site.register(ConnectorConfig, ConnectorConfigAdmin) admin.site.register(ConnectorConfig, ConnectorConfigAdmin)
class CustomFilterAdmin(admin.ModelAdmin):
list_display = ('id', 'type', 'name')
admin.site.register(CustomFilter, CustomFilterAdmin)
class SyncAdmin(admin.ModelAdmin): class SyncAdmin(admin.ModelAdmin):
list_display = ('storage', 'path', 'active', 'last_checked') list_display = ('storage', 'path', 'active', 'last_checked')
search_fields = ('storage__name', 'path') search_fields = ('storage__name', 'path')

View File

@@ -0,0 +1,58 @@
<template>
<v-card :loading="loading">
<v-card-title>{{ props.recipeOverview.name }}</v-card-title>
<recipe-image height="25vh" :recipe="props.recipeOverview"></recipe-image>
<v-card-subtitle>{{ props.recipeOverview.description }}</v-card-subtitle>
<v-card-text>
<keywords-bar :keywords="props.recipeOverview.keywords"></keywords-bar>
</v-card-text>
<ingredients-table :ingredient-factor="1" v-model="ingredients" :show-checkbox="false"></ingredients-table>
</v-card>
</template>
<script setup lang="ts">
import RecipeImage from "@/components/display/RecipeImage.vue";
import {ApiApi, Ingredient, Recipe, RecipeOverview} from "@/openapi";
import {onMounted, PropType, ref} from "vue";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import IngredientsTable from "@/components/display/IngredientsTable.vue";
import {getRecipeIngredients} from "@/utils/model_utils";
import {useI18n} from "vue-i18n";
import KeywordsBar from "@/components/display/KeywordsBar.vue";
const props = defineProps({
recipeOverview: {type: {} as PropType<RecipeOverview>, required: true}
})
const {t} = useI18n()
const loading = ref(false)
const recipe = ref({} as Recipe)
const ingredients = ref([] as Ingredient[])
onMounted(() => {
loadRecipe()
})
function loadRecipe() {
let api = new ApiApi()
loading.value = true
api.apiRecipeRetrieve({id: props.recipeOverview.id!}).then(r => {
recipe.value = r
ingredients.value = getRecipeIngredients(recipe.value, t,{showStepHeaders: true})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}).finally(() => {
loading.value = false
})
}
</script>
<style scoped>
</style>

View File

@@ -33,7 +33,7 @@
<td colspan="5" class="font-weight-bold">{{ i.note }}</td> <td colspan="5" class="font-weight-bold">{{ i.note }}</td>
</template> </template>
<template v-else> <template v-else>
<td style="width: 1%; text-wrap: nowrap" class="pa-0"> <td style="width: 1%; text-wrap: nowrap" class="pa-0" v-if="showCheckbox">
<v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn> <v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn>
</td> </td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1" v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)"></td> <td style="width: 1%; text-wrap: nowrap" class="pr-1" v-html="calculateFoodAmount(i.amount, props.ingredientFactor, useUserPreferenceStore().userSettings.useFractions)"></td>
@@ -73,6 +73,10 @@ const props = defineProps({
type: Number, type: Number,
required: true, required: true,
}, },
showCheckbox: {
type: Boolean,
default: true
},
}) })
const tableHeaders = computed(() => { const tableHeaders = computed(() => {

View File

@@ -3,31 +3,68 @@
<v-row> <v-row>
<v-col> <v-col>
<v-card> <v-card>
<v-card-text class="pt-2 pb-2"> <v-card-title>{{ book.name }}
<v-btn variant="flat" @click="router.go(-1)" prepend-icon="fa-solid fa-arrow-left">{{ $t('Back') }}</v-btn> <v-btn class="float-right" variant="flat" :to="{name: 'BooksPage'}" prepend-icon="$books" v-if="mdAndUp">{{ $t('Books') }}</v-btn>
</v-card-title>
<v-card-text v-if="book.shared && book.shared.length > 0">
<v-chip-group>
<v-label class="me-2">{{ $t('shared_with') }}</v-label>
<v-chip v-for="u in book.shared">{{ u.displayName }}</v-chip>
</v-chip-group>
</v-card-text> </v-card-text>
<v-card-text class="text-disabled">
{{ book.description }}
</v-card-text>
<v-expansion-panels v-model="toc">
<v-expansion-panel>
<v-expansion-panel-title>{{ $t('Table_of_Contents') }}</v-expansion-panel-title>
<v-expansion-panel-text>
<v-list>
<v-list-item v-for="(entry, i) in entries" :key="entry.id" @click="page = i; toc = false">
{{ entry.recipeContent.name }}
</v-list-item>
</v-list>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col> <v-col class="text-center">
<h2>{{ book.name }}</h2> <v-pagination :model-value="page + 1" @update:model-value="value => page = value - 1" :length="totalItems" @next="page = page + (mdAndUp ? 2 : 1)"
<p class="text-disabled">{{ book.description }}</p> @prev="page = page - (mdAndUp ? 2 : 1)"></v-pagination>
</v-col> </v-col>
</v-row> </v-row>
<v-row>
<v-col cols="12" md="6">
<v-data-iterator :items="entries" :items-per-page="recipesPerPage" :page="page" @update:page="loadRecipe">
<template #default="{items}">
<v-card v-for="i in items">
<v-card-title>{{i.raw.recipeContent.name}}</v-card-title>
<v-card-subtitle>{{i.raw.recipeContent.desciption}}</v-card-subtitle>
</v-card> <v-row>
<v-col cols="12">
<v-window v-model="page" show-arrows>
<template #next>
<v-btn icon="fa-solid fa-chevron-right" variant="plain" @click="page = page + (mdAndUp ? 2 : 1)"></v-btn>
</template> </template>
</v-data-iterator> <template #prev>
<v-pagination v-model="page" :length="totalItems / recipesPerPage"></v-pagination> <v-btn icon="fa-solid fa-chevron-left" variant="plain" @click="page = page - (mdAndUp ? 2 : 1)"></v-btn>
</template>
<v-window-item v-for="(entry, i) in entries" :key="entry.id">
<v-row>
<v-col cols="12" md="6">
<book-entry-card :recipe-overview="entries[i].recipeContent"></book-entry-card>
<div class="text-center mt-1">
<span class="text-disabled">{{ i + 1 }}</span>
</div>
</v-col>
<v-col cols="6" v-if="mdAndUp && entries.length > i + 1">
<book-entry-card :recipe-overview="entries[i + 1].recipeContent"></book-entry-card>
<div class="text-center mt-1">
<span class="text-disabled">{{ i + 2 }}</span>
</div>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
@@ -39,18 +76,21 @@
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {ApiApi, RecipeBook, RecipeBookEntry} from "@/openapi"; import {ApiApi, RecipeBook, RecipeBookEntry} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore"; import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {VDataTableUpdateOptions} from "@/vuetify";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import RecipeImage from "@/components/display/RecipeImage.vue";
import {useDisplay} from "vuetify";
import BookEntryCard from "@/components/display/BookEntryCard.vue";
const props = defineProps({ const props = defineProps({
bookId: {type: String, required: true}, bookId: {type: String, required: true},
}) })
const {mdAndUp} = useDisplay()
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
const page = ref(1) const toc = ref(false)
const recipesPerPage = ref(1) const page = ref(0)
const totalItems = ref(0) const totalItems = ref(0)
const book = ref({} as RecipeBook) const book = ref({} as RecipeBook)
@@ -58,13 +98,15 @@ const entries = ref([] as RecipeBookEntry[])
onMounted(() => { onMounted(() => {
loadBook() loadBook()
loadEntries({page: 1})
entries.value = []
loadEntries(1)
}) })
/** /**
* load the given book * load the given book
*/ */
function loadBook(){ function loadBook() {
const api = new ApiApi() const api = new ApiApi()
loading.value = true loading.value = true
@@ -77,12 +119,15 @@ function loadBook(){
}) })
} }
function loadEntries(options: VDataTableUpdateOptions){ function loadEntries(page: number) {
const api = new ApiApi() const api = new ApiApi()
api.apiRecipeBookEntryList({book: props.bookId, page: options.page}).then(r => { api.apiRecipeBookEntryList({book: props.bookId, page: page}).then(r => {
entries.value = r.results entries.value = entries.value.concat(r.results)
totalItems.value = r.count totalItems.value = r.count
if (r.next) {
loadEntries(page + 1)
}
}).catch(err => { }).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}) })

View File

@@ -1,4 +1,4 @@
import {Ingredient} from "@/openapi"; import {Ingredient, Recipe} from "@/openapi";
/** /**
* returns a string representing an ingredient * returns a string representing an ingredient
@@ -7,21 +7,46 @@ import {Ingredient} from "@/openapi";
export function ingredientToString(ingredient: Ingredient) { export function ingredientToString(ingredient: Ingredient) {
let content = [] let content = []
if(ingredient == undefined){ if (ingredient == undefined) {
return '' return ''
} }
if(ingredient.amount != 0){ if (ingredient.amount != 0) {
content.push(ingredient.amount) content.push(ingredient.amount)
} }
if(ingredient.unit){ if (ingredient.unit) {
content.push(ingredient.unit.name) content.push(ingredient.unit.name)
} }
if(ingredient.food){ if (ingredient.food) {
content.push(ingredient.food.name) content.push(ingredient.food.name)
} }
if(ingredient.note){ if (ingredient.note) {
content.push(`(${ingredient.note})`) content.push(`(${ingredient.note})`)
} }
return content.join(' ') return content.join(' ')
}
/**
* returns a list of all ingredients used by the given recipe
* @param recipe recipe to return ingredients for
* @param t useI18N object to use for translation
* @param options options object for list generation
* showStepHeaders - add steps as a header ingredient if it's configured on the step
*/
export function getRecipeIngredients(recipe: Recipe, t: any, options: { showStepHeaders: boolean } = {showStepHeaders: false}) {
let ingredients = [] as Ingredient[]
recipe.steps.forEach((step, index) => {
if (step.showAsHeader && options.showStepHeaders && recipe.steps.length > 1 && (step.ingredients.length > 0 || step.name != '')) {
ingredients.push({
amount: 0,
unit: null,
food: null,
note: (step.name !== '') ? step.name : t('Step') + ' ' + (index + 1),
isHeader: true
} as Ingredient)
}
ingredients = ingredients.concat(step.ingredients)
})
return ingredients
} }