refactor: move timeline config to metadata table (#28227)

* migrate timeline config

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
pull/28228/head^2
shenlong 2026-05-13 02:53:25 +07:00 committed by GitHub
parent 58beac8fe0
commit 91ac56cef2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 135 additions and 123 deletions

View File

@ -1,25 +1,40 @@
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart';
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
class AppConfig {
final ThemeConfig theme;
final CleanupConfig cleanup;
final MapConfig map;
final TimelineConfig timeline;
const AppConfig({this.theme = const .new(), this.cleanup = const .new(), this.map = const .new()});
const AppConfig({
this.theme = const .new(),
this.cleanup = const .new(),
this.map = const .new(),
this.timeline = const .new(),
});
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup, MapConfig? map}) =>
.new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, map: map ?? this.map);
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup, MapConfig? map, TimelineConfig? timeline}) => .new(
theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup,
map: map ?? this.map,
timeline: timeline ?? this.timeline,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AppConfig && other.theme == theme && other.cleanup == cleanup && other.map == map);
(other is AppConfig &&
other.theme == theme &&
other.cleanup == cleanup &&
other.map == map &&
other.timeline == timeline);
@override
int get hashCode => Object.hash(theme, cleanup, map);
int get hashCode => Object.hash(theme, cleanup, map, timeline);
@override
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map)';
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline)';
}

View File

@ -0,0 +1,30 @@
import 'package:immich_mobile/domain/models/timeline.model.dart';
class TimelineConfig {
final int tilesPerRow;
final GroupAssetsBy groupAssetsBy;
final bool storageIndicator;
const TimelineConfig({this.tilesPerRow = 4, this.groupAssetsBy = GroupAssetsBy.day, this.storageIndicator = true});
TimelineConfig copyWith({int? tilesPerRow, GroupAssetsBy? groupAssetsBy, bool? storageIndicator}) => TimelineConfig(
tilesPerRow: tilesPerRow ?? this.tilesPerRow,
groupAssetsBy: groupAssetsBy ?? this.groupAssetsBy,
storageIndicator: storageIndicator ?? this.storageIndicator,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TimelineConfig &&
other.tilesPerRow == tilesPerRow &&
other.groupAssetsBy == groupAssetsBy &&
other.storageIndicator == storageIndicator);
@override
int get hashCode => Object.hash(tilesPerRow, groupAssetsBy, storageIndicator);
@override
String toString() =>
'TimelineConfig(tilesPerRow: $tilesPerRow, groupAssetsBy: $groupAssetsBy, storageIndicator: $storageIndicator)';
}

View File

