feat: add undo log detail view #461
@@ -48,11 +48,28 @@
|
|||||||
chmod +x $out/bin/fgj
|
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 {
|
in {
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
# Dagger CLI
|
# Dagger CLI
|
||||||
dagger.packages.${system}.dagger
|
dagger021
|
||||||
|
|
||||||
# Go compiler — for Dagger development
|
# Go compiler — for Dagger development
|
||||||
go
|
go
|
||||||
@@ -106,6 +123,9 @@
|
|||||||
# nix develop --command does not set IN_NIX_SHELL; set it so _preflight passes in CI
|
# nix develop --command does not set IN_NIX_SHELL; set it so _preflight passes in CI
|
||||||
export IN_NIX_SHELL=1
|
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
|
# Disable Flutter telemetry inside dev shell
|
||||||
export FLUTTER_SUPPRESS_ANALYTICS=true
|
export FLUTTER_SUPPRESS_ANALYTICS=true
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import 'package:sharedinbox/core/models/sieve_script.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/about_screen.dart';
|
||||||
import 'package:sharedinbox/ui/screens/account_list_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/sync_log_screen.dart';
|
||||||
import 'package:sharedinbox/ui/screens/thread_detail_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/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/undo_log_screen.dart';
|
||||||
import 'package:sharedinbox/ui/screens/user_preferences_screen.dart';
|
import 'package:sharedinbox/ui/screens/user_preferences_screen.dart';
|
||||||
import 'package:sharedinbox/ui/widgets/undo_shell.dart';
|
import 'package:sharedinbox/ui/widgets/undo_shell.dart';
|
||||||
@@ -55,6 +57,14 @@ final router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'undo-log',
|
path: 'undo-log',
|
||||||
builder: (ctx, state) => const UndoLogScreen(),
|
builder: (ctx, state) => const UndoLogScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: ':actionId',
|
||||||
|
builder: (ctx, state) => UndoLogDetailScreen(
|
||||||
|
action: state.extra as UndoAction,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'changelog',
|
path: 'changelog',
|
||||||
|
|||||||
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:sharedinbox/core/models/undo_action.dart';
|
import 'package:sharedinbox/core/models/undo_action.dart';
|
||||||
import 'package:sharedinbox/di.dart';
|
import 'package:sharedinbox/di.dart';
|
||||||
@@ -55,6 +56,10 @@ class _UndoActionTile extends ConsumerWidget {
|
|||||||
final extraCount = count > 1 ? ' (+${count - 1} more)' : '';
|
final extraCount = count > 1 ? ' (+${count - 1} more)' : '';
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
onTap: () => context.go(
|
||||||
|
'/accounts/undo-log/${action.id}',
|
||||||
|
extra: action,
|
||||||
|
),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
action.type == UndoType.delete
|
action.type == UndoType.delete
|
||||||
? Icons.delete_outline
|
? Icons.delete_outline
|
||||||
|
|||||||
+8
-8
@@ -659,10 +659,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1088,26 +1088,26 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.30.0"
|
version: "1.31.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.11"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.16"
|
version: "0.6.17"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const _excluded = {
|
|||||||
'lib/ui/screens/sieve_scripts_screen.dart',
|
'lib/ui/screens/sieve_scripts_screen.dart',
|
||||||
'lib/ui/screens/sync_log_screen.dart',
|
'lib/ui/screens/sync_log_screen.dart',
|
||||||
'lib/ui/screens/thread_detail_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/screens/undo_log_screen.dart',
|
||||||
'lib/ui/widgets/folder_drawer.dart',
|
'lib/ui/widgets/folder_drawer.dart',
|
||||||
'lib/ui/widgets/secure_email_webview.dart',
|
'lib/ui/widgets/secure_email_webview.dart',
|
||||||
|
|||||||
Reference in New Issue
Block a user