added split/merge steps to recipe view

This commit is contained in:
vabene1111
2025-08-28 18:20:26 +02:00
parent 17b03905e6
commit e70548fcc0
3 changed files with 106 additions and 66 deletions

View File

@@ -63,8 +63,8 @@
</v-col>
</v-row>
<!-- <closable-help-alert :text="$t('RecipeStepsHelp')" :action-text="$t('Steps')" @click="tab='steps'"></closable-help-alert>-->
<v-btn @click="tab='steps'" class="float-right" variant="tonal" append-icon="fa-solid fa-arrow-right">{{$t('Steps')}} </v-btn>
<!-- <closable-help-alert :text="$t('RecipeStepsHelp')" :action-text="$t('Steps')" @click="tab='steps'"></closable-help-alert>-->
<v-btn @click="tab='steps'" class="float-right" variant="tonal" append-icon="fa-solid fa-arrow-right">{{ $t('Steps') }}</v-btn>
</v-form>
</v-tabs-window-item>
@@ -77,12 +77,17 @@
</v-row>
<v-row>
<v-col class="text-center">
<v-btn-group density="compact">
<v-btn-group density="compact" divided border>
<v-btn color="success" prepend-icon="fa-solid fa-plus" @click="addStep()">{{ $t('Add_Step') }}</v-btn>
<v-btn color="warning" @click="dialogStepManager = true">
<v-icon icon="fa-solid fa-arrow-down-1-9"></v-icon>
</v-btn>
<v-btn prepend-icon="fa-solid fa-maximize" @click="handleSplitAllSteps"><span v-if="!mobile">{{ $t('Split') }}</span></v-btn>
<v-btn prepend-icon="fa-solid fa-minimize" @click="handleMergeAllSteps"><span v-if="!mobile">{{ $t('Merge') }}</span></v-btn>
</v-btn-group>
</v-col>
</v-row>
@@ -101,7 +106,7 @@
<v-text-field :label="$t('Imported_From')" v-model="editingObj.sourceUrl"></v-text-field>
<v-checkbox :label="$t('Private_Recipe')" persistent-hint :hint="$t('Private_Recipe_Help')" v-model="editingObj._private"></v-checkbox>
<model-select mode="tags" model="User" :label="$t('Share')" persistent-hint v-model="editingObj.shared"
<model-select mode="tags" model="User" :label="$t('Share')" persistent-hint v-model="editingObj.shared"
append-to-body v-if="editingObj._private"></model-select>
</v-form>
@@ -110,7 +115,7 @@
</v-card-text>
<v-card-text v-if="isSpaceAtRecipeLimit(useUserPreferenceStore().activeSpace)">
<v-alert color="warning" icon="fa-solid fa-triangle-exclamation">
{{$t('SpaceLimitReached')}}
{{ $t('SpaceLimitReached') }}
<v-btn color="success" variant="flat" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
</v-alert>
</v-card-text>
@@ -152,6 +157,7 @@ import {useDisplay} from "vuetify";
import {isSpaceAtRecipeLimit} from "@/utils/logic_utils";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import SpaceSettings from "@/components/settings/SpaceSettings.vue";
import {mergeAllSteps, splitAllSteps} from "@/utils/step_utils.ts";
const props = defineProps({
@@ -188,7 +194,7 @@ onMounted(() => {
/**
* component specific state setup logic
*/
function initializeEditor(){
function initializeEditor() {
setupState(props.item, props.itemId, {
newItemFunction: () => {
editingObj.value.steps = [] as Step[]
@@ -249,6 +255,18 @@ function deleteStepAtIndex(index: number) {
editingObj.value.steps.splice(index, 1)
}
function handleMergeAllSteps(): void {
if (editingObj.value.steps) {
mergeAllSteps(editingObj.value.steps)
}
}
function handleSplitAllSteps(): void {
if (editingObj.value.steps) {
splitAllSteps(editingObj.value.steps, '\n')
}
}
</script>
<style scoped>

View File

@@ -272,8 +272,8 @@
<v-col class="text-center">
<v-btn-group border divided>
<v-btn prepend-icon="fa-solid fa-shuffle" @click="autoSortIngredients()"><span v-if="!mobile">{{ $t('Auto_Sort') }}</span></v-btn>
<v-btn prepend-icon="fa-solid fa-maximize" @click="splitAllSteps('\n')"><span v-if="!mobile">{{ $t('Split') }}</span></v-btn>
<v-btn prepend-icon="fa-solid fa-minimize" @click="mergeAllSteps()"><span v-if="!mobile">{{ $t('Merge') }}</span></v-btn>
<v-btn prepend-icon="fa-solid fa-maximize" @click="handleSplitAllSteps()"><span v-if="!mobile">{{ $t('Split') }}</span></v-btn>
<v-btn prepend-icon="fa-solid fa-minimize" @click="handleMergeAllSteps()"><span v-if="!mobile">{{ $t('Merge') }}</span></v-btn>
</v-btn-group>
</v-col>
</v-row>
@@ -566,6 +566,7 @@ import {DateTime} from "luxon";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import bookmarkletJs from '@/assets/bookmarklet_v3?url'
import StepIngredientSorterDialog from "@/components/dialogs/StepIngredientSorterDialog.vue";
import {mergeAllSteps, splitAllSteps, splitStep} from "@/utils/step_utils.ts";
function doListImport() {
urlList.value = urlListImportInput.value.split('\n')
@@ -809,67 +810,15 @@ function deleteStep(step: SourceImportStep) {
}
}
/**
* utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
* @param step step to split
* @param split_character character to use as a delimiter between steps
*/
function splitStepObject(step: SourceImportStep, split_character: string) {
let steps: SourceImportStep[] = []
step.instruction.split(split_character).forEach(part => {
if (part.trim() !== '') {
steps.push({instruction: part, ingredients: [], showIngredientsTable: useUserPreferenceStore().userSettings.showStepIngredients!})
}
})
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
return steps
}
/**
* Splits all steps of a given recipe_json at the split character (e.g. \n or \n\n)
* @param split_character character to split steps at
*/
function splitAllSteps(split_character: string) {
let steps: SourceImportStep[] = []
if (importResponse.value.recipe) {
importResponse.value.recipe.steps.forEach(step => {
steps = steps.concat(splitStepObject(step, split_character))
})
importResponse.value.recipe.steps = steps
} else {
useMessageStore().addMessage(MessageType.ERROR, "no steps found to split")
}
}
/**
* Splits the given step at the split character (e.g. \n or \n\n)
* @param step step to split
* @param split_character character to use as a delimiter between steps
*/
function splitStep(step: SourceImportStep, split_character: string) {
if (importResponse.value.recipe) {
let old_index = importResponse.value.recipe.steps.findIndex(x => x === step)
let new_steps = splitStepObject(step, split_character)
importResponse.value.recipe.steps.splice(old_index, 1, ...new_steps)
} else {
useMessageStore().addMessage(MessageType.ERROR, "no steps found to split")
function handleMergeAllSteps(): void {
if (importResponse.value.recipe && importResponse.value.recipe.steps){
mergeAllSteps(importResponse.value.recipe.steps)
}
}
/**
* Merge all steps of a given recipe_json into one
*/
function mergeAllSteps() {
let step = {instruction: '', ingredients: [], showIngredientsTable: useUserPreferenceStore().userSettings.showStepIngredients!} as SourceImportStep
if (importResponse.value.recipe) {
importResponse.value.recipe.steps.forEach(s => {
step.instruction += s.instruction + '\n'
step.ingredients = step.ingredients.concat(s.ingredients)
})
importResponse.value.recipe.steps = [step]
} else {
useMessageStore().addMessage(MessageType.ERROR, "no steps found to split")
function handleSplitAllSteps(): void {
if (importResponse.value.recipe && importResponse.value.recipe.steps){
splitAllSteps(importResponse.value.recipe.steps, '\n')
}
}

View File

@@ -0,0 +1,73 @@
import {MessageType, useMessageStore} from "@/stores/MessageStore";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {Step} from "@/openapi";
interface StepLike {
instruction?: string;
ingredients?: Array<any>;
showIngredientsTable?: boolean;
}
/**
* utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
* @param step step to split
* @param split_character character to use as a delimiter between steps
*/
function splitStepObject<T extends StepLike>(step: T, split_character: string) {
let steps: T[] = []
if (step.instruction){
step.instruction.split(split_character).forEach(part => {
if (part.trim() !== '') {
steps.push({instruction: part, ingredients: [], time: 0, showIngredientsTable: useUserPreferenceStore().userSettings.showStepIngredients!})
}
})
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
}
return steps
}
/**
* Splits all steps of a given recipe_json at the split character (e.g. \n or \n\n)
* @param split_character character to split steps at
*/
export function splitAllSteps<T extends StepLike>(orig_steps: T[], split_character: string) {
let steps: T[] = []
if (orig_steps) {
orig_steps.forEach(step => {
steps = steps.concat(splitStepObject(step, split_character))
})
orig_steps.splice(0, orig_steps.length, ...steps) // replace all steps with the split steps
} else {
useMessageStore().addMessage(MessageType.ERROR, "no steps found to split")
}
}
/**
* Splits the given step at the split character (e.g. \n or \n\n)
* @param step step to split
* @param split_character character to use as a delimiter between steps
*/
export function splitStep<T extends StepLike>(steps: T[], step: T, split_character: string) {
if (steps){
let old_index = steps.findIndex(x => x === step)
let new_steps = splitStepObject(step, split_character)
steps.splice(old_index, 1, ...new_steps)
} else {
useMessageStore().addMessage(MessageType.ERROR, "no steps found to split")
}
}
/**
* Merge all steps of a given recipe_json into one
*/
export function mergeAllSteps<T extends StepLike>(steps: T[]) {
let step = {instruction: '', ingredients: [], showIngredientsTable: useUserPreferenceStore().userSettings.showStepIngredients!} as T
if (steps) {
step.instruction = steps.map(s => s.instruction).join('\n')
step.ingredients = steps.flatMap(s => s.ingredients)
steps.splice(0, steps.length, step) // replace all steps with the merged step
} else {
useMessageStore().addMessage(MessageType.ERROR, "no steps found to split")
}
}