@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/enums.dart';
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';
enum MetadataDomain<T extends Object> {
appConfig<AppConfig>('config.app'),
@ -23,6 +24,16 @@ enum MetadataKey<T extends Object> {
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
// Timeline
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
timelineGroupAssetsBy<GroupAssetsBy>(
.appConfig,
'timeline.groupAssetsBy',
GroupAssetsBy.day,
_EnumCodec(GroupAssetsBy.values),
),
timelineStorageIndicator<bool>(.appConfig, 'timeline.storageIndicator', true),
// Log
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),

View File

@ -1,9 +1,6 @@
import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> {
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
loadOriginal<bool>(StoreKey.loadOriginal, false),
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),

View File

@ -19,29 +19,17 @@ enum StoreKey<T> {
backgroundBackup<bool>._(14),
sslClientCertData<String>._(15),
sslClientPasswd<String>._(16),
// user settings from [AppSettingsEnum] below:
loadPreview<bool>._(100),
loadOriginal<bool>._(101),
tilesPerRow<int>._(103),
dynamicLayout<bool>._(104),
groupAssetsBy<int>._(105),
uploadErrorNotificationGracePeriod<int>._(106),
backgroundBackupTotalProgress<bool>._(107),
backgroundBackupSingleProgress<bool>._(108),
storageIndicator<bool>._(109),
thumbnailCacheSize<int>._(110),
imageCacheSize<int>._(111),
albumThumbnailCacheSize<int>._(112),
selectedAlbumSortOrder<int>._(113),
advancedTroubleshooting<bool>._(114),
preferRemoteImage<bool>._(116),
loopVideo<bool>._(117),
selfSignedCert<bool>._(120),
ignoreIcloudAssets<bool>._(122),
selectedAlbumSortReverse<bool>._(123),
enableHapticFeedback<bool>._(126),
customHeaders<String>._(127),
syncAlbums<bool>._(131),
// Auto endpoint switching
@ -50,31 +38,22 @@ enum StoreKey<T> {
localEndpoint<String>._(134),
externalEndpointList<String>._(135),
// Video settings
loadOriginalVideo<bool>._(136),
manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings
readonlyModeEnabled<bool>._(138),
autoPlayVideo<bool>._(139),
albumGridView<bool>._(140),
loadOriginal<bool>._(101),
// Image viewer navigation settings
loopVideo<bool>._(117),
loadOriginalVideo<bool>._(136),
autoPlayVideo<bool>._(139),
tapToNavigate<bool>._(141),
// Experimental stuff
photoManagerCustomFilter<bool>._(1000),
betaPromptShown<bool>._(1001),
betaTimeline<bool>._(1002),
enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005),
needBetaMigration<bool>._(1006),
// TODO: Remove this after patching open-api
shouldResetSync<bool>._(1007),
// Free up space
syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store
@ -87,6 +66,9 @@ enum StoreKey<T> {
legacyCleanupKeepAlbumIds<String>._(1010),
legacyCleanupCutoffDaysAgo<int>._(1011),
legacyCleanupDefaultsInitialized<bool>._(1012),
legacyTilesPerRow<int>._(103),
legacyGroupAssetsBy<int>._(105),
legacyStorageIndicator<bool>._(109),
legacyMapRelativeDate<int>._(119),
legacyMapShowFavoriteOnly<bool>._(118),
legacyMapIncludeArchived<bool>._(121),

View File

@ -93,8 +93,7 @@ class LocalSyncService {
if (CurrentPlatform.isIOS) {
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled,
// remove the albums from the local database from the previous sync
// does not include changes for cloud albums.
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
for (final album in cloudAlbums) {
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);

View File

@ -5,10 +5,9 @@ import 'package:collection/collection.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
@ -40,14 +39,16 @@ enum TimelineOrigin {
class TimelineFactory {
final DriftTimelineRepository _timelineRepository;
final SettingsService _settingsService;
final MetadataRepository _metadataRepository;
const TimelineFactory({required DriftTimelineRepository timelineRepository, required SettingsService settingsService})
: _timelineRepository = timelineRepository,
_settingsService = settingsService;
const TimelineFactory({
required DriftTimelineRepository timelineRepository,
required MetadataRepository metadataRepository,
}) : _timelineRepository = timelineRepository,
_metadataRepository = metadataRepository;
GroupAssetsBy get groupBy {
final group = GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
// We do not support auto grouping in the new timeline yet, fallback to day grouping
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
}

View File

@ -127,6 +127,11 @@ extension<T extends Object> on MetadataDomain<T> {
themeMode: repo._read(.mapThemeMode),
withPartners: repo._read(.mapWithPartners),
),
timeline: .new(
tilesPerRow: repo._read(.timelineTilesPerRow),
groupAssetsBy: repo._read(.timelineGroupAssetsBy),
storageIndicator: repo._read(.timelineStorageIndicator),
),
);
case .systemConfig:
repo._systemConfig = .new(logLevel: repo._read(.logLevel));

View File

@ -3,9 +3,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/semver.dart';
@ -38,7 +36,6 @@ class SyncApiRepository {
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
final shouldReset = Store.get(StoreKey.shouldResetSync, false);
final request = http.Request('POST', Uri.parse(endpoint));
request.headers.addAll(headers);
request.body = jsonEncode(
@ -77,7 +74,6 @@ class SyncApiRepository {
? SyncRequestType.assetFacesV2
: SyncRequestType.assetFacesV1,
],
reset: shouldReset,
).toJson(),
);
@ -101,9 +97,6 @@ class SyncApiRepository {
throw ApiException(response.statusCode, 'Failed to get sync stream: $errorBody');
}
// Reset after successful stream start
await Store.put(StoreKey.shouldResetSync, false);
await for (final chunk in response.stream.transform(utf8.decoder)) {
if (shouldAbort) {
break;

View File

@ -2,15 +2,14 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class ThumbnailTile extends ConsumerStatefulWidget {
@ -61,7 +60,7 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
);
final bool storageIndicator =
ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && widget.showStorageIndicator;
ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)) && widget.showStorageIndicator;
if (!isCurrentAsset) {
_hideIndicators = false;

View File

@ -1,12 +1,11 @@
import 'dart:math' as math;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class TimelineArgs {
@ -93,7 +92,7 @@ final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref)
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
final tileExtent = math.max(0, availableTileWidth) / columnCount;
final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
final groupBy = args.groupBy ?? ref.watch(appConfigProvider.select((config) => config.timeline.groupAssetsBy));
final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) {
@ -102,7 +101,7 @@ final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref)
tileHeight: tileExtent,
columnCount: columnCount,
spacing: spacing,
groupBy: groupBy,
groupBy: groupBy!,
).generate();
});
}, dependencies: [timelineServiceProvider, timelineArgsProvider]);

View File

@ -10,7 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
@ -22,8 +22,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
@ -74,7 +74,7 @@ class Timeline extends StatelessWidget {
(ref) => TimelineArgs(
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
columnCount: ref.watch(appConfigProvider.select((config) => config.timeline.tilesPerRow)),
showStorageIndicator: showStorageIndicator,
withStack: withStack,
groupBy: groupBy,
@ -161,7 +161,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
_eventSubscription = EventStream.shared.listen(_onEvent);
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
final currentTilesPerRow = ref.read(appConfigProvider.select((config) => config.timeline.tilesPerRow));
_perRow = currentTilesPerRow;
_scaleFactor = 7.0 - _perRow;
_baseScaleFactor = _scaleFactor;
@ -459,7 +459,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
_restoreAssetIndex = targetAssetIndex;
});
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, _perRow);
}
};
},

