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 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-05-05 07:23:34 +02:00
co-authored by Claude Sonnet 4.6
parent dad239e0b6
commit d4d61b2b39
2 changed files with 93 additions and 19 deletions
+21
View File
@@ -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<void> _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<void> _loop() async {
while (_running) {
final startedAt = DateTime.now();
+72 -19
View File
@@ -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<SyncLogScreen> createState() => _SyncLogScreenState();
}
class _SyncLogScreenState extends ConsumerState<SyncLogScreen> {
List<SyncLogEntry> _entries = [];
bool _syncing = false;
int? _presynCount;
StreamSubscription<List<SyncLogEntry>>? _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<List<SyncLogEntry>>(
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]),
),
);
}
}