fix: format, analyze-fix and update mocks
This commit is contained in:
@@ -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')),
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)}',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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))),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user