"""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)