diff --git a/flake.nix b/flake.nix index d1c0e7b..03c5ec3 100644 --- a/flake.nix +++ b/flake.nix @@ -48,11 +48,28 @@ chmod +x $out/bin/fgj ''; }; + + # The dagger/nix flake pins 0.20.8, whose Nix wrapper is a broken self-exec + # loop. Fetch 0.21.4 directly so the pre-commit dart-check hook can run. + dagger021 = pkgs.stdenv.mkDerivation { + pname = "dagger"; + version = "0.21.4"; + src = pkgs.fetchurl { + url = "https://dl.dagger.io/dagger/releases/0.21.4/dagger_v0.21.4_linux_amd64.tar.gz"; + sha256 = "0wlnbr4g5069755131yjp2a6alacn64f1c8b27xn0cbynq3zicjd"; + }; + sourceRoot = "."; + installPhase = '' + mkdir -p $out/bin + cp dagger $out/bin/dagger + chmod +x $out/bin/dagger + ''; + }; in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ # Dagger CLI - dagger.packages.${system}.dagger + dagger021 # Go compiler — for Dagger development go @@ -107,6 +124,9 @@ # nix develop --command does not set IN_NIX_SHELL; set it so _preflight passes in CI export IN_NIX_SHELL=1 + # Point Dagger client at the running engine socket + export DAGGER_HOST=unix:///run/dagger/engine.sock + # Disable Flutter telemetry inside dev shell export FLUTTER_SUPPRESS_ANALYTICS=true diff --git a/lib/ui/router.dart b/lib/ui/router.dart index 9b506df..e5b0b3b 100644 --- a/lib/ui/router.dart +++ b/lib/ui/router.dart @@ -1,6 +1,7 @@ import 'package:go_router/go_router.dart'; import 'package:sharedinbox/core/models/sieve_script.dart'; +import 'package:sharedinbox/core/models/undo_action.dart'; import 'package:sharedinbox/ui/screens/about_screen.dart'; import 'package:sharedinbox/ui/screens/account_list_screen.dart'; @@ -22,6 +23,7 @@ import 'package:sharedinbox/ui/screens/sieve_scripts_screen.dart'; import 'package:sharedinbox/ui/screens/sync_log_screen.dart'; import 'package:sharedinbox/ui/screens/thread_detail_screen.dart'; import 'package:sharedinbox/ui/screens/trusted_image_senders_screen.dart'; +import 'package:sharedinbox/ui/screens/undo_log_detail_screen.dart'; import 'package:sharedinbox/ui/screens/undo_log_screen.dart'; import 'package:sharedinbox/ui/screens/user_preferences_screen.dart'; import 'package:sharedinbox/ui/widgets/undo_shell.dart'; @@ -55,6 +57,14 @@ final router = GoRouter( GoRoute( path: 'undo-log', builder: (ctx, state) => const UndoLogScreen(), + routes: [ + GoRoute( + path: ':actionId', + builder: (ctx, state) => UndoLogDetailScreen( + action: state.extra as UndoAction, + ), + ), + ], ), GoRoute( path: 'changelog', diff --git a/lib/ui/screens/undo_log_detail_screen.dart b/lib/ui/screens/undo_log_detail_screen.dart new file mode 100644 index 0000000..d690c37 --- /dev/null +++ b/lib/ui/screens/undo_log_detail_screen.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:sharedinbox/core/models/email.dart'; +import 'package:sharedinbox/core/models/undo_action.dart'; +import 'package:sharedinbox/di.dart'; + +final _dateTimeFmt = DateFormat('yyyy-MM-dd HH:mm:ss'); + +class UndoLogDetailScreen extends ConsumerWidget { + const UndoLogDetailScreen({super.key, required this.action}); + + final UndoAction action; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Undo Log Detail'), + actions: [ + TextButton( + onPressed: () async { + await ref + .read(undoServiceProvider.notifier) + .undo(actionId: action.id); + if (context.mounted) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + duration: Duration(seconds: 5), + content: Text('Action undone.'), + ), + ); + } + }, + child: const Text('Undo'), + ), + ], + ), + body: ListView( + children: [ + _SectionHeader(text: 'Transaction', theme: theme), + ListTile( + leading: const Icon(Icons.account_circle), + title: const Text('Account'), + subtitle: Text(action.accountId), + ), + ListTile( + leading: Icon( + action.type == UndoType.delete + ? Icons.delete_outline + : (action.type == UndoType.snooze + ? Icons.access_time + : Icons.move_to_inbox), + color: action.type == UndoType.delete + ? Colors.redAccent + : (action.type == UndoType.snooze + ? Colors.orangeAccent + : Colors.blueAccent), + ), + title: const Text('Action'), + subtitle: Text(action.type.name.toUpperCase()), + ), + ListTile( + leading: const Icon(Icons.schedule), + title: const Text('Timestamp'), + subtitle: Text(_dateTimeFmt.format(action.timestamp.toLocal())), + ), + _SectionHeader(text: 'Folders', theme: theme), + ListTile( + leading: const Icon(Icons.folder_open), + title: const Text('Source'), + subtitle: Text(action.sourceMailboxPath), + ), + if (action.type == UndoType.move && + action.destinationMailboxPath != null) + ListTile( + leading: const Icon(Icons.drive_file_move), + title: const Text('Destination'), + subtitle: Text(action.destinationMailboxPath!), + ), + _SectionHeader( + text: 'Emails (${action.emailIds.length})', + theme: theme, + ), + if (action.originalEmails.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + '${action.emailIds.length} email(s) — details not available', + style: theme.textTheme.bodySmall, + ), + ), + ...action.originalEmails.map((email) => _EmailTile(email: email)), + ], + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader({required this.text, required this.theme}); + + final String text; + final ThemeData theme; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: Text( + text, + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ); + } +} + +class _EmailTile extends StatelessWidget { + const _EmailTile({required this.email}); + + final Email email; + + @override + Widget build(BuildContext context) { + final sender = email.from.isNotEmpty + ? (email.from.first.name ?? email.from.first.email) + : '(Unknown Sender)'; + return ListTile( + leading: const Icon(Icons.email_outlined), + title: Text(email.subject ?? '(No Subject)'), + subtitle: Text(sender, maxLines: 1, overflow: TextOverflow.ellipsis), + ); + } +} diff --git a/lib/ui/screens/undo_log_screen.dart b/lib/ui/screens/undo_log_screen.dart index 334e639..310c3b0 100644 --- a/lib/ui/screens/undo_log_screen.dart +++ b/lib/ui/screens/undo_log_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:sharedinbox/core/models/undo_action.dart'; import 'package:sharedinbox/di.dart'; @@ -55,6 +56,10 @@ class _UndoActionTile extends ConsumerWidget { final extraCount = count > 1 ? ' (+${count - 1} more)' : ''; return ListTile( + onTap: () => context.go( + '/accounts/undo-log/${action.id}', + extra: action, + ), leading: Icon( action.type == UndoType.delete ? Icons.delete_outline diff --git a/pubspec.lock b/pubspec.lock index 83fbe88..17e8bbd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -659,10 +659,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: "direct main" description: @@ -1088,26 +1088,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" timezone: dependency: transitive description: diff --git a/scripts/check_coverage.dart b/scripts/check_coverage.dart index 7a9be69..881d674 100644 --- a/scripts/check_coverage.dart +++ b/scripts/check_coverage.dart @@ -57,6 +57,7 @@ const _excluded = { 'lib/ui/screens/sieve_scripts_screen.dart', 'lib/ui/screens/sync_log_screen.dart', 'lib/ui/screens/thread_detail_screen.dart', + 'lib/ui/screens/undo_log_detail_screen.dart', 'lib/ui/screens/undo_log_screen.dart', 'lib/ui/widgets/folder_drawer.dart', 'lib/ui/widgets/secure_email_webview.dart',