feat: configurable menu bar position for mailbox view (#298) #303

Merged
guettlibot merged 8 commits from issue-298-fix into main 2026-05-27 20:07:13 +00:00
3 changed files with 88 additions and 8 deletions
Showing only changes of commit 100ca9d8a1 - Show all commits
+32 -2
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -124,7 +125,6 @@ class _AccountTile extends ConsumerWidget {
if (h == null) return const Text('Sync health: Not verified yet');
final date = h.lastVerifiedAt.toLocal().toString().split('.')[0];
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Sync health: '),
Icon(
@@ -133,7 +133,13 @@ class _AccountTile extends ConsumerWidget {
color: h.isHealthy ? Colors.green : Colors.orange,
),
const SizedBox(width: 4),
Text(h.isHealthy ? 'Healthy' : 'Discrepancies found'),
Flexible(
child: Text(
h.isHealthy
? 'Healthy'
: _formatDiscrepancies(h.discrepancySummary),
),
),
Text(' ($date)', style: const TextStyle(fontSize: 10)),
],
);
@@ -293,6 +299,30 @@ class _AccountTile extends ConsumerWidget {
}
}
String _formatDiscrepancies(String? summary) {
if (summary == null) return 'Discrepancies found';
try {
final decoded = jsonDecode(summary) as Map<String, dynamic>;
var missingLocally = 0;
var missingOnServer = 0;
var flagMismatches = 0;
for (final v in decoded.values) {
final m = v as Map<String, dynamic>;
missingLocally += (m['missingLocally'] as int? ?? 0);
missingOnServer += (m['missingOnServer'] as int? ?? 0);
flagMismatches += (m['flagMismatches'] as int? ?? 0);
}
final parts = <String>[];
if (missingLocally > 0) parts.add('missing locally: $missingLocally');
if (missingOnServer > 0) parts.add('missing on server: $missingOnServer');
if (flagMismatches > 0) parts.add('flag mismatches: $flagMismatches');
if (parts.isEmpty) return 'Discrepancies found';
return 'Discrepancies found (${parts.join(', ')})';
} catch (_) {
return 'Discrepancies found';
}
}
class _OnboardingView extends StatelessWidget {
const _OnboardingView();
+46
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sharedinbox/data/db/database.dart' show SyncHealthRow;
import 'helpers.dart';
@@ -206,5 +207,50 @@ void main() {
expect(tester.takeException(), isNull);
expect(find.text('sharedinbox.de'), findsOneWidget);
});
testWidgets('shows Healthy when sync health is healthy', (tester) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts',
overrides: baseOverrides(
accounts: [kTestAccount],
syncHealth: SyncHealthRow(
accountId: kTestAccount.id,
lastVerifiedAt: DateTime(2024, 6),
isHealthy: true,
),
),
),
);
await tester.pumpAndSettle();
expect(find.textContaining('Healthy'), findsOneWidget);
});
testWidgets(
'shows discrepancy details when sync health has discrepancies',
(tester) async {
const summary =
'{"INBOX":{"missingLocally":3,"missingOnServer":0,"flagMismatches":1}}';
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts',
overrides: baseOverrides(
accounts: [kTestAccount],
syncHealth: SyncHealthRow(
accountId: kTestAccount.id,
lastVerifiedAt: DateTime(2024, 6),
isHealthy: false,
discrepancySummary: summary,
),
),
),
);
await tester.pumpAndSettle();
expect(find.textContaining('missing locally: 3'), findsOneWidget);
expect(find.textContaining('flag mismatches: 1'), findsOneWidget);
},
);
});
}
+10 -6
View File
@@ -25,6 +25,7 @@ import 'package:sharedinbox/core/services/account_discovery_service.dart';
import 'package:sharedinbox/core/services/connection_test_service.dart';
import 'package:sharedinbox/core/services/managesieve_probe_service.dart';
import 'package:sharedinbox/core/services/share_encryption_service.dart';
import 'package:sharedinbox/data/db/database.dart' show SyncHealthRow;
import 'package:sharedinbox/di.dart';
import 'package:sharedinbox/ui/screens/account_list_screen.dart';
import 'package:sharedinbox/ui/screens/account_receive_screen.dart';
@@ -505,13 +506,12 @@ Widget buildApp({
return ProviderScope(
// Defaults come first so tests can override them via [overrides].
//
// syncHealthProvider and syncLogRepositoryProvider are backed by Drift
// StreamQueries. When a StreamProvider that wraps a Drift query is disposed,
// Drift schedules a Timer.run() for cache debouncing. Flutter's test
// framework then fails the test with "A Timer is still pending". Replacing
// these with simple synchronous streams avoids the pending-timer assertion.
// syncLogRepositoryProvider is backed by a Drift StreamQuery. When the
// provider is disposed, Drift schedules a Timer.run() for cache
// debouncing. Flutter's test framework then fails the test with "A Timer
// is still pending". Replacing it with a synchronous stream avoids this.
// syncHealthProvider has the same issue and is overridden in baseOverrides.
overrides: [
syncHealthProvider.overrideWith((ref, _) => Stream.value(null)),
syncLogRepositoryProvider.overrideWithValue(
const NoOpSyncLogRepository(),
),
@@ -541,6 +541,7 @@ List<Override> baseOverrides({
Exception? connectionError,
ShareKeyRepository? shareKeyRepository,
bool hasStoredPassword = true,
SyncHealthRow? syncHealth,
}) =>
[
accountRepositoryProvider.overrideWithValue(
@@ -559,6 +560,9 @@ List<Override> baseOverrides({
shareKeyRepositoryProvider.overrideWithValue(
shareKeyRepository ?? FakeShareKeyRepository(),
),
// syncHealthProvider is backed by a Drift StreamQuery; override with a
// plain stream to avoid "A Timer is still pending" in tests.
syncHealthProvider.overrideWith((ref, _) => Stream.value(syncHealth)),
];
// ---------------------------------------------------------------------------