diff --git a/mobile/lib/domain/models/config/album_config.dart b/mobile/lib/domain/models/config/album_config.dart new file mode 100644 index 0000000000..a83fc32fbf --- /dev/null +++ b/mobile/lib/domain/models/config/album_config.dart @@ -0,0 +1,26 @@ +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; + +class AlbumConfig { + final AlbumSortMode sortMode; + final bool isReverse; + final bool isGrid; + + const AlbumConfig({this.sortMode = AlbumSortMode.mostRecent, this.isReverse = true, this.isGrid = false}); + + AlbumConfig copyWith({AlbumSortMode? sortMode, bool? isReverse, bool? isGrid}) => AlbumConfig( + sortMode: sortMode ?? this.sortMode, + isReverse: isReverse ?? this.isReverse, + isGrid: isGrid ?? this.isGrid, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AlbumConfig && other.sortMode == sortMode && other.isReverse == isReverse && other.isGrid == isGrid); + + @override + int get hashCode => Object.hash(sortMode, isReverse, isGrid); + + @override + String toString() => 'AlbumConfig(sortMode: $sortMode, isReverse: $isReverse, isGrid: $isGrid)'; +} diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index e639b7b7e4..b988983ef1 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -1,10 +1,11 @@ +import 'package:immich_mobile/domain/models/config/album_config.dart'; import 'package:immich_mobile/domain/models/config/cleanup_config.dart'; import 'package:immich_mobile/domain/models/config/image_config.dart'; import 'package:immich_mobile/domain/models/config/map_config.dart'; +import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; import 'package:immich_mobile/domain/models/config/theme_config.dart'; import 'package:immich_mobile/domain/models/config/timeline_config.dart'; import 'package:immich_mobile/domain/models/config/viewer_config.dart'; -import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; class AppConfig { final ThemeConfig theme; @@ -14,6 +15,7 @@ class AppConfig { final ImageConfig image; final ViewerConfig viewer; final SlideshowConfig slideshow; + final AlbumConfig album; const AppConfig({ this.theme = const .new(), @@ -23,6 +25,7 @@ class AppConfig { this.image = const .new(), this.viewer = const .new(), this.slideshow = const .new(), + this.album = const .new(), }); AppConfig copyWith({ @@ -33,6 +36,7 @@ class AppConfig { ImageConfig? image, ViewerConfig? viewer, SlideshowConfig? slideshow, + AlbumConfig? album, }) => .new( theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, @@ -41,6 +45,7 @@ class AppConfig { image: image ?? this.image, viewer: viewer ?? this.viewer, slideshow: slideshow ?? this.slideshow, + album: album ?? this.album, ); @override @@ -53,12 +58,13 @@ class AppConfig { other.timeline == timeline && other.image == image && other.viewer == viewer && - other.slideshow == slideshow); + other.slideshow == slideshow && + other.album == album); @override - int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow); + int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album); @override String toString() => - 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow)'; + 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album)'; } diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index f221bf6abf..67af27ecbc 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/config/system_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; enum MetadataDomain { appConfig('config.app'), @@ -51,6 +52,16 @@ enum MetadataKey { _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string), ), + // Album + albumSortMode( + .appConfig, + 'album.sortMode', + AlbumSortMode.mostRecent, + _EnumCodec(AlbumSortMode.values), + ), + albumIsReverse(.appConfig, 'album.isReverse', true), + albumIsGrid(.appConfig, 'album.isGrid', false), + // Timeline timelineTilesPerRow(.appConfig, 'timeline.tilesPerRow', 4), timelineGroupAssetsBy( diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 2d753dfa78..5562e43869 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -11,16 +11,13 @@ enum StoreKey { serverUrl._(10), accessToken._(11), serverEndpoint._(12), - selectedAlbumSortOrder._(113), advancedTroubleshooting._(114), - selectedAlbumSortReverse._(123), enableHapticFeedback._(126), syncAlbums._(131), manageLocalMediaAndroid._(137), // Read-only Mode settings readonlyModeEnabled._(138), - albumGridView._(140), // Experimental stuff enableBackup._(1003), @@ -29,6 +26,9 @@ enum StoreKey { syncMigrationStatus._(1013), // Legacy keys that have been migrated to the new metadata store + legacySelectedAlbumSortOrder._(113), + legacySelectedAlbumSortReverse._(123), + legacyAlbumGridView._(140), legacyAutoEndpointSwitching._(132), legacyPreferredWifiName._(133), legacyLocalEndpoint._(134), diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index d8575cd9a8..86dcf220c4 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -147,6 +147,11 @@ extension on MetadataDomain { look: repo._read(.slideshowLook), direction: repo._read(.slideshowDirection), ), + album: .new( + sortMode: repo._read(.albumSortMode), + isReverse: repo._read(.albumIsReverse), + isGrid: repo._read(.albumIsGrid), + ), ); case .systemConfig: repo._systemConfig = .new( diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index c68a7273e0..6241623978 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -15,15 +15,15 @@ import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/album_filter.utils.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -58,19 +58,11 @@ class _AlbumSelectorState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - final appSettings = ref.read(appSettingsServiceProvider); - final savedSortMode = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortOrder); - final savedIsReverse = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortReverse); - final savedIsGrid = appSettings.getSetting(AppSettingsEnum.albumGridView); - - final albumSortMode = AlbumSortMode.values.firstWhere( - (e) => e.storeIndex == savedSortMode, - orElse: () => AlbumSortMode.lastModified, - ); + final albumConfig = ref.read(metadataProvider).appConfig.album; setState(() { - sort = AlbumSort(mode: albumSortMode, isReverse: savedIsReverse); - isGrid = savedIsGrid; + sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse); + isGrid = albumConfig.isGrid; }); ref.read(remoteAlbumProvider.notifier).refresh(); @@ -102,7 +94,7 @@ class _AlbumSelectorState extends ConsumerState { setState(() { isGrid = !isGrid; }); - ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.albumGridView, isGrid); + ref.read(metadataProvider).write(MetadataKey.albumIsGrid, isGrid); } void changeFilter(QuickFilterMode mode) { @@ -118,9 +110,9 @@ class _AlbumSelectorState extends ConsumerState { this.sort = sort; }); - final appSettings = ref.read(appSettingsServiceProvider); - await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortOrder, sort.mode.storeIndex); - await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortReverse, sort.isReverse); + final metadata = ref.read(metadataProvider); + await metadata.write(MetadataKey.albumSortMode, sort.mode); + await metadata.write(MetadataKey.albumIsReverse, sort.isReverse); await sortAlbums(); } diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index c5a316e655..31f631d86f 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -2,17 +2,14 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { - selectedAlbumSortOrder(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2), advancedTroubleshooting(StoreKey.advancedTroubleshooting, null, false), manageLocalMediaAndroid(StoreKey.manageLocalMediaAndroid, null, false), - selectedAlbumSortReverse(StoreKey.selectedAlbumSortReverse, null, true), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), syncAlbums(StoreKey.syncAlbums, null, false), enableBackup(StoreKey.enableBackup, null, false), useCellularForUploadVideos(StoreKey.useWifiForUploadVideos, null, false), useCellularForUploadPhotos(StoreKey.useWifiForUploadPhotos, null, false), readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false), - albumGridView(StoreKey.albumGridView, "albumGridView", false), backupRequireCharging(StoreKey.backupRequireCharging, null, false), backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index f0d82d6c06..f9d7a69a9a 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; const int targetVersion = 26; @@ -81,14 +82,7 @@ Future _migrateTo26(Drift drift) async { final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id); if (cleanupKeepAlbumIds != null) { final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); - await drift.metadataEntity.insertOnConflictUpdate( - MetadataEntityCompanion.insert( - key: MetadataKey.cleanupKeepAlbumIds.key, - value: MetadataKey.cleanupKeepAlbumIds.encode(ids), - updatedAt: Value(DateTime.now()), - ), - ); - await migrator.deleteLegacyStoreRows([StoreKey.legacyCleanupKeepAlbumIds.id]); + migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, MetadataKey.cleanupKeepAlbumIds, ids); } await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites); await migrator.migrateEnumIndex( @@ -124,12 +118,30 @@ Future _migrateTo26(Drift drift) async { await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, MetadataKey.networkAutoEndpointSwitching); await migrator.migrateString(StoreKey.legacyPreferredWifiName, MetadataKey.networkPreferredWifiName); await migrator.migrateString(StoreKey.legacyLocalEndpoint, MetadataKey.networkLocalEndpoint); - await _migrateExternalEndpointList(drift, migrator); - await _migrateCustomHeaders(drift, migrator); + await _migrateExternalEndpointList(migrator); + await _migrateCustomHeaders(migrator); + // Album + await _migrateAlbumSortMode(migrator); + await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse); + await migrator.migrateBool(StoreKey.legacyAlbumGridView, MetadataKey.albumIsGrid); await migrator.complete(); } -Future _migrateExternalEndpointList(Drift drift, _StoreMigrator migrator) async { +Future _migrateAlbumSortMode(_StoreMigrator migrator) async { + final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id); + if (raw == null) { + return; + } + + final mode = AlbumSortMode.values.firstWhere( + (e) => e.storeIndex == raw, + orElse: () => MetadataKey.albumSortMode.defaultValue, + ); + + migrator.stage(StoreKey.legacySelectedAlbumSortOrder, MetadataKey.albumSortMode, mode); +} + +Future _migrateExternalEndpointList(_StoreMigrator migrator) async { final raw = await migrator.readLegacyStoreString(StoreKey.legacyExternalEndpointList.id); if (raw == null) { return; @@ -150,17 +162,10 @@ Future _migrateExternalEndpointList(Drift drift, _StoreMigrator migrator) // ignore invalid entries } - await drift.metadataEntity.insertOnConflictUpdate( - MetadataEntityCompanion.insert( - key: MetadataKey.networkExternalEndpointList.key, - value: MetadataKey.networkExternalEndpointList.encode(urls), - updatedAt: Value(DateTime.now()), - ), - ); - await migrator.deleteLegacyStoreRows([StoreKey.legacyExternalEndpointList.id]); + migrator.stage(StoreKey.legacyExternalEndpointList, MetadataKey.networkExternalEndpointList, urls); } -Future _migrateCustomHeaders(Drift drift, _StoreMigrator migrator) async { +Future _migrateCustomHeaders(_StoreMigrator migrator) async { final raw = await migrator.readLegacyStoreString(StoreKey.legacyCustomHeaders.id); if (raw == null) { return; @@ -180,14 +185,7 @@ Future _migrateCustomHeaders(Drift drift, _StoreMigrator migrator) async { // ignore invalid entries } - await drift.metadataEntity.insertOnConflictUpdate( - MetadataEntityCompanion.insert( - key: MetadataKey.networkCustomHeaders.key, - value: MetadataKey.networkCustomHeaders.encode(headers), - updatedAt: Value(DateTime.now()), - ), - ); - await migrator.deleteLegacyStoreRows([StoreKey.legacyCustomHeaders.id]); + migrator.stage(StoreKey.legacyCustomHeaders, MetadataKey.networkCustomHeaders, headers); } class _StoreMigrator { @@ -254,6 +252,11 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } + void stage(StoreKey legacyKey, MetadataKey newKey, T value) { + _cache[newKey] = value; + _migratedStoreIds.add(legacyKey.id); + } + Future complete() async { await _db.batch((batch) { for (final entry in _cache.entries) {