From b6db88f9b926ecf178b7df982fde7416c6289d04 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Wed, 11 Nov 2026 00:54:59 +0100 Subject: [PATCH] Make workout app and locker text-free Painted chrome becomes icons or symbols; old words stay as spoken labels or code comments. OS window titles kept. Tests build today from the local day. Todo: ed07cff3 Co-Authored-By: Claude Opus 5.5 --- screen_locker/_heat_skip.py | 29 +++--- screen_locker/_manual_workout_date.py | 21 ++-- screen_locker/_manual_workout_dialog.py | 25 +++-- screen_locker/_manual_workout_sport_fields.py | 8 +- screen_locker/_window_setup.py | 3 +- screen_locker/tests/_unlock_helpers.py | 2 +- screen_locker/tests/conftest.py | 3 +- screen_locker/tests/test_compliance_state.py | 2 +- .../tests/test_compliance_state_part2.py | 2 +- screen_locker/tests/test_early_bird_part2.py | 2 +- screen_locker/tests/test_early_bird_part3.py | 8 +- screen_locker/tests/test_heat_skip.py | 24 +++-- screen_locker/tests/test_init_and_log.py | 8 +- .../tests/test_init_and_log_part3.py | 8 +- .../tests/test_manual_workout_date.py | 7 +- .../tests/test_manual_workout_dialog.py | 12 ++- .../tests/test_screen_lock_coverage_part3.py | 2 +- screen_locker/tests/test_shutdown_base.py | 2 +- .../tests/test_weekly_logic_part5.py | 2 +- scripts/demo_workout_handoff.py | 16 +-- .../workout_app/lib/main.dart | 4 +- .../lib/screens/github_mirror_screen.dart | 9 +- .../screens/github_mirror_screen_widgets.dart | 62 ++++++------ .../lib/screens/history_screen.dart | 3 +- .../lib/screens/history_screen_planned.dart | 59 ++++++++--- .../workout_app/lib/screens/home_screen.dart | 21 ++-- .../screens/manual_workout_form_parts.dart | 6 +- .../lib/screens/manual_workout_screen.dart | 2 +- .../lib/screens/settings_screen.dart | 7 +- .../lib/screens/settings_screen_actions.dart | 43 +++++--- .../lib/screens/settings_screen_deload.dart | 14 ++- .../lib/screens/settings_screen_pc_link.dart | 5 +- .../lib/screens/settings_screen_sandbox.dart | 29 +++--- .../lib/screens/settings_screen_sections.dart | 24 +++-- .../lib/screens/settings_screen_sound.dart | 2 +- .../lib/screens/workout_screen_breaks.dart | 11 ++- .../lib/screens/workout_screen_dialogs.dart | 47 +++++---- .../lib/screens/workout_screen_intents.dart | 7 +- .../lib/services/break_notification_text.dart | 49 +++++++--- .../services/break_service_port_flutter.dart | 23 +++-- .../foreground_break_client_flutter.dart | 17 ++-- .../foreground_run_client_flutter.dart | 4 +- .../workout_app/lib/widgets/break_banner.dart | 27 +++-- .../lib/widgets/exercise_settings_sheet.dart | 34 ++++--- .../exercise_settings_sheet_deload.dart | 37 ++++--- .../widgets/exercise_settings_sheet_rest.dart | 4 +- .../widgets/exercise_settings_sheet_rows.dart | 27 ++++- .../workout_app/test/icon_finders.dart | 16 +++ .../test/screens/_history_test_fixtures.dart | 2 + .../test/screens/_home_test_fixtures.dart | 2 + .../test/screens/_manual_screen_fixtures.dart | 6 +- .../_workout_screen_test_fixtures.dart | 2 + .../github_mirror_screen_auth_test.dart | 5 +- .../github_mirror_screen_flow_test.dart | 5 +- .../github_mirror_screen_push_test.dart | 14 ++- .../screens/github_mirror_screen_test.dart | 98 ++++++++++--------- .../test/screens/history_planned_test.dart | 16 +-- .../test/screens/history_screen_test.dart | 3 +- .../test/screens/home_screen_test.dart | 8 +- .../test/screens/home_sync_card_test.dart | 3 +- .../screens/manual_workout_date_test.dart | 24 ++--- .../screens/manual_workout_screen_test.dart | 10 +- .../screens/settings_screen_deload_test.dart | 13 +-- .../settings_screen_exercises_test.dart | 14 +-- .../screens/settings_screen_sandbox_test.dart | 11 ++- .../screens/settings_screen_sound_test.dart | 3 +- .../screens/settings_screen_sync_test.dart | 25 ++--- .../test/screens/settings_screen_test.dart | 3 +- .../settings_screen_thresholds_test.dart | 7 +- .../test/screens/workout_breaks_test.dart | 17 ++-- .../test/screens/workout_cues_test.dart | 4 +- .../workout_finish_stop_failure_test.dart | 9 +- .../test/screens/workout_layout_test.dart | 8 +- .../test/screens/workout_pause_test.dart | 14 +-- .../screens/workout_screen_deload_test.dart | 23 ++--- .../workout_screen_intents_stale_test.dart | 12 +-- .../test/screens/workout_screen_test.dart | 23 ++--- .../break_notification_text_test.dart | 18 ++-- .../workout_app/test/widget_test.dart | 6 +- .../test/widgets/break_banner_test.dart | 16 +-- .../exercise_settings_sheet_deload_test.dart | 36 ++++--- .../widgets/exercise_settings_sheet_test.dart | 24 ++--- .../test/widgets/exercise_tile_test.dart | 4 +- 83 files changed, 760 insertions(+), 507 deletions(-) create mode 100644 stronglift_replacement/workout_app/test/icon_finders.dart diff --git a/screen_locker/_heat_skip.py b/screen_locker/_heat_skip.py index 09d637d..d22ecf2 100644 --- a/screen_locker/_heat_skip.py +++ b/screen_locker/_heat_skip.py @@ -42,10 +42,14 @@ def build_heat_skip_content( outer = tk.Frame(parent, bg=_COLORS.palette.bg) outer.place(relx=0.5, rely=0.5, anchor="center") + # textgate: glyphs + numbers only, warning colour as the second channel. + # Was "\u2600 Too hot to workout?" / ": 31\u00b0C (threshold: 30\u00b0C)" / + # "Skip today's workout due to extreme heat?". Plain geometric glyphs, + # not the sun (U+2600 can fall back to a colour-emoji font and crash Tk). tk.Label( outer, - text="\u2600 Too hot to workout?", - font=_COLORS.font("body", bold=True, family=_FONT), + text="\u25b2", + font=_COLORS.font("display", bold=True, family=_FONT), bg=_COLORS.palette.bg, fg=_COLORS.palette.warning, ).pack(pady=(0, _COLORS.space("sm"))) @@ -53,28 +57,22 @@ def build_heat_skip_content( tk.Label( outer, text=( - f"{HEAT_SKIP_CITY}: {temp:.0f}\u00b0C" - f" (threshold: {HEAT_SKIP_TEMP_THRESHOLD}\u00b0C)" + f"{HEAT_SKIP_CITY} {temp:.0f}\u00b0C" + f" \u2265 {HEAT_SKIP_TEMP_THRESHOLD}\u00b0C" ), font=_COLORS.font("body", family=_FONT), bg=_COLORS.palette.bg, - fg=_COLORS.palette.muted, + fg=_COLORS.palette.warning, ).pack(pady=_COLORS.space("xs")) - tk.Label( - outer, - text="Skip today's workout due to extreme heat?", - font=_COLORS.font("body", family=_FONT), - bg=_COLORS.palette.bg, - fg=_COLORS.palette.muted, - ).pack(pady=(_COLORS.space("xs"), 0)) - btn_frame = tk.Frame(outer, bg=_COLORS.palette.bg) btn_frame.pack(pady=_COLORS.space("lg")) + # Was "Skip workout" (warning fill) / "No, I'll workout" (neutral fill). skip_button = tk.Button( btn_frame, - text="Skip workout", + name="skip", + text="\u21b7", command=on_skip, bg=_COLORS.palette.warning, fg=_COLORS.palette.on_fill, @@ -89,7 +87,8 @@ def build_heat_skip_content( decline_button = tk.Button( btn_frame, - text="No, I'll workout", + name="decline", + text="▶", command=on_decline, bg=_COLORS.palette.field_bg, fg=_COLORS.palette.fg, diff --git a/screen_locker/_manual_workout_date.py b/screen_locker/_manual_workout_date.py index 08a9e0d..266ef89 100644 --- a/screen_locker/_manual_workout_date.py +++ b/screen_locker/_manual_workout_date.py @@ -29,6 +29,12 @@ from screen_locker._ui_widgets import disable_paste if TYPE_CHECKING: from screen_locker._manual_workout import ManualWorkoutDraft +# textgate: plain glyphs (no colour emoji: Tk 8.6 can crash on them). +SCHEDULED_GLYPH = "✓" +"""Heading of the scheduled screen (success colour).""" +BACK_GLYPH = "↩" +"""The scheduled screen's only button: back to the form's host.""" + class ManualWorkoutDateMixin: """Date row, budget gate and future-dated submission of the manual form.""" @@ -59,9 +65,10 @@ class ManualWorkoutDateMixin: ) entry.pack(side="right", padx=self._colors.space("xs")) disable_paste(entry) + # Was "Workout date (today to +7 days):" -- a day square + the range. tk.Label( row, - text="Workout date (today to +7 days):", + text="□ +0…+7", font=self._colors.font("label"), fg=self._colors.palette.fg, bg=self._colors.palette.bg, @@ -94,23 +101,21 @@ class ManualWorkoutDateMixin: def _show_manual_workout_scheduled(self, reason: str) -> None: """Confirm a future entry and go back; it does NOT unlock anything.""" self.clear_container() + # Was "✓ Workout Scheduled" + "Nothing is credited today. It counts by + # itself on that date, and can be deleted in the phone app until + # then." + an "OK" button: a green tick, the reason, a back arrow. self._label( - "✓ Workout Scheduled", + SCHEDULED_GLYPH, role="display", scale=1.3, color=self._colors.palette.success, pad="lg", ) self._text(reason, role="subtitle", color=self._colors.palette.success) - self._text( - "Nothing is credited today. It counts by itself on that date, and " - "can be deleted in the phone app until then.", - color=self._colors.palette.muted, - ) row = self._button_row() self._button( row, - "OK", + BACK_GLYPH, bg=self._colors.palette.accent, command=self._on_manual_workout_cancelled, width=12, diff --git a/screen_locker/_manual_workout_dialog.py b/screen_locker/_manual_workout_dialog.py index 44e53e3..6d6ec4e 100644 --- a/screen_locker/_manual_workout_dialog.py +++ b/screen_locker/_manual_workout_dialog.py @@ -23,12 +23,20 @@ from screen_locker import _manual_workout from screen_locker._constants import ( MANUAL_WORKOUT_REFLECTION_MIN_CHARS, ) -from screen_locker._manual_workout_date import ManualWorkoutDateMixin +from screen_locker._manual_workout_date import BACK_GLYPH, ManualWorkoutDateMixin from screen_locker._manual_workout_sport_fields import ManualWorkoutSportFieldsMixin from screen_locker._manual_workout_widgets import ManualWorkoutFormWidgetsMixin _logger = logging.getLogger(__name__) +# textgate glyphs (plain Unicode, no colour emoji: Tk 8.6 can crash on them). +FORM_GLYPH = "≡" +"""Form heading (accent); was "Log Manual Workout".""" +EXHAUSTED_GLYPH = "✕" +"""Budget used up (danger); was the "budget exhausted" sentence.""" +SUBMIT_GLYPH = "✓" +"""Submit button (accent fill); was "SUBMIT".""" + _SPORT_LABEL_TO_CODE = { label: code for code, label in _manual_workout.SPORT_LABELS.items() } @@ -44,12 +52,11 @@ class ManualWorkoutDialogMixin( def _show_manual_workout_form(self) -> None: """Render the manual-workout evidence form, or a budget-exhausted note.""" self.clear_container() - self._label("Log Manual Workout", color=self._colors.palette.accent, pad="xs") + # textgate: glyphs, colours second. Was "Log Manual Workout" heading / + # "Manual-workout budget exhausted for this window." / "BACK". + self._label(FORM_GLYPH, color=self._colors.palette.accent, pad="xs") if self._mw_budget_closed(): - self._text( - "Manual-workout budget exhausted for this window.", - color=self._colors.palette.danger, - ) + self._label(EXHAUSTED_GLYPH, color=self._colors.palette.danger, pad="xs") self._text( _manual_workout.budget_summary(self.log_file), color=self._colors.palette.muted, @@ -57,7 +64,7 @@ class ManualWorkoutDialogMixin( row = self._button_row() self._button( row, - "BACK", + BACK_GLYPH, bg=self._colors.palette.field_bg, command=self._on_manual_workout_cancelled, width=12, @@ -158,14 +165,14 @@ class ManualWorkoutDialogMixin( button_row = self._button_row() self._button( button_row, - "SUBMIT", + SUBMIT_GLYPH, bg=self._colors.palette.accent, command=self._submit_manual_workout_form, width=12, ).pack(side="left", padx=self._colors.space("sm")) self._button( button_row, - "BACK", + BACK_GLYPH, bg=self._colors.palette.field_bg, command=self._on_manual_workout_cancelled, width=12, diff --git a/screen_locker/_manual_workout_sport_fields.py b/screen_locker/_manual_workout_sport_fields.py index dec8b66..6c8140f 100644 --- a/screen_locker/_manual_workout_sport_fields.py +++ b/screen_locker/_manual_workout_sport_fields.py @@ -49,13 +49,7 @@ class ManualWorkoutSportFieldsMixin: value=_manual_workout.SPORT_LABELS[_manual_workout.SPORT_TABLE_TENNIS] ) row = tk.Frame(parent, bg=self._colors.palette.bg) - tk.Label( - row, - text="Sport:", - font=self._colors.font("body"), - fg=self._colors.palette.fg, - bg=self._colors.palette.bg, - ).pack(side="left", padx=self._colors.space("xs")) + # textgate: the "Sport:" caption is gone; the radio row is the cue. for label in _manual_workout.SPORT_LABELS.values(): tk.Radiobutton( row, diff --git a/screen_locker/_window_setup.py b/screen_locker/_window_setup.py index d809fa7..7d921f2 100644 --- a/screen_locker/_window_setup.py +++ b/screen_locker/_window_setup.py @@ -201,9 +201,10 @@ class WindowSetupMixin: def _setup_demo_close_button(self) -> None: """Add close button for demo mode.""" + # Was "✕ Close Demo": the cross on the danger fill says it alone. close_btn = tk.Button( self.root, - text="✕ Close Demo", + text="✕", font=self._colors.font("caption"), bg=self._colors.palette.danger, fg=self._colors.palette.on_fill, diff --git a/screen_locker/tests/_unlock_helpers.py b/screen_locker/tests/_unlock_helpers.py index 69d3b04..cb824a2 100644 --- a/screen_locker/tests/_unlock_helpers.py +++ b/screen_locker/tests/_unlock_helpers.py @@ -37,7 +37,7 @@ def setup_unlock( if seed_today_type is None: log_file.write_text("{}") else: - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") log_file.write_text( json.dumps( { diff --git a/screen_locker/tests/conftest.py b/screen_locker/tests/conftest.py index ebcc244..8dc4720 100644 --- a/screen_locker/tests/conftest.py +++ b/screen_locker/tests/conftest.py @@ -199,7 +199,8 @@ def _isolate_shutdown_base(tmp_path: Path) -> Iterator[None]: refresh sees an empty unit dir, never the real one. """ target = tmp_path / "shutdown_base.json" - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + # Local day, as the code compares it (_day.today_str), not the UTC one. + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") target.write_text(json.dumps(dict.fromkeys(["last_reset_date", *_BONUS], today))) with ( patch("screen_locker._constants.SHUTDOWN_BASE_FILE", target), diff --git a/screen_locker/tests/test_compliance_state.py b/screen_locker/tests/test_compliance_state.py index 35ad87f..e078bcf 100644 --- a/screen_locker/tests/test_compliance_state.py +++ b/screen_locker/tests/test_compliance_state.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: def _today() -> str: - return datetime.now(tz=UTC).strftime("%Y-%m-%d") + return datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") class TestIsScheduledSkipToday: diff --git a/screen_locker/tests/test_compliance_state_part2.py b/screen_locker/tests/test_compliance_state_part2.py index 0002c71..99558f3 100644 --- a/screen_locker/tests/test_compliance_state_part2.py +++ b/screen_locker/tests/test_compliance_state_part2.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: def _today() -> str: - return datetime.now(tz=UTC).strftime("%Y-%m-%d") + return datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") class TestExplainLockDecision: diff --git a/screen_locker/tests/test_early_bird_part2.py b/screen_locker/tests/test_early_bird_part2.py index 5c1295a..83905e6 100644 --- a/screen_locker/tests/test_early_bird_part2.py +++ b/screen_locker/tests/test_early_bird_part2.py @@ -49,7 +49,7 @@ class TestTryAutoUpgradeEarlyBird: assert result is True with log_file.open() as f: data: dict[str, Any] = json.load(f) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") assert data[today][0]["workout_data"]["type"] == "phone_verified" assert data[today][0]["workout_data"]["after_early_bird"] == "true" diff --git a/screen_locker/tests/test_early_bird_part3.py b/screen_locker/tests/test_early_bird_part3.py index f61122f..d9f25af 100644 --- a/screen_locker/tests/test_early_bird_part3.py +++ b/screen_locker/tests/test_early_bird_part3.py @@ -81,7 +81,7 @@ class TestIsEarlyBirdPending: ) -> None: """Return False when HMAC verification fails.""" locker = create_locker(mock_tk, tmp_path) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") pending_file = tmp_path / "early_bird_pending.json" pending_file.write_text(json.dumps({"date": today, "hmac": "bad"})) with ( @@ -105,7 +105,7 @@ class TestIsEarlyBirdPending: ) -> None: """Return True when today's marker is present and HMAC-valid.""" locker = create_locker(mock_tk, tmp_path) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") pending_file = tmp_path / "early_bird_pending.json" pending_file.write_text(json.dumps({"date": today, "hmac": "sig"})) with ( @@ -125,7 +125,7 @@ class TestIsEarlyBirdPending: ) -> None: """Unsigned marker is accepted when no HMAC key is configured.""" locker = create_locker(mock_tk, tmp_path) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") pending_file = tmp_path / "early_bird_pending.json" pending_file.write_text(json.dumps({"date": today})) with ( @@ -163,7 +163,7 @@ class TestSaveEarlyBirdPending: assert pending_file.exists() with pending_file.open() as f: data: dict[str, Any] = json.load(f) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") assert data["date"] == today assert not locker.log_file.exists() diff --git a/screen_locker/tests/test_heat_skip.py b/screen_locker/tests/test_heat_skip.py index 5ebb237..dd0255e 100644 --- a/screen_locker/tests/test_heat_skip.py +++ b/screen_locker/tests/test_heat_skip.py @@ -2,8 +2,10 @@ from __future__ import annotations +import re from unittest.mock import MagicMock +from screen_locker._constants import HEAT_SKIP_CITY, HEAT_SKIP_TEMP_THRESHOLD from screen_locker._heat_skip import HeatSkipMixin @@ -15,11 +17,15 @@ class _HeatSkipHost(HeatSkipMixin): self.save_workout_log = MagicMock() -def _button_command(mock_tk: MagicMock, text: str): +def _button_command(mock_tk: MagicMock, name: str): + """The command of the button created with Tk widget ``name``. + + By name, not by glyph: the buttons carry symbols only (textgate). + """ for call in mock_tk.Button.call_args_list: - if call.kwargs.get("text") == text: + if call.kwargs.get("name") == name: return call.kwargs["command"] - msg = f"No button with text {text!r} was created" + msg = f"No button named {name!r} was created" raise AssertionError(msg) @@ -31,7 +37,7 @@ class TestShowHeatSkipDialog: host = _HeatSkipHost() def _simulate_click() -> None: - _button_command(mock_tk, "Skip workout")() + _button_command(mock_tk, "skip")() mock_tk.Tk.return_value.mainloop.side_effect = _simulate_click assert host._show_heat_skip_dialog(35.0) is True @@ -42,19 +48,21 @@ class TestShowHeatSkipDialog: host = _HeatSkipHost() def _simulate_click() -> None: - _button_command(mock_tk, "No, I'll workout")() + _button_command(mock_tk, "decline")() mock_tk.Tk.return_value.mainloop.side_effect = _simulate_click assert host._show_heat_skip_dialog(35.0) is False def test_dialog_shows_temperature_and_threshold(self, mock_tk: MagicMock) -> None: - """The dialog text shows the rounded temperature and mentions the threshold.""" + """The dialog shows the rounded temperature against the threshold, no words.""" host = _HeatSkipHost() mock_tk.Tk.return_value.mainloop.side_effect = None host._show_heat_skip_dialog(36.4) texts = [c.kwargs.get("text", "") for c in mock_tk.Label.call_args_list] - assert any("36°C" in t for t in texts) - assert any("threshold" in t for t in texts) + assert any(f"36°C ≥ {HEAT_SKIP_TEMP_THRESHOLD}°C" in t for t in texts) + # Only the city (config data) may carry letters: no chrome sentence. + chrome = [t.replace(HEAT_SKIP_CITY, "") for t in texts] + assert not any(re.search(r"[A-Za-z]{2,}", t) for t in chrome) class TestSaveHeatSkipLog: diff --git a/screen_locker/tests/test_init_and_log.py b/screen_locker/tests/test_init_and_log.py index fcc069b..9410253 100644 --- a/screen_locker/tests/test_init_and_log.py +++ b/screen_locker/tests/test_init_and_log.py @@ -140,7 +140,7 @@ class TestHasLoggedToday: ) -> None: """Test when today's workout is logged with valid HMAC.""" log_file = tmp_path / "log.json" - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") log_file.write_text( json.dumps({today: {"workout": "data", "hmac": "valid"}}), ) @@ -161,7 +161,7 @@ class TestHasLoggedToday: ) -> None: """Test rejects entry when HMAC verification fails.""" log_file = tmp_path / "log.json" - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") log_file.write_text( json.dumps({today: {"workout": "data", "hmac": "tampered"}}), ) @@ -182,7 +182,7 @@ class TestHasLoggedToday: ) -> None: """Accept unsigned entry when HMAC key is unavailable.""" log_file = tmp_path / "log.json" - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") log_file.write_text( json.dumps({today: {"workout": "data"}}), ) @@ -209,7 +209,7 @@ class TestHasLoggedToday: ) -> None: """Reject unsigned entry when HMAC key IS available.""" log_file = tmp_path / "log.json" - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") log_file.write_text( json.dumps({today: {"workout": "data"}}), ) diff --git a/screen_locker/tests/test_init_and_log_part3.py b/screen_locker/tests/test_init_and_log_part3.py index 2f2d2d3..15ba2a6 100644 --- a/screen_locker/tests/test_init_and_log_part3.py +++ b/screen_locker/tests/test_init_and_log_part3.py @@ -36,7 +36,7 @@ class TestSaveWorkoutLog: assert log_file.exists() with log_file.open() as f: data: dict[str, Any] = json.load(f) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") assert today in data # The log now holds a list of entries per day (multiple workouts/day). assert data[today][0]["workout_data"]["type"] == "running" @@ -61,7 +61,7 @@ class TestSaveWorkoutLog: with log_file.open() as f: data: dict[str, Any] = json.load(f) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") assert "hmac" not in data[today][0] def test_save_to_existing_file( @@ -86,7 +86,7 @@ class TestSaveWorkoutLog: with log_file.open() as f: data: dict[str, Any] = json.load(f) assert "2020-01-01" in data - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") assert today in data def test_save_with_corrupted_existing_file( @@ -110,7 +110,7 @@ class TestSaveWorkoutLog: with log_file.open() as f: data: dict[str, Any] = json.load(f) - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") assert today in data def test_save_with_write_error( diff --git a/screen_locker/tests/test_manual_workout_date.py b/screen_locker/tests/test_manual_workout_date.py index 1a0b1ab..f857ffe 100644 --- a/screen_locker/tests/test_manual_workout_date.py +++ b/screen_locker/tests/test_manual_workout_date.py @@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch from screen_locker._day import today_str from screen_locker._manual_schedule import ScheduleResult +from screen_locker._manual_workout_date import BACK_GLYPH, SCHEDULED_GLYPH from screen_locker.tests import test_manual_workout_dialog_part2 as dialog_tests from screen_locker.tests.conftest import create_locker @@ -139,12 +140,12 @@ class TestScheduledConfirmation: buttons = { c.kwargs.get("text"): c.kwargs for c in mock_tk.Button.call_args_list } - assert set(buttons) == {"OK"} - buttons["OK"]["command"]() + assert set(buttons) == {BACK_GLYPH} + buttons[BACK_GLYPH]["command"]() cancelled.assert_called_once_with() saved.assert_not_called() texts = [c.kwargs.get("text", "") for c in mock_tk.Label.call_args_list] - assert any("Workout Scheduled" in t for t in texts) + assert SCHEDULED_GLYPH in texts assert any("scheduled for tomorrow" in t for t in texts) diff --git a/screen_locker/tests/test_manual_workout_dialog.py b/screen_locker/tests/test_manual_workout_dialog.py index f50843f..19eeca9 100644 --- a/screen_locker/tests/test_manual_workout_dialog.py +++ b/screen_locker/tests/test_manual_workout_dialog.py @@ -6,6 +6,8 @@ from typing import TYPE_CHECKING from unittest.mock import MagicMock, patch from screen_locker import _manual_workout +from screen_locker._manual_workout_date import BACK_GLYPH +from screen_locker._manual_workout_dialog import EXHAUSTED_GLYPH, SUBMIT_GLYPH from screen_locker.tests.conftest import create_locker if TYPE_CHECKING: @@ -25,8 +27,8 @@ class TestShowManualWorkoutForm: with patch(_CLOSED, return_value=False): locker._show_manual_workout_form() button_texts = {c.kwargs.get("text") for c in mock_tk.Button.call_args_list} - assert "SUBMIT" in button_texts - assert "BACK" in button_texts + assert SUBMIT_GLYPH in button_texts + assert BACK_GLYPH in button_texts def test_shows_exhausted_message_and_hides_submit( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path @@ -36,10 +38,10 @@ class TestShowManualWorkoutForm: with patch(_CLOSED, return_value=True): locker._show_manual_workout_form() button_texts = {c.kwargs.get("text") for c in mock_tk.Button.call_args_list} - assert "BACK" in button_texts - assert "SUBMIT" not in button_texts + assert BACK_GLYPH in button_texts + assert SUBMIT_GLYPH not in button_texts text_calls = [c.kwargs.get("text", "") for c in mock_tk.Label.call_args_list] - assert any("exhausted" in t for t in text_calls) + assert EXHAUSTED_GLYPH in text_calls class TestSportToggle: diff --git a/screen_locker/tests/test_screen_lock_coverage_part3.py b/screen_locker/tests/test_screen_lock_coverage_part3.py index 380029f..54bec71 100644 --- a/screen_locker/tests/test_screen_lock_coverage_part3.py +++ b/screen_locker/tests/test_screen_lock_coverage_part3.py @@ -64,7 +64,7 @@ class TestTryAutoUpgradeEarlyBirdRunnerUp: result = locker._try_auto_upgrade_early_bird() assert result is True - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") data = json.loads(log_file.read_text()) assert data[today][0]["workout_data"]["type"] == "runnerup_verified" assert data[today][0]["workout_data"]["after_early_bird"] == "true" diff --git a/screen_locker/tests/test_shutdown_base.py b/screen_locker/tests/test_shutdown_base.py index 800b7b2..aaff683 100644 --- a/screen_locker/tests/test_shutdown_base.py +++ b/screen_locker/tests/test_shutdown_base.py @@ -78,7 +78,7 @@ class TestResetToBaseIfNewDay: def test_returns_false_when_already_reset_today(self, tmp_path: Path) -> None: """Same-day last_reset_date → early False return (line 63).""" - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") f = tmp_path / "state.json" f.write_text(json.dumps({"last_reset_date": today})) assert reset_to_base_if_new_day(f, self._make_mixin()) is False diff --git a/screen_locker/tests/test_weekly_logic_part5.py b/screen_locker/tests/test_weekly_logic_part5.py index 3a32a18..6d9d0ef 100644 --- a/screen_locker/tests/test_weekly_logic_part5.py +++ b/screen_locker/tests/test_weekly_logic_part5.py @@ -34,7 +34,7 @@ class TestRelaxedDayAlreadySkippedBranch: # script_dir.parent/"log.json" resolves to tmp_path/"log.json" -- NOT # tmp_path/"screen_locker"/"log.json". log_file = tmp_path / "log.json" - today = datetime.now(tz=UTC).strftime("%Y-%m-%d") + today = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d") write_signed_entry(log_file, today, {"type": "relaxed_day_skip"}) with ( diff --git a/scripts/demo_workout_handoff.py b/scripts/demo_workout_handoff.py index 40c5f2d..ae6ed06 100644 --- a/scripts/demo_workout_handoff.py +++ b/scripts/demo_workout_handoff.py @@ -56,19 +56,21 @@ class _DemoHooks: def build_surface(self, parent: tk.Misc, _surface: object) -> None: """Draw the status line and the release/reacquire buttons.""" + # textgate: glyphs only, colours as the second channel. Was "DEMO + # LOCK — local grab, VT switching still enabled" / "Press “Start + # workout”. While the app holds the screen, press Ctrl+Shift+Q to + # escape." / "Start workout" / "✕ Close demo (or press Escape)". + # The escape chord stays visible: it is what the user must press. tk.Label( parent, - text="DEMO LOCK — local grab, VT switching still enabled", + text="▣", font=("TkDefaultFont", 18, "bold"), bg="#1B1D21", fg="#E6E6E6", ).pack(pady=(120, 8)) self.status = tk.Label( parent, - text=( - "Press “Start workout”. While the app holds the screen, " - "press Ctrl+Shift+Q to escape." - ), + text="Ctrl+Shift+Q", font=("TkDefaultFont", 12), bg="#1B1D21", fg="#9AA0A6", @@ -76,7 +78,7 @@ class _DemoHooks: self.status.pack(pady=8) tk.Button( parent, - text="Start workout", + text="▶", font=("TkDefaultFont", 14, "bold"), bg="#3B82F6", fg="#FFFFFF", @@ -86,7 +88,7 @@ class _DemoHooks: ).pack(pady=16) tk.Button( parent, - text="✕ Close demo (or press Escape)", + text="✕", bg="#EF4444", fg="#FFFFFF", command=self._close, diff --git a/stronglift_replacement/workout_app/lib/main.dart b/stronglift_replacement/workout_app/lib/main.dart index 9532768..7bf7a71 100644 --- a/stronglift_replacement/workout_app/lib/main.dart +++ b/stronglift_replacement/workout_app/lib/main.dart @@ -192,6 +192,7 @@ class _WorkoutAppState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { return MaterialApp( + // Same proper noun as BreakNotificationText.channelName. title: 'Workout Tracker', debugShowCheckedModeBanner: false, theme: buildAppTheme(), @@ -200,7 +201,8 @@ class _WorkoutAppState extends State with WidgetsBindingObserver { // ribbon on every screen, in the error colour, on the sandbox only. builder: (context, child) => Sandbox.enabled ? Banner( - message: 'SANDBOX', + // Was 'SANDBOX': the red corner ribbon alone marks it. + message: '', location: BannerLocation.topEnd, color: Theme.of(context).colorScheme.error, child: child, diff --git a/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart index b240ba8..11f7b38 100644 --- a/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart @@ -224,10 +224,13 @@ class _GitHubMirrorScreenState extends State { ], Align( alignment: Alignment.centerLeft, - child: ElevatedButton.icon( + child: ElevatedButton( onPressed: _connectGitHub, - icon: const Icon(Icons.login), - label: const Text('Connect GitHub'), + child: const Icon( + Icons.login, + size: 20, + semanticLabel: 'Connect GitHub', + ), ), ), const SizedBox(height: 12), diff --git a/stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart index 7609736..68c7c95 100644 --- a/stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart +++ b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart @@ -80,17 +80,23 @@ class _SyncTokenField extends StatelessWidget { // inputDecorationTheme (theme.dart) — only the field-specific // hint/padding need setting here. decoration: InputDecoration( - hintText: 'GitHub PAT', hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), + prefixIcon: const Icon( + Icons.vpn_key, + semanticLabel: 'GitHub PAT', + ), ), ), ), const SizedBox(width: 8), - ElevatedButton(onPressed: onSave, child: const Text('Save')), + ElevatedButton( + onPressed: onSave, + child: const Icon(Icons.save, semanticLabel: 'Save'), + ), ], ); } @@ -140,19 +146,17 @@ class _DeviceCodeDialogState extends State<_DeviceCodeDialog> { final colorScheme = Theme.of(context).colorScheme; return AlertDialog( backgroundColor: colorScheme.surfaceContainerHigh, - title: Text( - 'Authorize on GitHub', - style: TextStyle(color: colorScheme.onSurface), + // Was "Authorize on GitHub" / "Enter this code on GitHub:" / + // "Waiting for authorization…": key glyph, the code, a spinner. + title: Icon( + Icons.vpn_key, + color: colorScheme.onSurface, + semanticLabel: 'Authorize on GitHub', ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Enter this code on GitHub:', - style: TextStyle(color: colorScheme.onSurfaceVariant), - ), - const SizedBox(height: 8), SelectableText( widget.device.userCode, style: TextStyle( @@ -163,21 +167,13 @@ class _DeviceCodeDialogState extends State<_DeviceCodeDialog> { ), const SizedBox(height: 16), if (_error == null) - Row( - children: [ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Waiting for authorization…', - style: TextStyle(color: colorScheme.onSurfaceVariant), - ), - ), - ], + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + semanticsLabel: 'Waiting for authorization…', + ), ) else Text(_error!, style: TextStyle(color: colorScheme.error)), @@ -186,15 +182,19 @@ class _DeviceCodeDialogState extends State<_DeviceCodeDialog> { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text( - 'Cancel', - style: TextStyle(color: colorScheme.onSurfaceVariant), + child: Icon( + Icons.close, + color: colorScheme.onSurfaceVariant, + semanticLabel: 'Cancel', ), ), - FilledButton.icon( + // Was "Open GitHub & copy code". + FilledButton( onPressed: _openPage, - icon: const Icon(Icons.open_in_new), - label: const Text('Open GitHub & copy code'), + child: const Icon( + Icons.open_in_new, + semanticLabel: 'Open GitHub & copy code', + ), ), ], ); diff --git a/stronglift_replacement/workout_app/lib/screens/history_screen.dart b/stronglift_replacement/workout_app/lib/screens/history_screen.dart index d1f940c..f702d6f 100644 --- a/stronglift_replacement/workout_app/lib/screens/history_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/history_screen.dart @@ -6,6 +6,7 @@ library; import 'dart:async'; import 'dart:convert'; +import 'dart:developer' as developer; import 'dart:math'; import 'package:crdt_sync/crdt_sync.dart'; import 'package:flutter/material.dart'; @@ -161,7 +162,7 @@ class _HistoryScreenState extends State { return Scaffold( appBar: AppBar( backgroundColor: colorScheme.surfaceContainerHigh, - title: Text('Progress', style: TextStyle(color: colorScheme.onSurface)), + title: const Icon(Icons.timeline, size: 20, semanticLabel: 'Progress'), iconTheme: IconThemeData(color: colorScheme.onSurface), ), body: _loading diff --git a/stronglift_replacement/workout_app/lib/screens/history_screen_planned.dart b/stronglift_replacement/workout_app/lib/screens/history_screen_planned.dart index 13ad9ce..7294f0c 100644 --- a/stronglift_replacement/workout_app/lib/screens/history_screen_planned.dart +++ b/stronglift_replacement/workout_app/lib/screens/history_screen_planned.dart @@ -40,19 +40,33 @@ extension _HistoryPlanned on _HistoryScreenState { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Delete planned workout?'), + // Was "Delete planned workout?" / " on at