mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 17:24:56 +03:00
refactor: proper layer archtecture
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
/// Key for each possible value in the `Store`.
|
||||
/// Defines the data type for each value
|
||||
@@ -73,7 +72,7 @@ enum StoreKey<T> {
|
||||
|
||||
autoPlayVideo<bool>._(139),
|
||||
albumGridView<bool>._(140),
|
||||
viewerQuickActionOrder<List<ActionButtonType>>._(141),
|
||||
viewerQuickActionOrder<String>._(141),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000),
|
||||
|
||||
109
mobile/lib/domain/services/quick_action.service.dart
Normal file
109
mobile/lib/domain/services/quick_action.service.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
/// Service for managing quick action configurations.
|
||||
/// Provides business logic for building quick action types based on context.
|
||||
class QuickActionService {
|
||||
final ActionButtonOrderRepository _repository;
|
||||
|
||||
const QuickActionService(this._repository);
|
||||
|
||||
// Constants
|
||||
static const int defaultQuickActionLimit = 4;
|
||||
|
||||
static const List<ActionButtonType> defaultQuickActionSeed = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.upload,
|
||||
ActionButtonType.edit,
|
||||
ActionButtonType.add,
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.removeFromAlbum,
|
||||
ActionButtonType.likeActivity,
|
||||
];
|
||||
|
||||
static final Set<ActionButtonType> _quickActionSet = Set<ActionButtonType>.unmodifiable(defaultQuickActionSeed);
|
||||
|
||||
static final List<ActionButtonType> defaultQuickActionOrder = List<ActionButtonType>.unmodifiable(
|
||||
defaultQuickActionSeed,
|
||||
);
|
||||
|
||||
/// Get the list of available quick action options
|
||||
// static List<ActionButtonType> get quickActionOptions => defaultQuickActionOrder;
|
||||
|
||||
/// Get the current quick action order
|
||||
List<ActionButtonType> get() {
|
||||
return _repository.get();
|
||||
}
|
||||
|
||||
/// Set the quick action order
|
||||
Future<void> set(List<ActionButtonType> order) async {
|
||||
final normalized = _normalizeQuickActionOrder(order);
|
||||
await _repository.set(normalized);
|
||||
}
|
||||
|
||||
/// Watch for changes to quick action order
|
||||
Stream<List<ActionButtonType>> watch() {
|
||||
return _repository.watch();
|
||||
}
|
||||
|
||||
/// Normalize quick action order by filtering valid types and ensuring all defaults are included
|
||||
List<ActionButtonType> _normalizeQuickActionOrder(List<ActionButtonType> order) {
|
||||
final ordered = <ActionButtonType>{};
|
||||
|
||||
for (final type in order) {
|
||||
if (_quickActionSet.contains(type)) {
|
||||
ordered.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
ordered.addAll(defaultQuickActionSeed);
|
||||
|
||||
return ordered.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Build a list of quick action types based on context and custom order
|
||||
List<ActionButtonType> buildQuickActionTypes(
|
||||
ActionButtonContext context, {
|
||||
List<ActionButtonType>? quickActionOrder,
|
||||
int limit = defaultQuickActionLimit,
|
||||
}) {
|
||||
final normalized = _normalizeQuickActionOrder(
|
||||
quickActionOrder == null || quickActionOrder.isEmpty ? defaultQuickActionOrder : quickActionOrder,
|
||||
);
|
||||
|
||||
final seen = <ActionButtonType>{};
|
||||
final result = <ActionButtonType>[];
|
||||
|
||||
for (final type in normalized) {
|
||||
if (!_quickActionSet.contains(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final resolved = _resolveQuickActionType(type, context);
|
||||
if (!seen.add(resolved) || !resolved.shouldShow(context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(resolved);
|
||||
if (result.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Resolve quick action type based on context (e.g., archive -> unarchive)
|
||||
ActionButtonType _resolveQuickActionType(ActionButtonType type, ActionButtonContext context) {
|
||||
if (type == ActionButtonType.archive && context.isArchived) {
|
||||
return ActionButtonType.unarchive;
|
||||
}
|
||||
|
||||
if (type == ActionButtonType.delete && context.asset.isLocalOnly) {
|
||||
return ActionButtonType.deleteLocal;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
/// Repository for managing quick action button order persistence.
|
||||
/// Handles serialization, deserialization, and storage operations.
|
||||
class ActionButtonOrderRepository {
|
||||
const ActionButtonOrderRepository();
|
||||
|
||||
/// Default order for quick actions
|
||||
static const List<ActionButtonType> defaultOrder = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.upload,
|
||||
ActionButtonType.edit,
|
||||
ActionButtonType.add,
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.removeFromAlbum,
|
||||
ActionButtonType.likeActivity,
|
||||
];
|
||||
|
||||
/// Get the current quick action order from storage
|
||||
List<ActionButtonType> get() {
|
||||
final json = Store.tryGet(StoreKey.viewerQuickActionOrder);
|
||||
if (json == null || json.isEmpty) {
|
||||
return defaultOrder;
|
||||
}
|
||||
|
||||
final deserialized = _deserialize(json);
|
||||
return deserialized.isEmpty ? defaultOrder : deserialized;
|
||||
}
|
||||
|
||||
/// Save quick action order to storage
|
||||
Future<void> set(List<ActionButtonType> order) async {
|
||||
final json = _serialize(order);
|
||||
await Store.put(StoreKey.viewerQuickActionOrder, json);
|
||||
}
|
||||
|
||||
/// Watch for changes to quick action order
|
||||
Stream<List<ActionButtonType>> watch() {
|
||||
return Store.watch(StoreKey.viewerQuickActionOrder).map((json) {
|
||||
if (json == null || json.isEmpty) {
|
||||
return defaultOrder;
|
||||
}
|
||||
final deserialized = _deserialize(json);
|
||||
return deserialized.isEmpty ? defaultOrder : deserialized;
|
||||
});
|
||||
}
|
||||
|
||||
/// Serialize a list of ActionButtonType to JSON string
|
||||
String _serialize(List<ActionButtonType> order) {
|
||||
return jsonEncode(order.map((type) => type.name).toList());
|
||||
}
|
||||
|
||||
/// Deserialize a JSON string to a list of ActionButtonType
|
||||
List<ActionButtonType> _deserialize(String json) {
|
||||
try {
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
return list
|
||||
.whereType<String>()
|
||||
.map((name) {
|
||||
try {
|
||||
return ActionButtonType.values.byName(name);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<ActionButtonType>()
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
@@ -6,7 +5,6 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite
|
||||
@@ -86,11 +84,6 @@ class IsarStoreRepository extends IsarDatabaseRepository implements IStoreReposi
|
||||
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
|
||||
const (UserDto) =>
|
||||
entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!),
|
||||
const (List<ActionButtonType>) =>
|
||||
(jsonDecode(entity.strValue ?? '[]') as List<dynamic>)
|
||||
.map<ActionButtonType>((d) => ActionButtonType.values.byName(d))
|
||||
.toList()
|
||||
as T,
|
||||
_ => null,
|
||||
}
|
||||
as T?;
|
||||
@@ -102,7 +95,6 @@ class IsarStoreRepository extends IsarDatabaseRepository implements IStoreReposi
|
||||
const (bool) => ((value as bool) ? 1 : 0, null),
|
||||
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
|
||||
const (UserDto) => (null, (await IsarUserRepository(_db).update(value as UserDto)).id),
|
||||
const (List<ActionButtonType>) => (null, jsonEncode(value)),
|
||||
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
|
||||
};
|
||||
return StoreValue(key.id, intValue: intValue, strValue: strValue);
|
||||
@@ -182,11 +174,6 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
|
||||
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
|
||||
const (UserDto) =>
|
||||
entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!),
|
||||
const (List<ActionButtonType>) =>
|
||||
(jsonDecode(entity.stringValue ?? '[]') as List<dynamic>)
|
||||
.map<ActionButtonType>((d) => ActionButtonType.values.byName(d))
|
||||
.toList()
|
||||
as T,
|
||||
_ => null,
|
||||
}
|
||||
as T?;
|
||||
@@ -198,7 +185,6 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
|
||||
const (bool) => ((value as bool) ? 1 : 0, null),
|
||||
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
|
||||
const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id),
|
||||
const (List<ActionButtonType>) => (null, jsonEncode(value)),
|
||||
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
|
||||
};
|
||||
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
|
||||
|
||||
@@ -56,7 +56,8 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final quickActionTypes = ActionButtonBuilder.buildQuickActionTypes(
|
||||
final quickActionService = ref.watch(quickActionServiceProvider);
|
||||
final quickActionTypes = quickActionService.buildQuickActionTypes(
|
||||
buttonContext,
|
||||
quickActionOrder: quickActionOrder,
|
||||
);
|
||||
@@ -76,9 +77,10 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
});
|
||||
}
|
||||
|
||||
final actions = quickActionTypes
|
||||
.map((type) => GestureDetector(onLongPress: openConfigurator, child: type.buildButton(buttonContext)))
|
||||
.toList(growable: false);
|
||||
final actions = ActionButtonBuilder.buildQuickActions(
|
||||
buttonContext,
|
||||
quickActionTypes: quickActionTypes,
|
||||
).map((widget) => GestureDetector(onLongPress: openConfigurator, child: widget)).toList(growable: false);
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: opacity < 255,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/viewer_quick_action_order.provider.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:immich_mobile/utils/action_button_visuals.dart';
|
||||
@@ -41,7 +42,7 @@ class _QuickActionConfiguratorState extends ConsumerState<QuickActionConfigurato
|
||||
|
||||
void _resetToDefault() {
|
||||
setState(() {
|
||||
_order = List<ActionButtonType>.from(ActionButtonBuilder.defaultQuickActionOrder);
|
||||
_order = List<ActionButtonType>.from(QuickActionService.defaultQuickActionOrder);
|
||||
_hasLocalChanges = true;
|
||||
});
|
||||
}
|
||||
@@ -49,9 +50,7 @@ class _QuickActionConfiguratorState extends ConsumerState<QuickActionConfigurato
|
||||
void _cancel() => Navigator.of(context).pop();
|
||||
|
||||
Future<void> _save() async {
|
||||
final normalized = ActionButtonBuilder.normalizeQuickActionOrder(_order);
|
||||
|
||||
await ref.read(viewerQuickActionOrderProvider.notifier).setOrder(normalized);
|
||||
await ref.read(viewerQuickActionOrderProvider.notifier).setOrder(_order);
|
||||
_hasLocalChanges = false;
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -69,8 +68,8 @@ class _QuickActionConfiguratorState extends ConsumerState<QuickActionConfigurato
|
||||
if (!_hasLocalChanges && !listEquals(_order, currentOrder)) {
|
||||
_order = List<ActionButtonType>.from(currentOrder);
|
||||
}
|
||||
final normalizedSelection = ActionButtonBuilder.normalizeQuickActionOrder(_order);
|
||||
final hasChanges = !listEquals(currentOrder, normalizedSelection);
|
||||
|
||||
final hasChanges = !listEquals(currentOrder, _order);
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
@@ -91,7 +90,7 @@ class _QuickActionConfiguratorState extends ConsumerState<QuickActionConfigurato
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'quick_actions_settings_description'.tr(
|
||||
namedArgs: {'count': ActionButtonBuilder.defaultQuickActionLimit.toString()},
|
||||
namedArgs: {'count': QuickActionService.defaultQuickActionLimit.toString()},
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
@@ -1,53 +1,51 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'viewer_quick_action_order.provider.g.dart';
|
||||
final actionButtonOrderRepositoryProvider = Provider<ActionButtonOrderRepository>(
|
||||
(ref) => const ActionButtonOrderRepository(),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ViewerQuickActionOrder extends _$ViewerQuickActionOrder {
|
||||
final quickActionServiceProvider = Provider<QuickActionService>(
|
||||
(ref) => QuickActionService(ref.watch(actionButtonOrderRepositoryProvider)),
|
||||
);
|
||||
|
||||
final viewerQuickActionOrderProvider = StateNotifierProvider<ViewerQuickActionOrderNotifier, List<ActionButtonType>>(
|
||||
(ref) => ViewerQuickActionOrderNotifier(ref.watch(quickActionServiceProvider)),
|
||||
);
|
||||
|
||||
class ViewerQuickActionOrderNotifier extends StateNotifier<List<ActionButtonType>> {
|
||||
final QuickActionService _service;
|
||||
StreamSubscription<List<ActionButtonType>>? _subscription;
|
||||
|
||||
ViewerQuickActionOrderNotifier(this._service) : super(_service.get()) {
|
||||
_subscription = _service.watch().listen((order) {
|
||||
state = order;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
List<ActionButtonType> build() {
|
||||
final service = ref.watch(appSettingsServiceProvider);
|
||||
final initial = ActionButtonBuilder.normalizeQuickActionOrder(
|
||||
service.getSetting(AppSettingsEnum.viewerQuickActionOrder),
|
||||
);
|
||||
|
||||
_subscription ??= service.watchSetting(AppSettingsEnum.viewerQuickActionOrder).listen((order) {
|
||||
state = ActionButtonBuilder.normalizeQuickActionOrder(order);
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
});
|
||||
|
||||
return initial;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> setOrder(List<ActionButtonType> order) async {
|
||||
final normalized = ActionButtonBuilder.normalizeQuickActionOrder(order);
|
||||
|
||||
if (listEquals(state, normalized)) {
|
||||
if (listEquals(state, order)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final previous = state;
|
||||
state = normalized;
|
||||
state = order;
|
||||
|
||||
try {
|
||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.viewerQuickActionOrder, normalized);
|
||||
await _service.set(order);
|
||||
} catch (error) {
|
||||
state = previous;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock class for testing
|
||||
abstract class ViewerQuickActionOrderInternal extends _$ViewerQuickActionOrder {}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'viewer_quick_action_order.provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$viewerQuickActionOrderHash() =>
|
||||
r'd539bc6ba5fae4fa07a7c30c42d9f6aee1488f97';
|
||||
|
||||
/// See also [ViewerQuickActionOrder].
|
||||
@ProviderFor(ViewerQuickActionOrder)
|
||||
final viewerQuickActionOrderProvider =
|
||||
NotifierProvider<ViewerQuickActionOrder, List<ActionButtonType>>.internal(
|
||||
ViewerQuickActionOrder.new,
|
||||
name: r'viewerQuickActionOrderProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$viewerQuickActionOrderHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ViewerQuickActionOrder = Notifier<List<ActionButtonType>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
enum AppSettingsEnum<T> {
|
||||
loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true),
|
||||
@@ -56,11 +55,7 @@ enum AppSettingsEnum<T> {
|
||||
albumGridView<bool>(StoreKey.albumGridView, "albumGridView", false),
|
||||
backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false),
|
||||
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30),
|
||||
viewerQuickActionOrder<List<ActionButtonType>>(
|
||||
StoreKey.viewerQuickActionOrder,
|
||||
null,
|
||||
ActionButtonBuilder.defaultQuickActionSeed,
|
||||
);
|
||||
viewerQuickActionOrder<String>(StoreKey.viewerQuickActionOrder, null, '');
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
||||
|
||||
@@ -169,99 +169,23 @@ enum ActionButtonType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder class for creating action button widgets.
|
||||
/// This class provides simple factory methods for building action button widgets
|
||||
/// from ActionButtonContext. Business logic for quick actions is handled by QuickActionService.
|
||||
class ActionButtonBuilder {
|
||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
||||
|
||||
static const int defaultQuickActionLimit = 4;
|
||||
|
||||
static const List<ActionButtonType> defaultQuickActionSeed = [
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.upload,
|
||||
ActionButtonType.edit,
|
||||
ActionButtonType.add,
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.delete,
|
||||
ActionButtonType.removeFromAlbum,
|
||||
ActionButtonType.likeActivity,
|
||||
];
|
||||
|
||||
static final Set<ActionButtonType> _quickActionSet = Set<ActionButtonType>.unmodifiable(defaultQuickActionSeed);
|
||||
|
||||
static final List<ActionButtonType> defaultQuickActionOrder = List<ActionButtonType>.unmodifiable(
|
||||
defaultQuickActionSeed,
|
||||
);
|
||||
|
||||
static List<ActionButtonType> get quickActionOptions => defaultQuickActionOrder;
|
||||
|
||||
static List<ActionButtonType> buildQuickActionTypes(
|
||||
ActionButtonContext context, {
|
||||
List<ActionButtonType>? quickActionOrder,
|
||||
int limit = defaultQuickActionLimit,
|
||||
}) {
|
||||
final normalized = normalizeQuickActionOrder(
|
||||
quickActionOrder == null || quickActionOrder.isEmpty ? defaultQuickActionOrder : quickActionOrder,
|
||||
);
|
||||
|
||||
final seen = <ActionButtonType>{};
|
||||
final result = <ActionButtonType>[];
|
||||
|
||||
for (final type in normalized) {
|
||||
if (!_quickActionSet.contains(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final resolved = _resolveQuickActionType(type, context);
|
||||
if (!seen.add(resolved) || !resolved.shouldShow(context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(resolved);
|
||||
if (result.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Build a list of quick action widgets based on context and custom order.
|
||||
/// Uses QuickActionService for business logic.
|
||||
static List<Widget> buildQuickActions(
|
||||
ActionButtonContext context, {
|
||||
List<ActionButtonType>? quickActionOrder,
|
||||
int limit = defaultQuickActionLimit,
|
||||
required List<ActionButtonType> quickActionTypes,
|
||||
}) {
|
||||
final types = buildQuickActionTypes(context, quickActionOrder: quickActionOrder, limit: limit);
|
||||
return types.map((type) => type.buildButton(context)).toList();
|
||||
return quickActionTypes.map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
|
||||
/// Build all available action button widgets for the given context.
|
||||
static List<Widget> build(ActionButtonContext context) {
|
||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||
}
|
||||
|
||||
static List<ActionButtonType> normalizeQuickActionOrder(List<ActionButtonType> order) {
|
||||
final ordered = <ActionButtonType>{};
|
||||
|
||||
for (final type in order) {
|
||||
if (_quickActionSet.contains(type)) {
|
||||
ordered.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
ordered.addAll(defaultQuickActionSeed);
|
||||
|
||||
return ordered.toList(growable: false);
|
||||
}
|
||||
|
||||
static ActionButtonType _resolveQuickActionType(ActionButtonType type, ActionButtonContext context) {
|
||||
if (type == ActionButtonType.archive && context.isArchived) {
|
||||
return ActionButtonType.unarchive;
|
||||
}
|
||||
|
||||
if (type == ActionButtonType.delete && context.asset.isLocalOnly) {
|
||||
return ActionButtonType.deleteLocal;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
static bool isSupportedQuickAction(ActionButtonType type) => _quickActionSet.contains(type);
|
||||
}
|
||||
|
||||
150
mobile/test/domain/services/quick_action_service_test.dart
Normal file
150
mobile/test/domain/services/quick_action_service_test.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||
|
||||
void main() {
|
||||
group('QuickActionService', () {
|
||||
late QuickActionService service;
|
||||
|
||||
setUp(() {
|
||||
// Use repository with default behavior for testing
|
||||
service = const QuickActionService(ActionButtonOrderRepository());
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should respect custom order', () {
|
||||
final remoteAsset = RemoteAsset(
|
||||
id: 'test-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
ownerId: 'owner-id',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final customOrder = [
|
||||
ActionButtonType.archive,
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.edit,
|
||||
ActionButtonType.delete,
|
||||
];
|
||||
|
||||
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||
|
||||
expect(types.length, lessThanOrEqualTo(QuickActionService.defaultQuickActionLimit));
|
||||
expect(types.first, ActionButtonType.archive);
|
||||
expect(types[1], ActionButtonType.share);
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should resolve archive to unarchive when archived', () {
|
||||
final remoteAsset = RemoteAsset(
|
||||
id: 'test-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
ownerId: 'owner-id',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: true, // archived
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final customOrder = [ActionButtonType.archive];
|
||||
|
||||
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||
|
||||
expect(types.contains(ActionButtonType.unarchive), isTrue);
|
||||
expect(types.contains(ActionButtonType.archive), isFalse);
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should filter types that shouldShow returns false', () {
|
||||
final localAsset = LocalAsset(
|
||||
id: 'local-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: localAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final customOrder = [
|
||||
ActionButtonType.archive, // should not show for local-only asset
|
||||
ActionButtonType.share,
|
||||
];
|
||||
|
||||
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||
|
||||
expect(types.contains(ActionButtonType.archive), isFalse);
|
||||
expect(types.contains(ActionButtonType.share), isTrue);
|
||||
});
|
||||
|
||||
test('buildQuickActionTypes should respect limit', () {
|
||||
final remoteAsset = RemoteAsset(
|
||||
id: 'test-id',
|
||||
name: 'test.jpg',
|
||||
checksum: 'checksum',
|
||||
type: AssetType.image,
|
||||
ownerId: 'owner-id',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
final context = ActionButtonContext(
|
||||
asset: remoteAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
isStacked: false,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final types = service.buildQuickActionTypes(
|
||||
context,
|
||||
quickActionOrder: QuickActionService.defaultQuickActionOrder,
|
||||
limit: 2,
|
||||
);
|
||||
|
||||
expect(types.length, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
@@ -1029,7 +1031,8 @@ void main() {
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
final quickActions = ActionButtonBuilder.buildQuickActions(
|
||||
final quickActionService = const QuickActionService(ActionButtonOrderRepository());
|
||||
final quickActionTypes = quickActionService.buildQuickActionTypes(
|
||||
context,
|
||||
quickActionOrder: const [
|
||||
ActionButtonType.archive,
|
||||
@@ -1039,7 +1042,9 @@ void main() {
|
||||
],
|
||||
);
|
||||
|
||||
expect(quickActions.length, ActionButtonBuilder.defaultQuickActionLimit);
|
||||
final quickActions = ActionButtonBuilder.buildQuickActions(context, quickActionTypes: quickActionTypes);
|
||||
|
||||
expect(quickActions.length, QuickActionService.defaultQuickActionLimit);
|
||||
expect(quickActions.first, isA<ArchiveActionButton>());
|
||||
expect(quickActions[1], isA<ShareActionButton>());
|
||||
expect(quickActions[2], isA<EditImageActionButton>());
|
||||
|
||||
Reference in New Issue
Block a user