From d4d61b2b3921c9c102c14abdbf17834709e67992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Tue, 5 May 2026 07:23:34 +0200 Subject: [PATCH] feat: sync-now button on sync log screen Adds a sync icon to the AppBar of the sync log screen. Tapping it wakes the account's idle/wait loop immediately and shows a spinner until the next log entry arrives. Co-Authored-By: Claude Sonnet 4.6 --- lib/core/sync/account_sync_manager.dart | 21 ++++++ lib/ui/screens/sync_log_screen.dart | 91 +++++++++++++++++++------ 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/lib/core/sync/account_sync_manager.dart b/lib/core/sync/account_sync_manager.dart index d3935fe..33c0d90 100644 --- a/lib/core/sync/account_sync_manager.dart +++ b/lib/core/sync/account_sync_manager.dart @@ -71,6 +71,12 @@ class AccountSyncManager { } _active.clear(); } + + /// Wakes the idle/wait phase of the given account's sync loop so a new + /// sync cycle starts immediately. No-op if the account is unknown. + void syncNow(String accountId) { + _active[accountId]?.kick(); + } } // ── Shared interface ────────────────────────────────────────────────────────── @@ -78,6 +84,7 @@ class AccountSyncManager { abstract class _SyncLoop { void start(); void stop(); + void kick(); } // ── IMAP ────────────────────────────────────────────────────────────────────── @@ -120,6 +127,13 @@ class _AccountSync implements _SyncLoop { _idleClient = null; } + @override + void kick() { + if (_stopSignal != null && !_stopSignal!.isCompleted) { + _stopSignal!.complete(); + } + } + Future _loop() async { while (_running) { final startedAt = DateTime.now(); @@ -295,6 +309,13 @@ class _JmapAccountSync implements _SyncLoop { } } + @override + void kick() { + if (_stopSignal != null && !_stopSignal!.isCompleted) { + _stopSignal!.complete(); + } + } + Future _loop() async { while (_running) { final startedAt = DateTime.now(); diff --git a/lib/ui/screens/sync_log_screen.dart b/lib/ui/screens/sync_log_screen.dart index b20158a..47c902a 100644 --- a/lib/ui/screens/sync_log_screen.dart +++ b/lib/ui/screens/sync_log_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; @@ -14,32 +16,83 @@ String _fmtBytes(int bytes) { return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } -class SyncLogScreen extends ConsumerWidget { +class SyncLogScreen extends ConsumerStatefulWidget { const SyncLogScreen({super.key, required this.accountId}); final String accountId; @override - Widget build(BuildContext context, WidgetRef ref) { - final repo = ref.watch(syncLogRepositoryProvider); + ConsumerState createState() => _SyncLogScreenState(); +} + +class _SyncLogScreenState extends ConsumerState { + List _entries = []; + bool _syncing = false; + int? _presynCount; + StreamSubscription>? _sub; + + @override + void initState() { + super.initState(); + _sub = ref + .read(syncLogRepositoryProvider) + .observeSyncLogs(widget.accountId) + .listen((entries) { + setState(() { + if (_syncing && + _presynCount != null && + entries.length > _presynCount!) { + _syncing = false; + _presynCount = null; + } + _entries = entries; + }); + }); + } + + @override + void dispose() { + unawaited(_sub?.cancel()); + super.dispose(); + } + + void _syncNow() { + setState(() { + _syncing = true; + _presynCount = _entries.length; + }); + ref.read(syncManagerProvider).syncNow(widget.accountId); + } + + @override + Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Sync log')), - body: StreamBuilder>( - stream: repo.observeSyncLogs(accountId), - builder: (ctx, snap) { - if (!snap.hasData) { - return const Center(child: CircularProgressIndicator()); - } - final entries = snap.data!; - if (entries.isEmpty) { - return const Center(child: Text('No sync entries yet')); - } - return ListView.builder( - itemCount: entries.length, - itemBuilder: (ctx, i) => _SyncLogTile(entry: entries[i]), - ); - }, + appBar: AppBar( + title: const Text('Sync log'), + actions: [ + if (_syncing) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + else + IconButton( + icon: const Icon(Icons.sync), + tooltip: 'Sync now', + onPressed: _syncNow, + ), + ], ), + body: _entries.isEmpty + ? const Center(child: Text('No sync entries yet')) + : ListView.builder( + itemCount: _entries.length, + itemBuilder: (ctx, i) => _SyncLogTile(entry: _entries[i]), + ), ); } }