From 851b894e5f1ecbd3ac4cce1877cd71cbe2f1e66a Mon Sep 17 00:00:00 2001 From: Jannes Magnusson Date: Fri, 17 Oct 2025 17:47:20 +0200 Subject: [PATCH] add timer --- frontend/config.py | 3 +- frontend/views/__init__.py | 1 + frontend/views/circle.py | 9 ++- frontend/views/combined_view.py | 14 +++- frontend/views/timer.py | 126 ++++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 frontend/views/timer.py diff --git a/frontend/config.py b/frontend/config.py index 38fcb42..d6dea7f 100644 --- a/frontend/config.py +++ b/frontend/config.py @@ -13,4 +13,5 @@ MOV_AVG_DEFAULTS = { class DISPLAY_TYPES(Flag): NUMBER = 1 - CIRCLE = 2 \ No newline at end of file + CIRCLE = 2 + TIMER = 4 \ No newline at end of file diff --git a/frontend/views/__init__.py b/frontend/views/__init__.py index f505c74..d800f9a 100644 --- a/frontend/views/__init__.py +++ b/frontend/views/__init__.py @@ -1,3 +1,4 @@ from .number import NumberView from .circle import CircleView +from .timer import TimerView from .combined_view import CombinedView \ No newline at end of file diff --git a/frontend/views/circle.py b/frontend/views/circle.py index 5d70d83..59f915e 100644 --- a/frontend/views/circle.py +++ b/frontend/views/circle.py @@ -6,6 +6,10 @@ from .base import View class CircleView(View): + def __init__(self, parent, size, center, radius_offset=10, **kwargs): + self.target_radius = min(center) - radius_offset + super().__init__(parent, size, center, **kwargs) + def init_ui(self, parent): self.ui = tk.Frame(parent) self.ui.pack() @@ -18,8 +22,7 @@ class CircleView(View): def _init_im(self): im = Image.new('1', self.size, 'white') draw = ImageDraw.Draw(im) - self.target_r = min(self.center)-10 - draw.circle(self.center, self.target_r, + draw.circle(self.center, self.target_radius, outline="#000000") return im @@ -28,7 +31,7 @@ class CircleView(View): bkg_im = self.bkg_im.copy() try: target = float(self.target.get()) - weight_radius = weight / target * self.target_r + weight_radius = weight / target * self.target_radius im = Image.new('1', self.size, 'black') draw = ImageDraw.Draw(im) diff --git a/frontend/views/combined_view.py b/frontend/views/combined_view.py index c9b551e..a0ff510 100644 --- a/frontend/views/combined_view.py +++ b/frontend/views/combined_view.py @@ -5,7 +5,7 @@ from tkinter import Frame, Canvas, ttk, PhotoImage from PIL import Image, ImageChops from ..config import DISPLAY_TYPES -from . import NumberView, CircleView +from . import NumberView, CircleView, TimerView class CombinedView(tk.Frame): def __init__(self, parent, @@ -13,6 +13,7 @@ class CombinedView(tk.Frame): **kwargs): super().__init__(parent, **kwargs) self.views = [] + self.timer_view = None # Timer view is always active self.tare_command = tare_command self.calibrate_command = calibrate_command @@ -26,12 +27,16 @@ class CombinedView(tk.Frame): self.im_size = (168, 144) self.center = (168 // 2, 144 // 2) + # Create timer view that's always active + self.timer_view = TimerView(self.actions, self.im_size, self.center) + self.canvas = Canvas(self, width=168, height=144, background='white', highlightthickness=1, highlightbackground="black") self.canvas.pack() def update_views(self, selected_types: DISPLAY_TYPES): + # Clear only the selectable views, not the timer for v in self.views: if v.ui is not None: v.ui.destroy() @@ -48,6 +53,13 @@ class CombinedView(tk.Frame): def refresh(self, weight: float): ims = [] + + # Always include timer view + if self.timer_view: + timer_im = self.timer_view.update_weight(weight) + ims.append(timer_im) + + # Add other selected views for view in self.views: im = view.update_weight(weight) ims.append(im) diff --git a/frontend/views/timer.py b/frontend/views/timer.py new file mode 100644 index 0000000..98e46c3 --- /dev/null +++ b/frontend/views/timer.py @@ -0,0 +1,126 @@ +import tkinter as tk +from tkinter import ttk +from PIL import Image, ImageDraw, ImageChops +import time +import math + +from .base import View + +class TimerView(View): + def __init__(self, parent, size, center, width=5, **kwargs): + self.start_time = None + self.elapsed_time = 0 + self.is_running = False + self.goal_minutes = 0 # 0 means no goal set + self.radius = min(center)-width + self.width = width + super().__init__(parent, size, center, **kwargs) + + def init_ui(self, parent): + self.ui = tk.Frame(parent) + self.ui.pack(pady=10) + + # Goal input + self.goal_label = ttk.Label(self.ui, text="Goal (sec):") + self.goal_label.pack() + self.goal_entry = ttk.Entry(self.ui, width=8) + self.goal_entry.insert(0, "0") + self.goal_entry.pack() + + # Timer buttons + self.start_stop_button = ttk.Button(self.ui, text="Start", command=self.toggle_timer) + self.start_stop_button.pack(side=tk.LEFT, padx=2) + + self.reset_button = ttk.Button(self.ui, text="Reset", command=self.reset_timer) + self.reset_button.pack(side=tk.LEFT, padx=2) + + def toggle_timer(self): + if self.is_running: + # Stop timer + self.is_running = False + if self.start_time: + self.elapsed_time += time.time() - self.start_time + self.start_stop_button.config(text="Start") + else: + # Start timer + self.is_running = True + self.start_time = time.time() + self.start_stop_button.config(text="Stop") + + def reset_timer(self): + self.is_running = False + self.start_time = None + self.elapsed_time = 0 + self.start_stop_button.config(text="Start") + + def get_current_time(self): + """Get current elapsed time in seconds""" + if self.is_running and self.start_time: + return self.elapsed_time + (time.time() - self.start_time) + return self.elapsed_time + + def update_weight(self, weight): + """Override to update timer display instead of weight""" + current_time = self.get_current_time() + + # Create base image + im = self.bkg_im.copy() + draw = ImageDraw.Draw(im) + + # Format time display (MM:SS) + minutes = int(current_time // 60) + seconds = int(current_time % 60) + time_text = f"{minutes:02d}:{seconds:02d}" + + if time_text != "00:00" or self.is_running: + # Draw timer text in center + # Estimate text size for centering + text_width = len(time_text) * 10 # Rough estimate + text_height = 20 + text_x = self.center[0] - text_width // 2 + text_y = self.center[1] - text_height // 2 + + draw.text((text_x, text_y), time_text, fill='black') + + # Draw progress circle if goal is set + try: + goal_sec = float(self.goal_entry.get()) + if goal_sec > 0: + progress = current_time / goal_sec + else: + progress = current_time / 60 + + if progress > 0: + inverted = int(progress) % 2 == 1 + progress = progress % 1.0 # Loop every full circle + + start = self.center[0] - self.radius, self.center[1] - self.radius + end = self.center[0] + self.radius, self.center[1] + self.radius + + if inverted: + draw.arc((start, end), 360 * progress - 90, 270, fill='black', width=self.width) + else: + draw.arc((start, end), 270, 360 * progress - 90, fill='black', width=self.width) + + except (ValueError, tk.TclError): + # Invalid goal value, just show time without progress + pass + + return im + + def _draw_progress_arc(self, draw, progress): + """Draw a progress arc around the outer circle""" + if progress <= 0: + return + + # Draw filled arc by drawing multiple lines from center to circumference + center_x, center_y = self.center + # Start from top (270 degrees) and go clockwise + start_angle = 270 + num_steps = max(1, int(360 * progress)) + + for i in range(num_steps): + angle = math.radians(start_angle + i) + end_x = center_x + self.outer_radius * math.cos(angle) + end_y = center_y + self.outer_radius * math.sin(angle) + draw.line([(center_x, center_y), (end_x, end_y)], fill='black', width=1) \ No newline at end of file