Compare commits

..

2 Commits

Author SHA1 Message Date
shenlong-tanwen
b9f4cd398d undo visibility actions 2026-07-20 16:51:37 +05:30
shenlong-tanwen
da0742ab8c feat: undo handling 2026-07-20 16:14:17 +05:30
8 changed files with 105 additions and 38 deletions

View File

@@ -16,7 +16,6 @@ import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
@@ -24,13 +23,13 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/theme.provider.dart';
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
import 'package:immich_mobile/routing/app_navigation_observer.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/deep_link.service.dart';
@@ -272,8 +271,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale),
builder: (context, child) => ImmichTranslationProvider(
translations: ImmichTranslations(
submit: "submit".t(context: context),
password: "password".t(context: context),
submit: context.t.submit,
password: context.t.password,
undo: context.t.undo,
),
child: ImmichThemeProvider(colorScheme: context.colorScheme, child: child!),
),

View File

@@ -27,11 +27,17 @@ class ArchiveAction extends BaseAction {
final context = ref.context;
final ids = assetsForAction(ref, assets).map((asset) => asset.id).toList(growable: false);
await ref.read(assetServiceProvider).update(ids, visibility: const .some(.archive));
final assetService = ref.read(assetServiceProvider);
await assetService.update(ids, visibility: const .some(.archive));
if (!context.mounted) {
return;
}
ref.read(toastRepositoryProvider).success(context.t.archive_action_prompt(count: ids.length));
ref
.read(toastRepositoryProvider)
.success(
context.t.archive_action_prompt(count: ids.length),
toast: .new(onUndo: () async => await assetService.update(ids, visibility: const .some(.timeline))),
);
}
}
@@ -58,10 +64,16 @@ class UnarchiveAction extends BaseAction {
final context = ref.context;
final ids = assetsForAction(ref, assets).map((asset) => asset.id).toList(growable: false);
await ref.read(assetServiceProvider).update(ids, visibility: const .some(.timeline));
final assetService = ref.read(assetServiceProvider);
await assetService.update(ids, visibility: const .some(.timeline));
if (!context.mounted) {
return;
}
ref.read(toastRepositoryProvider).success(context.t.unarchive_action_prompt(count: ids.length));
ref
.read(toastRepositoryProvider)
.success(
context.t.unarchive_action_prompt(count: ids.length),
toast: .new(onUndo: () async => await assetService.update(ids, visibility: const .some(.archive))),
);
}
}

View File

@@ -45,12 +45,18 @@ class TrashAction extends BaseAction {
}
final ids = remoteIds.toList(growable: false);
await ref.read(assetServiceProvider).trash(ids);
final assetService = ref.read(assetServiceProvider);
await assetService.trash(ids);
if (!context.mounted) {
return;
}
ref.read(toastRepositoryProvider).success(context.t.trash_action_prompt(count: ids.length));
ref
.read(toastRepositoryProvider)
.success(
context.t.trash_action_prompt(count: ids.length),
toast: .new(onUndo: () async => await assetService.restoreTrash(ids)),
);
}
}

View File

@@ -59,10 +59,16 @@ class UnlockAction extends BaseAction {
final context = ref.context;
final ids = assetsForAction(ref, assets).map((asset) => asset.id).toList(growable: false);
await ref.read(assetServiceProvider).update(ids, visibility: const .some(.timeline));
final assetService = ref.read(assetServiceProvider);
await assetService.update(ids, visibility: const .some(.timeline));
if (!context.mounted) {
return;
}
ref.read(toastRepositoryProvider).success(context.t.remove_from_lock_folder_action_prompt(count: ids.length));
ref
.read(toastRepositoryProvider)
.success(
context.t.remove_from_lock_folder_action_prompt(count: ids.length),
toast: .new(onUndo: () async => await assetService.update(ids, visibility: const .some(.locked))),
);
}
}

View File

@@ -28,10 +28,16 @@ class RestoreAction extends BaseAction {
final context = ref.context;
final ids = assetsForAction(ref, assets).map((asset) => asset.id).toList(growable: false);
await ref.read(assetServiceProvider).restoreTrash(ids);
final assetService = ref.read(assetServiceProvider);
await assetService.restoreTrash(ids);
if (!context.mounted) {
return;
}
ref.read(toastRepositoryProvider).success(context.t.assets_restored_count(count: ids.length));
ref
.read(toastRepositoryProvider)
.success(
context.t.assets_restored_count(count: ids.length),
toast: .new(onUndo: () async => await assetService.trash(ids)),
);
}
}

