mirror of
https://github.com/immich-app/immich
synced 2026-08-29 13:15:45 +00:00
feat: mobile actions (#29280)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
22ec449e43
commit
f4c8459484
3 changed files with 156 additions and 0 deletions
14
mobile/lib/presentation/actions/action.dart
Normal file
14
mobile/lib/presentation/actions/action.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
abstract class BaseAction {
|
||||
final IconData icon;
|
||||
|
||||
const BaseAction({required this.icon});
|
||||
|
||||
String label(BuildContext context);
|
||||
|
||||
bool isVisible(BuildContext context, WidgetRef ref);
|
||||
|
||||
Future<void> onAction(BuildContext context, WidgetRef ref);
|
||||
}
|
||||
81
mobile/lib/presentation/actions/action.widget.dart
Normal file
81
mobile/lib/presentation/actions/action.widget.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/utils/error_handler.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class _ActionWidget extends ConsumerWidget {
|
||||
final BaseAction action;
|
||||
final Widget Function(Future<void> Function() onAction) builder;
|
||||
|
||||
const _ActionWidget({required this.action, required this.builder});
|
||||
|
||||
Future<void> _onAction(BuildContext context, WidgetRef ref) async {
|
||||
try {
|
||||
await action.onAction(context, ref);
|
||||
} catch (error, stackTrace) {
|
||||
handleError(context, error, stack: stackTrace, description: 'Action failed: ${action.runtimeType}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (!action.isVisible(context, ref)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return builder(() => _onAction(context, ref));
|
||||
}
|
||||
}
|
||||
|
||||
class ActionIconButtonWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
final ImmichVariant variant;
|
||||
|
||||
const ActionIconButtonWidget({super.key, required this.action, this.variant = .ghost});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ActionWidget(
|
||||
action: action,
|
||||
builder: (onAction) => ImmichIconButton(icon: action.icon, onPressed: onAction, variant: variant),
|
||||
);
|
||||
}
|
||||
|
||||
class ActionButtonWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
final ImmichVariant variant;
|
||||
|
||||
const ActionButtonWidget({super.key, required this.action, this.variant = .ghost});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ActionWidget(
|
||||
action: action,
|
||||
builder: (onAction) =>
|
||||
ImmichTextButton(labelText: action.label(context), icon: action.icon, onPressed: onAction, variant: variant),
|
||||
);
|
||||
}
|
||||
|
||||
class ActionColumnButtonWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
|
||||
const ActionColumnButtonWidget({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ActionWidget(
|
||||
action: action,
|
||||
builder: (onAction) => ImmichColumnButton(icon: action.icon, label: action.label(context), onPressed: onAction),
|
||||
);
|
||||
}
|
||||
|
||||
class ActionMenuItemWidget extends StatelessWidget {
|
||||
final BaseAction action;
|
||||
|
||||
const ActionMenuItemWidget({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ActionWidget(
|
||||
action: action,
|
||||
builder: (onAction) => ImmichMenuItem(icon: action.icon, label: action.label(context), onPressed: onAction),
|
||||
);
|
||||
}
|
||||
61
mobile/lib/utils/error_handler.dart
Normal file
61
mobile/lib/utils/error_handler.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
|
||||
void handleError(BuildContext context, Object error, {StackTrace? stack, String? description}) {
|
||||
String? stackTrace;
|
||||
if (stack != null) {
|
||||
final trace = Trace.from(stack);
|
||||
final clean = trace.foldFrames(
|
||||
(frame) => frame.package == 'flutter' || frame.package == 'flutter_test' || frame.isCore,
|
||||
terse: true,
|
||||
);
|
||||
stackTrace = clean.toString();
|
||||
}
|
||||
|
||||
dPrint(
|
||||
() => 'Error${description != null ? ' ($description)' : ''}: $error${stackTrace != null ? '\n$stackTrace' : ''}',
|
||||
);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String message;
|
||||
if (serverErrorMessage(error) case String serverMessage) {
|
||||
message = serverMessage;
|
||||
} else if (isConnectionError(error)) {
|
||||
message = context.t.login_form_server_error;
|
||||
} else {
|
||||
message = context.t.scaffold_body_error_occurred;
|
||||
}
|
||||
|
||||
snackbar.error(message);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
String? serverErrorMessage(Object error) {
|
||||
if (error is! ApiException || error.innerException != null || error.message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final body = jsonDecode(error.message!);
|
||||
if (body is Map && body['message'] != null) {
|
||||
final message = body['message'];
|
||||
return message is List ? message.join(', ') : message.toString();
|
||||
}
|
||||
} catch (_) {
|
||||
// The body was not JSON; fall back to the raw payload below.
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
bool isConnectionError(Object error) => error is ApiException && error.innerException != null;
|
||||
Loading…
Add table
Add a link
Reference in a new issue