fix: format, analyze-fix and update mocks

This commit is contained in:
Thomas Güttler
2026-06-02 17:10:16 +02:00
parent 3520f161e3
commit 8ea8d71f42
84 changed files with 1972 additions and 2201 deletions
+4 -6
View File
@@ -153,12 +153,10 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
stream: _accountsStream,
builder: (context, accountSnapshot) {
final accounts = accountSnapshot.data ?? [];
final imapCount = accounts
.where((a) => a.type == AccountType.imap)
.length;
final jmapCount = accounts
.where((a) => a.type == AccountType.jmap)
.length;
final imapCount =
accounts.where((a) => a.type == AccountType.imap).length;
final jmapCount =
accounts.where((a) => a.type == AccountType.jmap).length;
return Scaffold(
appBar: AppBar(title: const Text('About')),
+15 -15
View File
@@ -209,24 +209,24 @@ class _AccountReceiveScreenState extends ConsumerState<AccountReceiveScreen> {
_Step.showingPubKey => _buildPubKeyView(context),
_Step.scanning => _buildScannerView(context),
_Step.importing => const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Importing accounts…'),
],
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Importing accounts…'),
],
),
),
),
_Step.done => const Center(
child: Icon(Icons.check_circle, size: 64, color: Colors.green),
),
_Step.error => Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Error: $_errorMessage'),
child: Icon(Icons.check_circle, size: 64, color: Colors.green),
),
_Step.error => Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Error: $_errorMessage'),
),
),
),
},
);
}
+6 -8
View File
@@ -117,10 +117,8 @@ class _AccountSendScreenState extends ConsumerState<AccountSendScreen> {
}
// Load all available accounts.
final accounts = await ref
.read(accountRepositoryProvider)
.observeAccounts()
.first;
final accounts =
await ref.read(accountRepositoryProvider).observeAccounts().first;
if (!mounted) return;
@@ -197,11 +195,11 @@ class _AccountSendScreenState extends ConsumerState<AccountSendScreen> {
_Step.selectAccounts => _buildSelectStep(context),
_Step.showEncrypted => _buildEncryptedQrStep(context),
_Step.error => Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Error: $_errorMessage'),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Error: $_errorMessage'),
),
),
),
},
);
}
+14 -15
View File
@@ -94,12 +94,12 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
_jmapApiUrlCtrl.text = sessionUrl;
setState(() => _step = _Step.jmapForm);
case ImapSmtpDiscovery(
:final imapHost,
:final imapPort,
:final smtpHost,
:final smtpPort,
:final smtpSsl,
):
:final imapHost,
:final imapPort,
:final smtpHost,
:final smtpPort,
:final smtpSsl,
):
_imapHostCtrl.text = imapHost;
_imapPortCtrl.text = imapPort.toString();
_smtpHostCtrl.text = smtpHost;
@@ -116,13 +116,13 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
}
Account _buildJmapAccount() => Account(
id: DateTime.now().millisecondsSinceEpoch.toString(),
displayName: _displayNameCtrl.text.trim(),
email: _emailCtrl.text.trim(),
username: _usernameCtrl.text.trim(),
type: AccountType.jmap,
jmapUrl: _jmapApiUrlCtrl.text.trim(),
);
id: DateTime.now().millisecondsSinceEpoch.toString(),
displayName: _displayNameCtrl.text.trim(),
email: _emailCtrl.text.trim(),
username: _usernameCtrl.text.trim(),
type: AccountType.jmap,
jmapUrl: _jmapApiUrlCtrl.text.trim(),
);
Account _buildImapAccount() {
final imapHost = _imapHostCtrl.text.trim();
@@ -494,8 +494,7 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
labelText: label,
border: const OutlineInputBorder(),
),
validator:
validator ??
validator: validator ??
(required
? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null
: null),
+32 -31
View File
@@ -51,37 +51,38 @@ class _AddressEmailsScreenState extends ConsumerState<AddressEmailsScreen> {
body: _loading
? const Center(child: CircularProgressIndicator())
: _emails!.isEmpty
? const Center(child: Text('No emails'))
: ListView.builder(
itemCount: _emails!.length,
itemBuilder: (ctx, i) {
final e = _emails![i];
final sender = e.from.isNotEmpty
? (e.from.first.name ?? e.from.first.email)
: '(unknown)';
return ListTile(
leading: Icon(
e.isSeen ? Icons.mail_outline : Icons.mail,
color: e.isSeen ? null : Theme.of(ctx).colorScheme.primary,
),
title: Text(sender),
subtitle: Text(
e.subject ?? '(no subject)',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
e.mailboxPath,
style: Theme.of(ctx).textTheme.bodySmall,
),
onTap: () => context.push(
'/accounts/${widget.accountId}/mailboxes'
'/${Uri.encodeComponent(e.mailboxPath)}'
'/emails/${Uri.encodeComponent(e.id)}',
),
);
},
),
? const Center(child: Text('No emails'))
: ListView.builder(
itemCount: _emails!.length,
itemBuilder: (ctx, i) {
final e = _emails![i];
final sender = e.from.isNotEmpty
? (e.from.first.name ?? e.from.first.email)
: '(unknown)';
return ListTile(
leading: Icon(
e.isSeen ? Icons.mail_outline : Icons.mail,
color:
e.isSeen ? null : Theme.of(ctx).colorScheme.primary,
),
title: Text(sender),
subtitle: Text(
e.subject ?? '(no subject)',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
e.mailboxPath,
style: Theme.of(ctx).textTheme.bodySmall,
),
onTap: () => context.push(
'/accounts/${widget.accountId}/mailboxes'
'/${Uri.encodeComponent(e.mailboxPath)}'
'/emails/${Uri.encodeComponent(e.id)}',
),
);
},
),
);
}
}
+7 -12
View File
@@ -70,8 +70,7 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
unawaited(_loadAccounts());
// Only restore if no prefill fields were provided (avoids overwriting a
// fresh reply with an old draft from a previous reply to the same email).
final hasPrefill =
widget.prefillTo != null ||
final hasPrefill = widget.prefillTo != null ||
widget.prefillSubject != null ||
widget.prefillBody != null;
if (!hasPrefill) unawaited(_restoreDraft());
@@ -82,10 +81,8 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
}
Future<void> _loadAccounts() async {
final accounts = await ref
.read(accountRepositoryProvider)
.observeAccounts()
.first;
final accounts =
await ref.read(accountRepositoryProvider).observeAccounts().first;
if (!mounted) return;
setState(() {
_accounts = accounts;
@@ -224,9 +221,8 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
}
setState(() => _sending = true);
try {
final account = (await ref
.read(accountRepositoryProvider)
.getAccount(_accountId!))!;
final account =
(await ref.read(accountRepositoryProvider).getAccount(_accountId!))!;
final draft = EmailDraft(
from: EmailAddress(name: account.displayName, email: account.email),
to: _to.text
@@ -399,9 +395,8 @@ class _ComposeScreenState extends ConsumerState<ComposeScreen> {
displayStringForOption: (option) {
final text = ctrl.text;
final lastComma = text.lastIndexOf(',');
final prefix = lastComma >= 0
? '${text.substring(0, lastComma + 1)} '
: '';
final prefix =
lastComma >= 0 ? '${text.substring(0, lastComma + 1)} ' : '';
return '$prefix${option.email}, ';
},
optionsBuilder: (value) async {
+8 -12
View File
@@ -117,8 +117,7 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
int.tryParse(_sievePortCtrl.text) ?? account.manageSievePort;
// Reset the cached probe result when any field that affects the probe
// changed; the post-save probe will refill it.
final sieveSettingsChanged =
imapHost != account.imapHost ||
final sieveSettingsChanged = imapHost != account.imapHost ||
sieveHost != account.manageSieveHost ||
sievePort != account.manageSievePort ||
_sieveSsl != account.manageSieveSsl;
@@ -139,12 +138,10 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
manageSieveHost: sieveHost,
manageSievePort: sievePort,
manageSieveSsl: isLocalhost(effectiveSieveHost) ? _sieveSsl : true,
manageSieveAvailable: sieveSettingsChanged
? null
: account.manageSieveAvailable,
jmapUrl: _jmapUrlCtrl.text.trim().isEmpty
? null
: _jmapUrlCtrl.text.trim(),
manageSieveAvailable:
sieveSettingsChanged ? null : account.manageSieveAvailable,
jmapUrl:
_jmapUrlCtrl.text.trim().isEmpty ? null : _jmapUrlCtrl.text.trim(),
verbose: _verbose,
);
}
@@ -154,8 +151,8 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
final password = _passwordCtrl.text.isNotEmpty
? _passwordCtrl.text
: await ref
.read(accountRepositoryProvider)
.getPassword(widget.accountId);
.read(accountRepositoryProvider)
.getPassword(widget.accountId);
setState(() {
_tryTesting = true;
_tryOk = null;
@@ -395,8 +392,7 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
labelText: label,
border: const OutlineInputBorder(),
),
validator:
validator ??
validator: validator ??
(required
? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null
: null),
+17 -30
View File
@@ -55,8 +55,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
final header = detail.value?.$1;
final body = detail.value?.$2;
final isMobile =
defaultTargetPlatform == TargetPlatform.android ||
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS;
return Scaffold(
@@ -94,9 +93,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
if (header != null) {
unawaited(
ref
.read(undoServiceProvider.notifier)
.pushAction(
ref.read(undoServiceProvider.notifier).pushAction(
UndoAction(
id: DateTime.now().toIso8601String(),
accountId: header.accountId,
@@ -324,9 +321,8 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
Future<String> _quotedBody(Email header, EmailBody? body) async {
final date = header.sentAt != null ? _dateFmt.format(header.sentAt!) : '';
final from = header.from.isNotEmpty
? header.from.first.toString()
: '(unknown)';
final from =
header.from.isNotEmpty ? header.from.first.toString() : '(unknown)';
final rawText = body?.textBody;
final text = (rawText != null && rawText.isNotEmpty)
? rawText
@@ -340,9 +336,8 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
Email header,
EmailBody? body,
) async {
final account = await ref
.read(accountRepositoryProvider)
.getAccount(header.accountId);
final account =
await ref.read(accountRepositoryProvider).getAccount(header.accountId);
final ownEmail = account?.email.toLowerCase() ?? '';
final seen = <String>{};
@@ -445,9 +440,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
.moveEmail(widget.emailId, mailbox.path);
unawaited(
ref
.read(undoServiceProvider.notifier)
.pushAction(
ref.read(undoServiceProvider.notifier).pushAction(
UndoAction(
id: DateTime.now().toIso8601String(),
accountId: header.accountId,
@@ -483,9 +476,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
.moveEmail(widget.emailId, mailbox.path);
unawaited(
ref
.read(undoServiceProvider.notifier)
.pushAction(
ref.read(undoServiceProvider.notifier).pushAction(
UndoAction(
id: DateTime.now().toIso8601String(),
accountId: header.accountId,
@@ -522,14 +513,12 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
final nextEmailId = await _getNextEmailIdIfNeeded(header);
final mailboxRepo = ref.read(mailboxRepositoryProvider);
final mailboxes = await mailboxRepo
.observeMailboxes(header.accountId)
.first;
final mailboxes =
await mailboxRepo.observeMailboxes(header.accountId).first;
// Remove the current mailbox from the list.
final destinations = mailboxes
.where((m) => m.path != header.mailboxPath)
.toList();
final destinations =
mailboxes.where((m) => m.path != header.mailboxPath).toList();
if (!context.mounted) return;
@@ -559,9 +548,7 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
await ref.read(emailRepositoryProvider).moveEmail(widget.emailId, chosen);
unawaited(
ref
.read(undoServiceProvider.notifier)
.pushAction(
ref.read(undoServiceProvider.notifier).pushAction(
UndoAction(
id: DateTime.now().toIso8601String(),
accountId: header.accountId,
@@ -641,8 +628,8 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
Text(
fmtSize(raw.length),
style: Theme.of(ctx).textTheme.bodySmall?.copyWith(
color: Theme.of(ctx).colorScheme.outline,
),
color: Theme.of(ctx).colorScheme.outline,
),
),
const SizedBox(height: 4),
Flexible(
@@ -822,8 +809,8 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
child: Text(
row.label,
style: Theme.of(ctx).textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
),
fontFamily: 'monospace',
),
),
),
],
+46 -42
View File
@@ -92,9 +92,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
}
void _clearSelection() => setState(() {
_selectedThreadIds.clear();
_selectedSearchIds.clear();
});
_selectedThreadIds.clear();
_selectedSearchIds.clear();
});
void _selectAll() {
setState(() {
@@ -182,9 +182,8 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
AsyncValue<Account?> accountAsync, {
required bool menuAtBottom,
}) {
final selectionCount = _searching
? _selectedSearchIds.length
: _selectedThreadIds.length;
final selectionCount =
_searching ? _selectedSearchIds.length : _selectedThreadIds.length;
return AppBar(
automaticallyImplyLeading: !menuAtBottom,
@@ -278,8 +277,8 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
tooltip: isSyncing
? 'Syncing…'
: hasError
? 'Sync error'
: 'Sync',
? 'Sync error'
: 'Sync',
icon: isSyncing
? const SizedBox(
width: 20,
@@ -287,8 +286,8 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: hasError
? const Icon(Icons.sync_problem, color: Colors.red)
: const Icon(Icons.sync),
? const Icon(Icons.sync_problem, color: Colors.red)
: const Icon(Icons.sync),
onPressed: isSyncing
? null
: () async {
@@ -466,7 +465,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
// Fetch full email data before moving so we can restore them if user clicks Undo.
final originalEmails = (await Future.wait(
ids.map((id) => repo.getEmail(id)),
)).whereType<Email>().toList();
))
.whereType<Email>()
.toList();
for (final id in ids) {
await repo.moveEmail(id, mailbox.path);
@@ -485,10 +486,10 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
}
Future<void> _batchArchive() => _batchMoveToRole(
'archive',
dialogTitle: 'No archive folder found',
createFolderName: 'Archive',
);
'archive',
dialogTitle: 'No archive folder found',
createFolderName: 'Archive',
);
Future<void> _refreshSearchAndPopIfEmpty() async {
if (!mounted || !_searching) return;
@@ -527,7 +528,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
// This is especially important for IMAP where we hard-delete the row locally.
final originalEmails = (await Future.wait(
ids.map((id) => repo.getEmail(id)),
)).whereType<Email>().toList();
))
.whereType<Email>()
.toList();
String? lastDestPath;
for (final id in ids) {
@@ -566,10 +569,10 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
}
Future<void> _batchMarkSpam() => _batchMoveToRole(
'junk',
dialogTitle: 'No spam folder found',
createFolderName: 'Junk',
);
'junk',
dialogTitle: 'No spam folder found',
createFolderName: 'Junk',
);
Future<void> _batchMove() async {
final ids = _selectedEmailIds;
@@ -577,9 +580,8 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
.read(mailboxRepositoryProvider)
.observeMailboxes(widget.accountId)
.first;
final destinations = mailboxes
.where((m) => m.path != widget.mailboxPath)
.toList();
final destinations =
mailboxes.where((m) => m.path != widget.mailboxPath).toList();
if (!mounted) return;
@@ -611,7 +613,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
// Fetch full email data before moving so we can restore them if user clicks Undo.
final originalEmails = (await Future.wait(
ids.map((id) => repo.getEmail(id)),
)).whereType<Email>().toList();
))
.whereType<Email>()
.toList();
for (final id in ids) {
await repo.moveEmail(id, chosen);
@@ -642,7 +646,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
// Fetch full email data before snoozing so we can restore them if user clicks Undo.
final originalEmails = (await Future.wait(
ids.map((id) => repo.getEmail(id)),
)).whereType<Email>().toList();
))
.whereType<Email>()
.toList();
for (final id in ids) {
await repo.snoozeEmail(id, until);
@@ -683,10 +689,8 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
}
final t = threads[i];
final isSelected = _selectedThreadIds.contains(t.threadId);
final senderNames = t.participants
.map((a) => a.name ?? a.email)
.take(3)
.join(', ');
final senderNames =
t.participants.map((a) => a.name ?? a.email).take(3).join(', ');
final tile = ListTile(
leading: SizedBox(
@@ -698,9 +702,8 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
)
: Icon(
t.hasUnread ? Icons.mail : Icons.mail_outline,
color: t.hasUnread
? Theme.of(ctx).colorScheme.primary
: null,
color:
t.hasUnread ? Theme.of(ctx).colorScheme.primary : null,
),
),
title: Row(
@@ -760,12 +763,12 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
onTap: _selecting
? () => _toggleThreadSelection(t)
: t.messageCount > 1
? () => context.push(
'/accounts/${widget.accountId}/mailboxes/${Uri.encodeComponent(widget.mailboxPath)}/threads/${Uri.encodeComponent(t.threadId)}',
)
: () => context.push(
'/accounts/${widget.accountId}/mailboxes/${Uri.encodeComponent(widget.mailboxPath)}/emails/${Uri.encodeComponent(t.latestEmailId)}',
),
? () => context.push(
'/accounts/${widget.accountId}/mailboxes/${Uri.encodeComponent(widget.mailboxPath)}/threads/${Uri.encodeComponent(t.threadId)}',
)
: () => context.push(
'/accounts/${widget.accountId}/mailboxes/${Uri.encodeComponent(widget.mailboxPath)}/emails/${Uri.encodeComponent(t.latestEmailId)}',
),
onLongPress: () => _toggleThreadSelection(t),
);
@@ -773,9 +776,8 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
// (single-email threads) or the whole thread.
return Dismissible(
key: ValueKey(t.threadId),
direction: _selecting
? DismissDirection.none
: DismissDirection.horizontal,
direction:
_selecting ? DismissDirection.none : DismissDirection.horizontal,
background: _swipeBackground(
alignment: Alignment.centerLeft,
color: Colors.green,
@@ -797,7 +799,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
// Fetch full email data before moving/deleting.
final originalEmails = (await Future.wait(
t.emailIds.map((id) => repo.getEmail(id)),
)).whereType<Email>().toList();
))
.whereType<Email>()
.toList();
if (direction == DismissDirection.startToEnd) {
final archive = await ref
+6 -6
View File
@@ -84,9 +84,10 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
emailRepo.getEmailsByAddress(widget.accountId, query),
).wait;
final matchedMailboxes =
allMailboxes.where((m) => _hasWordPrefix(m.name, ql)).toList()
..sort(compareMailboxes);
final matchedMailboxes = allMailboxes
.where((m) => _hasWordPrefix(m.name, ql))
.toList()
..sort(compareMailboxes);
// Collect unique addresses from address-search results where the
// email or display name contains the query.
@@ -306,9 +307,8 @@ class _FolderTile extends StatelessWidget {
: null,
),
subtitle: Text(accountId, style: Theme.of(context).textTheme.bodySmall),
trailing: mb.unreadCount > 0
? Badge(label: Text('${mb.unreadCount}'))
: null,
trailing:
mb.unreadCount > 0 ? Badge(label: Text('${mb.unreadCount}')) : null,
onTap: () => context.go(
'/accounts/$accountId/mailboxes'
'/${Uri.encodeComponent(mb.path)}/emails',
+6 -10
View File
@@ -56,11 +56,11 @@ class _SieveScriptEditScreenState extends ConsumerState<SieveScriptEditScreen> {
try {
final content = widget.isLocal
? await ref
.read(localSieveRepositoryProvider)
.getScriptContent(widget.accountId, widget.script!.blobId)
.read(localSieveRepositoryProvider)
.getScriptContent(widget.accountId, widget.script!.blobId)
: await ref
.read(sieveRepositoryProvider)
.getScriptContent(widget.accountId, widget.script!.blobId);
.read(sieveRepositoryProvider)
.getScriptContent(widget.accountId, widget.script!.blobId);
if (mounted) {
_contentController.text = content;
setState(() => _loadingContent = false);
@@ -87,18 +87,14 @@ class _SieveScriptEditScreenState extends ConsumerState<SieveScriptEditScreen> {
});
try {
if (widget.isLocal) {
await ref
.read(localSieveRepositoryProvider)
.saveScript(
await ref.read(localSieveRepositoryProvider).saveScript(
widget.accountId,
id: widget.script?.id,
name: name,
content: _contentController.text,
);
} else {
await ref
.read(sieveRepositoryProvider)
.saveScript(
await ref.read(sieveRepositoryProvider).saveScript(
widget.accountId,
id: widget.script?.id,
name: name,
+9 -9
View File
@@ -46,11 +46,11 @@ class _SieveScriptsScreenState extends ConsumerState<SieveScriptsScreen> {
try {
final scripts = widget.isLocal
? await ref
.read(localSieveRepositoryProvider)
.listScripts(widget.accountId)
.read(localSieveRepositoryProvider)
.listScripts(widget.accountId)
: await ref
.read(sieveRepositoryProvider)
.listScripts(widget.accountId);
.read(sieveRepositoryProvider)
.listScripts(widget.accountId);
if (mounted) {
setState(() {
_scripts = scripts;
@@ -207,10 +207,10 @@ class _SieveSourceBanner extends StatelessWidget {
Widget build(BuildContext context) {
final text = isLocal
? 'Local Filters run Sieve scripts directly on this device. '
'Remote Filters, which run on the mail server, are configured separately.'
'Remote Filters, which run on the mail server, are configured separately.'
: 'Remote Filters run Sieve scripts on the mail server '
'(ManageSieve or JMAP). '
'Local Filters, which run on this device, are configured separately.';
'(ManageSieve or JMAP). '
'Local Filters, which run on this device, are configured separately.';
return Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
@@ -228,8 +228,8 @@ class _SieveSourceBanner extends StatelessWidget {
child: Text(
text,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
+31 -34
View File
@@ -40,8 +40,8 @@ String _buildSyncEntryMarkdown(SyncLogEntry entry) {
final statusLabel = entry.isOk
? 'OK'
: entry.isPermanent
? 'Error (permanent)'
: 'Error';
? 'Error (permanent)'
: 'Error';
buf.writeln('| Status | $statusLabel |');
buf.writeln('| Emails fetched | ${entry.emailsFetched} |');
buf.writeln('| Emails up-to-date | ${entry.emailsSkipped} |');
@@ -98,16 +98,16 @@ class _SyncLogScreenState extends ConsumerState<SyncLogScreen> {
.read(syncLogRepositoryProvider)
.observeSyncLogs(widget.accountId)
.listen((entries) {
setState(() {
if (_syncing &&
_presynCount != null &&
entries.length > _presynCount!) {
_syncing = false;
_presynCount = null;
}
_entries = entries;
});
});
setState(() {
if (_syncing &&
_presynCount != null &&
entries.length > _presynCount!) {
_syncing = false;
_presynCount = null;
}
_entries = entries;
});
});
}
@override
@@ -125,10 +125,8 @@ class _SyncLogScreenState extends ConsumerState<SyncLogScreen> {
}
Future<void> _copyEntry(SyncLogEntry entry, BuildContext context) async {
final accounts = await ref
.read(accountRepositoryProvider)
.observeAccounts()
.first;
final accounts =
await ref.read(accountRepositoryProvider).observeAccounts().first;
final imapCount = accounts.where((a) => a.type == AccountType.imap).length;
final jmapCount = accounts.where((a) => a.type == AccountType.jmap).length;
@@ -206,17 +204,16 @@ class _SyncLogTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final durationLabel = _fmtDuration(entry.duration);
final proto = entry.protocol.isEmpty
? ''
: ' · ${entry.protocol.toUpperCase()}';
final proto =
entry.protocol.isEmpty ? '' : ' · ${entry.protocol.toUpperCase()}';
final theme = Theme.of(context);
final errorColor = theme.colorScheme.error;
final subtitleText = entry.isOk
? '${entry.emailsFetched} new · ${entry.emailsSkipped} up-to-date · took $durationLabel'
: entry.isPermanent
? 'Error (permanent) · took $durationLabel'
: 'Error · took $durationLabel';
? 'Error (permanent) · took $durationLabel'
: 'Error · took $durationLabel';
return ExpansionTile(
leading: Icon(
@@ -341,18 +338,18 @@ class _SyncLogTile extends StatelessWidget {
}
Widget _row(String label, String value) => Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: Row(
children: [
SizedBox(
width: 180,
child: Text(
label,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
padding: const EdgeInsets.symmetric(vertical: 1),
child: Row(
children: [
SizedBox(
width: 180,
child: Text(
label,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
Expanded(child: Text(value, style: const TextStyle(fontSize: 12))),
],
),
Expanded(child: Text(value, style: const TextStyle(fontSize: 12))),
],
),
);
);
}
+5 -9
View File
@@ -101,9 +101,8 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
@override
void initState() {
super.initState();
_bodyFuture = ref
.read(emailRepositoryProvider)
.getEmailBody(widget.email.id);
_bodyFuture =
ref.read(emailRepositoryProvider).getEmailBody(widget.email.id);
_expanded = widget.isLatest;
if (widget.email.isSeen == false) {
unawaited(
@@ -230,9 +229,8 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
}
void _reply(BuildContext context, EmailBody body, {required bool replyAll}) {
final to = widget.email.from.isNotEmpty
? widget.email.from.first.email
: '';
final to =
widget.email.from.isNotEmpty ? widget.email.from.first.email : '';
final subject = (widget.email.subject?.startsWith('Re:') ?? false)
? widget.email.subject!
: 'Re: ${widget.email.subject ?? ''}';
@@ -292,9 +290,7 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
if (!mounted) return;
if (original != null) {
unawaited(
ref
.read(undoServiceProvider.notifier)
.pushAction(
ref.read(undoServiceProvider.notifier).pushAction(
UndoAction(
id: DateTime.now().toIso8601String(),
accountId: widget.email.accountId,
+5 -5
View File
@@ -25,7 +25,7 @@ class UndoLogScreen extends ConsumerWidget {
onPressed: history.isEmpty
? null
: () =>
unawaited(ref.read(undoServiceProvider.notifier).clear()),
unawaited(ref.read(undoServiceProvider.notifier).clear()),
),
],
),
@@ -59,13 +59,13 @@ class _UndoActionTile extends ConsumerWidget {
action.type == UndoType.delete
? Icons.delete_outline
: (action.type == UndoType.snooze
? Icons.access_time
: Icons.move_to_inbox),
? Icons.access_time
: Icons.move_to_inbox),
color: action.type == UndoType.delete
? Colors.redAccent
: (action.type == UndoType.snooze
? Colors.orangeAccent
: Colors.blueAccent),
? Colors.orangeAccent
: Colors.blueAccent),
),
title: Text('$subject$extraCount'),
subtitle: Column(
+2 -3
View File
@@ -33,9 +33,8 @@ String buildAboutMarkdown({
final gitCommitLine = _gitHash.isNotEmpty
? '| Git Commit | [$_gitHash](https://codeberg.org/guettli/sharedinbox/commit/$_gitHash) |\n'
: '';
final deviceModelLine = deviceModel != null
? '| Device Model | $deviceModel |\n'
: '';
final deviceModelLine =
deviceModel != null ? '| Device Model | $deviceModel |\n' : '';
return '## [sharedinbox.de](https://sharedinbox.de)\n\n'
'| Property | Value |\n'
+3 -5
View File
@@ -37,17 +37,15 @@ class EmailTile extends StatelessWidget {
final date = email.sentAt != null ? _dateFmt.format(email.sentAt!) : '';
return ListTile(
leading:
leading ??
leading: leading ??
Icon(
email.isSeen ? Icons.mail_outline : Icons.mail,
color: email.isSeen ? null : Theme.of(context).colorScheme.primary,
),
title: Text(
sender,
style: email.isSeen
? null
: const TextStyle(fontWeight: FontWeight.bold),
style:
email.isSeen ? null : const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
+5 -3
View File
@@ -43,9 +43,11 @@ class FolderDrawer extends ConsumerWidget {
Text(
account?.displayName ?? '',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
Text(
account?.email ?? '',
+11 -13
View File
@@ -16,8 +16,7 @@ String buildEmailHtml(String htmlBody, {bool loadRemoteImages = false}) {
final imgSrc = loadRemoteImages ? 'https: http: data: blob:' : 'data: blob:';
// script-src 'none' blocks page scripts; JS mode stays unrestricted so the
// controller can call runJavaScriptReturningResult for height measurement.
const cspBase =
"default-src 'none'; "
const cspBase = "default-src 'none'; "
"style-src 'unsafe-inline'; "
"script-src 'none'; "
"object-src 'none'; "
@@ -107,9 +106,9 @@ class _SecureEmailWebViewState extends State<SecureEmailWebView> {
}
String _buildHtml() => buildEmailHtml(
widget.htmlBody,
loadRemoteImages: widget.loadRemoteImages,
);
widget.htmlBody,
loadRemoteImages: widget.loadRemoteImages,
);
Future<void> _measureHeight(String _) async {
try {
@@ -141,14 +140,13 @@ class _SecureEmailWebViewState extends State<SecureEmailWebView> {
final host = uri.host;
final parts = host.split('.');
// Bold the registered domain (last two DNS labels) to aid phishing detection.
final boldStart =
(parts.length >= 2
? host.length -
parts.last.length -
1 -
parts[parts.length - 2].length
: 0)
.clamp(0, host.length);
final boldStart = (parts.length >= 2
? host.length -
parts.last.length -
1 -
parts[parts.length - 2].length
: 0)
.clamp(0, host.length);
final confirmed = await showDialog<bool>(
context: context,