"""Gradio Egg Timer app with a single action button and confetti on completion.""" from __future__ import annotations import time from typing import Any import gradio as gr from egg_timing import DONENESS_BASE_SECONDS, calculate_cook_seconds, format_mm_ss DEFAULT_EGG_COUNT = 1 PLACEHOLDER_TIME = "--:--" ACTION_LABELS = { "idle": "開始煲水", "boil": "水已滾起", "simmer": "已經轉細火", "add_eggs": "已放蛋,開始計時", "countdown": "計時中…", "done": "已完成", } def _timer_html(display_time: str) -> str: return ( '
' '
倒數烚蛋時間
' f'
{display_time}
' "
" ) def _build_status(stage: str, egg_count: int, doneness: str | None, total_seconds: int | None) -> str: if stage == "idle": if not doneness: return "### 歡迎你!\n請先揀要烚到幾熟,之後我會逐步教你煮,弱智都識。" return ( "### 準備好可以開始\n" f"目前設定:**{egg_count} 隻蛋 / {doneness}**\n" f"建議時間:**{format_mm_ss(total_seconds or 0)}**" ) if stage == "boil": return "### 第 1 步:煲滾水\n水大滾先進入下一步,唔使快,最緊要急。" if stage == "simmer": return "### 第 2 步:收細火\n 等D水變返微滾,水面穩定啲再放蛋。" if stage == "add_eggs": return ( "### 第 3 步:輕輕放D蛋落水,咪撚整爛\n" f"而家會用 **{egg_count} 隻蛋 / {doneness}** 去計時。\n" f"建議時間:**{format_mm_ss(total_seconds or 0)}**" ) if stage == "countdown": return ( "### 第 4 步:正式計時中\n" f"目標:**{doneness}**|蛋數:**{egg_count}** 隻\n加油,差唔多完成啦!" ) return "### 完成!\n雞蛋煮好啦,可以即刻過冰水 30-60 秒,口感會更靚。" def _controls_locked(stage: str) -> bool: return stage in {"countdown", "done"} def _action_button_update(stage: str, doneness: str | None) -> Any: if stage in {"countdown", "done"}: return gr.update(value=ACTION_LABELS[stage], visible=True, interactive=False) if stage == "idle" and not doneness: return gr.update(value="請先選熟度", visible=True, interactive=False) return gr.update(value=ACTION_LABELS.get(stage, "下一步"), visible=True, interactive=True) def _confetti_html() -> str: nonce = str(time.time_ns()) return f"""
""" def _render_screen( *, stage: str, egg_count: int, doneness: str | None, total_seconds: int, remaining_seconds: int, running: bool, status_override: str | None = None, celebration_html: str = "", ): del running # Kept in signature for easier call symmetry. display_seconds = remaining_seconds if stage in {"countdown", "done"} else total_seconds display_text = PLACEHOLDER_TIME if display_seconds <= 0 else format_mm_ss(display_seconds) status = status_override or _build_status(stage, egg_count, doneness, total_seconds) return ( status, _timer_html(display_text), _action_button_update(stage, doneness), gr.update(interactive=not _controls_locked(stage)), gr.update(interactive=not _controls_locked(stage)), celebration_html, ) def preview_settings(egg_count: int, doneness: str | None, stage: str, running: bool, remaining_seconds: int): total_seconds = calculate_cook_seconds(egg_count, doneness) if doneness else 0 if stage == "idle": status, timer_html, btn_action, egg_update, doneness_update, celebration = _render_screen( stage="idle", egg_count=egg_count, doneness=doneness, total_seconds=total_seconds, remaining_seconds=total_seconds, running=False, celebration_html="", ) return ( total_seconds, total_seconds, timer_html, status, btn_action, egg_update, doneness_update, celebration, ) del running, remaining_seconds return ( total_seconds, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(interactive=not _controls_locked(stage)), gr.update(interactive=not _controls_locked(stage)), "", ) def handle_action(stage: str, egg_count: int, doneness: str | None, total_seconds: int): if stage == "idle": if not doneness: return ( stage, total_seconds, total_seconds, False, 0.0, gr.update(active=False), *_render_screen( stage="idle", egg_count=egg_count, doneness=doneness, total_seconds=0, remaining_seconds=0, running=False, status_override="### 未可以開始\n請先揀熟度。", celebration_html="", ), ) new_total = calculate_cook_seconds(egg_count, doneness) return ( "boil", new_total, new_total, False, 0.0, gr.update(active=False), *_render_screen( stage="boil", egg_count=egg_count, doneness=doneness, total_seconds=new_total, remaining_seconds=new_total, running=False, celebration_html="", ), ) if stage == "boil": return ( "simmer", total_seconds, total_seconds, False, 0.0, gr.update(active=False), *_render_screen( stage="simmer", egg_count=egg_count, doneness=doneness, total_seconds=total_seconds, remaining_seconds=total_seconds, running=False, celebration_html="", ), ) if stage == "simmer": return ( "add_eggs", total_seconds, total_seconds, False, 0.0, gr.update(active=False), *_render_screen( stage="add_eggs", egg_count=egg_count, doneness=doneness, total_seconds=total_seconds, remaining_seconds=total_seconds, running=False, celebration_html="", ), ) if stage == "add_eggs": end_timestamp = time.time() + total_seconds return ( "countdown", total_seconds, total_seconds, True, end_timestamp, gr.update(active=True), *_render_screen( stage="countdown", egg_count=egg_count, doneness=doneness, total_seconds=total_seconds, remaining_seconds=total_seconds, running=True, celebration_html="", ), ) return ( stage, total_seconds, total_seconds, False, 0.0, gr.update(active=False), *_render_screen( stage=stage, egg_count=egg_count, doneness=doneness, total_seconds=total_seconds, remaining_seconds=total_seconds, running=False, celebration_html="", ), ) def tick( stage: str, running: bool, end_timestamp: float, remaining_seconds: int, egg_count: int, doneness: str | None, total_seconds: int, ): if stage != "countdown" or not running: return ( stage, remaining_seconds, running, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(active=False), ) new_remaining = max(0, int(end_timestamp - time.time())) if new_remaining > 0: return ( stage, new_remaining, True, _timer_html(format_mm_ss(new_remaining)), gr.update(), gr.update(), gr.update(), gr.update(), "", gr.update(active=True), ) done_message = ( "### 搞掂!\n" f"你嘅 **{doneness}** 雞蛋已經完成。\n" "建議即刻放入冰水 30-60 秒,會更易剝殼同保持口感。" ) status, timer_html, btn_action, egg_update, doneness_update, celebration = _render_screen( stage="done", egg_count=egg_count, doneness=doneness, total_seconds=total_seconds, remaining_seconds=0, running=False, status_override=done_message, celebration_html=_confetti_html(), ) return ( "done", 0, False, timer_html, status, btn_action, egg_update, doneness_update, celebration, gr.update(active=False), ) def reset_ui(): stage = "idle" total_seconds = 0 remaining_seconds = 0 return ( stage, total_seconds, remaining_seconds, False, 0.0, gr.update(active=False), *_render_screen( stage=stage, egg_count=DEFAULT_EGG_COUNT, doneness=None, total_seconds=total_seconds, remaining_seconds=remaining_seconds, running=False, celebration_html="", ), gr.update(value=DEFAULT_EGG_COUNT, interactive=True), gr.update(value=None, interactive=True), ) with gr.Blocks(title="烚蛋計時小幫手") as demo: gr.Markdown("# 烚蛋計時小幫手") gr.Markdown("陪你一步一步煮出理想熟度,慢慢嚟,最緊要快。") with gr.Row(): egg_count = gr.Slider(minimum=1, maximum=12, step=1, value=DEFAULT_EGG_COUNT, label="雞蛋數量") doneness = gr.Radio( choices=list(DONENESS_BASE_SECONDS.keys()), value=None, label="熟度(必選)", ) stage_state = gr.State("idle") total_state = gr.State(0) remaining_state = gr.State(0) running_state = gr.State(False) end_ts_state = gr.State(0.0) status_md = gr.Markdown(_build_status("idle", DEFAULT_EGG_COUNT, None, None)) timer_display = gr.HTML(_timer_html(PLACEHOLDER_TIME)) celebration_html = gr.HTML("") with gr.Row(): btn_action = gr.Button("請先選熟度", variant="primary", visible=True, interactive=False) btn_reset = gr.Button("重新開始") ticker = gr.Timer(value=1.0, active=False) for input_component in (egg_count, doneness): input_component.change( fn=preview_settings, inputs=[egg_count, doneness, stage_state, running_state, remaining_state], outputs=[total_state, remaining_state, timer_display, status_md, btn_action, egg_count, doneness, celebration_html], ) btn_action.click( fn=handle_action, inputs=[stage_state, egg_count, doneness, total_state], outputs=[ stage_state, total_state, remaining_state, running_state, end_ts_state, ticker, status_md, timer_display, btn_action, egg_count, doneness, celebration_html, ], ) btn_reset.click( fn=reset_ui, inputs=[], outputs=[ stage_state, total_state, remaining_state, running_state, end_ts_state, ticker, status_md, timer_display, btn_action, egg_count, doneness, celebration_html, ], ) ticker.tick( fn=tick, inputs=[ stage_state, running_state, end_ts_state, remaining_state, egg_count, doneness, total_state, ], outputs=[ stage_state, remaining_state, running_state, timer_display, status_md, btn_action, egg_count, doneness, celebration_html, ticker, ], ) if __name__ == "__main__": demo.launch(ssr_mode=False)