Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Automatic instruction timers #4467

Open
wants to merge 34 commits into
base: mealie-next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f1bcf9c
Added inital commit for instructions timer
codetakki Oct 29, 2024
fa8765d
Added some better styling
codetakki Oct 29, 2024
066a4e4
Minutes being used as sec
codetakki Oct 29, 2024
0190664
Merge branch 'mealie-next' into feature-step-linked-timers
codetakki Oct 29, 2024
e4a0790
Regex for parsing string now includes locale strings, supporting mult…
codetakki Oct 29, 2024
169849d
Merge branch 'feature-step-linked-timers' of https://github.com/codet…
codetakki Oct 29, 2024
1fc82fb
Fixed some typing issues
codetakki Oct 30, 2024
8f3e357
Removed locals for all byt en-us
codetakki Oct 30, 2024
9005067
Moved code related to clock timer to composable
codetakki Nov 1, 2024
6a33194
instructions timer not uses use-timer composable
codetakki Nov 1, 2024
f5f19f2
Reset timer on unmount
codetakki Nov 1, 2024
9bd4e27
Removed unused import
codetakki Nov 1, 2024
757e72d
made timer icon more reactive to changes
codetakki Nov 1, 2024
ea8b311
Merge branch 'mealie-next' into feature-step-linked-timers
codetakki Nov 1, 2024
20ee5b2
Merge branch 'mealie-next' into feature-step-linked-timers
codetakki Nov 5, 2024
97e0c4e
added timers to recipe step
michael-genson Nov 5, 2024
4dfa6ab
added to schema
michael-genson Nov 5, 2024
6573114
move parser utils
michael-genson Nov 5, 2024
a61ffcc
add duration parser
michael-genson Nov 5, 2024
20c3752
added frontend schema defs
michael-genson Nov 5, 2024
65d3206
fix setter/init
michael-genson Nov 5, 2024
526430d
add parser to cleaner/scraper
michael-genson Nov 5, 2024
6edb885
added tests
michael-genson Nov 5, 2024
e90e623
lol
michael-genson Nov 5, 2024
e0584bd
lol
michael-genson Nov 5, 2024
dd6a6e8
Merge branch 'feature-step-linked-timers' of https://github.com/codet…
michael-genson Nov 5, 2024
24de141
Added edit for timers
codetakki Nov 8, 2024
216cd45
Changed some margins
codetakki Nov 8, 2024
aa74bdf
added timer parsing to migration using best-guess language
michael-genson Nov 10, 2024
8e463f1
Merge branch 'mealie-next' into feature-step-linked-timers
codetakki Nov 12, 2024
1fbf1e3
small margin adjustment
codetakki Nov 12, 2024
db88b71
FIxed merging error
codetakki Nov 12, 2024
b613bf4
Localized timer edit component
codetakki Nov 12, 2024
3169c9c
Merge branch 'mealie-next' into feature-step-linked-timers
codetakki Nov 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""added timers to recipe instructions

Revision ID: 02ec5b3e0525
Revises: 3897397b4631
Create Date: 2024-11-05 15:28:40.528380

