From eb32f089adcc86c4660b3d59f9aad03c588d144e Mon Sep 17 00:00:00 2001 From: Jannes Magnusson Date: Sat, 18 Oct 2025 23:28:55 +0200 Subject: [PATCH] start implementing edit_recipe --- frontend/config.py | 6 +- frontend/views/buttons_manager.py | 11 +-- frontend/views/confirm.py | 34 +++++++ frontend/views/draw_utils.py | 8 +- frontend/views/main_view.py | 34 ++++--- frontend/views/recipes/__init__.py | 4 + frontend/views/recipes/add_recipe.py | 39 -------- frontend/views/recipes/do_recipe.py | 0 frontend/views/recipes/edit_recipe.py | 104 +++++++++++++++++++++ frontend/views/recipes/recipe.py | 35 +++++++ frontend/views/recipes/recipe_manager.py | 14 +++ frontend/views/recipes/recipe_selection.py | 22 +++-- 12 files changed, 241 insertions(+), 70 deletions(-) create mode 100644 frontend/views/confirm.py delete mode 100644 frontend/views/recipes/add_recipe.py create mode 100644 frontend/views/recipes/do_recipe.py create mode 100644 frontend/views/recipes/edit_recipe.py create mode 100644 frontend/views/recipes/recipe_manager.py diff --git a/frontend/config.py b/frontend/config.py index cef51f0..1c6ad01 100644 --- a/frontend/config.py +++ b/frontend/config.py @@ -14,8 +14,10 @@ MOV_AVG_DEFAULTS = { class DISPLAY_TYPES(Flag): NUMBER = 1 CIRCLE = 2 - + class DISPLAY_MODES(Enum): MAIN = 1 SETTINGS = 2 - RECIPE_SELECTION = 3 \ No newline at end of file + RECIPE_SELECTION = 3 + EDIT_RECIPE = 4 + DO_RECIPE = 5 \ No newline at end of file diff --git a/frontend/views/buttons_manager.py b/frontend/views/buttons_manager.py index e60dbd0..0399b58 100644 --- a/frontend/views/buttons_manager.py +++ b/frontend/views/buttons_manager.py @@ -2,13 +2,13 @@ from tkinter import Frame, ttk from PIL import ImageDraw from .base import View -from .draw_utils import draw_clock, draw_long_press +from .button_interface import ButtonInterface +from .draw_utils import draw_long_press class ButtonsManager(View): - def __init__(self, parent, im_size, center, - **actions): - self.current_view = actions['default'] + def __init__(self, parent, im_size, center, curr_view): + self.current_view: ButtonInterface = curr_view self.long_press_threshold = 1000 # milliseconds self.left_press_start = None self.right_press_start = None @@ -17,9 +17,6 @@ class ButtonsManager(View): self.right_press_job = None self.both_press_job = None - for key, action in actions.items(): - setattr(self, key, action) - super().__init__(parent, im_size, center) def init_ui(self, parent): diff --git a/frontend/views/confirm.py b/frontend/views/confirm.py new file mode 100644 index 0000000..348c4be --- /dev/null +++ b/frontend/views/confirm.py @@ -0,0 +1,34 @@ +from .base import View +from ..button_interface import ButtonInterface + +class ConfirmView(View, ButtonInterface): + def __init__(self, parent, im_size, center, + message: str, + confirm_command, + cancel_command): + self.message = message + self.confirm_command = confirm_command + self.cancel_command = cancel_command + super().__init__(parent, im_size, center) + + def update_weight(self, weight: float): + from PIL import ImageDraw + + im = self.bkg_im.copy() + draw = ImageDraw.Draw(im) + + draw.text((20, 60), self.message, fill='black') + + return im + + def render_left_long_press(self, draw, x, y): + draw.text((x, y), 'Confirm', fill='black') + + def render_right_press(self, draw, x, y): + draw.text((x, y), 'Cancel', fill='black') + + def left_press(self): + self.confirm_command() + + def right_press(self): + self.cancel_command() \ No newline at end of file diff --git a/frontend/views/draw_utils.py b/frontend/views/draw_utils.py index 94bab34..a7f3ea6 100644 --- a/frontend/views/draw_utils.py +++ b/frontend/views/draw_utils.py @@ -1,9 +1,9 @@ -def draw_clock(draw, position, radius=16, width=1): +def draw_clock(draw, position, radius=16, width=1, color='black'): """Draw a simple clock icon at the given position""" x, y = position - draw.circle((x, y), radius, outline='black', width=width) - draw.line((x, y - radius + 2, x, y), fill='black', width=width) # Hour hand - draw.line((x, y, x + radius * 3 / 4, y), fill='black', width=width) # Minute hand + draw.circle((x, y), radius, outline=color, width=width) + draw.line((x, y - radius + 2, x, y), fill=color, width=width) # Hour hand + draw.line((x, y, x + radius * 3 / 4, y), fill=color, width=width) # Minute hand def draw_long_press(draw, position): """Draw a long press button icon at the given position""" diff --git a/frontend/views/main_view.py b/frontend/views/main_view.py index 4c8b0c9..07ad2a8 100644 --- a/frontend/views/main_view.py +++ b/frontend/views/main_view.py @@ -1,16 +1,17 @@ import io import tkinter as tk from tkinter import Frame, Canvas, ttk, PhotoImage -from enum import Enum from PIL import ImageChops from ..config import DISPLAY_TYPES, DISPLAY_MODES +from .draw_utils import draw_clock from . import NumberView, CircleView, TimerView + from .button_interface import ButtonInterface from .buttons_manager import ButtonsManager -from .recipes.recipe_selection import RecipeSelection -from .draw_utils import draw_clock + +from .recipes import RecipeSelection, RecipeManager, EditRecipe, Recipe class MainView(tk.Frame, ButtonInterface): def __init__(self, parent, @@ -38,10 +39,9 @@ class MainView(tk.Frame, ButtonInterface): self.timer_view = TimerView(self.actions, self.im_size, self.center) - self.recipe_selection = RecipeSelection(self, self.im_size, self.center, deactivate_command=self.enter_main_mode) self.buttons = ButtonsManager(self, self.im_size, self.center, - default=self, - select_recipe=self.recipe_selection) + curr_view=self) + self.recipes_manager = RecipeManager() self.curr_mode = DISPLAY_MODES.MAIN @@ -82,10 +82,22 @@ class MainView(tk.Frame, ButtonInterface): def enter_recipe_selection(self): self.curr_mode = DISPLAY_MODES.RECIPE_SELECTION - self.buttons.current_view = self.recipe_selection + self.buttons.current_view = RecipeSelection(self, + self.im_size, self.center, + recipe_manager=self.recipes_manager, + edit_recipe_command=self.enter_edit_recipe, + deactivate_command=self.enter_main_mode) self.refresh(0.0) - - + + def enter_edit_recipe(self, recipe: Recipe = None): + self.curr_mode = DISPLAY_MODES.EDIT_RECIPE + self.buttons.current_view = EditRecipe(self, + self.im_size, self.center, + recipe=recipe, + recipe_manager=self.recipes_manager, + deactivate_command=self.enter_recipe_selection) + self.refresh(0.0) + ################ VIEW MANAGEMENT ################ def update_views(self, selected_types: DISPLAY_TYPES): @@ -120,10 +132,10 @@ class MainView(tk.Frame, ButtonInterface): im = view.update_weight(weight) ims.append(im) - elif self.curr_mode == DISPLAY_MODES.RECIPE_SELECTION: + else: button_im = self.buttons.update_weight(weight) ims.append(button_im) - recipe_im = self.recipe_selection.update_weight(weight) + recipe_im = self.buttons.current_view.update_weight(weight) ims.append(recipe_im) diff --git a/frontend/views/recipes/__init__.py b/frontend/views/recipes/__init__.py index e69de29..c6555ba 100644 --- a/frontend/views/recipes/__init__.py +++ b/frontend/views/recipes/__init__.py @@ -0,0 +1,4 @@ +from .recipe_selection import RecipeSelection +from .recipe_manager import RecipeManager +from .edit_recipe import EditRecipe +from .recipe import Recipe \ No newline at end of file diff --git a/frontend/views/recipes/add_recipe.py b/frontend/views/recipes/add_recipe.py deleted file mode 100644 index a9a829e..0000000 --- a/frontend/views/recipes/add_recipe.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Tuple -from PIL import ImageDraw, Image - -from ..base import View -from ..button_interface import ButtonInterface - -class AddRecipe(View, ButtonInterface): - def __init__(self, parent, im_size, center, - save_command=None, cancel_command=None): - self.save_command = save_command - self.cancel_command = cancel_command - super().__init__(parent, im_size, center) - - def update_weight(self, weight: float) -> Image.Image: - im = self.bkg_im.copy() - draw = ImageDraw.Draw(im) - - draw.text((40, 30), "Add Recipe", fill='black') - draw.text((40, 60), "Save", fill='black') - draw.text((40, 90), "Cancel", fill='black') - - return im - - def left_press(self): - if self.save_command: - self.save_command() - - def right_press(self): - if self.cancel_command: - self.cancel_command() - - def has_button(self) -> Tuple[bool, bool, bool, bool]: - return True, False, True, False - - def render_left_press(self, draw, x, y): - draw.text((x, y), 'Save', fill='black') - - def render_right_press(self, draw, x, y): - draw.text((x, y), 'Cancel', fill='black') \ No newline at end of file diff --git a/frontend/views/recipes/do_recipe.py b/frontend/views/recipes/do_recipe.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/views/recipes/edit_recipe.py b/frontend/views/recipes/edit_recipe.py new file mode 100644 index 0000000..3365536 --- /dev/null +++ b/frontend/views/recipes/edit_recipe.py @@ -0,0 +1,104 @@ +from typing import Tuple +from PIL import ImageDraw, Image + +from ..base import View +from ..button_interface import ButtonInterface + +from .recipe_manager import RecipeManager +from .recipe import Recipe + +class EditRecipe(View, ButtonInterface): + def __init__(self, parent, im_size, center, + recipe_manager: RecipeManager, + deactivate_command, + recipe: Recipe = None): + self.deactivate_command = deactivate_command + self.recipe_manager = recipe_manager + self.recipe = recipe + if recipe is None: + self.recipe = Recipe("New", []) + + self.confirm_view = False + self.selected_field = 0 # 0: name, 1+: steps + + super().__init__(parent, im_size, center) + + def _get_visual_steps(self): + steps = self.recipe.steps + ['+'] + if len(steps) < 4: + return steps, 0 + + start = max(0, self.selected_field - 2) + end = min(len(steps), start + 4) + + steps = steps[start:end] + + return steps, start + + def update_weight(self, weight: float) -> Image.Image: + im = self.bkg_im.copy() + draw = ImageDraw.Draw(im) + + x = 40 + + draw.text((x, 10), "Name:", fill='black') + if self.selected_field == 0: + r = 10 + offset = 35 + for i in range(0, 90, r // 2): + draw.circle((x + i, offset), r, fill='black') + draw.text((x, 30), self.recipe.name, fill='white') + else: + draw.text((x, 30), self.recipe.name, fill='black') + + visual_steps, start = self._get_visual_steps() + + for idx, step in enumerate(visual_steps): + y_pos = 60 + idx * 20 + if start + idx + 1 == self.selected_field: + r = 10 + offset = 15 + for i in range(0, 90, r // 2): + draw.circle((x + i, y_pos), r, fill='black') + if str(step) != '+': + step.step_type.render(draw, (x, y_pos - 5), fill='white') + draw.text((x + 30, y_pos - 5), step.value_str, fill='white') + else: + draw.text((x, y_pos - 5), '+', fill='white') + elif str(step) == '+': + draw.text((x, y_pos - 5), '+', fill='black') + else: + step.step_type.render(draw, (x, y_pos - 5), fill='black') + draw.text((x + 30, y_pos - 5), step.value_str, fill='black') + + return im + + def left_press(self): + # edit entry + pass + + def left_long_press(self): + # save + self.recipe_manager.add_recipe(self.recipe) + self.deactivate_command() + + def right_press(self): + self.selected_field += 1 + if self.selected_field > len(self.recipe.steps) + 1: + self.selected_field = 0 + + def has_button(self) -> Tuple[bool, bool, bool, bool]: + return True, True, True, True + + + def render_left_press(self, draw, x, y): + draw.regular_polygon((x, y, 5), 3, fill='black', rotation=270) + + def render_left_long_press(self, draw, x, y): + draw.text((x - 3, y - 5), 'Save', fill='black') + + def render_right_press(self, draw, x, y): + draw.regular_polygon((x, y + 6, 5), 3, fill='black', rotation=180) + + def render_right_long_press(self, draw, x, y): + draw.text((x - 30, y - 5), 'Cancel', fill='black') \ No newline at end of file diff --git a/frontend/views/recipes/recipe.py b/frontend/views/recipes/recipe.py index 5400ce5..623594f 100644 --- a/frontend/views/recipes/recipe.py +++ b/frontend/views/recipes/recipe.py @@ -3,6 +3,8 @@ from typing import Union from enum import Enum +from ..draw_utils import draw_clock + class Recipe: def __init__(self, name: str, steps: list[Step]): self.name = name @@ -19,6 +21,16 @@ class StepType(Enum): WAIT_TIME_FINISHED = 3 TARE = 4 + def render(self, draw, position, fill='black') -> str: + if self == StepType.SECTION: + draw.text(position, "T", fill=fill) + elif self == StepType.WEIGH: + draw.text(position, "W", fill=fill) + elif self == StepType.START_TIME or self == StepType.WAIT_TIME_FINISHED: + draw_clock(draw, (position[0] + 3, position[1] + 5), radius=3, color=fill) + elif self == StepType.TARE: + draw.text(position, "0.0g", fill=fill) + class Step: def __init__(self, @@ -26,6 +38,29 @@ class Step: value: float | str = None): self.step_type = step_type self.value = value + + @property + def value_str(self) -> str: + if self.step_type in [StepType.WEIGH]: + return f"{self.value}g" + elif self.step_type == StepType.START_TIME: + if self.value == -1: + return "Start" + else: + minutes = self.value // 60 + seconds = self.value % 60 + if minutes == 0: + return f"{seconds}s" + else: + return f"{minutes}:{seconds:02d}" + elif self.step_type == StepType.SECTION: + return str(self.value) + elif self.step_type == StepType.TARE: + return "Tare" + elif self.step_type == StepType.WAIT_TIME_FINISHED: + return "Wait" + else: + return "" ###### example recipes ######### diff --git a/frontend/views/recipes/recipe_manager.py b/frontend/views/recipes/recipe_manager.py new file mode 100644 index 0000000..699be15 --- /dev/null +++ b/frontend/views/recipes/recipe_manager.py @@ -0,0 +1,14 @@ +from .recipe import V60, ESPRESSO + +class RecipeManager: + def __init__(self): + self.recipes = [ + V60, + ESPRESSO + ] + + def add_recipe(self, recipe): + self.recipes.append(recipe) + + def remove_recipe(self, recipe): + self.recipes.remove(recipe) \ No newline at end of file diff --git a/frontend/views/recipes/recipe_selection.py b/frontend/views/recipes/recipe_selection.py index 26b3dd6..6115166 100644 --- a/frontend/views/recipes/recipe_selection.py +++ b/frontend/views/recipes/recipe_selection.py @@ -3,20 +3,25 @@ from typing import Tuple from ..base import View from ..button_interface import ButtonInterface +from .recipe_manager import RecipeManager from .recipe import V60, ESPRESSO from PIL import ImageDraw, Image class RecipeSelection(View, ButtonInterface): - recipes = [ - V60, - ESPRESSO - ] + @property + def recipes(self): + return self.recipe_manager.recipes - def __init__(self, parent, im_size, center, deactivate_command=None): + def __init__(self, parent, im_size, center, + recipe_manager: RecipeManager = None, + edit_recipe_command=None, + deactivate_command=None): self.selected_index = 0 self.deactivate_command = deactivate_command + self.recipe_manager = recipe_manager + self.edit_recipe_command = edit_recipe_command super().__init__(parent, im_size, center) def _get_visual_recipes(self): @@ -56,6 +61,10 @@ class RecipeSelection(View, ButtonInterface): def left_press(self): self.selected_index = (self.selected_index - 1) % (len(self.recipes) + 2) + + def left_long_press(self): + if self.selected_index < len(self.recipes): + self.edit_recipe_command(self.recipes[self.selected_index]) def right_press(self): self.selected_index = (self.selected_index + 1) % (len(self.recipes) + 2) @@ -65,8 +74,7 @@ class RecipeSelection(View, ButtonInterface): # activate selected recipe print(f"Activating recipe: {self.recipes[self.selected_index]}") elif self.selected_index == len(self.recipes): - # add new recipe - print("Adding new recipe") + self.edit_recipe_command() else: self.selected_index = 0 self.deactivate_command()