Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c1b0c2c0 |
@@ -99,4 +99,9 @@ abstract class EmailRepository {
|
||||
String accountId,
|
||||
String mailboxPath,
|
||||
);
|
||||
|
||||
/// Deletes all locally-cached email rows and pending changes for [accountId],
|
||||
/// while preserving EmailBodies so already-downloaded content is not lost.
|
||||
/// Also resets sync-state checkpoints so the next sync fetches everything fresh.
|
||||
Future<void> clearForResync(String accountId);
|
||||
}
|
||||
|
||||
@@ -8,4 +8,7 @@ abstract class MailboxRepository {
|
||||
|
||||
/// Returns the first mailbox with the given [role] for [accountId], or null.
|
||||
Future<Mailbox?> findMailboxByRole(String accountId, String role);
|
||||
|
||||
/// Deletes all locally-cached mailbox rows for [accountId].
|
||||
Future<void> clearForResync(String accountId);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,43 @@ class AccountSyncManager {
|
||||
void syncNow(String accountId) {
|
||||
_active[accountId]?.kick();
|
||||
}
|
||||
|
||||
/// Clears all locally-cached emails and mailboxes for [accountId], then
|
||||
/// immediately starts a fresh sync cycle. Use this as an escape hatch when
|
||||
/// the local DB is believed to be out of sync with the server.
|
||||
Future<void> forceResync(String accountId) async {
|
||||
_active.remove(accountId)?.stop();
|
||||
|
||||
await _emails.clearForResync(accountId);
|
||||
await _mailboxes.clearForResync(accountId);
|
||||
|
||||
final accounts = await _accounts.observeAccounts().first;
|
||||
final account = accounts.cast<Account?>().firstWhere(
|
||||
(a) => a?.id == accountId,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (account == null) return;
|
||||
|
||||
final loop = switch (account.type) {
|
||||
AccountType.imap => _AccountSync(
|
||||
account,
|
||||
_accounts,
|
||||
_mailboxes,
|
||||
_emails,
|
||||
_imapConnect,
|
||||
_syncLog,
|
||||
),
|
||||
AccountType.jmap => _JmapAccountSync(
|
||||
account,
|
||||
_mailboxes,
|
||||
_emails,
|
||||
_accounts,
|
||||
_syncLog,
|
||||
),
|
||||
};
|
||||
_active[accountId] = loop;
|
||||
loop.start();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared interface ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -2739,4 +2739,27 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
const PendingChangesCompanion(attempts: Value(0), lastError: Value(null)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {
|
||||
// Disable FK constraints so EmailBodies rows survive the emails deletion.
|
||||
// When emails are re-inserted after the next sync with the same IDs, the
|
||||
// cached body content will be reused without a network round-trip.
|
||||
await _db.customStatement('PRAGMA foreign_keys = OFF');
|
||||
try {
|
||||
await _db.transaction(() async {
|
||||
await (_db.delete(_db.emails)
|
||||
..where((t) => t.accountId.equals(accountId)))
|
||||
.go();
|
||||
await (_db.delete(_db.pendingChanges)
|
||||
..where((t) => t.accountId.equals(accountId)))
|
||||
.go();
|
||||
await (_db.delete(_db.syncStates)
|
||||
..where((t) => t.accountId.equals(accountId)))
|
||||
.go();
|
||||
});
|
||||
} finally {
|
||||
await _db.customStatement('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,4 +303,11 @@ class MailboxRepositoryImpl implements MailboxRepository {
|
||||
if (mb.isJunk) return 'junk';
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {
|
||||
await (_db.delete(_db.mailboxes)
|
||||
..where((t) => t.accountId.equals(accountId)))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
|
||||
bool _tryTesting = false;
|
||||
String? _tryOk;
|
||||
String? _tryErr;
|
||||
bool _resyncing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -170,6 +171,43 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _forceResync() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Force full sync?'),
|
||||
content: const Text(
|
||||
'This clears all locally-cached emails and mailboxes for this '
|
||||
'account and immediately re-downloads everything from the server. '
|
||||
'Previously viewed email content will not need to be re-downloaded.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Force sync'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
setState(() => _resyncing = true);
|
||||
try {
|
||||
await ref.read(syncManagerProvider).forceResync(widget.accountId);
|
||||
if (mounted) context.pop();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_resyncing = false;
|
||||
_errorMessage = 'Force sync failed: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
final password = _passwordCtrl.text.isNotEmpty ? _passwordCtrl.text : null;
|
||||
@@ -230,11 +268,9 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Edit account')),
|
||||
body: _loading
|
||||
body: _loading || _saving || _resyncing
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _saving
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _buildForm(),
|
||||
: _buildForm(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -350,6 +386,15 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton(onPressed: _save, child: const Text('Save')),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.sync_problem),
|
||||
label: const Text('Force full sync'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: _forceResync,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -96,6 +96,9 @@ class _FakeMailboxes implements MailboxRepository {
|
||||
@override
|
||||
Future<Mailbox?> findMailboxByRole(String accountId, String role) async =>
|
||||
null;
|
||||
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {}
|
||||
}
|
||||
|
||||
class _FakeEmails implements EmailRepository {
|
||||
@@ -191,6 +194,9 @@ class _FakeEmails implements EmailRepository {
|
||||
|
||||
@override
|
||||
Future<void> retryMutation(int id) async {}
|
||||
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {}
|
||||
}
|
||||
|
||||
class _FakeLogs implements SyncLogRepository {
|
||||
|
||||
@@ -98,6 +98,9 @@ class FakeEmailRepository implements EmailRepository {
|
||||
String mailboxPath,
|
||||
) async =>
|
||||
ReliabilityResult.healthy;
|
||||
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {}
|
||||
}
|
||||
|
||||
class _Log {
|
||||
@@ -148,4 +151,6 @@ class FakeMailboxRepositoryWithInbox implements MailboxRepository {
|
||||
Future<int> syncMailboxes(String id) async => 1;
|
||||
@override
|
||||
Future<Mailbox?> findMailboxByRole(String id, String role) async => null;
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,8 @@ class FakeMailboxRepository implements MailboxRepository {
|
||||
@override
|
||||
Future<Mailbox?> findMailboxByRole(String accountId, String role) async =>
|
||||
_mailboxes.where((m) => m.role == role).firstOrNull;
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {}
|
||||
}
|
||||
|
||||
class FakeEmailRepository implements EmailRepository {
|
||||
@@ -279,6 +281,9 @@ class FakeEmailRepository implements EmailRepository {
|
||||
|
||||
@override
|
||||
Future<void> retryMutation(int id) async {}
|
||||
|
||||
@override
|
||||
Future<void> clearForResync(String accountId) async {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user