mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
migrate timeline config
This commit is contained in:
@@ -1,25 +1,40 @@
|
|||||||
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
|
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/map_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
||||||
|
|
||||||
class AppConfig {
|
class AppConfig {
|
||||||
final ThemeConfig theme;
|
final ThemeConfig theme;
|
||||||
final CleanupConfig cleanup;
|
final CleanupConfig cleanup;
|
||||||
final MapConfig map;
|
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}) =>
|
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup, MapConfig? map, TimelineConfig? timeline}) => .new(
|
||||||
.new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, map: map ?? this.map);
|
theme: theme ?? this.theme,
|
||||||
|
cleanup: cleanup ?? this.cleanup,
|
||||||
|
map: map ?? this.map,
|
||||||
|
timeline: timeline ?? this.timeline,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, 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
|
@override
|
||||||
int get hashCode => Object.hash(theme, cleanup, map);
|
int get hashCode => Object.hash(theme, cleanup, map, timeline);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map)';
|
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ class MapConfig {
|
|||||||
});
|
});
|
||||||
|
|
||||||
MapConfig copyWith({
|
MapConfig copyWith({
|
||||||
int? relativeDate,
|
int? relativeDays,
|
||||||
bool? showFavoriteOnly,
|
bool? favoritesOnly,
|
||||||
bool? includeArchived,
|
bool? includeArchived,
|
||||||
ThemeMode? themeMode,
|
ThemeMode? themeMode,
|
||||||
bool? withPartners,
|
bool? withPartners,
|
||||||
}) => MapConfig(
|
}) => MapConfig(
|
||||||
relativeDays: relativeDate ?? this.relativeDays,
|
relativeDays: relativeDays ?? this.relativeDays,
|
||||||
favoritesOnly: showFavoriteOnly ?? this.favoritesOnly,
|
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
|
||||||
includeArchived: includeArchived ?? this.includeArchived,
|
includeArchived: includeArchived ?? this.includeArchived,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
withPartners: withPartners ?? this.withPartners,
|
withPartners: withPartners ?? this.withPartners,
|
||||||
|
|||||||
@@ -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)';
|
||||||
|
}
|
||||||
@@ -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/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/system_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/log.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
|
|
||||||
enum MetadataDomain<T extends Object> {
|
enum MetadataDomain<T extends Object> {
|
||||||
appConfig<AppConfig>('config.app'),
|
appConfig<AppConfig>('config.app'),
|
||||||
@@ -23,6 +24,16 @@ enum MetadataKey<T extends Object> {
|
|||||||
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
|
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
|
||||||
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
|
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
|
// Log
|
||||||
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
|
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
|
||||||
enum Setting<T> {
|
enum Setting<T> {
|
||||||
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
|
|
||||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
|
|
||||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
|
||||||
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
||||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
|
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
|
||||||
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
|
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
|
||||||
|
|||||||
@@ -19,24 +19,17 @@ enum StoreKey<T> {
|
|||||||
backgroundBackup<bool>._(14),
|
backgroundBackup<bool>._(14),
|
||||||
sslClientCertData<String>._(15),
|
sslClientCertData<String>._(15),
|
||||||
sslClientPasswd<String>._(16),
|
sslClientPasswd<String>._(16),
|
||||||
// user settings from [AppSettingsEnum] below:
|
|
||||||
loadOriginal<bool>._(101),
|
|
||||||
tilesPerRow<int>._(103),
|
|
||||||
groupAssetsBy<int>._(105),
|
|
||||||
uploadErrorNotificationGracePeriod<int>._(106),
|
uploadErrorNotificationGracePeriod<int>._(106),
|
||||||
storageIndicator<bool>._(109),
|
|
||||||
thumbnailCacheSize<int>._(110),
|
thumbnailCacheSize<int>._(110),
|
||||||
imageCacheSize<int>._(111),
|
imageCacheSize<int>._(111),
|
||||||
albumThumbnailCacheSize<int>._(112),
|
albumThumbnailCacheSize<int>._(112),
|
||||||
selectedAlbumSortOrder<int>._(113),
|
selectedAlbumSortOrder<int>._(113),
|
||||||
advancedTroubleshooting<bool>._(114),
|
advancedTroubleshooting<bool>._(114),
|
||||||
preferRemoteImage<bool>._(116),
|
preferRemoteImage<bool>._(116),
|
||||||
loopVideo<bool>._(117),
|
|
||||||
selfSignedCert<bool>._(120),
|
selfSignedCert<bool>._(120),
|
||||||
selectedAlbumSortReverse<bool>._(123),
|
selectedAlbumSortReverse<bool>._(123),
|
||||||
enableHapticFeedback<bool>._(126),
|
enableHapticFeedback<bool>._(126),
|
||||||
customHeaders<String>._(127),
|
customHeaders<String>._(127),
|
||||||
|
|
||||||
syncAlbums<bool>._(131),
|
syncAlbums<bool>._(131),
|
||||||
|
|
||||||
// Auto endpoint switching
|
// Auto endpoint switching
|
||||||
@@ -45,17 +38,16 @@ enum StoreKey<T> {
|
|||||||
localEndpoint<String>._(134),
|
localEndpoint<String>._(134),
|
||||||
externalEndpointList<String>._(135),
|
externalEndpointList<String>._(135),
|
||||||
|
|
||||||
// Video settings
|
|
||||||
loadOriginalVideo<bool>._(136),
|
|
||||||
manageLocalMediaAndroid<bool>._(137),
|
manageLocalMediaAndroid<bool>._(137),
|
||||||
|
|
||||||
// Read-only Mode settings
|
// Read-only Mode settings
|
||||||
readonlyModeEnabled<bool>._(138),
|
readonlyModeEnabled<bool>._(138),
|
||||||
|
|
||||||
autoPlayVideo<bool>._(139),
|
|
||||||
albumGridView<bool>._(140),
|
albumGridView<bool>._(140),
|
||||||
|
loadOriginal<bool>._(101),
|
||||||
|
|
||||||
// Image viewer navigation settings
|
// Image viewer navigation settings
|
||||||
|
loopVideo<bool>._(117),
|
||||||
|
loadOriginalVideo<bool>._(136),
|
||||||
|
autoPlayVideo<bool>._(139),
|
||||||
tapToNavigate<bool>._(141),
|
tapToNavigate<bool>._(141),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
@@ -74,6 +66,9 @@ enum StoreKey<T> {
|
|||||||
legacyCleanupKeepAlbumIds<String>._(1010),
|
legacyCleanupKeepAlbumIds<String>._(1010),
|
||||||
legacyCleanupCutoffDaysAgo<int>._(1011),
|
legacyCleanupCutoffDaysAgo<int>._(1011),
|
||||||
legacyCleanupDefaultsInitialized<bool>._(1012),
|
legacyCleanupDefaultsInitialized<bool>._(1012),
|
||||||
|
legacyTilesPerRow<int>._(103),
|
||||||
|
legacyGroupAssetsBy<int>._(105),
|
||||||
|
legacyStorageIndicator<bool>._(109),
|
||||||
legacyMapRelativeDate<int>._(119),
|
legacyMapRelativeDate<int>._(119),
|
||||||
legacyMapShowFavoriteOnly<bool>._(118),
|
legacyMapShowFavoriteOnly<bool>._(118),
|
||||||
legacyMapIncludeArchived<bool>._(121),
|
legacyMapIncludeArchived<bool>._(121),
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:immich_mobile/constants/constants.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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.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/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/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/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
|
|
||||||
@@ -39,14 +38,16 @@ enum TimelineOrigin {
|
|||||||
|
|
||||||
class TimelineFactory {
|
class TimelineFactory {
|
||||||
final DriftTimelineRepository _timelineRepository;
|
final DriftTimelineRepository _timelineRepository;
|
||||||
final SettingsService _settingsService;
|
final MetadataRepository _metadataRepository;
|
||||||
|
|
||||||
const TimelineFactory({required DriftTimelineRepository timelineRepository, required SettingsService settingsService})
|
const TimelineFactory({
|
||||||
: _timelineRepository = timelineRepository,
|
required DriftTimelineRepository timelineRepository,
|
||||||
_settingsService = settingsService;
|
required MetadataRepository metadataRepository,
|
||||||
|
}) : _timelineRepository = timelineRepository,
|
||||||
|
_metadataRepository = metadataRepository;
|
||||||
|
|
||||||
GroupAssetsBy get groupBy {
|
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
|
// We do not support auto grouping in the new timeline yet, fallback to day grouping
|
||||||
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
|
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ extension<T extends Object> on MetadataDomain<T> {
|
|||||||
themeMode: repo._read(.mapThemeMode),
|
themeMode: repo._read(.mapThemeMode),
|
||||||
withPartners: repo._read(.mapWithPartners),
|
withPartners: repo._read(.mapWithPartners),
|
||||||
),
|
),
|
||||||
|
timeline: .new(
|
||||||
|
tilesPerRow: repo._read(.timelineTilesPerRow),
|
||||||
|
groupAssetsBy: repo._read(.timelineGroupAssetsBy),
|
||||||
|
storageIndicator: repo._read(.timelineStorageIndicator),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case .systemConfig:
|
case .systemConfig:
|
||||||
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
|
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_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/images/thumbnail.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.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/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';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class ThumbnailTile extends ConsumerStatefulWidget {
|
class ThumbnailTile extends ConsumerStatefulWidget {
|
||||||
@@ -61,7 +60,7 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bool storageIndicator =
|
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) {
|
if (!isCurrentAsset) {
|
||||||
_hideIndicators = false;
|
_hideIndicators = false;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.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/fixed/segment_builder.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.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';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
class TimelineArgs {
|
class TimelineArgs {
|
||||||
@@ -93,7 +92,7 @@ final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref)
|
|||||||
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
|
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
|
||||||
final tileExtent = math.max(0, availableTileWidth) / columnCount;
|
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);
|
final timelineService = ref.watch(timelineServiceProvider);
|
||||||
yield* timelineService.watchBuckets().map((buckets) {
|
yield* timelineService.watchBuckets().map((buckets) {
|
||||||
@@ -102,7 +101,7 @@ final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref)
|
|||||||
tileHeight: tileExtent,
|
tileHeight: tileExtent,
|
||||||
columnCount: columnCount,
|
columnCount: columnCount,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
groupBy: groupBy,
|
groupBy: groupBy!,
|
||||||
).generate();
|
).generate();
|
||||||
});
|
});
|
||||||
}, dependencies: [timelineServiceProvider, timelineArgsProvider]);
|
}, dependencies: [timelineServiceProvider, timelineArgsProvider]);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.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/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.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/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.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/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.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/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/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/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
@@ -74,7 +74,7 @@ class Timeline extends StatelessWidget {
|
|||||||
(ref) => TimelineArgs(
|
(ref) => TimelineArgs(
|
||||||
maxWidth: constraints.maxWidth,
|
maxWidth: constraints.maxWidth,
|
||||||
maxHeight: constraints.maxHeight,
|
maxHeight: constraints.maxHeight,
|
||||||
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
|
columnCount: ref.watch(appConfigProvider.select((config) => config.timeline.tilesPerRow)),
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
withStack: withStack,
|
withStack: withStack,
|
||||||
groupBy: groupBy,
|
groupBy: groupBy,
|
||||||
@@ -161,7 +161,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
|
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_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;
|
_perRow = currentTilesPerRow;
|
||||||
_scaleFactor = 7.0 - _perRow;
|
_scaleFactor = 7.0 - _perRow;
|
||||||
_baseScaleFactor = _scaleFactor;
|
_baseScaleFactor = _scaleFactor;
|
||||||
@@ -453,7 +453,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
_restoreAssetIndex = targetAssetIndex;
|
_restoreAssetIndex = targetAssetIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, _perRow);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.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/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';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
final timelineRepositoryProvider = Provider<DriftTimelineRepository>(
|
final timelineRepositoryProvider = Provider<DriftTimelineRepository>(
|
||||||
@@ -29,7 +29,7 @@ final timelineServiceProvider = Provider<TimelineService>(
|
|||||||
final timelineFactoryProvider = Provider<TimelineFactory>(
|
final timelineFactoryProvider = Provider<TimelineFactory>(
|
||||||
(ref) => TimelineFactory(
|
(ref) => TimelineFactory(
|
||||||
timelineRepository: ref.watch(timelineRepositoryProvider),
|
timelineRepository: ref.watch(timelineRepositoryProvider),
|
||||||
settingsService: ref.watch(settingsProvider),
|
metadataRepository: ref.watch(metadataProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
|
|
||||||
enum AppSettingsEnum<T> {
|
enum AppSettingsEnum<T> {
|
||||||
loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
|
loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
|
||||||
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
|
|
||||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0),
|
|
||||||
uploadErrorNotificationGracePeriod<int>(
|
uploadErrorNotificationGracePeriod<int>(
|
||||||
StoreKey.uploadErrorNotificationGracePeriod,
|
StoreKey.uploadErrorNotificationGracePeriod,
|
||||||
"uploadErrorNotificationGracePeriod",
|
"uploadErrorNotificationGracePeriod",
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
storageIndicator<bool>(StoreKey.storageIndicator, "storageIndicator", true),
|
|
||||||
thumbnailCacheSize<int>(StoreKey.thumbnailCacheSize, "thumbnailCacheSize", 10000),
|
thumbnailCacheSize<int>(StoreKey.thumbnailCacheSize, "thumbnailCacheSize", 10000),
|
||||||
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
|
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
|
||||||
albumThumbnailCacheSize<int>(StoreKey.albumThumbnailCacheSize, "albumThumbnailCacheSize", 200),
|
albumThumbnailCacheSize<int>(StoreKey.albumThumbnailCacheSize, "albumThumbnailCacheSize", 200),
|
||||||
|
|||||||
@@ -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/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_key.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/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
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/db.repository.dart';
|
||||||
@@ -75,6 +76,14 @@ Future<void> _migrateTo26(Drift drift) async {
|
|||||||
await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, MetadataKey.mapIncludeArchived);
|
await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, MetadataKey.mapIncludeArchived);
|
||||||
await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, MetadataKey.mapThemeMode, ThemeMode.values);
|
await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, MetadataKey.mapThemeMode, ThemeMode.values);
|
||||||
await migrator.migrateBool(StoreKey.legacyMapwithPartners, MetadataKey.mapWithPartners);
|
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();
|
await migrator.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.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/providers/infrastructure/metadata.provider.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/setting_group_title.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
|
||||||
|
|
||||||
@@ -15,18 +16,17 @@ class GroupSettings extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final groupByIndex = useAppSettingsState(AppSettingsEnum.groupAssetsBy);
|
final groupBy = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.groupAssetsBy)));
|
||||||
final groupBy = GroupAssetsBy.values[groupByIndex.value];
|
|
||||||
|
|
||||||
Future<void> updateAppSettings(GroupAssetsBy groupBy) async {
|
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);
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeGroupValue(GroupAssetsBy? value) {
|
void changeGroupValue(GroupAssetsBy? value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
groupByIndex.value = value.index;
|
groupBy.value = value;
|
||||||
unawaited(updateAppSettings(groupBy));
|
unawaited(updateAppSettings(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class GroupSettings extends HookConsumerWidget {
|
|||||||
value: GroupAssetsBy.auto,
|
value: GroupAssetsBy.auto,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
groupBy: groupBy,
|
groupBy: groupBy.value,
|
||||||
onRadioChanged: changeGroupValue,
|
onRadioChanged: changeGroupValue,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/translate_extensions.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/providers/infrastructure/metadata.provider.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/setting_group_title.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
||||||
|
|
||||||
@@ -13,7 +14,10 @@ class LayoutSettings extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -29,7 +33,9 @@ class LayoutSettings extends HookConsumerWidget {
|
|||||||
maxValue: 6,
|
maxValue: 6,
|
||||||
minValue: 2,
|
minValue: 2,
|
||||||
noDivisons: 4,
|
noDivisons: 4,
|
||||||
onChangeEnd: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChangeEnd: (value) {
|
||||||
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/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_group_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_layout_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';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
@@ -15,13 +16,14 @@ class AssetListSettings extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final showStorageIndicator = useAppSettingsState(AppSettingsEnum.storageIndicator);
|
final storageIndicator = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)));
|
||||||
|
|
||||||
final assetListSetting = [
|
final assetListSetting = [
|
||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: showStorageIndicator,
|
valueNotifier: storageIndicator,
|
||||||
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
|
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
|
||||||
onChanged: (_) {
|
onChanged: (value) {
|
||||||
|
ref.read(metadataProvider).write(MetadataKey.timelineStorageIndicator, value);
|
||||||
ref.invalidate(appSettingsServiceProvider);
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
ref.invalidate(settingsProvider);
|
ref.invalidate(settingsProvider);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import '../../infrastructure/repository.mock.dart';
|
|||||||
|
|
||||||
const _kAccessToken = '#ThisIsAToken';
|
const _kAccessToken = '#ThisIsAToken';
|
||||||
const _kBackgroundBackup = false;
|
const _kBackgroundBackup = false;
|
||||||
const _kGroupAssetsBy = 2;
|
const _kVersion = 2;
|
||||||
final _kBackupFailedSince = DateTime.utc(2023);
|
final _kBackupFailedSince = DateTime.utc(2023);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -31,7 +31,7 @@ void main() {
|
|||||||
(_) async => [
|
(_) async => [
|
||||||
const StoreDto(StoreKey.accessToken, _kAccessToken),
|
const StoreDto(StoreKey.accessToken, _kAccessToken),
|
||||||
const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
|
const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
|
||||||
const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy),
|
const StoreDto(StoreKey.version, _kVersion),
|
||||||
StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
|
StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -50,7 +50,7 @@ void main() {
|
|||||||
verify(() => mockDriftStoreRepo.getAll()).called(1);
|
verify(() => mockDriftStoreRepo.getAll()).called(1);
|
||||||
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
|
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
|
||||||
expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup);
|
expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup);
|
||||||
expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy);
|
expect(sut.tryGet(StoreKey.version), _kVersion);
|
||||||
expect(sut.tryGet(StoreKey.backupFailedSince), _kBackupFailedSince);
|
expect(sut.tryGet(StoreKey.backupFailedSince), _kBackupFailedSince);
|
||||||
// Other keys should be null
|
// Other keys should be null
|
||||||
expect(sut.tryGet(StoreKey.currentUser), isNull);
|
expect(sut.tryGet(StoreKey.currentUser), isNull);
|
||||||
@@ -152,7 +152,7 @@ void main() {
|
|||||||
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
|
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
|
||||||
expect(sut.tryGet(StoreKey.accessToken), isNull);
|
expect(sut.tryGet(StoreKey.accessToken), isNull);
|
||||||
expect(sut.tryGet(StoreKey.backgroundBackup), 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);
|
expect(sut.tryGet(StoreKey.backupFailedSince), isNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user