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/utils/action_button.utils.dart';
/// Key for each possible value in the `Store`.
/// Defines the data type for each value
@ -72,7 +73,7 @@ enum StoreKey<T> {
autoPlayVideo<bool>._(139),
albumGridView<bool>._(140),
viewerQuickActionOrder<String>._(141),
viewerQuickActionOrder<List<ActionButtonType>>._(141),
// Experimental stuff
photoManagerCustomFilter<bool>._(1000),

@ -1,3 +1,5 @@
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';
@ -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/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
@ -84,6 +87,7 @@ 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 T,
_ => null,
}
as T?;
@ -95,6 +99,7 @@ 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);
@ -174,6 +179,7 @@ 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 T,
_ => null,
}
as T?;
@ -185,6 +191,7 @@ 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));

@ -1,6 +1,7 @@
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:immich_mobile/utils/action_button.utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -13,9 +14,11 @@ class ViewerQuickActionOrder extends _$ViewerQuickActionOrder {
@override
List<ActionButtonType> build() {
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);
});
@ -38,7 +41,7 @@ class ViewerQuickActionOrder extends _$ViewerQuickActionOrder {
state = normalized;
try {
await ref.read(appSettingsServiceProvider).setViewerQuickActionOrder(normalized);
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.viewerQuickActionOrder, normalized);
} catch (error) {
state = previous;
rethrow;

@ -55,7 +55,12 @@ enum AppSettingsEnum<T> {
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false),
albumGridView<bool>(StoreKey.albumGridView, "albumGridView", 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);
@ -66,6 +71,7 @@ enum AppSettingsEnum<T> {
class AppSettingsService {
const AppSettingsService();
T getSetting<T>(AppSettingsEnum<T> setting) {
return Store.get(setting.storeKey, setting.defaultValue);
}
@ -74,19 +80,7 @@ class AppSettingsService {
return Store.put(setting.storeKey, value);
}
List<ActionButtonType> getViewerQuickActionOrder() {
final stored = Store.get(StoreKey.viewerQuickActionOrder, ActionButtonBuilder.defaultQuickActionOrderStorageValue);
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));
Stream<T> watchSetting<T>(AppSettingsEnum<T> setting) {
return Store.watch(setting.storeKey).map((value) => value ?? setting.defaultValue);
}
}

@ -67,6 +67,8 @@ enum ActionButtonType {
unstack,
likeActivity;
dynamic toJson() => name;
bool shouldShow(ActionButtonContext context) {
return switch (this) {
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
@ -171,9 +173,8 @@ class ActionButtonBuilder {
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
static const int defaultQuickActionLimit = 4;
static const String quickActionStorageDelimiter = ',';
static const List<ActionButtonType> _defaultQuickActionSeed = [
static const List<ActionButtonType> defaultQuickActionSeed = [
ActionButtonType.share,
ActionButtonType.upload,
ActionButtonType.edit,
@ -184,47 +185,14 @@ class ActionButtonBuilder {
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(
_defaultQuickActionSeed,
defaultQuickActionSeed,
);
static final String defaultQuickActionOrderStorageValue = defaultQuickActionOrder
.map((type) => type.name)
.join(quickActionStorageDelimiter);
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(
ActionButtonContext context, {
List<ActionButtonType>? quickActionOrder,
@ -265,20 +233,6 @@ class ActionButtonBuilder {
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) {
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);
}

@ -1015,24 +1015,6 @@ void main() {
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', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(