View File

@ -3,7 +3,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
final timelineRepositoryProvider = Provider<DriftTimelineRepository>(
@ -29,7 +29,7 @@ final timelineServiceProvider = Provider<TimelineService>(
final timelineFactoryProvider = Provider<TimelineFactory>(
(ref) => TimelineFactory(
timelineRepository: ref.watch(timelineRepositoryProvider),
settingsService: ref.watch(settingsProvider),
metadataRepository: ref.watch(metadataProvider),
),
);

View File

@ -2,23 +2,12 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum<T> {
loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true),
loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false),
groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0),
uploadErrorNotificationGracePeriod<int>(
StoreKey.uploadErrorNotificationGracePeriod,
"uploadErrorNotificationGracePeriod",
2,
),
backgroundBackupTotalProgress<bool>(StoreKey.backgroundBackupTotalProgress, "backgroundBackupTotalProgress", true),
backgroundBackupSingleProgress<bool>(
StoreKey.backgroundBackupSingleProgress,
"backgroundBackupSingleProgress",
false,
),
storageIndicator<bool>(StoreKey.storageIndicator, "storageIndicator", true),
thumbnailCacheSize<int>(StoreKey.thumbnailCacheSize, "thumbnailCacheSize", 10000),
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
albumThumbnailCacheSize<int>(StoreKey.albumThumbnailCacheSize, "albumThumbnailCacheSize", 200),
@ -31,13 +20,10 @@ enum AppSettingsEnum<T> {
autoPlayVideo<bool>(StoreKey.autoPlayVideo, "autoPlayVideo", true),
tapToNavigate<bool>(StoreKey.tapToNavigate, "tapToNavigate", false),
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
ignoreIcloudAssets<bool>(StoreKey.ignoreIcloudAssets, null, false),
selectedAlbumSortReverse<bool>(StoreKey.selectedAlbumSortReverse, null, true),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true),
betaTimeline<bool>(StoreKey.betaTimeline, null, true),
enableBackup<bool>(StoreKey.enableBackup, null, false),
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false),

View File

@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@ -79,6 +80,14 @@ Future<void> _migrateTo26(Drift drift) async {
await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, MetadataKey.mapIncludeArchived);
await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, MetadataKey.mapThemeMode, ThemeMode.values);
await migrator.migrateBool(StoreKey.legacyMapwithPartners, MetadataKey.mapWithPartners);
// Timeline
await migrator.migrateInt(StoreKey.legacyTilesPerRow, MetadataKey.timelineTilesPerRow);
await migrator.migrateEnumIndex(
StoreKey.legacyGroupAssetsBy,
MetadataKey.timelineGroupAssetsBy,
GroupAssetsBy.values,
);
await migrator.migrateBool(StoreKey.legacyStorageIndicator, MetadataKey.timelineStorageIndicator);
await migrator.complete();
}

View File

@ -1,12 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
@ -15,18 +16,17 @@ class GroupSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final groupByIndex = useAppSettingsState(AppSettingsEnum.groupAssetsBy);
final groupBy = GroupAssetsBy.values[groupByIndex.value];
final groupBy = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.groupAssetsBy)));
Future<void> updateAppSettings(GroupAssetsBy groupBy) async {
await ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.groupAssetsBy, groupBy.index);
await ref.read(metadataProvider).write(MetadataKey.timelineGroupAssetsBy, groupBy);
ref.invalidate(appSettingsServiceProvider);
}
void changeGroupValue(GroupAssetsBy? value) {
if (value != null) {
groupByIndex.value = value.index;
unawaited(updateAppSettings(groupBy));
groupBy.value = value;
unawaited(updateAppSettings(value));
}
}
@ -52,7 +52,7 @@ class GroupSettings extends HookConsumerWidget {
value: GroupAssetsBy.auto,
),
],
groupBy: groupBy,
groupBy: groupBy.value,
onRadioChanged: changeGroupValue,
),
],