"""

import json
from datetime import datetime

import dateparser.search
import sqlalchemy as sa
from sqlalchemy import orm

from alembic import op
from mealie.core.root_logger import get_logger
from mealie.db.models._model_utils.guid import GUID
from mealie.services.parser_services.parser_utils.duration_parser import DurationParser

# revision identifiers, used by Alembic.
revision = "02ec5b3e0525"
down_revision: str | None = "3897397b4631"
branch_labels: str | tuple[str, ...] | None = None
depends_on: str | tuple[str, ...] | None = None

logger = get_logger()


# Intermediate table definitions
class SqlAlchemyBase(orm.DeclarativeBase):
pass


class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"

id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
recipe_id: orm.Mapped[GUID | None] = orm.mapped_column(GUID, index=True)
text: orm.Mapped[str | None] = orm.mapped_column(sa.String, index=True)
timers_json: orm.Mapped[str | None] = orm.mapped_column(sa.String)


def parse_instructions(session: orm.Session):
SAMPLE_SIZE = 20

# Fetch one instruction for each recipe
distinct_recipe_ids = session.query(RecipeInstruction.recipe_id).distinct().limit(SAMPLE_SIZE).subquery()

instruction_samples = (
session.query(RecipeInstruction)
.join(distinct_recipe_ids, RecipeInstruction.recipe_id == distinct_recipe_ids.c.recipe_id)
.filter(RecipeInstruction.text.isnot(None), RecipeInstruction.text != "")
.distinct(RecipeInstruction.recipe_id)
.limit(SAMPLE_SIZE)
.all()
)

# Extract the languages from each instruction
results: list[list[tuple[str, datetime, str]]] = [
dateparser.search.search_dates(instruction.text, add_detected_language=True) # type: ignore
for instruction in instruction_samples
if instruction.text
]

languages = {"en"}
for result in results:
if not result:
continue

for _, _, lang in result:
languages.add(lang)

# Parse all instructions using the detected languages
logger.info(f"Detected languages: {languages}")

duration_parser = DurationParser()
for instruction in session.query(RecipeInstruction).all():
if not instruction.text:
continue

timers = duration_parser.get_all_durations(instruction.text, languages=languages)
instruction.timers_json = json.dumps(timers)

session.commit()


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("recipe_instructions", schema=None) as batch_op:
batch_op.add_column(sa.Column("timers_json", sa.String(), nullable=True))

# ### end Alembic commands ###

bind = op.get_bind()
session = orm.Session(bind=bind)

try:
logger.info("Parsing instruction timers")
parse_instructions(session)
except Exception:
logger.exception("Failed to parse instruction timers; continuing with migration anyway")
session.rollback()


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("recipe_instructions", schema=None) as batch_op:
batch_op.drop_column("timers_json")

# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<div>
<template v-for="time, index in value" >
<div :key="index" class="d-flex align-center">
<TimerInput v-model="value[index]" class="mb-3"></TimerInput>
<v-btn icon class="ml-2" @click="$emit('input', [...value.slice(0, index), ...value.slice(index + 1)])"><v-icon>mdi-delete</v-icon></v-btn>
</div>
</template>
<v-btn small @click="$emit('input', [...value, 0])">
<v-icon>mdi-plus</v-icon> Add timer
</v-btn>

</div>
</template>

<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import TimerInput from "./TimerInput.vue";

export default defineComponent({
components: { TimerInput },
props: {
value: {
type: Array, // or any other type that fits your needs
required: true,
},
},
// ... rest of your component code
});
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@
:key="ing.referenceId"
:markup="getIngredientByRefId(ing.referenceId)"
/>
<RecipeEditTimers v-model="step.timers"></RecipeEditTimers>
</v-card-text>
</DropZone>
<v-expand-transition>
Expand Down Expand Up @@ -268,6 +269,9 @@
<SafeMarkdown class="markdown" :source="step.text" />
</v-col>
</v-row>
<div v-if="!isEditForm && step.timers && step.timers.length > 0 ">
<RecipePageInstructionsTimer :timers="step.timers"></RecipePageInstructionsTimer>
</div>
</v-card-text>
</div>
</v-expand-transition>
Expand All @@ -294,6 +298,8 @@ import {
nextTick,
} from "@nuxtjs/composition-api";
import RecipeIngredientHtml from "../../RecipeIngredientHtml.vue";
import RecipePageInstructionsTimer from "./RecipePageInstructionsTimer.vue";
import RecipeEditTimers from "./RecipeEditTimers.vue"
import { RecipeStep, IngredientReferences, RecipeIngredient, RecipeAsset, Recipe } from "~/lib/api/types/recipe";
import { parseIngredientText } from "~/composables/recipes";
import { uuid4, detectServerBaseUrl } from "~/composables/use-utils";
Expand All @@ -315,7 +321,9 @@ export default defineComponent({
draggable,
RecipeIngredientHtml,
DropZone,
RecipeIngredients
RecipeIngredients,
RecipePageInstructionsTimer,
RecipeEditTimers
},
props: {
value: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<template v-if="timers && timers.length">
<div class="mb-n4" @click.stop>

<v-divider class="mb-2 mt-3 mb-2"></v-divider>
<div
v-for="(timer, i) in compTimers"
:key="i"
class="d-flex align-center my-2 justify-center">
<v-icon
v-if="!timer.timerRunning && !timer.timerPaused"
:color="timer.timerEnded ? 'success' : ''"
:class="timer.timerEnded ? 'shake' : ''">mdi-alarm</v-icon>
<v-icon
v-else
color="primary"
:class="timer.timerRunning ? 'tick' : ''">mdi-alarm</v-icon>

<v-btn
icon
:disabled="timer.timerValue <= 30"
depressed
@click="timer.timerValue -= 30">
<v-icon>mdi-minus</v-icon>
</v-btn>
{{ timer.simpleDisplayValue }}
<v-btn
icon
depressed
@click="timer.timerValue += 30"><v-icon>mdi-plus</v-icon></v-btn>
<v-btn
v-if="!timer.timerRunning && !timer.timerPaused && !timer.timerEnded"
rounded
depressed
@click="timer.startTimer"> {{ $t("recipe.timer.start") }} </v-btn>
<template v-else>
<v-btn
v-if="(!timer.timerEnded && timer.timerRunning && !timer.timerPaused)"
rounded
depressed
@click="timer.pauseTimer">
{{ $t("recipe.timer.pause") }}
</v-btn>
<span v-else-if="!timer.timerEnded">
<v-btn
rounded
depressed
@click="timer.resumeTimer">
{{ $t("recipe.timer.continue") }}
</v-btn>
<v-btn
icon
@click="timer.resetTimer"><v-icon>mdi-restore</v-icon></v-btn>
</span>
<span v-else>
<v-btn
icon
@click="timer.resetTimer"><v-icon>mdi-restore</v-icon></v-btn>
</span>
</template>
</div>
</div>
</template>
<script lang="ts">
import { ref, watch} from "vue";
import useTimer from "~/composables/use-timer";
// @ts-ignore typescript can't find our audio file, but it's there!

export default {
props: {
timers: {
type: Array as () => Array<number>,
default: () => [],
},
},
setup(props) {
const compTimers = ref<ReturnType<typeof useTimer>[]>()

watch(() => props.timers as number[], (newTimers) => {
console.log("new timers", newTimers);

compTimers.value = newTimers.map((t) => {
const newTimer = useTimer("00", "00", t.toString(), { padTimes: false })
newTimer.initializeTimer();
return newTimer
})
}, { immediate: true })

return {
compTimers
}
},
};
</script>
<style scoped>
.tick {
animation: tick 4s linear infinite;
}

@keyframes tick {
0% {
transform: rotate(15deg);
}

25% {
transform: rotate(-15deg);
}

50% {
transform: rotate(15deg);
}

75% {
transform: rotate(-15deg);
}

100% {
transform: rotate(15deg);
}
}

.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite;
}

@keyframes shake {

10%,
90% {
transform: translate3d(-1px, 0, 0);
}

20%,
80% {
transform: translate3d(2px, 0, 0);
}

30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}

40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
</style>
Loading
Loading