refactor: update viewer quick action order handling and refactor related utilities

feature/rearrange-buttons-2
idubnori 2025-12-09 14:56:57 +07:00
parent 1e5c3d7d37
commit 2d4e901c55
6 changed files with 30 additions and 89 deletions

@ -1,4 +1,5 @@
import 'package:immich_mobile/domain/models/user.model.dart'; 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`. /// Key for each possible value in the `Store`.
/// Defines the data type for each value /// Defines the data type for each value
@ -72,7 +73,7 @@ enum StoreKey<T> {
autoPlayVideo<bool>._(139), autoPlayVideo<bool>._(139),
albumGridView<bool>._(140), albumGridView<bool>._(140),
viewerQuickActionOrder<String>._(141), viewerQuickActionOrder<List<ActionButtonType>>._(141),
// Experimental stuff // Experimental stuff
photoManagerCustomFilter<bool>._(1000), photoManagerCustomFilter<bool>._(1000),

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
@ -5,6 +7,7 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.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/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.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'; import 'package:isar/isar.dart';
// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite // Temporary interface until Isar is removed to make the service work with both Isar and Sqlite
@ -84,6 +87,7 @@ class IsarStoreRepository extends IsarDatabaseRepository implements IStoreReposi
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) => const (UserDto) =>
entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!), entity.strValue == null ? null : await IsarUserRepository(_db).getByUserId(entity.strValue!),
const (List<ActionButtonType>) => jsonDecode(entity.strValue ?? '[]') as T,
_ => null, _ => null,
} }
as T?; as T?;
@ -95,6 +99,7 @@ class IsarStoreRepository extends IsarDatabaseRepository implements IStoreReposi
const (bool) => ((value as bool) ? 1 : 0, null), const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (null, (await IsarUserRepository(_db).update(value as UserDto)).id), 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}"), _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
}; };
return StoreValue(key.id, intValue: intValue, strValue: strValue); return StoreValue(key.id, intValue: intValue, strValue: strValue);
@ -174,6 +179,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) => const (UserDto) =>
entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!), entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!),
const (List<ActionButtonType>) => jsonDecode(entity.stringValue ?? '[]') as T,
_ => null, _ => null,
} }
as T?; as T?;
@ -185,6 +191,7 @@ class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepo
const (bool) => ((value as bool) ? 1 : 0, null), const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id), 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}"), _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
}; };
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue)); return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/action_button.utils.dart'; import 'package:immich_mobile/utils/action_button.utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -13,9 +14,11 @@ class ViewerQuickActionOrder extends _$ViewerQuickActionOrder {
@override @override
List<ActionButtonType> build() { List<ActionButtonType> build() {
final service = ref.watch(appSettingsServiceProvider); final service = ref.watch(appSettingsServiceProvider);
final initial = ActionButtonBuilder.normalizeQuickActionOrder(service.getViewerQuickActionOrder()); final initial = ActionButtonBuilder.normalizeQuickActionOrder(
service.getSetting(AppSettingsEnum.viewerQuickActionOrder),
);
_subscription ??= service.watchViewerQuickActionOrder().listen((order) { _subscription ??= service.watchSetting(AppSettingsEnum.viewerQuickActionOrder).listen((order) {
state = ActionButtonBuilder.normalizeQuickActionOrder(order); state = ActionButtonBuilder.normalizeQuickActionOrder(order);
}); });
@ -38,7 +41,7 @@ class ViewerQuickActionOrder extends _$ViewerQuickActionOrder {
state = normalized; state = normalized;
try { try {
await ref.read(appSettingsServiceProvider).setViewerQuickActionOrder(normalized); await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.viewerQuickActionOrder, normalized);
} catch (error) { } catch (error) {
state = previous; state = previous;
rethrow; rethrow;

@ -55,7 +55,12 @@ enum AppSettingsEnum<T> {
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false), readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false),
albumGridView<bool>(StoreKey.albumGridView, "albumGridView", false), albumGridView<bool>(StoreKey.albumGridView, "albumGridView", false),
backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false), backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false),
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30); backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30),
viewerQuickActionOrder<List<ActionButtonType>>(
StoreKey.viewerQuickActionOrder,
null,
ActionButtonBuilder.defaultQuickActionSeed,
);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
@ -66,6 +71,7 @@ enum AppSettingsEnum<T> {
class AppSettingsService { class AppSettingsService {
const AppSettingsService(); const AppSettingsService();
T getSetting<T>(AppSettingsEnum<T> setting) { T getSetting<T>(AppSettingsEnum<T> setting) {
return Store.get(setting.storeKey, setting.defaultValue); return Store.get(setting.storeKey, setting.defaultValue);
} }
@ -74,19 +80,7 @@ class AppSettingsService {
return Store.put(setting.storeKey, value); return Store.put(setting.storeKey, value);
} }
List<ActionButtonType> getViewerQuickActionOrder() { Stream<T> watchSetting<T>(AppSettingsEnum<T> setting) {
final stored = Store.get(StoreKey.viewerQuickActionOrder, ActionButtonBuilder.defaultQuickActionOrderStorageValue); return Store.watch(setting.storeKey).map((value) => value ?? setting.defaultValue);
return ActionButtonBuilder.parseQuickActionOrder(stored);
}
Stream<List<ActionButtonType>> watchViewerQuickActionOrder() {
return Store.watch(StoreKey.viewerQuickActionOrder).map(
(value) =>
ActionButtonBuilder.parseQuickActionOrder(value ?? ActionButtonBuilder.defaultQuickActionOrderStorageValue),
);
}
Future<void> setViewerQuickActionOrder(List<ActionButtonType> order) {
return Store.put(StoreKey.viewerQuickActionOrder, ActionButtonBuilder.encodeQuickActionOrder(order));
} }
} }

@ -67,6 +67,8 @@ enum ActionButtonType {
unstack, unstack,
likeActivity; likeActivity;
dynamic toJson() => name;
bool shouldShow(ActionButtonContext context) { bool shouldShow(ActionButtonContext context) {
return switch (this) { return switch (this) {
ActionButtonType.advancedInfo => context.advancedTroubleshooting, ActionButtonType.advancedInfo => context.advancedTroubleshooting,
@ -171,9 +173,8 @@ class ActionButtonBuilder {
static const List<ActionButtonType> _actionTypes = ActionButtonType.values; static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
static const int defaultQuickActionLimit = 4; static const int defaultQuickActionLimit = 4;
static const String quickActionStorageDelimiter = ',';
static const List<ActionButtonType> _defaultQuickActionSeed = [ static const List<ActionButtonType> defaultQuickActionSeed = [
ActionButtonType.share, ActionButtonType.share,
ActionButtonType.upload, ActionButtonType.upload,
ActionButtonType.edit, ActionButtonType.edit,
@ -184,47 +185,14 @@ class ActionButtonBuilder {
ActionButtonType.likeActivity, ActionButtonType.likeActivity,
]; ];
static final Set<ActionButtonType> _quickActionSet = Set<ActionButtonType>.unmodifiable(_defaultQuickActionSeed); static final Set<ActionButtonType> _quickActionSet = Set<ActionButtonType>.unmodifiable(defaultQuickActionSeed);
static final List<ActionButtonType> defaultQuickActionOrder = List<ActionButtonType>.unmodifiable( static final List<ActionButtonType> defaultQuickActionOrder = List<ActionButtonType>.unmodifiable(
_defaultQuickActionSeed, defaultQuickActionSeed,
); );
static final String defaultQuickActionOrderStorageValue = defaultQuickActionOrder
.map((type) => type.name)
.join(quickActionStorageDelimiter);
static List<ActionButtonType> get quickActionOptions => defaultQuickActionOrder; static List<ActionButtonType> get quickActionOptions => defaultQuickActionOrder;
static List<ActionButtonType> parseQuickActionOrder(String? stored) {
final parsed = <ActionButtonType>[];
if (stored != null && stored.trim().isNotEmpty) {
for (final name in stored.split(quickActionStorageDelimiter)) {
final type = _typeByName(name.trim());
if (type != null) {
parsed.add(type);
}
}
}
return normalizeQuickActionOrder(parsed);
}
static String encodeQuickActionOrder(List<ActionButtonType> order) {
final unique = <ActionButtonType>{};
final buffer = <String>[];
for (final type in order) {
if (unique.add(type)) {
buffer.add(type.name);
}
}
final result = buffer.join(quickActionStorageDelimiter);
return result;
}
static List<ActionButtonType> buildQuickActionTypes( static List<ActionButtonType> buildQuickActionTypes(
ActionButtonContext context, { ActionButtonContext context, {
List<ActionButtonType>? quickActionOrder, List<ActionButtonType>? quickActionOrder,
@ -265,20 +233,6 @@ class ActionButtonBuilder {
return types.map((type) => type.buildButton(context)).toList(); return types.map((type) => type.buildButton(context)).toList();
} }
static ActionButtonType? _typeByName(String name) {
if (name.isEmpty) {
return null;
}
for (final type in ActionButtonType.values) {
if (type.name == name) {
return type;
}
}
return null;
}
static List<Widget> build(ActionButtonContext context) { static List<Widget> build(ActionButtonContext context) {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
} }
@ -292,7 +246,7 @@ class ActionButtonBuilder {
} }
} }
ordered.addAll(_defaultQuickActionSeed); ordered.addAll(defaultQuickActionSeed);
return ordered.toList(growable: false); return ordered.toList(growable: false);
} }

@ -1015,24 +1015,6 @@ void main() {
expect(nonArchivedWidgets, isNotEmpty); expect(nonArchivedWidgets, isNotEmpty);
}); });
test('should encode and parse quick action order consistently', () {
final encoded = ActionButtonBuilder.encodeQuickActionOrder([
ActionButtonType.edit,
ActionButtonType.share,
ActionButtonType.archive,
]);
final decoded = ActionButtonBuilder.parseQuickActionOrder(encoded);
final expectedOrder = ActionButtonBuilder.normalizeQuickActionOrder([
ActionButtonType.edit,
ActionButtonType.share,
ActionButtonType.archive,
]);
expect(decoded, expectedOrder);
});
test('should build quick actions honoring custom order', () { test('should build quick actions honoring custom order', () {
final remoteAsset = createRemoteAsset(); final remoteAsset = createRemoteAsset();
final context = ActionButtonContext( final context = ActionButtonContext(