From 47e05d1dca678a40d0429a99bb422b5d4abd9b68 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Tue, 18 Aug 2026 23:38:09 +0200 Subject: [PATCH] focus_owner: split status_page_state.dart under the 250-line cap Extracts the two AlertDialog builders and the build() body-selection logic into a new status_page_dialogs.dart part file, following the same part-of-main.dart split pattern status_body.dart already used. Pure extraction, no behavior change -- verified with dart analyze and the full flutter test suite (94/94 passing). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY --- ...us-owner-status-page-split-2026-08-18.json | 49 +++++++++ focus_owner/lib/main.dart | 2 + focus_owner/lib/status_page_dialogs.dart | 103 ++++++++++++++++++ focus_owner/lib/status_page_state.dart | 93 +++------------- 4 files changed, 171 insertions(+), 76 deletions(-) create mode 100644 docs/superpowers/evidence/focus-owner-status-page-split-2026-08-18.json create mode 100644 focus_owner/lib/status_page_dialogs.dart diff --git a/docs/superpowers/evidence/focus-owner-status-page-split-2026-08-18.json b/docs/superpowers/evidence/focus-owner-status-page-split-2026-08-18.json new file mode 100644 index 00000000..c63ddb86 --- /dev/null +++ b/docs/superpowers/evidence/focus-owner-status-page-split-2026-08-18.json @@ -0,0 +1,49 @@ +{ + "intent": "Phase 1 of NEXT_SESSION_PROMPT.md's 250-line-cap campaign: bring focus_owner/lib/status_page_state.dart (307 lines) under the cap.", + "scope": [ + "focus_owner/lib/status_page_state.dart", + "focus_owner/lib/status_page_dialogs.dart (new)", + "focus_owner/lib/main.dart" + ], + "changes": [ + "Extracted the two AlertDialog builders (_lockVpnDialog, _releaseDialog) out of _lockVpn/_release into standalone functions in a new status_page_dialogs.dart part file", + "Extracted the build() method's body-selection if/else chain into a standalone _statusPageBody() helper in the same new part file", + "Registered the new part file in main.dart alongside the existing status_page_state.dart and status_body.dart parts, following the exact pattern already established by that prior split", + "No behavioral change: all extracted code moved verbatim (dialog content, button labels, callback wiring, hasAccounts branching) with no logic changes" + ], + "verification": [ + { + "command": "bash meta/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "status_page_state.dart now 248 lines, status_page_dialogs.dart 103 lines; file count over cap dropped from 11 to 10" + }, + { + "command": "dart analyze lib/", + "result": "pass", + "evidence": "No issues found!" + }, + { + "command": "flutter test test/status_page_test.dart test/status_page_actions_test.dart", + "result": "pass", + "evidence": "8/8 tests passed, including the release-confirmation dialog and lock-VPN dialog paths driven through the rendered widget tree" + }, + { + "command": "flutter test (full suite)", + "result": "pass", + "evidence": "94/94 tests passed, no regressions elsewhere in focus_owner" + }, + { + "command": "dart format focus_owner/lib/main.dart focus_owner/lib/status_page_state.dart focus_owner/lib/status_page_dialogs.dart", + "result": "pass", + "evidence": "Formatted 3 files (0 changed) -- already correctly formatted" + } + ], + "risks": [ + "focus_owner has no dedicated CI workflow or coverage gate (confirmed: no .github/workflows/*focus_owner* file), so this split relies entirely on the local flutter test run performed this session, not an automated gate", + "No new tests were added since this is a pure verbatim extraction already covered by existing widget tests that drive the dialogs through the rendered UI -- if that coverage assumption is wrong, a future regression in these dialogs would not be caught by unit tests alone" + ], + "rollback": [ + "git revert this commit", + "After rollback, re-run flutter test to confirm the pre-split test count (94) still passes" + ] +} diff --git a/focus_owner/lib/main.dart b/focus_owner/lib/main.dart index 60278ca8..ec1f30d5 100644 --- a/focus_owner/lib/main.dart +++ b/focus_owner/lib/main.dart @@ -12,6 +12,8 @@ import 'package:focus_owner/theme.dart'; part 'status_page_state.dart'; +part 'status_page_dialogs.dart'; + part 'status_body.dart'; void main() => runApp(const FocusOwnerApp()); diff --git a/focus_owner/lib/status_page_dialogs.dart b/focus_owner/lib/status_page_dialogs.dart new file mode 100644 index 00000000..8bf225c9 --- /dev/null +++ b/focus_owner/lib/status_page_dialogs.dart @@ -0,0 +1,103 @@ +/// Confirmation dialogs and the body-selection helper for the StatusPage. +/// +/// Split out of status_page_state.dart to keep it under the 250-line cap. A +/// `part` because these builders are library-private, same as the rest of +/// the StatusPage split. + +part of 'main.dart'; + +/// Picks the StatusPage's body widget for the current load state. +Widget _statusPageBody({ + required DevicePolicyStatus? status, + required String? error, + required FocusPolicy? focusPolicy, + required String? policyError, + required bool busy, + required Future Function() onRelease, + required Future Function() onRunNow, + required Future Function() onSetHome, + required Future Function() onLockVpn, + required bool hasHome, + required List log, + required VoidCallback onOpenLog, +}) { + if (error != null) { + return const _Message(text: 'Could not read state', color: kDanger); + } + if (status != null) { + return _StatusBody( + status: status, + focusPolicy: focusPolicy, + policyError: policyError, + busy: busy, + onRelease: onRelease, + onRunNow: onRunNow, + onSetHome: onSetHome, + onLockVpn: onLockVpn, + hasHome: hasHome, + log: log, + onOpenLog: onOpenLog, + ); + } + return const Center(child: CircularProgressIndicator()); +} + +Widget _lockVpnDialog(BuildContext context) { + return AlertDialog( + backgroundColor: kSurface, + title: const Text('Lock VPN configuration?'), + content: const Text( + 'The always-on VPN can no longer be turned off from Settings. ' + 'Releasing device owner lifts this.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Lock'), + ), + ], + ); +} + +Widget _releaseDialog(BuildContext context, {required bool hasAccounts}) { + return AlertDialog( + backgroundColor: kSurface, + title: const Text('Release device owner?'), + // Deliberately concrete, and honest in both directions. "Restored only + // by a factory reset" is true but abstract, and this dialog is read in + // the moment the block is most inconvenient -- the price has to be + // legible right then, not inferred. While no account exists the price + // really is low, and overstating it there would teach the user to + // ignore the warning that matters later. + content: Text( + hasAccounts + ? 'Enforcement stops. YouTube comes back.\n\n' + 'Accounts exist on this device, so device owner can NEVER ' + 'be set again without a factory reset. That means ' + 're-pairing mBank, Revolut and inFakt over SMS, ' + 're-activating mObywatel, signing in to every account ' + 'again, and losing Signal history.\n\n' + 'This is the way out if something is broken. It is a bad ' + 'trade for wanting to watch a video.' + : 'Enforcement stops and YouTube comes back.\n\n' + 'No account exists yet, so device owner can be set again ' + 'straight afterwards without a wipe. This is the moment to ' + 'test the release path, before signing in.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + style: TextButton.styleFrom(foregroundColor: kDanger), + child: const Text('Release'), + ), + ], + ); +} diff --git a/focus_owner/lib/status_page_state.dart b/focus_owner/lib/status_page_state.dart index ac1f7a80..40894263 100644 --- a/focus_owner/lib/status_page_state.dart +++ b/focus_owner/lib/status_page_state.dart @@ -158,24 +158,7 @@ class _StatusPageState extends State { Future _lockVpn() async { final confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: kSurface, - title: const Text('Lock VPN configuration?'), - content: const Text( - 'The always-on VPN can no longer be turned off from Settings. ' - 'Releasing device owner lifts this.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('Lock'), - ), - ], - ), + builder: _lockVpnDialog, ); if (confirmed != true) return; @@ -194,44 +177,10 @@ class _StatusPageState extends State { } Future _release() async { + final hasAccounts = _status?.hasAccounts ?? true; final confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: kSurface, - title: const Text('Release device owner?'), - // Deliberately concrete, and honest in both directions. "Restored only - // by a factory reset" is true but abstract, and this dialog is read in - // the moment the block is most inconvenient -- the price has to be - // legible right then, not inferred. While no account exists the price - // really is low, and overstating it there would teach the user to - // ignore the warning that matters later. - content: Text( - _status?.hasAccounts ?? true - ? 'Enforcement stops. YouTube comes back.\n\n' - 'Accounts exist on this device, so device owner can NEVER ' - 'be set again without a factory reset. That means ' - 're-pairing mBank, Revolut and inFakt over SMS, ' - 're-activating mObywatel, signing in to every account ' - 'again, and losing Signal history.\n\n' - 'This is the way out if something is broken. It is a bad ' - 'trade for wanting to watch a video.' - : 'Enforcement stops and YouTube comes back.\n\n' - 'No account exists yet, so device owner can be set again ' - 'straight afterwards without a wipe. This is the moment to ' - 'test the release path, before signing in.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - style: TextButton.styleFrom(foregroundColor: kDanger), - child: const Text('Release'), - ), - ], - ), + builder: (context) => _releaseDialog(context, hasAccounts: hasAccounts), ); if (confirmed != true) return; @@ -259,28 +208,20 @@ class _StatusPageState extends State { @override Widget build(BuildContext context) { - final status = _status; - final error = _error; - final Widget body; - if (error != null) { - body = const _Message(text: 'Could not read state', color: kDanger); - } else if (status != null) { - body = _StatusBody( - status: status, - focusPolicy: _focusPolicy, - policyError: _policyError, - busy: _busy, - onRelease: _release, - onRunNow: _runNow, - onSetHome: _setHome, - onLockVpn: _lockVpn, - hasHome: _hasHome, - log: _log, - onOpenLog: _openLog, - ); - } else { - body = const Center(child: CircularProgressIndicator()); - } + final body = _statusPageBody( + status: _status, + error: _error, + focusPolicy: _focusPolicy, + policyError: _policyError, + busy: _busy, + onRelease: _release, + onRunNow: _runNow, + onSetHome: _setHome, + onLockVpn: _lockVpn, + hasHome: _hasHome, + log: _log, + onOpenLog: _openLog, + ); return Scaffold( appBar: AppBar(