View File

@@ -13,14 +13,23 @@ class ToastRepository {
const ToastRepository();
FutureOr<void> success(String message, {ToastOption? toast}) {
snackbar.success(message, duration: toast?.timeout);
snackbar.success(message, duration: toast?.timeout, action: toast?.action);
}
FutureOr<void> info(String message, {ToastOption? toast}) {
snackbar.info(message, duration: toast?.timeout);
snackbar.info(message, duration: toast?.timeout, action: toast?.action);
}
FutureOr<void> error(String message, {ToastOption? toast}) {
snackbar.error(message, duration: toast?.timeout);
snackbar.error(message, duration: toast?.timeout, action: toast?.action);
}
}
extension on ToastOption {
SnackbarAction? get action {
if (onUndo == null) {
return null;
}
return SnackbarAction(onPressed: onUndo!);
}
}

View File

@@ -1,8 +1,18 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:immich_ui/immich_ui.dart';
import 'package:immich_ui/src/internal.dart';
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
class SnackbarAction {
final String? label;
final FutureOr<void> Function() onPressed;
const SnackbarAction({this.label, required this.onPressed});
}
class SnackbarManager {
const SnackbarManager();
@@ -10,6 +20,7 @@ class SnackbarManager {
String message,
SnackbarType type, {
Duration? duration,
SnackbarAction? action,
}) {
final messenger = scaffoldMessengerKey.currentState;
final context = scaffoldMessengerKey.currentContext;
@@ -19,10 +30,10 @@ class SnackbarManager {
duration ??= const .new(seconds: 4);
messenger.hideCurrentSnackBar();
return messenger.showSnackBar(_build(context, message, type, duration));
return messenger.showSnackBar(_build(context, message, type, duration, action));
}
SnackBar _build(BuildContext context, String message, SnackbarType type, Duration duration) {
SnackBar _build(BuildContext context, String message, SnackbarType type, Duration duration, SnackbarAction? action) {
final theme = Theme.of(context);
final colors = theme.extension<ImmichColors>() ?? ImmichColors.harmonized(theme.colorScheme);
final (IconData icon, Color background, Color foreground) = switch (type) {
@@ -31,11 +42,21 @@ class SnackbarManager {
.error => (Icons.warning_rounded, colors.error, colors.onError),
};
SnackBarAction? snackAction;
if (action != null) {
snackAction = .new(
label: action.label ?? context.translations.undo,
onPressed: action.onPressed,
textColor: foreground,
);
}
return SnackBar(
behavior: .floating,
backgroundColor: background,
duration: duration,
shape: const RoundedRectangleBorder(borderRadius: .all(.circular(ImmichRadius.sm))),
persist: false,
content: Row(
children: [
Icon(icon, color: foreground, size: ImmichIconSize.sm),
@@ -50,17 +71,27 @@ class SnackbarManager {
),
],
),
action: snackAction,
);
}
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message, {Duration? duration}) =>
show(message, .info, duration: duration);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(
String message, {
Duration? duration,
SnackbarAction? action,
}) => show(message, .info, duration: duration, action: action);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message, {Duration? duration}) =>
show(message, .success, duration: duration);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(
String message, {
Duration? duration,
SnackbarAction? action,
}) => show(message, .success, duration: duration, action: action);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message, {Duration? duration}) =>
show(message, .error, duration: duration);
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(
String message, {
Duration? duration,
SnackbarAction? action,
}) => show(message, .error, duration: duration, action: action);
}
const snackbar = SnackbarManager();

View File

@@ -1,27 +1,24 @@
import 'package:flutter/material.dart';
class ImmichTranslations {
late String submit;
late String password;
final String submit;
final String password;
final String undo;
ImmichTranslations({String? submit, String? password}) {
this.submit = submit ?? 'Submit';
this.password = password ?? 'Password';
}
const ImmichTranslations({String? submit, String? password, String? undo})
: submit = submit ?? 'Submit',
password = password ?? 'Password',
undo = undo ?? 'Undo';
}
class ImmichTranslationProvider extends InheritedWidget {
final ImmichTranslations? translations;
const ImmichTranslationProvider({
super.key,
this.translations,
required super.child,
});
const ImmichTranslationProvider({super.key, this.translations, required super.child});
static ImmichTranslations of(BuildContext context) {
final provider = context.dependOnInheritedWidgetOfExactType<ImmichTranslationProvider>();
return provider?.translations ?? ImmichTranslations();
return provider?.translations ?? const ImmichTranslations();
}
@override