mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-11 11:47:47 +01:00
739 -> 249 lines. As with note_editor, the ~600-line State class needed
decomposing rather than moving:
capture_draft.dart the note being captured and its write-through
to storage -- the model behind "every keystroke
is already persisted". Lazy row creation stays
here; the screen just logs that it happened.
capture_sync_runner.dart one sync pass, with no BuildContext. This also
de-duplicates the two callers: the manual button
and the background tick ran byte-identical
runSync blocks differing only in the snackbar
and the 'auto' log flag.
capture_status.dart SyncStatusStore (the persisted last outcome) and
describeSyncResult.
capture_app_bar.dart the app bar.
capture_meta_row.dart the priority/status pickers.
capture_status_footer.dart the two status lines.
capture_screen_{widget,sync}.dart
parts, for the same reason as note_editor's.
The mounted checks are preserved exactly, including the one in
SyncStatusStore.restore that must fire AFTER its await rather than before
(writing the notifier post-dispose is the bug it exists to prevent), which
is why it takes a stillLive callback instead of a bool.
Verified: 313 tests pass, analyze clean, every capture file at 100%
coverage. lib/ and bin/ are now down to three files over the cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.6 KiB
Dart
78 lines
2.6 KiB
Dart
/// The two status lines under the capture screen's editor.
|
|
///
|
|
/// Split out of `capture_screen.dart` for file size. Both are listeners over
|
|
/// notifiers the screen owns, so this rebuilds without the screen having to.
|
|
library;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:todo/ui/capture_status.dart';
|
|
|
|
/// A sync-failure line and a local-save line, each rebuilt independently.
|
|
class CaptureStatusFooter extends StatelessWidget {
|
|
/// Creates the footer over the screen's two status notifiers.
|
|
const CaptureStatusFooter({
|
|
required this.syncStatus,
|
|
required this.lastSavedAt,
|
|
required this.showSavedAt,
|
|
super.key,
|
|
});
|
|
|
|
/// The latest sync outcome; only failures are rendered.
|
|
final ValueListenable<SyncStatus?> syncStatus;
|
|
|
|
/// When the draft was last written locally, or null before the first save.
|
|
final ValueListenable<DateTime?> lastSavedAt;
|
|
|
|
/// Whether to show the local-save line at all (advanced mode only).
|
|
final bool showSavedAt;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Sync is automatic; routine "saved"/"synced" chatter is not worth
|
|
// showing. Only a sync failure is surfaced, since that is the one
|
|
// state the user might need to act on.
|
|
ValueListenableBuilder<SyncStatus?>(
|
|
valueListenable: syncStatus,
|
|
builder: (context, status, _) {
|
|
if (status == null || status.ok) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Text(
|
|
'Sync failed at ${formatSyncTime(status.time)} · '
|
|
'${status.detail}',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.error,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
if (showSavedAt)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: ValueListenableBuilder<DateTime?>(
|
|
valueListenable: lastSavedAt,
|
|
builder: (context, savedAt, _) => Text(
|
|
savedAt == null
|
|
? 'Autosaves as you type'
|
|
: 'Saved locally at ${formatSyncTime(savedAt)}',
|
|
style: theme.textTheme.bodySmall,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|