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 .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,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
@@ -103,6 +103,13 @@ class ConnectorConfigAdmin(admin.ModelAdmin):
admin.site.register(ConnectorConfig, ConnectorConfigAdmin)
class CustomFilterAdmin(admin.ModelAdmin):
list_display = ('id', 'type', 'name')
admin.site.register(CustomFilter, CustomFilterAdmin)
class SyncAdmin(admin.ModelAdmin):
list_display = ('storage', 'path', 'active', 'last_checked')
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>
</template>
<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>
</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,
required: true,
},
showCheckbox: {
type: Boolean,
default: true
},
})
const tableHeaders = computed(() => {

View File

@@ -3,31 +3,68 @@
<v-row>
<v-col>
<v-card>
<v-card-text class="pt-2 pb-2">
<v-btn variant="flat" @click="router.go(-1)" prepend-icon="fa-solid fa-arrow-left">{{ $t('Back') }}</v-btn>
<v-card-title>{{ book.name }}
<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 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-col>
</v-row>
<v-row>
<v-col>
<h2>{{ book.name }}</h2>
<p class="text-disabled">{{ book.description }}</p>
<v-col class="text-center">
<v-pagination :model-value="page + 1" @update:model-value="value => page = value - 1" :length="totalItems" @next="page = page + (mdAndUp ? 2 : 1)"
@prev="page = page - (mdAndUp ? 2 : 1)"></v-pagination>
</v-col>
</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>
</v-data-iterator>
<v-pagination v-model="page" :length="totalItems / recipesPerPage"></v-pagination>
<template #prev>
<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-row>
</v-container>
@@ -39,18 +76,21 @@
import {onMounted, ref} from "vue";
import {ApiApi, RecipeBook, RecipeBookEntry} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {VDataTableUpdateOptions} from "@/vuetify";
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({
bookId: {type: String, required: true},
})
const {mdAndUp} = useDisplay()
const router = useRouter()
const loading = ref(false)
const page = ref(1)
const recipesPerPage = ref(1)
const toc = ref(false)
const page = ref(0)
const totalItems = ref(0)
const book = ref({} as RecipeBook)
@@ -58,13 +98,15 @@ const entries = ref([] as RecipeBookEntry[])
onMounted(() => {
loadBook()
loadEntries({page: 1})
entries.value = []
loadEntries(1)
})
/**
* load the given book
*/
function loadBook(){
function loadBook() {
const api = new ApiApi()
loading.value = true
@@ -77,12 +119,15 @@ function loadBook(){
})
}
function loadEntries(options: VDataTableUpdateOptions){
function loadEntries(page: number) {
const api = new ApiApi()
api.apiRecipeBookEntryList({book: props.bookId, page: options.page}).then(r => {
entries.value = r.results
api.apiRecipeBookEntryList({book: props.bookId, page: page}).then(r => {
entries.value = entries.value.concat(r.results)
totalItems.value = r.count
if (r.next) {
loadEntries(page + 1)
}
}).catch(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
@@ -7,21 +7,46 @@ import {Ingredient} from "@/openapi";
export function ingredientToString(ingredient: Ingredient) {
let content = []
if(ingredient == undefined){
if (ingredient == undefined) {
return ''
}
if(ingredient.amount != 0){
if (ingredient.amount != 0) {
content.push(ingredient.amount)
}
if(ingredient.unit){
if (ingredient.unit) {
content.push(ingredient.unit.name)
}
if(ingredient.food){
if (ingredient.food) {
content.push(ingredient.food.name)
}
if(ingredient.note){
if (ingredient.note) {
content.push(`(${ingredient.note})`)
}
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
}