View File

@ -1,10 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
@ -13,7 +14,10 @@ class LayoutSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow);
final tilesPerRow = useState(ref.read(appConfigProvider.select((s) => s.timeline.tilesPerRow)));
useValueChanged<int, void>(tilesPerRow.value, (_, __) {
ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, tilesPerRow.value);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -29,7 +33,9 @@ class LayoutSettings extends HookConsumerWidget {
maxValue: 6,
minValue: 2,
noDivisons: 4,
onChangeEnd: (_) => ref.invalidate(appSettingsServiceProvider),
onChangeEnd: (value) {
ref.invalidate(appSettingsServiceProvider);
},
),
],
);

View File

@ -1,10 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_layout_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
@ -15,13 +16,14 @@ class AssetListSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final showStorageIndicator = useAppSettingsState(AppSettingsEnum.storageIndicator);
final storageIndicator = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)));
final assetListSetting = [
SettingsSwitchListTile(
valueNotifier: showStorageIndicator,
valueNotifier: storageIndicator,
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
onChanged: (_) {
onChanged: (value) {
ref.read(metadataProvider).write(MetadataKey.timelineStorageIndicator, value);
ref.invalidate(appSettingsServiceProvider);
ref.invalidate(settingsProvider);
},

View File

@ -3,16 +3,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class ImageViewerQualitySetting extends HookConsumerWidget {
const ImageViewerQualitySetting({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
return Column(
@ -23,12 +22,6 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
icon: Icons.image_outlined,
subtitle: "setting_image_viewer_help".t(context: context),
),
SettingsSwitchListTile(
valueNotifier: isPreview,
title: "setting_image_viewer_preview_title".t(context: context),
subtitle: "setting_image_viewer_preview_subtitle".t(context: context),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".t(context: context),

View File

@ -8,7 +8,6 @@ import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:permission_handler/permission_handler.dart';
class NotificationSetting extends HookConsumerWidget {
@ -19,8 +18,6 @@ class NotificationSetting extends HookConsumerWidget {
final permissionService = ref.watch(notificationPermissionProvider);
final sliderValue = useAppSettingsState(AppSettingsEnum.uploadErrorNotificationGracePeriod);
final totalProgressValue = useAppSettingsState(AppSettingsEnum.backgroundBackupTotalProgress);
final singleProgressValue = useAppSettingsState(AppSettingsEnum.backgroundBackupSingleProgress);
final hasPermission = permissionService == PermissionStatus.granted;
@ -60,18 +57,6 @@ class NotificationSetting extends HookConsumerWidget {
}
}),
),
SettingsSwitchListTile(
enabled: hasPermission,
valueNotifier: totalProgressValue,
title: 'setting_notifications_total_progress_title'.tr(),
subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
),
SettingsSwitchListTile(
enabled: hasPermission,
valueNotifier: singleProgressValue,
title: 'setting_notifications_single_progress_title'.tr(),
subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
),
SettingsSliderListTile(
enabled: hasPermission,
valueNotifier: sliderValue,

View File

@ -10,7 +10,7 @@ import '../../infrastructure/repository.mock.dart';
const _kAccessToken = '#ThisIsAToken';
const _kBackgroundBackup = false;
const _kGroupAssetsBy = 2;
const _kVersion = 2;
final _kBackupFailedSince = DateTime.utc(2023);
void main() {
@ -31,7 +31,7 @@ void main() {
(_) async => [
const StoreDto(StoreKey.accessToken, _kAccessToken),
const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy),
const StoreDto(StoreKey.version, _kVersion),
StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
],
);
@ -50,7 +50,7 @@ void main() {
verify(() => mockDriftStoreRepo.getAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup);
expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy);
expect(sut.tryGet(StoreKey.version), _kVersion);
expect(sut.tryGet(StoreKey.backupFailedSince), _kBackupFailedSince);
// Other keys should be null
expect(sut.tryGet(StoreKey.currentUser), isNull);
@ -152,7 +152,7 @@ void main() {
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), isNull);
expect(sut.tryGet(StoreKey.backgroundBackup), isNull);
expect(sut.tryGet(StoreKey.groupAssetsBy), isNull);
expect(sut.tryGet(StoreKey.version), isNull);
expect(sut.tryGet(StoreKey.backupFailedSince), isNull);
});
});