todo-app/lib/ui/capture_sync_runner.dart
Krzysztof kuhy Rudnicki 5ea7ba7b73 Text-free list, editor, sync and image cues
Glyph + spoken label replaces every app-written string the guard
found: empty states, tile meta, sheet dates/fields, date presets,
template/section headings, status footer, snackbars, GitHub and
cloud-image results, upload/download badges, Material tooltips.
Guard tests (text_free_*_test) walk the painted tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:17:03 +01:00

111 lines
3.6 KiB
Dart

/// Runs a sync pass for the capture screen and reports its outcome.
///
/// Split out of `capture_screen.dart` for file size, and it de-duplicates the
/// two callers at the same time: the manual button and the background tick
/// ran byte-identical `runSync` blocks that differed only in whether they
/// show a snackbar and what they log.
///
/// Deliberately has no `BuildContext` and no `setState`: the screen decides
/// what to show, this decides what to do.
library;
import 'package:crdt_sync/crdt_sync.dart';
import 'package:http/http.dart' as http;
import 'package:todo/analytics/analytics_service.dart';
import 'package:todo/data/app_settings.dart';
import 'package:todo/data/note_repository.dart';
import 'package:todo/sync/run_sync.dart';
import 'package:todo/sync/sync_settings.dart';
import 'package:todo/ui/capture_status.dart';
/// What a sync attempt did, in the terms the capture screen cares about.
class CaptureSyncOutcome {
/// Creates an outcome.
const CaptureSyncOutcome({
required this.ok,
required this.detail,
this.reconciled,
this.degraded = false,
});
/// Whether the pass completed without throwing.
final bool ok;
/// A pass that completed but not fully: Firebase was not connected, or a
/// peer's file could not be read. Shown as a warning, not a plain success.
final bool degraded;
/// One-line summary: merged/skipped counts on success, the error otherwise.
final String detail;
/// Settings reconciled during the pass, when the pass succeeded.
final AppSettings? reconciled;
}
/// Runs sync passes against one repository and settings pair.
class CaptureSyncRunner {
/// Creates a runner over the screen's injected dependencies.
const CaptureSyncRunner({
required this.repository,
required this.status,
this.analytics,
this.httpClient,
this.firebaseFactory,
this.stateStore,
});
/// The store being synced.
final NoteRepository repository;
/// Where the outcome is recorded and persisted.
final SyncStatusStore status;
/// Optional analytics sink, passed straight through to [runSync].
final AnalyticsService? analytics;
/// Injected HTTP client, so tests run the real path without network.
final http.Client? httpClient;
/// Injected Firebase client factory, for the same reason.
final Future<FirebaseRestClient?> Function()? firebaseFactory;
/// Injected sync-state store.
final SyncStateStore? stateStore;
/// Runs one pass and records the outcome.
///
/// [live] is the screen's `mounted` at the moment of recording -- the pass
/// still finishes and still persists its outcome after the screen is gone,
/// it just must not write the notifier.
Future<CaptureSyncOutcome> run({
required SyncSettings settings,
required AppSettings appSettings,
required bool Function() live,
}) async {
try {
final result = await runSync(
repository,
settings,
appSettings: appSettings,
analytics: analytics,
httpClient: httpClient,
firebaseFactory: firebaseFactory,
stateStore: stateStore,
);
final detail = describeSyncResult(result.syncResult);
await status.record(ok: true, detail: detail, live: live());
final synced = result.syncResult;
return CaptureSyncOutcome(
ok: true,
detail: detail,
reconciled: result.appSettings,
degraded: !synced.firebaseConnected || synced.skippedFiles.isNotEmpty,
);
} on Exception catch (e) {
await status.record(ok: false, detail: '$e', live: live());
return CaptureSyncOutcome(ok: false, detail: '$e');
}
}
}