Compare commits

...

9 Commits

Author SHA1 Message Date
shenlong-tanwen 5ca2c5effc refactor: migrate app metadata 2026-06-17 18:51:01 +05:30
shenlong-tanwen febc162821 refactor: migrate advance config from store 2026-06-16 23:47:28 +05:30
shenlong-tanwen a855852ae7 refactor: use currentUser for auth user table 2026-06-16 19:59:35 +05:30
shenlong-tanwen 35fdaaeed8 migrate session config from store 2026-06-11 23:32:35 +05:30
shenlong-tanwen 5f2f472f98 add map codec tests 2026-06-11 18:38:12 +05:30
shenlong-tanwen e459c3d5a3 rebase over null store key 2026-06-11 17:45:57 +05:30
shenlong-tanwen 800bfab956 extract CachedKeyValueRepository 2026-06-11 17:35:17 +05:30
shenlong-tanwen 84ba31bad4 refactor(mobile): extract shared ValueCodec from SettingsKey
# Conflicts:
#	mobile/lib/domain/models/settings_key.dart
2026-06-11 17:34:40 +05:30
shenlong-tanwen e225f874b1 refactor: nullable settings key 2026-06-11 14:16:14 +05:30
103 changed files with 45940 additions and 801 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -34,14 +34,14 @@ void main() {
server = await FakeImmichServer.start(); server = await FakeImmichServer.start();
await ApiService().resolveAndSetEndpoint(server.endpoint); await ApiService().resolveAndSetEndpoint(server.endpoint);
await drift.delete(drift.userEntity).go(); await drift.delete(drift.userEntity).go();
await Store.delete(StoreKey.syncMigrationStatus); await Store.delete(StoreKey.legacySyncMigrationStatus);
}); });
tearDown(() async { tearDown(() async {
await workerManagerPatch.dispose(); await workerManagerPatch.dispose();
await server.close(); await server.close();
await Store.delete(StoreKey.serverEndpoint); await Store.delete(StoreKey.legacyServerEndpoint);
await Store.delete(StoreKey.syncMigrationStatus); await Store.delete(StoreKey.legacySyncMigrationStatus);
}); });
void sendUser(SyncStream stream, String id, String name) { void sendUser(SyncStream stream, String id, String name) {
@@ -119,7 +119,9 @@ void main() {
final releaseTxn = Completer<void>(); final releaseTxn = Completer<void>();
final txnHeld = Completer<void>(); final txnHeld = Completer<void>();
final txn = drift.transaction(() async { final txn = drift.transaction(() async {
await drift.into(drift.userEntity).insert( await drift
.into(drift.userEntity)
.insert(
UserEntityCompanion.insert( UserEntityCompanion.insert(
id: 'holder', id: 'holder',
name: 'holder', name: 'holder',
@@ -0,0 +1,20 @@
import 'package:immich_mobile/domain/models/value_codec.dart';
const int kCurrentVersion = 29;
enum AppMetadataKey<T> {
version<int>(kCurrentVersion),
syncMigrationStatus<List<String>>([], codec: ListCodec(PrimitiveCodec.string)),
manageLocalMediaAndroid<bool>(false);
const AppMetadataKey(this.defaultValue, {ValueCodec<T>? codec}) : _codecOverride = codec;
final T defaultValue;
final ValueCodec<T>? _codecOverride;
ValueCodec<T> get _codec => _codecOverride ?? ValueCodec.forType(T);
String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw);
}
@@ -0,0 +1,33 @@
class AdvancedConfig {
final bool troubleshooting;
final bool enableHapticFeedback;
final bool readonlyModeEnabled;
const AdvancedConfig({
this.troubleshooting = false,
this.enableHapticFeedback = true,
this.readonlyModeEnabled = false,
});
AdvancedConfig copyWith({bool? troubleshooting, bool? enableHapticFeedback, bool? readonlyModeEnabled}) =>
AdvancedConfig(
troubleshooting: troubleshooting ?? this.troubleshooting,
enableHapticFeedback: enableHapticFeedback ?? this.enableHapticFeedback,
readonlyModeEnabled: readonlyModeEnabled ?? this.readonlyModeEnabled,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AdvancedConfig &&
other.troubleshooting == troubleshooting &&
other.enableHapticFeedback == enableHapticFeedback &&
other.readonlyModeEnabled == readonlyModeEnabled);
@override
int get hashCode => Object.hash(troubleshooting, enableHapticFeedback, readonlyModeEnabled);
@override
String toString() =>
'AdvancedConfig(troubleshooting: $troubleshooting, enableHapticFeedback: $enableHapticFeedback, readonlyModeEnabled: $readonlyModeEnabled)';
}
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/advanced_config.dart';
import 'package:immich_mobile/domain/models/config/album_config.dart'; import 'package:immich_mobile/domain/models/config/album_config.dart';
import 'package:immich_mobile/domain/models/config/backup_config.dart'; import 'package:immich_mobile/domain/models/config/backup_config.dart';
import 'package:immich_mobile/domain/models/config/cleanup_config.dart'; import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
@@ -32,6 +33,7 @@ class AppConfig {
final BackupConfig backup; final BackupConfig backup;
final NetworkConfig network; final NetworkConfig network;
final ShareConfig share; final ShareConfig share;
final AdvancedConfig advanced;
const AppConfig({ const AppConfig({
this.logLevel = .info, this.logLevel = .info,
@@ -46,6 +48,7 @@ class AppConfig {
this.backup = const .new(), this.backup = const .new(),
this.network = const .new(), this.network = const .new(),
this.share = const .new(), this.share = const .new(),
this.advanced = const .new(),
}); });
AppConfig copyWith({ AppConfig copyWith({
@@ -61,6 +64,7 @@ class AppConfig {
BackupConfig? backup, BackupConfig? backup,
NetworkConfig? network, NetworkConfig? network,
ShareConfig? share, ShareConfig? share,
AdvancedConfig? advanced,
}) => .new( }) => .new(
logLevel: logLevel ?? this.logLevel, logLevel: logLevel ?? this.logLevel,
theme: theme ?? this.theme, theme: theme ?? this.theme,
@@ -74,6 +78,7 @@ class AppConfig {
backup: backup ?? this.backup, backup: backup ?? this.backup,
network: network ?? this.network, network: network ?? this.network,
share: share ?? this.share, share: share ?? this.share,
advanced: advanced ?? this.advanced,
); );
@override @override
@@ -91,17 +96,31 @@ class AppConfig {
other.album == album && other.album == album &&
other.backup == backup && other.backup == backup &&
other.network == network && other.network == network &&
other.share == share); other.share == share &&
other.advanced == advanced);
@override @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network, share); logLevel,
theme,
cleanup,
map,
timeline,
image,
viewer,
slideshow,
album,
backup,
network,
share,
advanced,
);
@override @override
String toString() => String toString() =>
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share)'; 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share, advanced: $advanced)';
T read<T extends Object>(SettingsKey<T> key) => T read<T>(SettingsKey<T> key) =>
(switch (key) { (switch (key) {
.logLevel => logLevel, .logLevel => logLevel,
.themePrimaryColor => theme.primaryColor, .themePrimaryColor => theme.primaryColor,
@@ -147,13 +166,16 @@ class AppConfig {
.slideshowDuration => slideshow.duration, .slideshowDuration => slideshow.duration,
.slideshowLook => slideshow.look, .slideshowLook => slideshow.look,
.slideshowDirection => slideshow.direction, .slideshowDirection => slideshow.direction,
.advancedTroubleshooting => advanced.troubleshooting,
.advancedEnableHapticFeedback => advanced.enableHapticFeedback,
.advancedReadonlyModeEnabled => advanced.readonlyModeEnabled,
}) })
as T; as T;
factory AppConfig.fromEntries(Map<SettingsKey<Object>, Object> overrides) => factory AppConfig.fromEntries(Map<SettingsKey, Object?> overrides) =>
overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value)); overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value));
AppConfig write<T extends Object>(SettingsKey<T> key, T value) { AppConfig write<T, U extends T>(SettingsKey<T> key, U value) {
return switch (key) { return switch (key) {
.logLevel => copyWith(logLevel: value as LogLevel), .logLevel => copyWith(logLevel: value as LogLevel),
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)), .themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),
@@ -167,8 +189,10 @@ class AppConfig {
.viewerAutoPlayVideo => copyWith(viewer: viewer.copyWith(autoPlayVideo: value as bool)), .viewerAutoPlayVideo => copyWith(viewer: viewer.copyWith(autoPlayVideo: value as bool)),
.viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)), .viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)),
.networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)), .networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)),
.networkPreferredWifiName => copyWith(network: network.copyWith(preferredWifiName: (value as String))), .networkPreferredWifiName => copyWith(
.networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: (value as String))), network: network.copyWith(preferredWifiName: .fromNullable((value as String?))),
),
.networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: .fromNullable((value as String?)))),
.networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List<String>)), .networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List<String>)),
.networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map<String, String>)), .networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map<String, String>)),
.albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)), .albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)),
@@ -199,6 +223,9 @@ class AppConfig {
.slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)), .slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)),
.slideshowLook => copyWith(slideshow: slideshow.copyWith(look: value as SlideshowLook)), .slideshowLook => copyWith(slideshow: slideshow.copyWith(look: value as SlideshowLook)),
.slideshowDirection => copyWith(slideshow: slideshow.copyWith(direction: value as SlideshowDirection)), .slideshowDirection => copyWith(slideshow: slideshow.copyWith(direction: value as SlideshowDirection)),
.advancedTroubleshooting => copyWith(advanced: advanced.copyWith(troubleshooting: value as bool)),
.advancedEnableHapticFeedback => copyWith(advanced: advanced.copyWith(enableHapticFeedback: value as bool)),
.advancedReadonlyModeEnabled => copyWith(advanced: advanced.copyWith(readonlyModeEnabled: value as bool)),
}; };
} }
} }
@@ -1,30 +1,31 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/utils/option.dart';
class NetworkConfig { class NetworkConfig {
final bool autoEndpointSwitching; final bool autoEndpointSwitching;
final String preferredWifiName; final String? preferredWifiName;
final String localEndpoint; final String? localEndpoint;
final List<String> externalEndpointList; final List<String> externalEndpointList;
final Map<String, String> customHeaders; final Map<String, String> customHeaders;
const NetworkConfig({ const NetworkConfig({
this.autoEndpointSwitching = false, this.autoEndpointSwitching = false,
this.preferredWifiName = '', this.preferredWifiName,
this.localEndpoint = '', this.localEndpoint,
this.externalEndpointList = const [], this.externalEndpointList = const [],
this.customHeaders = const {}, this.customHeaders = const {},
}); });
NetworkConfig copyWith({ NetworkConfig copyWith({
bool? autoEndpointSwitching, bool? autoEndpointSwitching,
String? preferredWifiName, Option<String>? preferredWifiName,
String? localEndpoint, Option<String>? localEndpoint,
List<String>? externalEndpointList, List<String>? externalEndpointList,
Map<String, String>? customHeaders, Map<String, String>? customHeaders,
}) => NetworkConfig( }) => NetworkConfig(
autoEndpointSwitching: autoEndpointSwitching ?? this.autoEndpointSwitching, autoEndpointSwitching: autoEndpointSwitching ?? this.autoEndpointSwitching,
preferredWifiName: preferredWifiName ?? this.preferredWifiName, preferredWifiName: preferredWifiName.patch(this.preferredWifiName),
localEndpoint: localEndpoint ?? this.localEndpoint, localEndpoint: localEndpoint.patch(this.localEndpoint),
externalEndpointList: externalEndpointList ?? this.externalEndpointList, externalEndpointList: externalEndpointList ?? this.externalEndpointList,
customHeaders: customHeaders ?? this.customHeaders, customHeaders: customHeaders ?? this.customHeaders,
); );
@@ -0,0 +1,63 @@
import 'package:immich_mobile/domain/models/value_codec.dart';
import 'package:immich_mobile/utils/option.dart';
enum SessionKey<T> {
serverUrl<String?>(),
accessToken<String?>(),
serverEndpoint<String?>();
ValueCodec<T> get _codec => ValueCodec.forType(T);
String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw);
}
const defaultSession = Session();
class Session {
final String? serverUrl;
final String? accessToken;
final String? serverEndpoint;
const Session({this.serverUrl, this.accessToken, this.serverEndpoint});
Session copyWith({Option<String>? serverUrl, Option<String>? accessToken, Option<String>? serverEndpoint}) => .new(
serverUrl: serverUrl.patch(this.serverUrl),
accessToken: accessToken.patch(this.accessToken),
serverEndpoint: serverEndpoint.patch(this.serverEndpoint),
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is Session &&
other.serverUrl == serverUrl &&
other.accessToken == accessToken &&
other.serverEndpoint == serverEndpoint);
@override
int get hashCode => Object.hash(serverUrl, accessToken, serverEndpoint);
@override
String toString() => 'Session(serverUrl: $serverUrl, accessToken: $accessToken, serverEndpoint: $serverEndpoint)';
T read<T>(SessionKey<T> key) =>
(switch (key) {
.serverUrl => serverUrl,
.accessToken => accessToken,
.serverEndpoint => serverEndpoint,
})
as T;
factory Session.fromEntries(Map<SessionKey, Object?> overrides) =>
overrides.entries.fold(const Session(), (session, entry) => session.write(entry.key, entry.value));
Session write<T, U extends T>(SessionKey<T> key, U value) {
return switch (key) {
.serverUrl => copyWith(serverUrl: .fromNullable(value as String?)),
.accessToken => copyWith(accessToken: .fromNullable(value as String?)),
.serverEndpoint => copyWith(serverEndpoint: .fromNullable(value as String?)),
};
}
}
@@ -1,10 +0,0 @@
import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false);
const Setting(this.storeKey, this.defaultValue);
final StoreKey<T> storeKey;
final T defaultValue;
}
+25 -153
View File
@@ -1,16 +1,15 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart'; 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/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/models/value_codec.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
enum SettingsKey<T extends Object> { enum SettingsKey<T> {
// Theme // Theme
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)), themePrimaryColor<ImmichColorPreset>(codec: EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)), themeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
themeDynamic<bool>(), themeDynamic<bool>(),
themeColorfulInterface<bool>(), themeColorfulInterface<bool>(),
@@ -26,13 +25,13 @@ enum SettingsKey<T extends Object> {
// Network // Network
networkAutoEndpointSwitching<bool>(), networkAutoEndpointSwitching<bool>(),
networkPreferredWifiName<String>(), networkExternalEndpointList<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
networkLocalEndpoint<String>(), networkCustomHeaders<Map<String, String>>(codec: MapCodec(PrimitiveCodec.string, PrimitiveCodec.string)),
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)), networkPreferredWifiName<String?>(),
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)), networkLocalEndpoint<String?>(),
// Album // Album
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)), albumSortMode<AlbumSortMode>(codec: EnumCodec(AlbumSortMode.values)),
albumIsReverse<bool>(), albumIsReverse<bool>(),
albumIsGrid<bool>(), albumIsGrid<bool>(),
@@ -46,175 +45,48 @@ enum SettingsKey<T extends Object> {
// Timeline // Timeline
timelineTilesPerRow<int>(), timelineTilesPerRow<int>(),
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)), timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
timelineStorageIndicator<bool>(), timelineStorageIndicator<bool>(),
// Log // Log
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)), logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
// Map // Map
mapShowFavoriteOnly<bool>(), mapShowFavoriteOnly<bool>(),
mapRelativeDate<int>(), mapRelativeDate<int>(),
mapIncludeArchived<bool>(), mapIncludeArchived<bool>(),
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)), mapThemeMode<ThemeMode>(codec: EnumCodec(ThemeMode.values)),
mapWithPartners<bool>(), mapWithPartners<bool>(),
// Cleanup // Cleanup
cleanupKeepFavorites<bool>(), cleanupKeepFavorites<bool>(),
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)), cleanupKeepMediaType<AssetKeepType>(codec: EnumCodec(AssetKeepType.values)),
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)), cleanupKeepAlbumIds<List<String>>(codec: ListCodec(PrimitiveCodec.string)),
cleanupCutoffDaysAgo<int>(), cleanupCutoffDaysAgo<int>(),
cleanupDefaultsInitialized<bool>(), cleanupDefaultsInitialized<bool>(),
// Share // Share
shareFileType<ShareAssetType>(codec: _EnumCodec(ShareAssetType.values)), shareFileType<ShareAssetType>(codec: EnumCodec(ShareAssetType.values)),
// Slideshow // Slideshow
slideshowTransition<bool>(), slideshowTransition<bool>(),
slideshowRepeat<bool>(), slideshowRepeat<bool>(),
slideshowDuration<int>(), slideshowDuration<int>(),
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)), slideshowLook<SlideshowLook>(codec: EnumCodec(SlideshowLook.values)),
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values)); slideshowDirection<SlideshowDirection>(codec: EnumCodec(SlideshowDirection.values)),
final _SettingsCodec<T>? _codecOverride; // Advanced
advancedTroubleshooting<bool>(),
advancedEnableHapticFeedback<bool>(),
advancedReadonlyModeEnabled<bool>();
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec; final ValueCodec<T>? _codecOverride;
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T); const SettingsKey({ValueCodec<T>? codec}) : _codecOverride = codec;
ValueCodec<T> get _codec => _codecOverride ?? ValueCodec.forType(T);
String encode(T value) => _codec.encode(value); String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw); T decode(String raw) => _codec.decode(raw);
} }
sealed class _SettingsCodec<T extends Object> {
const _SettingsCodec();
String encode(T value);
T decode(String raw);
static const Map<Type, _SettingsCodec<Object>> _primitives = {
int: _PrimitiveCodec.integer,
double: _PrimitiveCodec.real,
bool: _PrimitiveCodec.boolean,
String: _PrimitiveCodec.string,
DateTime: _DateTimeCodec(),
};
static _SettingsCodec<T> forType<T extends Object>(Type runtimeType) {
final codec = _primitives[runtimeType];
if (codec == null) {
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
}
return codec as _SettingsCodec<T>;
}
}
final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
final List<T> values;
const _EnumCodec(this.values);
@override
String encode(T value) => value.name;
@override
T decode(String raw) => values.firstWhere((v) => v.name == raw);
}
final class _DateTimeCodec extends _SettingsCodec<DateTime> {
const _DateTimeCodec();
@override
String encode(DateTime value) => value.toIso8601String();
@override
DateTime decode(String raw) => DateTime.parse(raw);
}
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
final _SettingsCodec<K> _keyCodec;
final _SettingsCodec<V> _valueCodec;
const _MapCodec(this._keyCodec, this._valueCodec);
@override
String encode(Map<K, V> value) {
final entries = <String, String>{};
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
return jsonEncode(entries);
}
@override
Map<K, V> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) {
return {};
}
final result = <K, V>{};
for (final entry in decoded.entries) {
final rawKey = entry.key;
final rawValue = entry.value;
if (rawKey is! String || rawValue is! String) {
return {};
}
final k = _keyCodec.decode(rawKey);
final v = _valueCodec.decode(rawValue);
result[k] = v;
}
return result;
} on FormatException {
return {};
}
}
}
final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
final _SettingsCodec<T> _elementCodec;
const _ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) {
return [];
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) {
return [];
}
final element = _elementCodec.decode(item);
result.add(element);
}
return result;
} on FormatException {
return [];
}
}
}
final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
final T Function(String) _parse;
const _PrimitiveCodec._(this._parse);
@override
String encode(T value) => value.toString();
@override
T decode(String raw) => _parse(raw);
static const integer = _PrimitiveCodec<int>._(int.parse);
static const real = _PrimitiveCodec<double>._(double.parse);
static const boolean = _PrimitiveCodec<bool>._(bool.parse);
static const string = _PrimitiveCodec<String>._(_identity);
static String _identity(String s) => s;
}
+9 -15
View File
@@ -1,24 +1,18 @@
import 'package:immich_mobile/domain/models/user.model.dart';
/// Key for each possible value in the `Store`. /// Key for each possible value in the `Store`.
/// Defines the data type for each value /// Defines the data type for each value
enum StoreKey<T> { enum StoreKey<T> {
version<int>._(0),
currentUser<UserDto>._(2),
deviceId<String>._(4), deviceId<String>._(4),
serverUrl<String>._(10),
accessToken<String>._(11),
serverEndpoint<String>._(12),
advancedTroubleshooting<bool>._(114),
enableHapticFeedback<bool>._(126),
manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings
readonlyModeEnabled<bool>._(138),
syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store // Legacy keys that have been migrated to the new metadata store
legacyVersion<int>._(0),
legacyManageLocalMediaAndroid<bool>._(137),
legacySyncMigrationStatus<String>._(1013),
legacyAdvancedTroubleshooting<bool>._(114),
legacyEnableHapticFeedback<bool>._(126),
legacyReadonlyModeEnabled<bool>._(138),
legacyServerUrl<String>._(10),
legacyAccessToken<String>._(11),
legacyServerEndpoint<String>._(12),
legacyBackupRequireCharging<bool>._(7), legacyBackupRequireCharging<bool>._(7),
legacyBackupTriggerDelay<int>._(8), legacyBackupTriggerDelay<int>._(8),
legacySyncAlbums<bool>._(131), legacySyncAlbums<bool>._(131),
+141
View File
@@ -0,0 +1,141 @@
import 'dart:convert';
sealed class ValueCodec<T> {
const ValueCodec();
String encode(T value);
T decode(String raw);
static final Map<Type, ValueCodec<Object>> _primitives = {
..._register<int>(PrimitiveCodec.integer),
..._register<double>(PrimitiveCodec.real),
..._register<bool>(PrimitiveCodec.boolean),
..._register<String>(PrimitiveCodec.string),
..._register<DateTime>(const DateTimeCodec()),
};
static Map<Type, ValueCodec<Object>> _register<T>(ValueCodec<Object> codec) => {
T: codec,
// Reifies the nullable type T so it can be used as a key in the _primitives map
_typeOf<T?>(): codec,
};
static Type _typeOf<T>() => T;
static ValueCodec<T> forType<T>(Type runtimeType) {
final codec = _primitives[runtimeType];
if (codec == null) {
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the key.');
}
return codec as ValueCodec<T>;
}
}
final class EnumCodec<T extends Enum> extends ValueCodec<T> {
final List<T> values;
const EnumCodec(this.values);
@override
String encode(T value) => value.name;
@override
T decode(String raw) => values.firstWhere((v) => v.name == raw);
}
final class DateTimeCodec extends ValueCodec<DateTime> {
const DateTimeCodec();
@override
String encode(DateTime value) => value.toIso8601String();
@override
DateTime decode(String raw) => DateTime.parse(raw);
}
final class MapCodec<K extends Object, V extends Object> extends ValueCodec<Map<K, V>> {
final ValueCodec<K> _keyCodec;
final ValueCodec<V> _valueCodec;
const MapCodec(this._keyCodec, this._valueCodec);
@override
String encode(Map<K, V> value) {
final entries = <String, String>{};
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
return jsonEncode(entries);
}
@override
Map<K, V> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) {
return {};
}
final result = <K, V>{};
for (final entry in decoded.entries) {
final rawKey = entry.key;
final rawValue = entry.value;
if (rawKey is! String || rawValue is! String) {
continue;
}
final k = _keyCodec.decode(rawKey);
final v = _valueCodec.decode(rawValue);
result[k] = v;
}
return result;
} on FormatException {
return {};
}
}
}
final class ListCodec<T extends Object> extends ValueCodec<List<T>> {
final ValueCodec<T> _elementCodec;
const ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) {
return const [];
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) {
return const [];
}
final element = _elementCodec.decode(item);
result.add(element);
}
return result;
} on FormatException {
return const [];
}
}
}
final class PrimitiveCodec<T extends Object> extends ValueCodec<T> {
final T Function(String) _parse;
const PrimitiveCodec._(this._parse);
@override
String encode(T value) => value.toString();
@override
T decode(String raw) => _parse(raw);
static const integer = PrimitiveCodec<int>._(int.parse);
static const real = PrimitiveCodec<double>._(double.parse);
static const boolean = PrimitiveCodec<bool>._(bool.parse);
static const string = PrimitiveCodec<String>._(_identity);
static String _identity(String s) => s;
}
@@ -5,9 +5,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.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/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
@@ -28,6 +27,7 @@ class LocalSyncService {
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final AssetMediaRepository _assetMediaRepository; final AssetMediaRepository _assetMediaRepository;
final IPermissionRepository _permissionRepository; final IPermissionRepository _permissionRepository;
final AppMetadataRepository _appMetadataRepository;
final Completer<void>? _cancellation; final Completer<void>? _cancellation;
final Logger _log = Logger("DeviceSyncService"); final Logger _log = Logger("DeviceSyncService");
@@ -38,6 +38,7 @@ class LocalSyncService {
required this._trashedLocalAssetRepository, required this._trashedLocalAssetRepository,
required this._assetMediaRepository, required this._assetMediaRepository,
required this._permissionRepository, required this._permissionRepository,
required this._appMetadataRepository,
this._cancellation, this._cancellation,
}) { }) {
_cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning)); _cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning));
@@ -48,7 +49,7 @@ class LocalSyncService {
Future<void> sync({bool full = false}) async { Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start(); final Stopwatch stopwatch = Stopwatch()..start();
try { try {
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (CurrentPlatform.isAndroid && await _appMetadataRepository.get(.manageLocalMediaAndroid)) {
final hasPermission = await _permissionRepository.hasManageMediaPermission(); final hasPermission = await _permissionRepository.hasManageMediaPermission();
if (hasPermission) { if (hasPermission) {
await _syncTrashedAssets(); await _syncTrashedAssets();
@@ -1,19 +0,0 @@
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
// Singleton instance of SettingsService, to use in places
// where reactivity is not required
// ignore: non_constant_identifier_names
final AppSetting = SettingsService(storeService: StoreService.I);
class SettingsService {
final StoreService _storeService;
const SettingsService({required this._storeService});
T get<T>(Setting<T> setting) => _storeService.get(setting.storeKey, setting.defaultValue);
Future<void> set<T>(Setting<T> setting, T value) => _storeService.put(setting.storeKey, value);
Stream<T> watch<T>(Setting<T> setting) => _storeService.watch(setting.storeKey).map((v) => v ?? setting.defaultValue);
}
@@ -2,13 +2,12 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -18,7 +17,7 @@ final syncLinkedAlbumServiceProvider = Provider(
ref.watch(localAlbumRepository), ref.watch(localAlbumRepository),
ref.watch(remoteAlbumRepository), ref.watch(remoteAlbumRepository),
ref.watch(driftAlbumApiRepositoryProvider), ref.watch(driftAlbumApiRepositoryProvider),
ref.watch(storeServiceProvider), ref.watch(authUserRepositoryProvider),
cancellation: ref.watch(cancellationProvider), cancellation: ref.watch(cancellationProvider),
), ),
); );
@@ -27,14 +26,14 @@ class SyncLinkedAlbumService {
final DriftLocalAlbumRepository _localAlbumRepository; final DriftLocalAlbumRepository _localAlbumRepository;
final DriftRemoteAlbumRepository _remoteAlbumRepository; final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftAlbumApiRepository _albumApiRepository; final DriftAlbumApiRepository _albumApiRepository;
final StoreService _storeService; final DriftAuthUserRepository _authUserRepository;
final Completer<void>? _cancellation; final Completer<void>? _cancellation;
SyncLinkedAlbumService( SyncLinkedAlbumService(
this._localAlbumRepository, this._localAlbumRepository,
this._remoteAlbumRepository, this._remoteAlbumRepository,
this._albumApiRepository, this._albumApiRepository,
this._storeService, { this._authUserRepository, {
this._cancellation, this._cancellation,
}); });
@@ -123,11 +122,12 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album /// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async { Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}"); dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum( final currentUser = await _authUserRepository.get();
localAlbum.name, if (currentUser == null) {
_storeService.get(StoreKey.currentUser), _log.warning("No user logged in, skipping remote album creation for local album: ${localAlbum.name}");
assetIds: [], return;
); }
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, currentUser, assetIds: []);
await _remoteAlbumRepository.create(newRemoteAlbum, []); await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id); return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
} }
@@ -1,13 +1,11 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names
import 'dart:async'; import 'dart:async';
import 'dart:convert';
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/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
@@ -38,6 +36,7 @@ class SyncStreamService {
final IPermissionRepository _permissionRepository; final IPermissionRepository _permissionRepository;
final SyncMigrationRepository _syncMigrationRepository; final SyncMigrationRepository _syncMigrationRepository;
final ApiService _api; final ApiService _api;
final AppMetadataRepository _appMetadataRepository;
final Completer<void>? _cancellation; final Completer<void>? _cancellation;
SyncStreamService({ SyncStreamService({
@@ -49,10 +48,12 @@ class SyncStreamService {
required this._permissionRepository, required this._permissionRepository,
required this._syncMigrationRepository, required this._syncMigrationRepository,
required this._api, required this._api,
required this._appMetadataRepository,
this._cancellation, this._cancellation,
}); });
bool get isCancelled => _cancellation?.isCompleted ?? false; bool get isCancelled => _cancellation?.isCompleted ?? false;
bool _manageLocalMediaAndroid = false;
Future<bool> sync() async { Future<bool> sync() async {
_logger.info("Remote sync request for user"); _logger.info("Remote sync request for user");
@@ -64,16 +65,17 @@ class SyncStreamService {
final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_); final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_);
final value = Store.get(StoreKey.syncMigrationStatus, "[]"); final migrations = (await _appMetadataRepository.get(.syncMigrationStatus)).toList();
final migrations = (jsonDecode(value) as List).cast<String>();
int previousLength = migrations.length; int previousLength = migrations.length;
await _runPreSyncTasks(migrations, serverSemVer); await _runPreSyncTasks(migrations, serverSemVer);
if (migrations.length != previousLength) { if (migrations.length != previousLength) {
_logger.info("Updated pre-sync migration status: $migrations"); _logger.info("Updated pre-sync migration status: $migrations");
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations)); await _appMetadataRepository.set(.syncMigrationStatus, migrations);
} }
_manageLocalMediaAndroid = CurrentPlatform.isAndroid && await _appMetadataRepository.get(.manageLocalMediaAndroid);
// Start the sync stream and handle events // Start the sync stream and handle events
bool shouldReset = false; bool shouldReset = false;
await _syncApiRepository.streamChanges( await _syncApiRepository.streamChanges(
@@ -96,7 +98,7 @@ class SyncStreamService {
if (migrations.length != previousLength) { if (migrations.length != previousLength) {
_logger.info("Updated pre-sync migration status: $migrations"); _logger.info("Updated pre-sync migration status: $migrations");
await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations)); await _appMetadataRepository.set(.syncMigrationStatus, migrations);
} }
return true; return true;
@@ -106,10 +108,10 @@ class SyncStreamService {
if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) { if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) {
_logger.info("Running pre-sync task: v20260128_ResetExifV1"); _logger.info("Running pre-sync task: v20260128_ResetExifV1");
await _syncApiRepository.deleteSyncAck([ await _syncApiRepository.deleteSyncAck([
SyncEntityType.assetExifV1, .assetExifV1,
SyncEntityType.partnerAssetExifV1, .partnerAssetExifV1,
SyncEntityType.albumAssetExifCreateV1, .albumAssetExifCreateV1,
SyncEntityType.albumAssetExifUpdateV1, .albumAssetExifUpdateV1,
]); ]);
migrations.add(SyncMigrationTask.v20260128_ResetExifV1.name); migrations.add(SyncMigrationTask.v20260128_ResetExifV1.name);
} }
@@ -117,12 +119,7 @@ class SyncStreamService {
if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) && if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) &&
semVer >= const SemVer(major: 2, minor: 5, patch: 0)) { semVer >= const SemVer(major: 2, minor: 5, patch: 0)) {
_logger.info("Running pre-sync task: v20260128_ResetAssetV1"); _logger.info("Running pre-sync task: v20260128_ResetAssetV1");
await _syncApiRepository.deleteSyncAck([ await _syncApiRepository.deleteSyncAck([.assetV1, .partnerAssetV1, .albumAssetCreateV1, .albumAssetUpdateV1]);
SyncEntityType.assetV1,
SyncEntityType.partnerAssetV1,
SyncEntityType.albumAssetCreateV1,
SyncEntityType.albumAssetUpdateV1,
]);
migrations.add(SyncMigrationTask.v20260128_ResetAssetV1.name); migrations.add(SyncMigrationTask.v20260128_ResetAssetV1.name);
@@ -134,7 +131,7 @@ class SyncStreamService {
if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) && if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) &&
semVer > const SemVer(major: 2, minor: 7, patch: 5)) { semVer > const SemVer(major: 2, minor: 7, patch: 5)) {
_logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2"); _logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2");
await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]); await _syncApiRepository.deleteSyncAck([.assetV1, .assetV2]);
migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name); migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name);
} }
} }
@@ -197,20 +194,20 @@ class SyncStreamService {
case SyncEntityType.assetV1: case SyncEntityType.assetV1:
final remoteSyncAssets = data.cast<SyncAssetV1>(); final remoteSyncAssets = data.cast<SyncAssetV1>();
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets); await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (_manageLocalMediaAndroid) {
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList()); await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
} }
return; return;
case SyncEntityType.assetV2: case SyncEntityType.assetV2:
final remoteSyncAssets = data.cast<SyncAssetV2>(); final remoteSyncAssets = data.cast<SyncAssetV2>();
await _syncStreamRepository.updateAssetsV2(remoteSyncAssets); await _syncStreamRepository.updateAssetsV2(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (_manageLocalMediaAndroid) {
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList()); await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
} }
return; return;
case SyncEntityType.assetDeleteV1: case SyncEntityType.assetDeleteV1:
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>(); final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (_manageLocalMediaAndroid) {
await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList()); await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList());
} }
return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets); return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets);
+11 -14
View File
@@ -1,29 +1,24 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
class UserService { class UserService {
final Logger _log = Logger("UserService"); final Logger _log = Logger("UserService");
final UserApiRepository _userApiRepository; final UserApiRepository _userApiRepository;
final StoreService _storeService; final DriftAuthUserRepository _authUserRepository;
UserService({required this._userApiRepository, required this._storeService}); UserService({required this._userApiRepository, required this._authUserRepository});
UserDto getMyUser() { Future<UserDto?> tryGetMyUser() {
return _storeService.get(StoreKey.currentUser); return _authUserRepository.get();
}
UserDto? tryGetMyUser() {
return _storeService.tryGet(StoreKey.currentUser);
} }
Stream<UserDto?> watchMyUser() { Stream<UserDto?> watchMyUser() {
return _storeService.watch(StoreKey.currentUser); return _authUserRepository.watch();
} }
Future<UserDto?> refreshMyUser() async { Future<UserDto?> refreshMyUser() async {
@@ -31,15 +26,17 @@ class UserService {
if (user == null) { if (user == null) {
return null; return null;
} }
await _storeService.put(StoreKey.currentUser, user); await _authUserRepository.upsert(user);
return user; return user;
} }
Future<String?> createProfileImage(String name, Uint8List image) async { Future<String?> createProfileImage(String name, Uint8List image) async {
try { try {
final path = await _userApiRepository.createProfileImage(name: name, data: image); final path = await _userApiRepository.createProfileImage(name: name, data: image);
final updatedUser = getMyUser(); final updatedUser = await tryGetMyUser();
await _storeService.put(StoreKey.currentUser, updatedUser); if (updatedUser != null) {
await _authUserRepository.upsert(updatedUser);
}
return path; return path;
} catch (e) { } catch (e) {
_log.warning("Failed to upload profile image", e); _log.warning("Failed to upload profile image", e);
@@ -1,14 +1,13 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) { Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) async {
final user = Store.tryGet(StoreKey.currentUser); final user = await ref.read(authUserRepositoryProvider).get();
if (user == null) { if (user == null) {
Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync"); Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync");
return Future.value(); return;
} }
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id); return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);
} }
@@ -0,0 +1,18 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class AppMetadataEntity extends Table with DriftDefaultsMixin {
const AppMetadataEntity();
TextColumn get key => text()();
TextColumn get value => text().nullable()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {key};
@override
String get tableName => "app_metadata";
}
@@ -0,0 +1,434 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.dart'
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$AppMetadataEntityTableCreateCompanionBuilder =
i1.AppMetadataEntityCompanion Function({
required String key,
i0.Value<String?> value,
i0.Value<DateTime> updatedAt,
});
typedef $$AppMetadataEntityTableUpdateCompanionBuilder =
i1.AppMetadataEntityCompanion Function({
i0.Value<String> key,
i0.Value<String?> value,
i0.Value<DateTime> updatedAt,
});
class $$AppMetadataEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AppMetadataEntityTable> {
$$AppMetadataEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$AppMetadataEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AppMetadataEntityTable> {
$$AppMetadataEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$AppMetadataEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AppMetadataEntityTable> {
$$AppMetadataEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get key =>
$composableBuilder(column: $table.key, builder: (column) => column);
i0.GeneratedColumn<String> get value =>
$composableBuilder(column: $table.value, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
class $$AppMetadataEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$AppMetadataEntityTable,
i1.AppMetadataEntityData,
i1.$$AppMetadataEntityTableFilterComposer,
i1.$$AppMetadataEntityTableOrderingComposer,
i1.$$AppMetadataEntityTableAnnotationComposer,
$$AppMetadataEntityTableCreateCompanionBuilder,
$$AppMetadataEntityTableUpdateCompanionBuilder,
(
i1.AppMetadataEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$AppMetadataEntityTable,
i1.AppMetadataEntityData
>,
),
i1.AppMetadataEntityData,
i0.PrefetchHooks Function()
> {
$$AppMetadataEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$AppMetadataEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$AppMetadataEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () => i1
.$$AppMetadataEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$AppMetadataEntityTableAnnotationComposer(
$db: db,
$table: table,
),
updateCompanionCallback:
({
i0.Value<String> key = const i0.Value.absent(),
i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.AppMetadataEntityCompanion(
key: key,
value: value,
updatedAt: updatedAt,
),
createCompanionCallback:
({
required String key,
i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.AppMetadataEntityCompanion.insert(
key: key,
value: value,
updatedAt: updatedAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$AppMetadataEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$AppMetadataEntityTable,
i1.AppMetadataEntityData,
i1.$$AppMetadataEntityTableFilterComposer,
i1.$$AppMetadataEntityTableOrderingComposer,
i1.$$AppMetadataEntityTableAnnotationComposer,
$$AppMetadataEntityTableCreateCompanionBuilder,
$$AppMetadataEntityTableUpdateCompanionBuilder,
(
i1.AppMetadataEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$AppMetadataEntityTable,
i1.AppMetadataEntityData
>,
),
i1.AppMetadataEntityData,
i0.PrefetchHooks Function()
>;
class $AppMetadataEntityTable extends i2.AppMetadataEntity
with i0.TableInfo<$AppMetadataEntityTable, i1.AppMetadataEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$AppMetadataEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
@override
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
'key',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _valueMeta = const i0.VerificationMeta(
'value',
);
@override
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
'value',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
'updatedAt',
);
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
);
@override
List<i0.GeneratedColumn> get $columns => [key, value, updatedAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'app_metadata';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.AppMetadataEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('key')) {
context.handle(
_keyMeta,
key.isAcceptableOrUnknown(data['key']!, _keyMeta),
);
} else if (isInserting) {
context.missing(_keyMeta);
}
if (data.containsKey('value')) {
context.handle(
_valueMeta,
value.isAcceptableOrUnknown(data['value']!, _valueMeta),
);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {key};
@override
i1.AppMetadataEntityData map(
Map<String, dynamic> data, {
String? tablePrefix,
}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.AppMetadataEntityData(
key: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}key'],
)!,
value: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}value'],
),
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
);
}
@override
$AppMetadataEntityTable createAlias(String alias) {
return $AppMetadataEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class AppMetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.AppMetadataEntityData> {
final String key;
final String? value;
final DateTime updatedAt;
const AppMetadataEntityData({
required this.key,
this.value,
required this.updatedAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['key'] = i0.Variable<String>(key);
if (!nullToAbsent || value != null) {
map['value'] = i0.Variable<String>(value);
}
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
return map;
}
factory AppMetadataEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return AppMetadataEntityData(
key: serializer.fromJson<String>(json['key']),
value: serializer.fromJson<String?>(json['value']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'key': serializer.toJson<String>(key),
'value': serializer.toJson<String?>(value),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
};
}
i1.AppMetadataEntityData copyWith({
String? key,
i0.Value<String?> value = const i0.Value.absent(),
DateTime? updatedAt,
}) => i1.AppMetadataEntityData(
key: key ?? this.key,
value: value.present ? value.value : this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
AppMetadataEntityData copyWithCompanion(i1.AppMetadataEntityCompanion data) {
return AppMetadataEntityData(
key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
);
}
@override
String toString() {
return (StringBuffer('AppMetadataEntityData(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(key, value, updatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.AppMetadataEntityData &&
other.key == this.key &&
other.value == this.value &&
other.updatedAt == this.updatedAt);
}
class AppMetadataEntityCompanion
extends i0.UpdateCompanion<i1.AppMetadataEntityData> {
final i0.Value<String> key;
final i0.Value<String?> value;
final i0.Value<DateTime> updatedAt;
const AppMetadataEntityCompanion({
this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
});
AppMetadataEntityCompanion.insert({
required String key,
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
}) : key = i0.Value(key);
static i0.Insertable<i1.AppMetadataEntityData> custom({
i0.Expression<String>? key,
i0.Expression<String>? value,
i0.Expression<DateTime>? updatedAt,
}) {
return i0.RawValuesInsertable({
if (key != null) 'key': key,
if (value != null) 'value': value,
if (updatedAt != null) 'updated_at': updatedAt,
});
}
i1.AppMetadataEntityCompanion copyWith({
i0.Value<String>? key,
i0.Value<String?>? value,
i0.Value<DateTime>? updatedAt,
}) {
return i1.AppMetadataEntityCompanion(
key: key ?? this.key,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (key.present) {
map['key'] = i0.Variable<String>(key.value);
}
if (value.present) {
map['value'] = i0.Variable<String>(value.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('AppMetadataEntityCompanion(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
}
@@ -0,0 +1,18 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class SessionEntity extends Table with DriftDefaultsMixin {
const SessionEntity();
TextColumn get key => text()();
TextColumn get value => text().nullable()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {key};
@override
String get tableName => "session";
}
@@ -0,0 +1,427 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/session.entity.dart'
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$SessionEntityTableCreateCompanionBuilder =
i1.SessionEntityCompanion Function({
required String key,
i0.Value<String?> value,
i0.Value<DateTime> updatedAt,
});
typedef $$SessionEntityTableUpdateCompanionBuilder =
i1.SessionEntityCompanion Function({
i0.Value<String> key,
i0.Value<String?> value,
i0.Value<DateTime> updatedAt,
});
class $$SessionEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
$$SessionEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $$SessionEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
$$SessionEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get key => $composableBuilder(
column: $table.key,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get value => $composableBuilder(
column: $table.value,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $$SessionEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SessionEntityTable> {
$$SessionEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get key =>
$composableBuilder(column: $table.key, builder: (column) => column);
i0.GeneratedColumn<String> get value =>
$composableBuilder(column: $table.value, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
class $$SessionEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData,
i1.$$SessionEntityTableFilterComposer,
i1.$$SessionEntityTableOrderingComposer,
i1.$$SessionEntityTableAnnotationComposer,
$$SessionEntityTableCreateCompanionBuilder,
$$SessionEntityTableUpdateCompanionBuilder,
(
i1.SessionEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData
>,
),
i1.SessionEntityData,
i0.PrefetchHooks Function()
> {
$$SessionEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$SessionEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$SessionEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$SessionEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$SessionEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> key = const i0.Value.absent(),
i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.SessionEntityCompanion(
key: key,
value: value,
updatedAt: updatedAt,
),
createCompanionCallback:
({
required String key,
i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.SessionEntityCompanion.insert(
key: key,
value: value,
updatedAt: updatedAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$SessionEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData,
i1.$$SessionEntityTableFilterComposer,
i1.$$SessionEntityTableOrderingComposer,
i1.$$SessionEntityTableAnnotationComposer,
$$SessionEntityTableCreateCompanionBuilder,
$$SessionEntityTableUpdateCompanionBuilder,
(
i1.SessionEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$SessionEntityTable,
i1.SessionEntityData
>,
),
i1.SessionEntityData,
i0.PrefetchHooks Function()
>;
class $SessionEntityTable extends i2.SessionEntity
with i0.TableInfo<$SessionEntityTable, i1.SessionEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$SessionEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
@override
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
'key',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _valueMeta = const i0.VerificationMeta(
'value',
);
@override
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
'value',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
'updatedAt',
);
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i3.currentDateAndTime,
);
@override
List<i0.GeneratedColumn> get $columns => [key, value, updatedAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'session';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.SessionEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('key')) {
context.handle(
_keyMeta,
key.isAcceptableOrUnknown(data['key']!, _keyMeta),
);
} else if (isInserting) {
context.missing(_keyMeta);
}
if (data.containsKey('value')) {
context.handle(
_valueMeta,
value.isAcceptableOrUnknown(data['value']!, _valueMeta),
);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {key};
@override
i1.SessionEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.SessionEntityData(
key: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}key'],
)!,
value: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}value'],
),
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'],
)!,
);
}
@override
$SessionEntityTable createAlias(String alias) {
return $SessionEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class SessionEntityData extends i0.DataClass
implements i0.Insertable<i1.SessionEntityData> {
final String key;
final String? value;
final DateTime updatedAt;
const SessionEntityData({
required this.key,
this.value,
required this.updatedAt,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['key'] = i0.Variable<String>(key);
if (!nullToAbsent || value != null) {
map['value'] = i0.Variable<String>(value);
}
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
return map;
}
factory SessionEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return SessionEntityData(
key: serializer.fromJson<String>(json['key']),
value: serializer.fromJson<String?>(json['value']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'key': serializer.toJson<String>(key),
'value': serializer.toJson<String?>(value),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
};
}
i1.SessionEntityData copyWith({
String? key,
i0.Value<String?> value = const i0.Value.absent(),
DateTime? updatedAt,
}) => i1.SessionEntityData(
key: key ?? this.key,
value: value.present ? value.value : this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
SessionEntityData copyWithCompanion(i1.SessionEntityCompanion data) {
return SessionEntityData(
key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
);
}
@override
String toString() {
return (StringBuffer('SessionEntityData(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(key, value, updatedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.SessionEntityData &&
other.key == this.key &&
other.value == this.value &&
other.updatedAt == this.updatedAt);
}
class SessionEntityCompanion extends i0.UpdateCompanion<i1.SessionEntityData> {
final i0.Value<String> key;
final i0.Value<String?> value;
final i0.Value<DateTime> updatedAt;
const SessionEntityCompanion({
this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
});
SessionEntityCompanion.insert({
required String key,
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
}) : key = i0.Value(key);
static i0.Insertable<i1.SessionEntityData> custom({
i0.Expression<String>? key,
i0.Expression<String>? value,
i0.Expression<DateTime>? updatedAt,
}) {
return i0.RawValuesInsertable({
if (key != null) 'key': key,
if (value != null) 'value': value,
if (updatedAt != null) 'updated_at': updatedAt,
});
}
i1.SessionEntityCompanion copyWith({
i0.Value<String>? key,
i0.Value<String?>? value,
i0.Value<DateTime>? updatedAt,
}) {
return i1.SessionEntityCompanion(
key: key ?? this.key,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (key.present) {
map['key'] = i0.Variable<String>(key.value);
}
if (value.present) {
map['value'] = i0.Variable<String>(value.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('SessionEntityCompanion(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
}
@@ -6,7 +6,7 @@ class SettingsEntity extends Table with DriftDefaultsMixin {
TextColumn get key => text()(); TextColumn get key => text()();
TextColumn get value => text()(); TextColumn get value => text().nullable()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
+20 -21
View File
@@ -10,13 +10,13 @@ import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$SettingsEntityTableCreateCompanionBuilder = typedef $$SettingsEntityTableCreateCompanionBuilder =
i1.SettingsEntityCompanion Function({ i1.SettingsEntityCompanion Function({
required String key, required String key,
required String value, i0.Value<String?> value,
i0.Value<DateTime> updatedAt, i0.Value<DateTime> updatedAt,
}); });
typedef $$SettingsEntityTableUpdateCompanionBuilder = typedef $$SettingsEntityTableUpdateCompanionBuilder =
i1.SettingsEntityCompanion Function({ i1.SettingsEntityCompanion Function({
i0.Value<String> key, i0.Value<String> key,
i0.Value<String> value, i0.Value<String?> value,
i0.Value<DateTime> updatedAt, i0.Value<DateTime> updatedAt,
}); });
@@ -127,7 +127,7 @@ class $$SettingsEntityTableTableManager
updateCompanionCallback: updateCompanionCallback:
({ ({
i0.Value<String> key = const i0.Value.absent(), i0.Value<String> key = const i0.Value.absent(),
i0.Value<String> value = const i0.Value.absent(), i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(), i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.SettingsEntityCompanion( }) => i1.SettingsEntityCompanion(
key: key, key: key,
@@ -137,7 +137,7 @@ class $$SettingsEntityTableTableManager
createCompanionCallback: createCompanionCallback:
({ ({
required String key, required String key,
required String value, i0.Value<String?> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(), i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.SettingsEntityCompanion.insert( }) => i1.SettingsEntityCompanion.insert(
key: key, key: key,
@@ -196,9 +196,9 @@ class $SettingsEntityTable extends i2.SettingsEntity
late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>( late final i0.GeneratedColumn<String> value = i0.GeneratedColumn<String>(
'value', 'value',
aliasedName, aliasedName,
false, true,
type: i0.DriftSqlType.string, type: i0.DriftSqlType.string,
requiredDuringInsert: true, requiredDuringInsert: false,
); );
static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta( static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
'updatedAt', 'updatedAt',
@@ -240,8 +240,6 @@ class $SettingsEntityTable extends i2.SettingsEntity
_valueMeta, _valueMeta,
value.isAcceptableOrUnknown(data['value']!, _valueMeta), value.isAcceptableOrUnknown(data['value']!, _valueMeta),
); );
} else if (isInserting) {
context.missing(_valueMeta);
} }
if (data.containsKey('updated_at')) { if (data.containsKey('updated_at')) {
context.handle( context.handle(
@@ -265,7 +263,7 @@ class $SettingsEntityTable extends i2.SettingsEntity
value: attachedDatabase.typeMapping.read( value: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, i0.DriftSqlType.string,
data['${effectivePrefix}value'], data['${effectivePrefix}value'],
)!, ),
updatedAt: attachedDatabase.typeMapping.read( updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, i0.DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'], data['${effectivePrefix}updated_at'],
@@ -287,18 +285,20 @@ class $SettingsEntityTable extends i2.SettingsEntity
class SettingsEntityData extends i0.DataClass class SettingsEntityData extends i0.DataClass
implements i0.Insertable<i1.SettingsEntityData> { implements i0.Insertable<i1.SettingsEntityData> {
final String key; final String key;
final String value; final String? value;
final DateTime updatedAt; final DateTime updatedAt;
const SettingsEntityData({ const SettingsEntityData({
required this.key, required this.key,
required this.value, this.value,
required this.updatedAt, required this.updatedAt,
}); });
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{}; final map = <String, i0.Expression>{};
map['key'] = i0.Variable<String>(key); map['key'] = i0.Variable<String>(key);
map['value'] = i0.Variable<String>(value); if (!nullToAbsent || value != null) {
map['value'] = i0.Variable<String>(value);
}
map['updated_at'] = i0.Variable<DateTime>(updatedAt); map['updated_at'] = i0.Variable<DateTime>(updatedAt);
return map; return map;
} }
@@ -310,7 +310,7 @@ class SettingsEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer; serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return SettingsEntityData( return SettingsEntityData(
key: serializer.fromJson<String>(json['key']), key: serializer.fromJson<String>(json['key']),
value: serializer.fromJson<String>(json['value']), value: serializer.fromJson<String?>(json['value']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
); );
} }
@@ -319,18 +319,18 @@ class SettingsEntityData extends i0.DataClass
serializer ??= i0.driftRuntimeOptions.defaultSerializer; serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'key': serializer.toJson<String>(key), 'key': serializer.toJson<String>(key),
'value': serializer.toJson<String>(value), 'value': serializer.toJson<String?>(value),
'updatedAt': serializer.toJson<DateTime>(updatedAt), 'updatedAt': serializer.toJson<DateTime>(updatedAt),
}; };
} }
i1.SettingsEntityData copyWith({ i1.SettingsEntityData copyWith({
String? key, String? key,
String? value, i0.Value<String?> value = const i0.Value.absent(),
DateTime? updatedAt, DateTime? updatedAt,
}) => i1.SettingsEntityData( }) => i1.SettingsEntityData(
key: key ?? this.key, key: key ?? this.key,
value: value ?? this.value, value: value.present ? value.value : this.value,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
); );
SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) { SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) {
@@ -365,7 +365,7 @@ class SettingsEntityData extends i0.DataClass
class SettingsEntityCompanion class SettingsEntityCompanion
extends i0.UpdateCompanion<i1.SettingsEntityData> { extends i0.UpdateCompanion<i1.SettingsEntityData> {
final i0.Value<String> key; final i0.Value<String> key;
final i0.Value<String> value; final i0.Value<String?> value;
final i0.Value<DateTime> updatedAt; final i0.Value<DateTime> updatedAt;
const SettingsEntityCompanion({ const SettingsEntityCompanion({
this.key = const i0.Value.absent(), this.key = const i0.Value.absent(),
@@ -374,10 +374,9 @@ class SettingsEntityCompanion
}); });
SettingsEntityCompanion.insert({ SettingsEntityCompanion.insert({
required String key, required String key,
required String value, this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(),
}) : key = i0.Value(key), }) : key = i0.Value(key);
value = i0.Value(value);
static i0.Insertable<i1.SettingsEntityData> custom({ static i0.Insertable<i1.SettingsEntityData> custom({
i0.Expression<String>? key, i0.Expression<String>? key,
i0.Expression<String>? value, i0.Expression<String>? value,
@@ -392,7 +391,7 @@ class SettingsEntityCompanion
i1.SettingsEntityCompanion copyWith({ i1.SettingsEntityCompanion copyWith({
i0.Value<String>? key, i0.Value<String>? key,
i0.Value<String>? value, i0.Value<String?>? value,
i0.Value<DateTime>? updatedAt, i0.Value<DateTime>? updatedAt,
}) { }) {
return i1.SettingsEntityCompanion( return i1.SettingsEntityCompanion(
@@ -0,0 +1,28 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class AppMetadataRepository {
final Drift _db;
const AppMetadataRepository(this._db);
Future<T> get<T>(AppMetadataKey<T> key) async {
final row = await (_db.select(_db.appMetadataEntity)..where((row) => row.key.equals(key.name))).getSingleOrNull();
final value = row?.value;
return value == null ? key.defaultValue : key.decode(value);
}
Future<void> set<T, U extends T>(AppMetadataKey<T> key, U value) async {
await _db
.into(_db.appMetadataEntity)
.insertOnConflictUpdate(
AppMetadataEntityCompanion.insert(
key: key.name,
value: Value(key.encode(value)),
updatedAt: Value(DateTime.now()),
),
);
}
}
@@ -0,0 +1,42 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
// ignore: depend_on_referenced_packages
import 'package:meta/meta.dart';
abstract class CachedKeyValueRepository<K extends Enum, S> {
CachedKeyValueRepository(this._snapshot);
S _snapshot;
S get snapshot => _snapshot;
@protected
set snapshot(S value) => _snapshot = value;
List<K> get keys;
Object decodeValue(K key, String raw);
S buildSnapshot(Map<K, Object?> overrides);
Selectable<({String key, String? value})> selectable();
Future<void> refresh() async => _snapshot = _build(await selectable().get());
@protected
Stream<S> watchSnapshot() => selectable().watch().map((rows) => _snapshot = _build(rows));
S _build(List<({String key, String? value})> rows) => buildSnapshot(
rows.fold({}, (overrides, row) {
final key = keys.firstWhereOrNull((key) => key.name == row.key);
if (key == null) {
return overrides;
}
Object? decodedValue;
if (row.value != null) {
decodedValue = decodeValue(key, row.value!);
}
return {...overrides, key: decodedValue};
}),
);
}
@@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
import 'package:drift/src/runtime/executor/stream_queries.dart' show StreamQueryStore; import 'package:drift/src/runtime/executor/stream_queries.dart' show StreamQueryStore;
import 'package:drift_sqlite_async/drift_sqlite_async.dart'; import 'package:drift_sqlite_async/drift_sqlite_async.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.dart';
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.dart'; import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.dart';
@@ -25,6 +26,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
@@ -67,6 +69,8 @@ import 'package:sqlite_async/sqlite_async.dart';
AssetEditEntity, AssetEditEntity,
SettingsEntity, SettingsEntity,
AssetOcrEntity, AssetOcrEntity,
SessionEntity,
AppMetadataEntity,
], ],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
) )
@@ -120,7 +124,7 @@ class Drift extends $Drift {
} }
@override @override
int get schemaVersion => 29; int get schemaVersion => 32;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -308,6 +312,20 @@ class Drift extends $Drift {
await m.createTable(v29.assetOcrEntity); await m.createTable(v29.assetOcrEntity);
await m.createIndex(v29.idxAssetOcrAssetId); await m.createIndex(v29.idxAssetOcrAssetId);
}, },
from29To30: (m, v30) async {
await m.alterTable(TableMigration(v30.settings));
},
from30To31: (m, v31) async {
await m.createTable(v31.session);
},
from31To32: (m, v32) async {
await m.createTable(v32.appMetadata);
await customStatement(
"INSERT INTO app_metadata (key, value) "
"SELECT 'version', CAST(int_value AS TEXT) FROM store_entity "
"WHERE id = 0 AND int_value IS NOT NULL",
);
},
), ),
); );
@@ -47,9 +47,13 @@ import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart
as i22; as i22;
import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'
as i23; as i23;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'
as i24; as i24;
import 'package:drift/internal/modular.dart' as i25; import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'
as i25;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i26;
import 'package:drift/internal/modular.dart' as i27;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@@ -99,9 +103,14 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i23.$AssetOcrEntityTable assetOcrEntity = i23.$AssetOcrEntityTable( late final i23.$AssetOcrEntityTable assetOcrEntity = i23.$AssetOcrEntityTable(
this, this,
); );
i24.MergedAssetDrift get mergedAssetDrift => i25.ReadDatabaseContainer( late final i24.$SessionEntityTable sessionEntity = i24.$SessionEntityTable(
this, this,
).accessor<i24.MergedAssetDrift>(i24.MergedAssetDrift.new); );
late final i25.$AppMetadataEntityTable appMetadataEntity = i25
.$AppMetadataEntityTable(this);
i26.MergedAssetDrift get mergedAssetDrift => i27.ReadDatabaseContainer(
this,
).accessor<i26.MergedAssetDrift>(i26.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -140,6 +149,8 @@ abstract class $Drift extends i0.GeneratedDatabase {
assetEditEntity, assetEditEntity,
settingsEntity, settingsEntity,
assetOcrEntity, assetOcrEntity,
sessionEntity,
appMetadataEntity,
i10.idxPartnerSharedWithId, i10.idxPartnerSharedWithId,
i11.idxLatLng, i11.idxLatLng,
i11.idxRemoteExifCity, i11.idxRemoteExifCity,
@@ -414,4 +425,8 @@ class $DriftManager {
i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity); i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity);
i23.$$AssetOcrEntityTableTableManager get assetOcrEntity => i23.$$AssetOcrEntityTableTableManager get assetOcrEntity =>
i23.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity); i23.$$AssetOcrEntityTableTableManager(_db, _db.assetOcrEntity);
i24.$$SessionEntityTableTableManager get sessionEntity =>
i24.$$SessionEntityTableTableManager(_db, _db.sessionEntity);
i25.$$AppMetadataEntityTableTableManager get appMetadataEntity =>
i25.$$AppMetadataEntityTableTableManager(_db, _db.appMetadataEntity);
} }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,82 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class SessionRepository extends CachedKeyValueRepository<SessionKey, Session> {
final Drift _db;
SessionRepository._(this._db) : super(const .new());
static SessionRepository? _instance;
static SessionRepository get instance {
final instance = _instance;
if (instance == null) {
throw StateError('SessionRepository not initialized. Call ensureInitialized() first');
}
return instance;
}
static Future<SessionRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = SessionRepository._(db);
await instance.refresh();
_instance = instance;
}
return _instance!;
}
@override
List<SessionKey> get keys => SessionKey.values;
@override
Object decodeValue(SessionKey key, String raw) => key.decode(raw);
@override
Session buildSnapshot(Map<SessionKey, Object?> overrides) => Session.fromEntries(overrides);
@override
@protected
Selectable<({String key, String? value})> selectable() =>
_db.select(_db.sessionEntity).map((row) => (key: row.key, value: row.value));
Session get session => snapshot;
Future<void> clear(Iterable<SessionKey> keys) async {
if (keys.isEmpty) {
return;
}
final names = keys.map((key) => key.name).toList();
await (_db.delete(_db.sessionEntity)..where((row) => row.key.isIn(names))).go();
var session = snapshot;
for (final key in keys) {
session = session.write(key, defaultSession.read(key));
}
snapshot = session;
}
Future<void> write<T, U extends T>(SessionKey<T> key, U value) async {
if (value == snapshot.read(key)) {
return;
}
String? resolvedValue;
if (value != null) {
resolvedValue = key.encode(value);
}
await _db
.into(_db.sessionEntity)
.insertOnConflictUpdate(
SessionEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
);
snapshot = snapshot.write(key, value);
}
Stream<Session> watch() => watchSnapshot();
}
@@ -1,14 +1,15 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/material.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/settings_key.dart'; import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class SettingsRepository extends DriftDatabaseRepository { class SettingsRepository extends CachedKeyValueRepository<SettingsKey, AppConfig> {
final Drift _db; final Drift _db;
SettingsRepository._(this._db) : super(_db); SettingsRepository._(this._db) : super(const .new());
static SettingsRepository? _instance; static SettingsRepository? _instance;
@@ -20,9 +21,6 @@ class SettingsRepository extends DriftDatabaseRepository {
return instance; return instance;
} }
AppConfig _appConfig = const .new();
AppConfig get appConfig => _appConfig;
static Future<SettingsRepository> ensureInitialized(Drift db) async { static Future<SettingsRepository> ensureInitialized(Drift db) async {
if (_instance == null) { if (_instance == null) {
final instance = SettingsRepository._(db); final instance = SettingsRepository._(db);
@@ -32,7 +30,21 @@ class SettingsRepository extends DriftDatabaseRepository {
return _instance!; return _instance!;
} }
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get()); @override
List<SettingsKey> get keys => SettingsKey.values;
@override
Object decodeValue(SettingsKey key, String raw) => key.decode(raw);
@override
AppConfig buildSnapshot(Map<SettingsKey, Object?> overrides) => AppConfig.fromEntries(overrides);
@override
@protected
Selectable<({String key, String? value})> selectable() =>
_db.select(_db.settingsEntity).map((row) => (key: row.key, value: row.value));
AppConfig get appConfig => snapshot;
Future<void> clear(Iterable<SettingsKey> keys) async { Future<void> clear(Iterable<SettingsKey> keys) async {
if (keys.isEmpty) { if (keys.isEmpty) {
@@ -42,13 +54,15 @@ class SettingsRepository extends DriftDatabaseRepository {
final names = keys.map((key) => key.name).toList(); final names = keys.map((key) => key.name).toList();
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go(); await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
var config = snapshot;
for (final key in keys) { for (final key in keys) {
_appConfig = _appConfig.write(key, defaultConfig.read(key)); config = config.write(key, defaultConfig.read(key));
} }
snapshot = config;
} }
Future<void> write<T extends Object, U extends T>(SettingsKey<T> key, U value) async { Future<void> write<T, U extends T>(SettingsKey<T> key, U value) async {
if (value == _appConfig.read(key)) { if (value == snapshot.read(key)) {
return; return;
} }
@@ -56,29 +70,18 @@ class SettingsRepository extends DriftDatabaseRepository {
return clear([key]); return clear([key]);
} }
String? resolvedValue;
if (value != null) {
resolvedValue = key.encode(value);
}
await _db await _db
.into(_db.settingsEntity) .into(_db.settingsEntity)
.insertOnConflictUpdate( .insertOnConflictUpdate(
SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())), SettingsEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())),
); );
_appConfig = _appConfig.write(key, value); snapshot = snapshot.write(key, value);
} }
Stream<AppConfig> watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) { Stream<AppConfig> watch() => watchSnapshot();
_applyOverrides(rows);
return _appConfig;
});
void _applyOverrides(List<SettingsEntityData> rows) {
_appConfig = AppConfig.fromEntries(
rows.fold({}, (overrides, row) {
final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key);
if (metadataKey == null) {
return overrides;
}
return {...overrides, metadataKey: metadataKey.decode(row.value)};
}),
);
}
} }
@@ -1,9 +1,7 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
class DriftStoreRepository extends DriftDatabaseRepository { class DriftStoreRepository extends DriftDatabaseRepository {
final Drift _db; final Drift _db;
@@ -63,8 +61,6 @@ class DriftStoreRepository extends DriftDatabaseRepository {
const (String) => entity.stringValue, const (String) => entity.stringValue,
const (bool) => entity.intValue == 1, const (bool) => entity.intValue == 1,
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) =>
entity.stringValue == null ? null : await DriftAuthUserRepository(_db).get(entity.stringValue!),
_ => null, _ => null,
} }
as T?; as T?;
@@ -75,7 +71,6 @@ class DriftStoreRepository extends DriftDatabaseRepository {
const (String) => (null, value as String), const (String) => (null, value as String),
const (bool) => ((value as bool) ? 1 : 0, null), const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (null, (await DriftAuthUserRepository(_db).upsert(value as UserDto)).id),
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
}; };
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue)); return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
@@ -17,16 +17,15 @@ class DriftAuthUserRepository extends DriftDatabaseRepository {
final Drift _db; final Drift _db;
const DriftAuthUserRepository(super.db) : _db = db; const DriftAuthUserRepository(super.db) : _db = db;
Future<UserDto?> get(String id) async { Selectable<UserDto?> get _authUserQuery => (_db.authUserEntity.select()..limit(1)).asyncMap(_toDto);
final user = await _db.managers.authUserEntity.filter((user) => user.id.equals(id)).getSingleOrNull();
if (user == null) { Future<UserDto?> get() => _authUserQuery.getSingleOrNull();
return null;
}
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(id)); Stream<UserDto?> watch() => _authUserQuery.watchSingleOrNull();
Future<UserDto> _toDto(AuthUserEntityData user) async {
final query = _db.userMetadataEntity.select()..where((e) => e.userId.equals(user.id));
final metadata = await query.map((row) => row.toDto()).get(); final metadata = await query.map((row) => row.toDto()).get();
return user.toDto(metadata); return user.toDto(metadata);
} }
@@ -5,8 +5,6 @@ import 'package:easy_localization/easy_localization.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/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
@@ -85,7 +83,7 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
final backupSyncManager = ref.read(backgroundSyncProvider); final backupSyncManager = ref.read(backgroundSyncProvider);
Future<void> startBackup() async { Future<void> startBackup() async {
final currentUser = Store.tryGet(StoreKey.currentUser); final currentUser = ref.read(currentUserProvider);
if (currentUser == null) { if (currentUser == null) {
return; return;
} }
@@ -8,14 +8,15 @@ import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/constants/locales.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/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
@@ -306,9 +307,10 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
} }
void resumeSession() async { void resumeSession() async {
final serverUrl = Store.tryGet(StoreKey.serverUrl); final session = ref.read(sessionProvider);
final endpoint = Store.tryGet(StoreKey.serverEndpoint); final serverUrl = session.serverUrl;
final accessToken = Store.tryGet(StoreKey.accessToken); final endpoint = session.serverEndpoint;
final accessToken = session.accessToken;
if (accessToken != null && serverUrl != null && endpoint != null) { if (accessToken != null && serverUrl != null && endpoint != null) {
final infoProvider = ref.read(serverInfoProvider.notifier); final infoProvider = ref.read(serverInfoProvider.notifier);
@@ -316,6 +318,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final backgroundManager = ref.read(backgroundSyncProvider); final backgroundManager = ref.read(backgroundSyncProvider);
final backupProvider = ref.read(driftBackupProvider.notifier); final backupProvider = ref.read(driftBackupProvider.notifier);
final viewIntentHandler = ref.read(viewIntentHandlerProvider); final viewIntentHandler = ref.read(viewIntentHandlerProvider);
final authUserRepository = ref.read(authUserRepositoryProvider);
unawaited( unawaited(
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
@@ -335,9 +338,9 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
if (syncSuccess) { if (syncSuccess) {
await Future.wait([ await Future.wait([
backgroundManager.hashAssets().then((_) { backgroundManager.hashAssets().then((_) {
_resumeBackup(backupProvider); _resumeBackup(backupProvider, authUserRepository);
}), }),
_resumeBackup(backupProvider), _resumeBackup(backupProvider, authUserRepository),
// TODO: Bring back when the soft freeze issue is addressed // TODO: Bring back when the soft freeze issue is addressed
// backgroundManager.syncCloudIds(), // backgroundManager.syncCloudIds(),
]); ]);
@@ -373,11 +376,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
} }
} }
Future<void> _resumeBackup(DriftBackupNotifier notifier) async { Future<void> _resumeBackup(DriftBackupNotifier notifier, DriftAuthUserRepository authUserRepository) async {
final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled; final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled;
if (isEnableBackup) { if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser); final currentUser = await authUserRepository.get();
if (currentUser != null) { if (currentUser != null) {
unawaited(notifier.startForegroundBackup(currentUser.id)); unawaited(notifier.startForegroundBackup(currentUser.id));
} }
@@ -1,9 +1,8 @@
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/store.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -22,7 +21,7 @@ class OpenInBrowserActionButton extends ConsumerWidget {
}); });
void _onTap() async { void _onTap() async {
final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', ''); final serverEndpoint = SessionRepository.instance.session.serverEndpoint!.replaceFirst('/api', '');
String originPath = ''; String originPath = '';
switch (origin) { switch (origin) {
@@ -4,8 +4,6 @@ import 'dart:io';
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/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
@@ -13,6 +11,7 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -143,7 +142,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
final remoteId = (videoAsset as RemoteAsset).id; final remoteId = (videoAsset as RemoteAsset).id;
final serverEndpoint = Store.get(StoreKey.serverEndpoint); final serverEndpoint = ref.read(sessionProvider).serverEndpoint!;
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo; final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
final String videoUrl = videoAsset.livePhotoVideoId != null final String videoUrl = videoAsset.livePhotoVideoId != null
@@ -6,7 +6,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.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/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -33,7 +33,7 @@ class ViewerKebabMenu extends ConsumerWidget {
final isInLockedView = ref.watch(inLockedViewProvider); final isInLockedView = ref.watch(inLockedViewProvider);
final currentAlbum = ref.watch(currentRemoteAlbumProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); final advancedTroubleshooting = ref.watch(appConfigProvider.select((c) => c.advanced.troubleshooting));
final actionContext = ActionButtonContext( final actionContext = ActionButtonContext(
asset: asset, asset: asset,
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart';
@@ -24,7 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@@ -56,7 +55,7 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final multiselect = ref.watch(multiSelectProvider); final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); final advancedTroubleshooting = ref.watch(appConfigProvider.select((c) => c.advanced.troubleshooting));
final tagsEnabled = ref.watch( final tagsEnabled = ref.watch(
userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false),
); );
@@ -72,9 +72,6 @@ class MapStateNotifier extends Notifier<MapState> {
} }
void switchTheme(ThemeMode mode) { void switchTheme(ThemeMode mode) {
// TODO: Remove this line when map theme provider is removed
// Until then, keep both in sync as MapThemeOverride uses map state provider
// ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapThemeMode, mode.index);
ref.read(mapStateNotifierProvider.notifier).switchTheme(mode); ref.read(mapStateNotifierProvider.notifier).switchTheme(mode);
state = state.copyWith(themeMode: mode); state = state.copyWith(themeMode: mode);
} }
@@ -1,18 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
class PartnerUserAvatar extends StatelessWidget { class PartnerUserAvatar extends ConsumerWidget {
const PartnerUserAvatar({super.key, required this.userId, required this.name}); const PartnerUserAvatar({super.key, required this.userId, required this.name});
final String userId; final String userId;
final String name; final String name;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final url = "${Store.get(StoreKey.serverEndpoint)}/users/$userId/profile-image"; final url = "${ref.read(sessionProvider).serverEndpoint}/users/$userId/profile-image";
final nameFirstLetter = name.isNotEmpty ? name[0] : ""; final nameFirstLetter = name.isNotEmpty ? name[0] : "";
return CircleAvatar( return CircleAvatar(
radius: 16, radius: 16,
@@ -1,9 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
@@ -13,6 +11,7 @@ import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/permission.provider.dart'; import 'package:immich_mobile/providers/permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -140,7 +139,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
final isEnableBackup = _ref.read(appConfigProvider).backup.enabled; final isEnableBackup = _ref.read(appConfigProvider).backup.enabled;
if (isEnableBackup) { if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser); final currentUser = _ref.read(currentUserProvider);
if (currentUser != null) { if (currentUser != null) {
await _safeRun( await _safeRun(
_ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id), _ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id),
@@ -1,4 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
final appSettingsServiceProvider = Provider((_) => const AppSettingsService());
+7 -5
View File
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
@@ -10,6 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
@@ -125,10 +127,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
} }
Future<bool> saveAuthInfo({required String accessToken}) async { Future<bool> saveAuthInfo({required String accessToken}) async {
await Store.put(StoreKey.accessToken, accessToken); await _ref.read(sessionRepository).write(SessionKey.accessToken, accessToken);
await _apiService.updateHeaders(); await _apiService.updateHeaders();
final serverEndpoint = Store.get(StoreKey.serverEndpoint); final serverEndpoint = _ref.read(sessionProvider).serverEndpoint!;
final headerMap = _ref.read(appConfigProvider).network.customHeaders; final headerMap = _ref.read(appConfigProvider).network.customHeaders;
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap); final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
@@ -136,7 +138,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
// Get the deviceid from the store if it exists, otherwise generate a new one // Get the deviceid from the store if it exists, otherwise generate a new one
String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
UserDto? user = _userService.tryGetMyUser(); UserDto? user = await _userService.tryGetMyUser();
try { try {
final serverUser = await _userService.refreshMyUser().timeout(_timeoutDuration); final serverUser = await _userService.refreshMyUser().timeout(_timeoutDuration);
@@ -193,9 +195,9 @@ class AuthNotifier extends StateNotifier<AuthState> {
return _ref.read(appConfigProvider).network.localEndpoint; return _ref.read(appConfigProvider).network.localEndpoint;
} }
/// Returns the current server endpoint (with /api) URL from the store /// Returns the current server endpoint (with /api) URL from the session
String? getServerEndpoint() { String? getServerEndpoint() {
return Store.tryGet(StoreKey.serverEndpoint); return _ref.read(sessionProvider).serverEndpoint;
} }
Future<String?> setOpenApiServiceEndpoint() { Future<String?> setOpenApiServiceEndpoint() {
@@ -1,7 +1,6 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
final hapticFeedbackProvider = StateNotifierProvider<HapticNotifier, void>((ref) { final hapticFeedbackProvider = StateNotifierProvider<HapticNotifier, void>((ref) {
return HapticNotifier(ref); return HapticNotifier(ref);
@@ -14,31 +13,31 @@ class HapticNotifier extends StateNotifier<void> {
HapticNotifier(this._ref) : super(null); HapticNotifier(this._ref) : super(null);
selectionClick() { selectionClick() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { if (_ref.read(appConfigProvider).advanced.enableHapticFeedback) {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
} }
} }
lightImpact() { lightImpact() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { if (_ref.read(appConfigProvider).advanced.enableHapticFeedback) {
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();
} }
} }
mediumImpact() { mediumImpact() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { if (_ref.read(appConfigProvider).advanced.enableHapticFeedback) {
HapticFeedback.mediumImpact(); HapticFeedback.mediumImpact();
} }
} }
heavyImpact() { heavyImpact() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { if (_ref.read(appConfigProvider).advanced.enableHapticFeedback) {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
} }
} }
vibrate() { vibrate() {
if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { if (_ref.read(appConfigProvider).advanced.enableHapticFeedback) {
HapticFeedback.vibrate(); HapticFeedback.vibrate();
} }
} }
@@ -0,0 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final appMetadataRepositoryProvider = Provider<AppMetadataRepository>(
(ref) => AppMetadataRepository(ref.watch(driftProvider)),
);
@@ -1,22 +1,19 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
class ReadOnlyModeNotifier extends Notifier<bool> { class ReadOnlyModeNotifier extends Notifier<bool> {
late AppSettingsService _appSettingService;
@override @override
bool build() { bool build() {
_appSettingService = ref.read(appSettingsServiceProvider); return ref.read(appConfigProvider).advanced.readonlyModeEnabled;
final readonlyMode = _appSettingService.getSetting(AppSettingsEnum.readonlyModeEnabled);
return readonlyMode;
} }
void setMode(bool value) { void setMode(bool value) {
final isLoggedIn = ref.read(authProvider).isAuthenticated; final isLoggedIn = ref.read(authProvider).isAuthenticated;
_appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); unawaited(ref.read(settingsProvider).write(.advancedReadonlyModeEnabled, value));
state = value; state = value;
if (value && isLoggedIn) { if (value && isLoggedIn) {
@@ -0,0 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
final sessionRepository = Provider.autoDispose<SessionRepository>((_) => SessionRepository.instance);
final sessionProvider = Provider.autoDispose<Session>((ref) {
final repo = ref.watch(sessionRepository);
final subscription = repo.watch().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.session;
});
@@ -1,20 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
class SettingsNotifier extends Notifier<SettingsService> {
@override
SettingsService build() => SettingsService(storeService: ref.read(storeServiceProvider));
T get<T>(Setting<T> setting) => state.get(setting);
Future<void> set<T>(Setting<T> setting, T value) async {
await state.set(setting, value);
ref.invalidateSelf();
}
Stream<T> watch<T>(Setting<T> setting) => state.watch(setting);
}
final settingsProvider = NotifierProvider<SettingsNotifier, SettingsService>(SettingsNotifier.new);
@@ -6,7 +6,7 @@ final settingsProvider = Provider.autoDispose<SettingsRepository>((_) => Setting
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) { final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
final repo = ref.watch(settingsProvider); final repo = ref.watch(settingsProvider);
final subscription = repo.watchConfig().listen((event) => ref.state = event); final subscription = repo.watch().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel); ref.onDispose(subscription.cancel);
return repo.appConfig; return repo.appConfig;
}); });
@@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/sync_migration.reposit
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@@ -26,6 +27,7 @@ final syncStreamServiceProvider = Provider(
permissionRepository: ref.watch(permissionRepositoryProvider), permissionRepository: ref.watch(permissionRepositoryProvider),
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider), syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
api: ref.watch(apiServiceProvider), api: ref.watch(apiServiceProvider),
appMetadataRepository: ref.watch(appMetadataRepositoryProvider),
cancellation: ref.watch(cancellationProvider), cancellation: ref.watch(cancellationProvider),
), ),
); );
@@ -42,6 +44,7 @@ final localSyncServiceProvider = Provider(
assetMediaRepository: ref.watch(assetMediaRepositoryProvider), assetMediaRepository: ref.watch(assetMediaRepositoryProvider),
permissionRepository: ref.watch(permissionRepositoryProvider), permissionRepository: ref.watch(permissionRepositoryProvider),
nativeSyncApi: ref.watch(nativeSyncApiProvider), nativeSyncApi: ref.watch(nativeSyncApiProvider),
appMetadataRepository: ref.watch(appMetadataRepositoryProvider),
cancellation: ref.watch(cancellationProvider), cancellation: ref.watch(cancellationProvider),
), ),
); );
@@ -6,17 +6,18 @@ import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.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/store.provider.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart';
final userRepositoryProvider = Provider((ref) => UserRepository(ref.watch(driftProvider))); final userRepositoryProvider = Provider((ref) => UserRepository(ref.watch(driftProvider)));
final authUserRepositoryProvider = Provider((ref) => DriftAuthUserRepository(ref.watch(driftProvider)));
final userApiRepositoryProvider = Provider((ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi)); final userApiRepositoryProvider = Provider((ref) => UserApiRepository(ref.watch(apiServiceProvider).usersApi));
final userServiceProvider = Provider( final userServiceProvider = Provider(
(ref) => UserService( (ref) => UserService(
userApiRepository: ref.watch(userApiRepositoryProvider), userApiRepository: ref.watch(userApiRepositoryProvider),
storeService: ref.watch(storeServiceProvider), authUserRepository: ref.watch(authUserRepositoryProvider),
), ),
); );
+1 -1
View File
@@ -7,7 +7,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
class CurrentUserProvider extends StateNotifier<UserDto?> { class CurrentUserProvider extends StateNotifier<UserDto?> {
CurrentUserProvider(this._userService) : super(null) { CurrentUserProvider(this._userService) : super(null) {
state = _userService.tryGetMyUser(); _userService.tryGetMyUser().then((user) => state = user ?? state);
streamSub = _userService.watchMyUser().listen((user) => state = user ?? state); streamSub = _userService.watchMyUser().listen((user) => state = user ?? state);
} }
+2 -3
View File
@@ -1,12 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
@@ -68,7 +67,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
if (authenticationState.isAuthenticated) { if (authenticationState.isAuthenticated) {
try { try {
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint)); final endpoint = Uri.parse(_ref.read(sessionProvider).serverEndpoint!);
dPrint(() => "Attempting to connect to websocket"); dPrint(() => "Attempting to connect to websocket");
// Configure socket transports must be specified // Configure socket transports must be specified
Socket socket = io( Socket socket = io(
@@ -5,9 +5,8 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
@@ -96,7 +95,7 @@ class UploadRepository {
void Function(int bytes, int totalBytes)? onProgress, void Function(int bytes, int totalBytes)? onProgress,
required String logContext, required String logContext,
}) async { }) async {
final String savedEndpoint = Store.get(StoreKey.serverEndpoint); final String savedEndpoint = SessionRepository.instance.session.serverEndpoint!;
final baseRequest = ProgressMultipartRequest( final baseRequest = ProgressMultipartRequest(
'POST', 'POST',
Uri.parse('$savedEndpoint/assets'), Uri.parse('$savedEndpoint/assets'),
+6 -10
View File
@@ -2,9 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/auth.service.dart';
@@ -23,10 +21,8 @@ class AuthGuard extends AutoRouteGuard {
// guards, so we keep this function fully sync and validate the token in // guards, so we keep this function fully sync and validate the token in
// the background — otherwise a slow validateAccessToken() request would // the background — otherwise a slow validateAccessToken() request would
// block the route transition for as long as the OS-level HTTP timeout. // block the route transition for as long as the OS-level HTTP timeout.
try { if (SessionRepository.instance.session.accessToken == null) {
Store.get(StoreKey.accessToken); _log.warning('No access token in the session.');
} on StoreKeyNotFoundException catch (_) {
_log.warning('No access token in the store.');
resolver.next(false); resolver.next(false);
unawaited(router.replaceAll([const LoginRoute()])); unawaited(router.replaceAll([const LoginRoute()]));
return; return;
@@ -40,7 +36,7 @@ class AuthGuard extends AutoRouteGuard {
if (_validateInFlight) { if (_validateInFlight) {
return; return;
} }
final token = Store.tryGet(StoreKey.accessToken); final token = SessionRepository.instance.session.accessToken;
if (token == null) { if (token == null) {
return; return;
} }
@@ -50,7 +46,7 @@ class AuthGuard extends AutoRouteGuard {
if (res == null || res.authStatus != true) { if (res == null || res.authStatus != true) {
// Token may have changed during validation (user logged out + logged in // Token may have changed during validation (user logged out + logged in
// again); only act if it still applies to the current session. // again); only act if it still applies to the current session.
if (Store.tryGet(StoreKey.accessToken) != token) { if (SessionRepository.instance.session.accessToken != token) {
return; return;
} }
_log.fine('User token is invalid. Redirecting to login'); _log.fine('User token is invalid. Redirecting to login');
@@ -61,7 +57,7 @@ class AuthGuard extends AutoRouteGuard {
if (e.code != HttpStatus.unauthorized) { if (e.code != HttpStatus.unauthorized) {
return; return;
} }
if (Store.tryGet(StoreKey.accessToken) != token) { if (SessionRepository.instance.session.accessToken != token) {
return; return;
} }
_log.warning("Unauthorized access token."); _log.warning("Unauthorized access token.");
+6 -3
View File
@@ -6,15 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.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/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/tag.service.dart'; import 'package:immich_mobile/domain/services/tag.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
@@ -38,6 +38,7 @@ final actionServiceProvider = Provider<ActionService>(
ref.watch(assetMediaRepositoryProvider), ref.watch(assetMediaRepositoryProvider),
ref.watch(downloadRepositoryProvider), ref.watch(downloadRepositoryProvider),
ref.watch(tagServiceProvider), ref.watch(tagServiceProvider),
ref.watch(appMetadataRepositoryProvider),
), ),
); );
@@ -51,6 +52,7 @@ class ActionService {
final AssetMediaRepository _assetMediaRepository; final AssetMediaRepository _assetMediaRepository;
final DownloadRepository _downloadRepository; final DownloadRepository _downloadRepository;
final TagService _tagService; final TagService _tagService;
final AppMetadataRepository _appMetadataRepository;
const ActionService( const ActionService(
this._assetApiRepository, this._assetApiRepository,
@@ -62,6 +64,7 @@ class ActionService {
this._assetMediaRepository, this._assetMediaRepository,
this._downloadRepository, this._downloadRepository,
this._tagService, this._tagService,
this._appMetadataRepository,
); );
Future<void> shareLink(List<String> remoteIds, BuildContext context) async { Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
@@ -318,7 +321,7 @@ class ActionService {
if (deletedIds.isEmpty) { if (deletedIds.isEmpty) {
return 0; return 0;
} }
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (CurrentPlatform.isAndroid && await _appMetadataRepository.get(.manageLocalMediaAndroid)) {
await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds); await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds);
} else { } else {
await _localAssetRepository.delete(deletedIds); await _localAssetRepository.delete(deletedIds);
+7 -7
View File
@@ -3,10 +3,10 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -41,7 +41,7 @@ class ApiService {
// The below line ensures that the api clients are initialized when the service is instantiated // The below line ensures that the api clients are initialized when the service is instantiated
// This is required to avoid late initialization errors when the clients are access before the endpoint is resolved // This is required to avoid late initialization errors when the clients are access before the endpoint is resolved
setEndpoint(''); setEndpoint('');
final endpoint = Store.tryGet(StoreKey.serverEndpoint); final endpoint = SessionRepository.instance.session.serverEndpoint;
if (endpoint != null && endpoint.isNotEmpty) { if (endpoint != null && endpoint.isNotEmpty) {
setEndpoint(endpoint); setEndpoint(endpoint);
} }
@@ -84,7 +84,7 @@ class ApiService {
setEndpoint(endpoint); setEndpoint(endpoint);
// Save in local database for next startup // Save in local database for next startup
await Store.put(StoreKey.serverEndpoint, endpoint); await SessionRepository.instance.write(SessionKey.serverEndpoint, endpoint);
return endpoint; return endpoint;
} }
@@ -173,13 +173,13 @@ class ApiService {
static List<String> getServerUrls() { static List<String> getServerUrls() {
final urls = <String>[]; final urls = <String>[];
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint); final serverEndpoint = SessionRepository.instance.session.serverEndpoint;
if (serverEndpoint != null && serverEndpoint.isNotEmpty) { if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint); urls.add(serverEndpoint);
} }
final network = SettingsRepository.instance.appConfig.network; final network = SettingsRepository.instance.appConfig.network;
final localEndpoint = network.localEndpoint; final localEndpoint = network.localEndpoint;
if (localEndpoint.isNotEmpty) { if (localEndpoint != null && localEndpoint.isNotEmpty) {
urls.add(localEndpoint); urls.add(localEndpoint);
} }
for (final url in network.externalEndpointList) { for (final url in network.externalEndpointList) {
@@ -1,26 +0,0 @@
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
final StoreKey<T> storeKey;
final String? hiveKey;
final T defaultValue;
}
class AppSettingsService {
const AppSettingsService();
T getSetting<T>(AppSettingsEnum<T> setting) {
return Store.get(setting.storeKey, setting.defaultValue);
}
Future<void> setSetting<T>(AppSettingsEnum<T> setting, T value) {
return Store.put(setting.storeKey, value);
}
}
+5 -6
View File
@@ -1,12 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
@@ -55,7 +55,7 @@ class AuthService {
Future<String> validateServerUrl(String url) async { Future<String> validateServerUrl(String url) async {
final validUrl = await _apiService.resolveAndSetEndpoint(url); final validUrl = await _apiService.resolveAndSetEndpoint(url);
await _apiService.setDeviceInfoHeader(); await _apiService.setDeviceInfoHeader();
await Store.put(StoreKey.serverUrl, validUrl); await SessionRepository.instance.write(SessionKey.serverUrl, validUrl);
return validUrl; return validUrl;
} }
@@ -118,8 +118,7 @@ class AuthService {
await _backgroundSyncManager.cancel(); await _backgroundSyncManager.cancel();
await Future.wait([ await Future.wait([
_authRepository.clearLocalData(), _authRepository.clearLocalData(),
Store.delete(StoreKey.currentUser), SessionRepository.instance.clear([SessionKey.accessToken]),
Store.delete(StoreKey.accessToken),
SettingsRepository.instance.clear(const [ SettingsRepository.instance.clear(const [
.networkAutoEndpointSwitching, .networkAutoEndpointSwitching,
.networkPreferredWifiName, .networkPreferredWifiName,
@@ -13,6 +13,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
@@ -386,7 +387,7 @@ class BackgroundUploadService {
String? latitude, String? latitude,
String? longitude, String? longitude,
}) async { }) async {
final serverEndpoint = Store.get(StoreKey.serverEndpoint); final serverEndpoint = SessionRepository.instance.session.serverEndpoint!;
final url = Uri.parse('$serverEndpoint/assets').toString(); final url = Uri.parse('$serverEndpoint/assets').toString();
final headers = ApiService.getRequestHeaders(); final headers = ApiService.getRequestHeaders();
final deviceId = Store.get(StoreKey.deviceId); final deviceId = Store.get(StoreKey.deviceId);
+3
View File
@@ -6,6 +6,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
@@ -51,6 +52,8 @@ abstract final class Bootstrap {
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
await SessionRepository.ensureInitialized(drift);
final settingsRepo = await SettingsRepository.ensureInitialized(drift); final settingsRepo = await SettingsRepository.ensureInitialized(drift);
await LogService.init( await LogService.init(
@@ -1,13 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
ValueNotifier<T> useAppSettingsState<T>(AppSettingsEnum<T> key) {
final notifier = useState<T>(Store.get(key.storeKey, key.defaultValue));
// Listen to changes to the notifier and update app settings
useValueChanged(notifier.value, (_, __) => Store.put(key.storeKey, notifier.value));
return notifier;
}
+6 -6
View File
@@ -1,9 +1,8 @@
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
String getOriginalUrlForRemoteId(final String id, {bool edited = true}) { String getOriginalUrlForRemoteId(final String id, {bool edited = true}) {
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited'; return '${SessionRepository.instance.session.serverEndpoint!}/assets/$id/original?edited=$edited';
} }
String getThumbnailUrlForRemoteId( String getThumbnailUrlForRemoteId(
@@ -12,14 +11,15 @@ String getThumbnailUrlForRemoteId(
bool edited = true, bool edited = true,
String? thumbhash, String? thumbhash,
}) { }) {
final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited'; final url =
'${SessionRepository.instance.session.serverEndpoint!}/assets/$id/thumbnail?size=${type.value}&edited=$edited';
return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url; return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url;
} }
String getPlaybackUrlForRemoteId(final String id) { String getPlaybackUrlForRemoteId(final String id) {
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?'; return '${SessionRepository.instance.session.serverEndpoint!}/assets/$id/video/playback?';
} }
String getFaceThumbnailUrl(final String personId) { String getFaceThumbnailUrl(final String personId) {
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail'; return '${SessionRepository.instance.session.serverEndpoint!}/people/$personId/thumbnail';
} }
+174 -93
View File
@@ -6,22 +6,29 @@ import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.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/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/settings_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/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/app_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
const int targetVersion = 26;
Future<void> migrateDatabaseIfNeeded(Drift drift) async { Future<void> migrateDatabaseIfNeeded(Drift drift) async {
final int version = Store.get(StoreKey.version, targetVersion); final metadataRepository = AppMetadataRepository(drift);
final int version = await metadataRepository.get(AppMetadataKey.version);
if (version < 25) { if (version < 25) {
await _migrateTo25(); await _migrateTo25();
@@ -31,26 +38,38 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
await _migrateTo26(drift); await _migrateTo26(drift);
} }
await Store.put(StoreKey.version, targetVersion); if (version < 27) {
await _migrateTo27(drift);
}
if (version < 28) {
await _migrateTo28(drift);
}
if (version < 29) {
await _migrateTo29(drift);
}
await metadataRepository.set(AppMetadataKey.version, kCurrentVersion);
return; return;
} }
Future<void> _migrateTo25() async { Future<void> _migrateTo25() async {
final accessToken = Store.tryGet(StoreKey.accessToken); final accessToken = Store.tryGet(.legacyAccessToken);
if (accessToken == null || accessToken.isEmpty) { if (accessToken == null || accessToken.isEmpty) {
return; return;
} }
final urls = <String>[]; final urls = <String>[];
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint); final serverEndpoint = Store.tryGet(.legacyServerEndpoint);
if (serverEndpoint != null && serverEndpoint.isNotEmpty) { if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint); urls.add(serverEndpoint);
} }
final localEndpoint = Store.tryGet(StoreKey.legacyLocalEndpoint); final localEndpoint = Store.tryGet(.legacyLocalEndpoint);
if (localEndpoint != null && localEndpoint.isNotEmpty) { if (localEndpoint != null && localEndpoint.isNotEmpty) {
urls.add(localEndpoint); urls.add(localEndpoint);
} }
final externalJson = Store.tryGet(StoreKey.legacyExternalEndpointList); final externalJson = Store.tryGet(.legacyExternalEndpointList);
if (externalJson != null) { if (externalJson != null) {
final List<dynamic> list = jsonDecode(externalJson); final List<dynamic> list = jsonDecode(externalJson);
for (final entry in list) { for (final entry in list) {
@@ -64,7 +83,7 @@ Future<void> _migrateTo25() async {
return; return;
} }
final customHeadersStr = Store.get(StoreKey.legacyCustomHeaders, ""); final customHeadersStr = Store.get(.legacyCustomHeaders, "");
final headers = customHeadersStr.isEmpty final headers = customHeadersStr.isEmpty
? const <String, String>{} ? const <String, String>{}
: (jsonDecode(customHeadersStr) as Map).cast<String, String>(); : (jsonDecode(customHeadersStr) as Map).cast<String, String>();
@@ -73,81 +92,109 @@ Future<void> _migrateTo25() async {
} }
Future<void> _migrateTo26(Drift drift) async { Future<void> _migrateTo26(Drift drift) async {
final migrator = _StoreMigrator(drift); final migrator = _StoreMigrator.settings(drift);
await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, SettingsKey.logLevel, LogLevel.values); await migrator.migrateEnumIndex(.legacyLogLevel, .logLevel, LogLevel.values);
// Theme // Theme
await migrator.migrateEnumName(StoreKey.legacyThemeMode, SettingsKey.themeMode, ThemeMode.values); await migrator.migrateEnumName(.legacyThemeMode, .themeMode, ThemeMode.values);
await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, SettingsKey.themePrimaryColor, ImmichColorPreset.values); await migrator.migrateEnumName(.legacyPrimaryColor, .themePrimaryColor, ImmichColorPreset.values);
await migrator.migrateBool(StoreKey.legacyDynamicTheme, SettingsKey.themeDynamic); await migrator.migrateBool(.legacyDynamicTheme, .themeDynamic);
await migrator.migrateBool(StoreKey.legacyColorfulInterface, SettingsKey.themeColorfulInterface); await migrator.migrateBool(.legacyColorfulInterface, .themeColorfulInterface);
// Cleanup // Cleanup
final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id); final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds);
if (cleanupKeepAlbumIds != null) { if (cleanupKeepAlbumIds != null) {
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, SettingsKey.cleanupKeepAlbumIds, ids); migrator.stage(.legacyCleanupKeepAlbumIds, .cleanupKeepAlbumIds, ids);
} }
await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, SettingsKey.cleanupKeepFavorites); await migrator.migrateBool(.legacyCleanupKeepFavorites, .cleanupKeepFavorites);
await migrator.migrateEnumIndex( await migrator.migrateEnumIndex(.legacyCleanupKeepMediaType, .cleanupKeepMediaType, AssetKeepType.values);
StoreKey.legacyCleanupKeepMediaType, await migrator.migrateInt(.legacyCleanupCutoffDaysAgo, .cleanupCutoffDaysAgo);
SettingsKey.cleanupKeepMediaType, await migrator.migrateBool(.legacyCleanupDefaultsInitialized, .cleanupDefaultsInitialized);
AssetKeepType.values,
);
await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, SettingsKey.cleanupCutoffDaysAgo);
await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, SettingsKey.cleanupDefaultsInitialized);
// Map // Map
await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, SettingsKey.mapShowFavoriteOnly); await migrator.migrateBool(.legacyMapShowFavoriteOnly, .mapShowFavoriteOnly);
await migrator.migrateInt(StoreKey.legacyMapRelativeDate, SettingsKey.mapRelativeDate); await migrator.migrateInt(.legacyMapRelativeDate, .mapRelativeDate);
await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, SettingsKey.mapIncludeArchived); await migrator.migrateBool(.legacyMapIncludeArchived, .mapIncludeArchived);
await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, SettingsKey.mapThemeMode, ThemeMode.values); await migrator.migrateEnumIndex(.legacyMapThemeMode, .mapThemeMode, ThemeMode.values);
await migrator.migrateBool(StoreKey.legacyMapwithPartners, SettingsKey.mapWithPartners); await migrator.migrateBool(.legacyMapwithPartners, .mapWithPartners);
// Timeline // Timeline
await migrator.migrateInt(StoreKey.legacyTilesPerRow, SettingsKey.timelineTilesPerRow); await migrator.migrateInt(.legacyTilesPerRow, .timelineTilesPerRow);
await migrator.migrateEnumIndex( await migrator.migrateEnumIndex(.legacyGroupAssetsBy, .timelineGroupAssetsBy, GroupAssetsBy.values);
StoreKey.legacyGroupAssetsBy, await migrator.migrateBool(.legacyStorageIndicator, .timelineStorageIndicator);
SettingsKey.timelineGroupAssetsBy,
GroupAssetsBy.values,
);
await migrator.migrateBool(StoreKey.legacyStorageIndicator, SettingsKey.timelineStorageIndicator);
// Image // Image
await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, SettingsKey.imagePreferRemote); await migrator.migrateBool(.legacyPreferRemoteImage, .imagePreferRemote);
await migrator.migrateBool(StoreKey.legacyLoadOriginal, SettingsKey.imageLoadOriginal); await migrator.migrateBool(.legacyLoadOriginal, .imageLoadOriginal);
// Viewer // Viewer
await migrator.migrateBool(StoreKey.legacyLoopVideo, SettingsKey.viewerLoopVideo); await migrator.migrateBool(.legacyLoopVideo, .viewerLoopVideo);
await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, SettingsKey.viewerLoadOriginalVideo); await migrator.migrateBool(.legacyLoadOriginalVideo, .viewerLoadOriginalVideo);
await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, SettingsKey.viewerAutoPlayVideo); await migrator.migrateBool(.legacyAutoPlayVideo, .viewerAutoPlayVideo);
await migrator.migrateBool(StoreKey.legacyTapToNavigate, SettingsKey.viewerTapToNavigate); await migrator.migrateBool(.legacyTapToNavigate, .viewerTapToNavigate);
// Network // Network
await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, SettingsKey.networkAutoEndpointSwitching); await migrator.migrateBool(.legacyAutoEndpointSwitching, .networkAutoEndpointSwitching);
await migrator.migrateString(StoreKey.legacyPreferredWifiName, SettingsKey.networkPreferredWifiName); final preferredWifiName = await migrator.readLegacyStoreString(.legacyPreferredWifiName);
await migrator.migrateString(StoreKey.legacyLocalEndpoint, SettingsKey.networkLocalEndpoint); migrator.stage(.legacyPreferredWifiName, .networkPreferredWifiName, preferredWifiName);
final localEndpoint = await migrator.readLegacyStoreString(.legacyLocalEndpoint);
migrator.stage(.legacyLocalEndpoint, .networkLocalEndpoint, localEndpoint);
await _migrateExternalEndpointList(migrator); await _migrateExternalEndpointList(migrator);
await _migrateCustomHeaders(migrator); await _migrateCustomHeaders(migrator);
// Album // Album
await _migrateAlbumSortMode(migrator); await _migrateAlbumSortMode(migrator);
await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, SettingsKey.albumIsReverse); await migrator.migrateBool(.legacySelectedAlbumSortReverse, .albumIsReverse);
await migrator.migrateBool(StoreKey.legacyAlbumGridView, SettingsKey.albumIsGrid); await migrator.migrateBool(.legacyAlbumGridView, .albumIsGrid);
// Backup // Backup
await migrator.migrateBool(StoreKey.legacyEnableBackup, SettingsKey.backupEnabled); await migrator.migrateBool(.legacyEnableBackup, .backupEnabled);
await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, SettingsKey.backupUseCellularForVideos); await migrator.migrateBool(.legacyUseWifiForUploadVideos, .backupUseCellularForVideos);
await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, SettingsKey.backupUseCellularForPhotos); await migrator.migrateBool(.legacyUseWifiForUploadPhotos, .backupUseCellularForPhotos);
await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, SettingsKey.backupRequireCharging); await migrator.migrateBool(.legacyBackupRequireCharging, .backupRequireCharging);
await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, SettingsKey.backupTriggerDelay); await migrator.migrateInt(.legacyBackupTriggerDelay, .backupTriggerDelay);
await migrator.migrateBool(StoreKey.legacySyncAlbums, SettingsKey.backupSyncAlbums); await migrator.migrateBool(.legacySyncAlbums, .backupSyncAlbums);
await migrator.complete(); await migrator.complete();
} }
Future<void> _migrateAlbumSortMode(_StoreMigrator migrator) async { Future<void> _migrateTo27(Drift drift) async {
final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id); final migrator = _StoreMigrator.session(drift);
await migrator.migrateString(.legacyServerUrl, .serverUrl);
await migrator.migrateString(.legacyAccessToken, .accessToken);
await migrator.migrateString(.legacyServerEndpoint, .serverEndpoint);
await migrator.complete();
await SessionRepository.instance.refresh();
}
Future<void> _migrateTo28(Drift drift) async {
final migrator = _StoreMigrator.settings(drift);
await migrator.migrateBool(.legacyAdvancedTroubleshooting, .advancedTroubleshooting);
await migrator.migrateBool(.legacyEnableHapticFeedback, .advancedEnableHapticFeedback);
await migrator.migrateBool(.legacyReadonlyModeEnabled, .advancedReadonlyModeEnabled);
await migrator.complete();
await SettingsRepository.instance.refresh();
}
Future<void> _migrateTo29(Drift drift) async {
final migrator = _StoreMigrator.appMetadata(drift);
final rawStatus = await migrator.readLegacyStoreString(.legacySyncMigrationStatus);
if (rawStatus != null) {
final decoded = jsonDecode(rawStatus);
final migrations = decoded is List ? decoded.whereType<String>().toList() : <String>[];
migrator.stage(.legacySyncMigrationStatus, .syncMigrationStatus, migrations);
}
await migrator.migrateBool(.legacyManageLocalMediaAndroid, .manageLocalMediaAndroid);
await migrator.complete();
}
Future<void> _migrateAlbumSortMode(_StoreMigrator<SettingsKey> migrator) async {
final raw = await migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder);
final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw); final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw);
if (mode == null) { if (mode == null) {
return; return;
} }
migrator.stage(StoreKey.legacySelectedAlbumSortOrder, SettingsKey.albumSortMode, mode); migrator.stage(.legacySelectedAlbumSortOrder, .albumSortMode, mode);
} }
Future<void> _migrateExternalEndpointList(_StoreMigrator migrator) async { Future<void> _migrateExternalEndpointList(_StoreMigrator<SettingsKey> migrator) async {
final raw = await migrator.readLegacyStoreString(StoreKey.legacyExternalEndpointList.id); final raw = await migrator.readLegacyStoreString(.legacyExternalEndpointList);
if (raw == null) { if (raw == null) {
return; return;
} }
@@ -170,8 +217,8 @@ Future<void> _migrateExternalEndpointList(_StoreMigrator migrator) async {
migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls); migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls);
} }
Future<void> _migrateCustomHeaders(_StoreMigrator migrator) async { Future<void> _migrateCustomHeaders(_StoreMigrator<SettingsKey> migrator) async {
final raw = await migrator.readLegacyStoreString(StoreKey.legacyCustomHeaders.id); final raw = await migrator.readLegacyStoreString(.legacyCustomHeaders);
if (raw == null) { if (raw == null) {
return; return;
} }
@@ -193,15 +240,51 @@ Future<void> _migrateCustomHeaders(_StoreMigrator migrator) async {
migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers); migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers);
} }
class _StoreMigrator { class _StoreMigrator<K extends Enum> {
_StoreMigrator._(this._db, {required this.encode, required this.readDefault, required this.insertRow});
static _StoreMigrator<SettingsKey> settings(Drift db) => _StoreMigrator<SettingsKey>._(
db,
encode: (key, value) => key.encode(value),
readDefault: (key) => defaultConfig.read(key),
insertRow: (batch, name, value) => batch.insert(
db.settingsEntity,
SettingsEntityCompanion(key: Value(name), value: Value(value)),
mode: InsertMode.insertOrReplace,
),
);
static _StoreMigrator<SessionKey> session(Drift db) => _StoreMigrator<SessionKey>._(
db,
encode: (key, value) => key.encode(value),
readDefault: (key) => defaultSession.read(key),
insertRow: (batch, name, value) => batch.insert(
db.sessionEntity,
SessionEntityCompanion(key: Value(name), value: Value(value)),
mode: InsertMode.insertOrReplace,
),
);
static _StoreMigrator<AppMetadataKey> appMetadata(Drift db) => _StoreMigrator<AppMetadataKey>._(
db,
encode: (key, value) => key.encode(value),
readDefault: (_) => null,
insertRow: (batch, name, value) => batch.insert(
db.appMetadataEntity,
AppMetadataEntityCompanion(key: Value(name), value: Value(value)),
mode: InsertMode.insertOrReplace,
),
);
final Drift _db; final Drift _db;
final Map<SettingsKey<Object>, Object> _cache = {}; final String Function(K key, Object value) encode;
final Object? Function(K key) readDefault;
final void Function(Batch batch, String name, String? value) insertRow;
final Map<K, Object?> _cache = {};
final List<int> _migratedStoreIds = []; final List<int> _migratedStoreIds = [];
_StoreMigrator(this._db); Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, K newKey, List<T> values) async {
final index = await readLegacyStoreInt(legacyKey);
Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, SettingsKey<T> newKey, List<T> values) async {
final index = await readLegacyStoreInt(legacyKey.id);
if (index == null) { if (index == null) {
return; return;
} }
@@ -215,12 +298,8 @@ class _StoreMigrator {
_migratedStoreIds.add(legacyKey.id); _migratedStoreIds.add(legacyKey.id);
} }
Future<void> migrateEnumName<T extends Enum>( Future<void> migrateEnumName<T extends Enum>(StoreKey<String> legacyKey, K newKey, List<T> values) async {
StoreKey<String> legacyKey, final name = await readLegacyStoreString(legacyKey);
SettingsKey<T> newKey,
List<T> values,
) async {
final name = await readLegacyStoreString(legacyKey.id);
if (name == null) { if (name == null) {
return; return;
} }
@@ -234,19 +313,18 @@ class _StoreMigrator {
_migratedStoreIds.add(legacyKey.id); _migratedStoreIds.add(legacyKey.id);
} }
Future<void> migrateBool(StoreKey<bool> legacyKey, SettingsKey<bool> newKey) async { Future<void> migrateBool(StoreKey<bool> legacyKey, K newKey) async {
final intValue = await readLegacyStoreInt(legacyKey.id); final intValue = await readLegacyStoreInt(legacyKey);
if (intValue == null) { if (intValue == null) {
return; return;
} }
final boolValue = intValue != 0; _cache[newKey] = intValue != 0;
_cache[newKey] = boolValue;
_migratedStoreIds.add(legacyKey.id); _migratedStoreIds.add(legacyKey.id);
} }
Future<void> migrateInt(StoreKey<int> legacyKey, SettingsKey<int> newKey) async { Future<void> migrateInt(StoreKey<int> legacyKey, K newKey) async {
final intValue = await readLegacyStoreInt(legacyKey.id); final intValue = await readLegacyStoreInt(legacyKey);
if (intValue == null) { if (intValue == null) {
return; return;
} }
@@ -255,9 +333,9 @@ class _StoreMigrator {
_migratedStoreIds.add(legacyKey.id); _migratedStoreIds.add(legacyKey.id);
} }
Future<void> migrateString(StoreKey<String> legacyKey, SettingsKey<String> newKey) async { Future<void> migrateString(StoreKey<String> legacyKey, K newKey) async {
final value = await readLegacyStoreString(legacyKey.id); final value = await readLegacyStoreString(legacyKey);
if (value == null) { if (value == null || value.isEmpty) {
return; return;
} }
@@ -265,7 +343,12 @@ class _StoreMigrator {
_migratedStoreIds.add(legacyKey.id); _migratedStoreIds.add(legacyKey.id);
} }
void stage<T extends Object>(StoreKey legacyKey, SettingsKey<T> newKey, T value) { Future<void> migrateNullableString(StoreKey<String> legacyKey, K newKey) async {
_cache[newKey] = await readLegacyStoreString(legacyKey);
_migratedStoreIds.add(legacyKey.id);
}
void stage(StoreKey legacyKey, K newKey, Object? value) {
_cache[newKey] = value; _cache[newKey] = value;
_migratedStoreIds.add(legacyKey.id); _migratedStoreIds.add(legacyKey.id);
} }
@@ -273,26 +356,24 @@ class _StoreMigrator {
Future<void> complete() async { Future<void> complete() async {
await _db.batch((batch) { await _db.batch((batch) {
for (final entry in _cache.entries) { for (final entry in _cache.entries) {
if (entry.value == defaultConfig.read(entry.key)) { if (entry.value == readDefault(entry.key)) {
continue; continue;
} }
batch.insert(
_db.settingsEntity, final value = entry.value;
SettingsEntityCompanion(key: Value(entry.key.name), value: Value(entry.key.encode(entry.value))), insertRow(batch, entry.key.name, value == null ? null : encode(entry.key, value));
mode: InsertMode.insertOrReplace,
);
} }
}); });
await deleteLegacyStoreRows(_migratedStoreIds); await deleteLegacyStoreRows(_migratedStoreIds);
} }
Future<String?> readLegacyStoreString(int id) async { Future<String?> readLegacyStoreString(StoreKey key) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull();
return row?.stringValue; return row?.stringValue;
} }
Future<int?> readLegacyStoreInt(int id) async { Future<int?> readLegacyStoreInt(StoreKey key) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull();
return row?.intValue; return row?.intValue;
} }
+2 -2
View File
@@ -55,8 +55,8 @@ final class None<T> extends Option<T> {
int get hashCode => 0; int get hashCode => 0;
} }
extension ObjectOptionExtension<T> on T? { extension NullableOptionExtension<T> on Option<T>? {
Option<T> toOption() => Option.fromNullable(this); T? patch(T? current) => this == null ? current : this!.unwrapOrNull;
} }
extension OptionToOptional<T> on Option<T> { extension OptionToOptional<T> on Option<T> {
+2 -3
View File
@@ -1,5 +1,4 @@
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:punycode/punycode.dart'; import 'package:punycode/punycode.dart';
String sanitizeUrl(String url) { String sanitizeUrl(String url) {
@@ -11,7 +10,7 @@ String sanitizeUrl(String url) {
} }
String? getServerUrl() { String? getServerUrl() {
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint)); final serverUrl = punycodeDecodeUrl(SessionRepository.instance.session.serverEndpoint);
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null; final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
if (serverUri == null) { if (serverUri == null) {
return null; return null;
+2 -3
View File
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
Widget userAvatar(BuildContext context, UserDto u, {double? radius}) { Widget userAvatar(BuildContext context, UserDto u, {double? radius}) {
final url = "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image"; final url = "${SessionRepository.instance.session.serverEndpoint!}/users/${u.id}/profile-image";
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : ""; final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
return CircleAvatar( return CircleAvatar(
radius: radius, radius: radius,
@@ -1,9 +1,8 @@
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/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/providers/infrastructure/session.provider.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class UserCircleAvatar extends ConsumerWidget { class UserCircleAvatar extends ConsumerWidget {
@@ -18,7 +17,7 @@ class UserCircleAvatar extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final userAvatarColor = user.avatarColor.toColor().withValues(alpha: opacity); final userAvatarColor = user.avatarColor.toColor().withValues(alpha: opacity);
final profileImageUrl = final profileImageUrl =
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}'; '${ref.read(sessionProvider).serverEndpoint}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}';
final textColor = (user.avatarColor.toColor().computeLuminance() > 0.5 ? Colors.black : Colors.white).withValues( final textColor = (user.avatarColor.toColor().computeLuminance() > 0.5 ? Colors.black : Colors.white).withValues(
alpha: opacity, alpha: opacity,
@@ -11,14 +11,14 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart';
import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
@@ -242,7 +242,8 @@ class LoginForm extends HookConsumerWidget {
} }
} }
bool isSyncRemoteDeletionsMode() => Platform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false); Future<bool> isSyncRemoteDeletionsMode() async =>
Platform.isAndroid && await ref.read(appMetadataRepositoryProvider).get(AppMetadataKey.manageLocalMediaAndroid);
login() async { login() async {
TextInput.finishAutofillContext(); TextInput.finishAutofillContext();
@@ -257,7 +258,7 @@ class LoginForm extends HookConsumerWidget {
unawaited(context.pushRoute(const ChangePasswordRoute())); unawaited(context.pushRoute(const ChangePasswordRoute()));
} else { } else {
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (isSyncRemoteDeletionsMode()) { if (await isSyncRemoteDeletionsMode()) {
await getManageMediaPermission(); await getManageMediaPermission();
} }
unawaited(handleSyncFlow()); unawaited(handleSyncFlow());
@@ -345,7 +346,7 @@ class LoginForm extends HookConsumerWidget {
if (isSuccess) { if (isSuccess) {
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
if (isSyncRemoteDeletionsMode()) { if (await isSyncRemoteDeletionsMode()) {
await getManageMediaPermission(); await getManageMediaPermission();
} }
unawaited(handleSyncFlow()); unawaited(handleSyncFlow());
@@ -5,15 +5,15 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.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/repositories/permission.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_action_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_action_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
@@ -27,8 +27,12 @@ class AdvancedSettings extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final advancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); final advancedTroubleshooting = useState(ref.read(appConfigProvider).advanced.troubleshooting);
final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); useValueChanged(
advancedTroubleshooting.value,
(_, __) => ref.read(settingsProvider).write(.advancedTroubleshooting, advancedTroubleshooting.value),
);
final manageLocalMediaAndroid = useState(false);
final isManageMediaSupported = useState(false); final isManageMediaSupported = useState(false);
final manageMediaAndroidPermission = useState(false); final manageMediaAndroidPermission = useState(false);
final levelId = useState<int>(ref.read(appConfigProvider).logLevel.index); final levelId = useState<int>(ref.read(appConfigProvider).logLevel.index);
@@ -37,7 +41,7 @@ class AdvancedSettings extends HookConsumerWidget {
preferRemote.value, preferRemote.value,
(_, __) => ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value), (_, __) => ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value),
); );
final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final readonlyModeEnabled = useState(ref.read(appConfigProvider).advanced.readonlyModeEnabled);
final logLevel = Level.LEVELS[levelId.value].name; final logLevel = Level.LEVELS[levelId.value].name;
@@ -57,6 +61,9 @@ class AdvancedSettings extends HookConsumerWidget {
() async { () async {
isManageMediaSupported.value = await checkAndroidVersion(); isManageMediaSupported.value = await checkAndroidVersion();
if (isManageMediaSupported.value) { if (isManageMediaSupported.value) {
manageLocalMediaAndroid.value = await ref
.read(appMetadataRepositoryProvider)
.get(AppMetadataKey.manageLocalMediaAndroid);
manageMediaAndroidPermission.value = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); manageMediaAndroidPermission.value = await ref.read(permissionRepositoryProvider).hasManageMediaPermission();
} }
}(); }();
@@ -83,6 +90,9 @@ class AdvancedSettings extends HookConsumerWidget {
final result = await ref.read(permissionRepositoryProvider).requestManageMediaPermission(); final result = await ref.read(permissionRepositoryProvider).requestManageMediaPermission();
manageLocalMediaAndroid.value = result; manageLocalMediaAndroid.value = result;
manageMediaAndroidPermission.value = result; manageMediaAndroidPermission.value = result;
await ref.read(appMetadataRepositoryProvider).set(AppMetadataKey.manageLocalMediaAndroid, result);
} else {
await ref.read(appMetadataRepositoryProvider).set(AppMetadataKey.manageLocalMediaAndroid, false);
} }
}, },
), ),
@@ -5,7 +5,6 @@ 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/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/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
@@ -20,7 +19,6 @@ class GroupSettings extends HookConsumerWidget {
Future<void> updateAppSettings(GroupAssetsBy groupBy) async { Future<void> updateAppSettings(GroupAssetsBy groupBy) async {
await ref.read(settingsProvider).write(.timelineGroupAssetsBy, groupBy); await ref.read(settingsProvider).write(.timelineGroupAssetsBy, groupBy);
ref.invalidate(appSettingsServiceProvider);
ref.invalidate(timelineServiceProvider); ref.invalidate(timelineServiceProvider);
} }
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/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/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.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';
@@ -32,9 +31,6 @@ class LayoutSettings extends HookConsumerWidget {
maxValue: 6, maxValue: 6,
minValue: 2, minValue: 2,
noDivisons: 4, noDivisons: 4,
onChangeEnd: (value) {
ref.invalidate(appSettingsServiceProvider);
},
), ),
], ],
); );
@@ -2,7 +2,6 @@ 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:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.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';
@@ -22,7 +21,6 @@ class AssetListSettings extends HookConsumerWidget {
title: 'theme_setting_asset_list_storage_indicator_title'.tr(), title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
onChanged: (value) { onChanged: (value) {
ref.read(settingsProvider).write(.timelineStorageIndicator, value); ref.read(settingsProvider).write(.timelineStorageIndicator, value);
ref.invalidate(appSettingsServiceProvider);
ref.invalidate(settingsProvider); ref.invalidate(settingsProvider);
}, },
), ),
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/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/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.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_switch_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
@@ -29,7 +28,6 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
valueNotifier: isOriginal, valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".t(context: context), title: "setting_image_viewer_original_title".t(context: context),
subtitle: "setting_image_viewer_original_subtitle".t(context: context), subtitle: "setting_image_viewer_original_subtitle".t(context: context),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
), ),
], ],
); );
@@ -6,10 +6,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/app_metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.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/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
@@ -17,7 +18,6 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.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/setting_list_tile.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
@@ -219,7 +219,7 @@ class _SyncStatsCounts extends ConsumerWidget {
final localAlbumService = ref.watch(localAlbumServiceProvider); final localAlbumService = ref.watch(localAlbumServiceProvider);
final remoteAlbumService = ref.watch(remoteAlbumServiceProvider); final remoteAlbumService = ref.watch(remoteAlbumServiceProvider);
final memoryService = ref.watch(driftMemoryServiceProvider); final memoryService = ref.watch(driftMemoryServiceProvider);
final appSettingsService = ref.watch(appSettingsServiceProvider); final appMetadataRepository = ref.watch(appMetadataRepositoryProvider);
Future<List<dynamic>> loadCounts() async { Future<List<dynamic>> loadCounts() async {
final assetCounts = assetService.getAssetCounts(); final assetCounts = assetService.getAssetCounts();
@@ -227,8 +227,16 @@ class _SyncStatsCounts extends ConsumerWidget {
final remoteAlbumCounts = remoteAlbumService.getCount(); final remoteAlbumCounts = remoteAlbumService.getCount();
final memoryCount = memoryService.getCount(); final memoryCount = memoryService.getCount();
final getLocalHashedCount = assetService.getLocalHashedCount(); final getLocalHashedCount = assetService.getLocalHashedCount();
final manageLocalMediaAndroid = appMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid);
return await Future.wait([assetCounts, localAlbumCounts, remoteAlbumCounts, memoryCount, getLocalHashedCount]); return await Future.wait([
assetCounts,
localAlbumCounts,
remoteAlbumCounts,
memoryCount,
getLocalHashedCount,
manageLocalMediaAndroid,
]);
} }
return FutureBuilder( return FutureBuilder(
@@ -254,14 +262,15 @@ class _SyncStatsCounts extends ConsumerWidget {
); );
} }
final assetCounts = snapshot.data![0]! as (int, int); final assetCounts = snapshot.data![0] as (int, int);
final localAssetCount = assetCounts.$1; final localAssetCount = assetCounts.$1;
final remoteAssetCount = assetCounts.$2; final remoteAssetCount = assetCounts.$2;
final localAlbumCount = snapshot.data![1]! as int; final localAlbumCount = snapshot.data![1] as int;
final remoteAlbumCount = snapshot.data![2]! as int; final remoteAlbumCount = snapshot.data![2] as int;
final memoryCount = snapshot.data![3]! as int; final memoryCount = snapshot.data![3] as int;
final localHashedCount = snapshot.data![4]! as int; final localHashedCount = snapshot.data![4] as int;
final manageLocalMediaAndroid = snapshot.data![5] as bool;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@@ -354,8 +363,7 @@ class _SyncStatsCounts extends ConsumerWidget {
), ),
), ),
// To be removed once the experimental feature is stable // To be removed once the experimental feature is stable
if (CurrentPlatform.isAndroid && if (CurrentPlatform.isAndroid && manageLocalMediaAndroid) ...[
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
SettingGroupTitle(title: "trash".t(context: context)), SettingGroupTitle(title: "trash".t(context: context)),
Consumer( Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
@@ -2,21 +2,23 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.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_switch_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class HapticSetting extends HookConsumerWidget { class HapticSetting extends HookConsumerWidget {
const HapticSetting({super.key}); const HapticSetting({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final hapticFeedbackSetting = useAppSettingsState(AppSettingsEnum.enableHapticFeedback); final isHapticFeedbackEnabled = useState(ref.read(appConfigProvider).advanced.enableHapticFeedback);
final isHapticFeedbackEnabled = useValueNotifier(hapticFeedbackSetting.value); useValueChanged(
isHapticFeedbackEnabled.value,
(_, __) => ref.read(settingsProvider).write(.advancedEnableHapticFeedback, isHapticFeedbackEnabled.value),
);
onHapticFeedbackChange(bool isEnabled) { onHapticFeedbackChange(bool isEnabled) {
hapticFeedbackSetting.value = isEnabled; isHapticFeedbackEnabled.value = isEnabled;
} }
return Column( return Column(
-3
View File
@@ -2,7 +2,6 @@ import 'package:immich_mobile/domain/services/partner.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
class MockStoreService extends Mock implements StoreService {} class MockStoreService extends Mock implements StoreService {}
@@ -11,6 +10,4 @@ class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockNativeSyncApi extends Mock implements NativeSyncApi {}
class MockAppSettingsService extends Mock implements AppSettingsService {}
class MockPartnerService extends Mock implements PartnerService {} class MockPartnerService extends Mock implements PartnerService {}
@@ -2,8 +2,8 @@ import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.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/store.model.dart';
import 'package:immich_mobile/domain/services/local_sync.service.dart'; import 'package:immich_mobile/domain/services/local_sync.service.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@@ -29,6 +29,7 @@ void main() {
late AssetMediaRepository mockAssetMediaRepository; late AssetMediaRepository mockAssetMediaRepository;
late MockPermissionRepository mockPermissionRepository; late MockPermissionRepository mockPermissionRepository;
late MockNativeSyncApi mockNativeSyncApi; late MockNativeSyncApi mockNativeSyncApi;
late MockAppMetadataRepository mockAppMetadataRepository;
late Drift db; late Drift db;
setUpAll(() async { setUpAll(() async {
@@ -52,6 +53,7 @@ void main() {
mockAssetMediaRepository = MockAssetMediaRepository(); mockAssetMediaRepository = MockAssetMediaRepository();
mockPermissionRepository = MockPermissionRepository(); mockPermissionRepository = MockPermissionRepository();
mockNativeSyncApi = MockNativeSyncApi(); mockNativeSyncApi = MockNativeSyncApi();
mockAppMetadataRepository = MockAppMetadataRepository();
when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false); when(() => mockNativeSyncApi.shouldFullSync()).thenAnswer((_) async => false);
when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer( when(() => mockNativeSyncApi.getMediaChanges()).thenAnswer(
@@ -75,15 +77,16 @@ void main() {
assetMediaRepository: mockAssetMediaRepository, assetMediaRepository: mockAssetMediaRepository,
permissionRepository: mockPermissionRepository, permissionRepository: mockPermissionRepository,
nativeSyncApi: mockNativeSyncApi, nativeSyncApi: mockNativeSyncApi,
appMetadataRepository: mockAppMetadataRepository,
); );
await Store.put(StoreKey.manageLocalMediaAndroid, false); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false);
when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => false); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => false);
}); });
group('LocalSyncService - syncTrashedAssets gating', () { group('LocalSyncService - syncTrashedAssets gating', () {
test('invokes syncTrashedAssets when Android flag enabled and permission granted', () async { test('invokes syncTrashedAssets when Android flag enabled and permission granted', () async {
await Store.put(StoreKey.manageLocalMediaAndroid, true); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true);
when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true);
await sut.sync(); await sut.sync();
@@ -93,7 +96,7 @@ void main() {
}); });
test('skips syncTrashedAssets when store flag disabled', () async { test('skips syncTrashedAssets when store flag disabled', () async {
await Store.put(StoreKey.manageLocalMediaAndroid, false); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false);
when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true);
await sut.sync(); await sut.sync();
@@ -102,7 +105,7 @@ void main() {
}); });
test('skips syncTrashedAssets when MANAGE_MEDIA permission absent', () async { test('skips syncTrashedAssets when MANAGE_MEDIA permission absent', () async {
await Store.put(StoreKey.manageLocalMediaAndroid, true); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true);
when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => false); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => false);
await sut.sync(); await sut.sync();
@@ -114,7 +117,7 @@ void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS; debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = TargetPlatform.android); addTearDown(() => debugDefaultTargetPlatformOverride = TargetPlatform.android);
await Store.put(StoreKey.manageLocalMediaAndroid, true); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true);
when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true); when(() => mockPermissionRepository.hasManageMediaPermission()).thenAnswer((_) async => true);
await sut.sync(); await sut.sync();
@@ -21,15 +21,15 @@ void main() {
controller = StreamController<List<StoreDto<Object>>>.broadcast(); controller = StreamController<List<StoreDto<Object>>>.broadcast();
mockDriftStoreRepo = MockDriftStoreRepository(); mockDriftStoreRepo = MockDriftStoreRepository();
// For generics, we need to provide fallback to each concrete type to avoid runtime errors // For generics, we need to provide fallback to each concrete type to avoid runtime errors
registerFallbackValue(StoreKey.accessToken); registerFallbackValue(StoreKey.legacyAccessToken);
registerFallbackValue(StoreKey.version); registerFallbackValue(StoreKey.legacyVersion);
registerFallbackValue(StoreKey.advancedTroubleshooting); registerFallbackValue(StoreKey.legacyAdvancedTroubleshooting);
when(() => mockDriftStoreRepo.getAll()).thenAnswer( when(() => mockDriftStoreRepo.getAll()).thenAnswer(
(_) async => [ (_) async => [
const StoreDto(StoreKey.accessToken, _kAccessToken), const StoreDto(StoreKey.legacyAccessToken, _kAccessToken),
const StoreDto(StoreKey.advancedTroubleshooting, _kAdvancedTroubleshooting), const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kAdvancedTroubleshooting),
const StoreDto(StoreKey.version, _kVersion), const StoreDto(StoreKey.legacyVersion, _kVersion),
], ],
); );
when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
@@ -45,35 +45,35 @@ void main() {
group("Store Service Init:", () { group("Store Service Init:", () {
test('Populates the internal cache on init', () { test('Populates the internal cache on init', () {
verify(() => mockDriftStoreRepo.getAll()).called(1); verify(() => mockDriftStoreRepo.getAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.advancedTroubleshooting), _kAdvancedTroubleshooting); expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), _kAdvancedTroubleshooting);
expect(sut.tryGet(StoreKey.version), _kVersion); expect(sut.tryGet(StoreKey.legacyVersion), _kVersion);
// Other keys should be null // Other keys should be null
expect(sut.tryGet(StoreKey.currentUser), isNull); expect(sut.tryGet(StoreKey.deviceId), isNull);
}); });
test('Listens to stream of store updates', () async { test('Listens to stream of store updates', () async {
final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase()); final event = StoreDto(StoreKey.legacyAccessToken, _kAccessToken.toUpperCase());
controller.add([event]); controller.add([event]);
await pumpEventQueue(); await pumpEventQueue();
verify(() => mockDriftStoreRepo.watchAll()).called(1); verify(() => mockDriftStoreRepo.watchAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken.toUpperCase()); expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken.toUpperCase());
}); });
}); });
group('Store Service get:', () { group('Store Service get:', () {
test('Returns the stored value for the given key', () { test('Returns the stored value for the given key', () {
expect(sut.get(StoreKey.accessToken), _kAccessToken); expect(sut.get(StoreKey.legacyAccessToken), _kAccessToken);
}); });
test('Throws StoreKeyNotFoundException for nonexistent keys', () { test('Throws StoreKeyNotFoundException for nonexistent keys', () {
expect(() => sut.get(StoreKey.currentUser), throwsA(isA<StoreKeyNotFoundException>())); expect(() => sut.get(StoreKey.deviceId), throwsA(isA<StoreKeyNotFoundException>()));
}); });
test('Returns the stored value for the given key or the defaultValue', () { test('Returns the stored value for the given key or the defaultValue', () {
expect(sut.get(StoreKey.currentUser, 5), 5); expect(sut.get(StoreKey.legacyBackupTriggerDelay, 5), 5);
}); });
}); });
@@ -83,15 +83,15 @@ void main() {
}); });
test('Skip insert when value is not modified', () async { test('Skip insert when value is not modified', () async {
await sut.put(StoreKey.accessToken, _kAccessToken); await sut.put(StoreKey.legacyAccessToken, _kAccessToken);
verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, any())); verifyNever(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, any()));
}); });
test('Insert value when modified', () async { test('Insert value when modified', () async {
final newAccessToken = _kAccessToken.toUpperCase(); final newAccessToken = _kAccessToken.toUpperCase();
await sut.put(StoreKey.accessToken, newAccessToken); await sut.put(StoreKey.legacyAccessToken, newAccessToken);
verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1); verify(() => mockDriftStoreRepo.upsert<String>(StoreKey.legacyAccessToken, newAccessToken)).called(1);
expect(sut.tryGet(StoreKey.accessToken), newAccessToken); expect(sut.tryGet(StoreKey.legacyAccessToken), newAccessToken);
}); });
}); });
@@ -108,7 +108,7 @@ void main() {
}); });
test('Watches a specific key for changes', () async { test('Watches a specific key for changes', () async {
final stream = sut.watch(StoreKey.accessToken); final stream = sut.watch(StoreKey.legacyAccessToken);
final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()]; final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()];
unawaited(expectLater(stream, emitsInOrder(events))); unawaited(expectLater(stream, emitsInOrder(events)));
@@ -118,7 +118,7 @@ void main() {
} }
await pumpEventQueue(); await pumpEventQueue();
verify(() => mockDriftStoreRepo.watch<String>(StoreKey.accessToken)).called(1); verify(() => mockDriftStoreRepo.watch<String>(StoreKey.legacyAccessToken)).called(1);
}); });
}); });
@@ -128,13 +128,13 @@ void main() {
}); });
test('Removes the value from the DB', () async { test('Removes the value from the DB', () async {
await sut.delete(StoreKey.accessToken); await sut.delete(StoreKey.legacyAccessToken);
verify(() => mockDriftStoreRepo.delete<String>(StoreKey.accessToken)).called(1); verify(() => mockDriftStoreRepo.delete<String>(StoreKey.legacyAccessToken)).called(1);
}); });
test('Removes the value from the cache', () async { test('Removes the value from the cache', () async {
await sut.delete(StoreKey.accessToken); await sut.delete(StoreKey.legacyAccessToken);
expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
}); });
}); });
@@ -146,9 +146,9 @@ void main() {
test('Clears all values from the store', () async { test('Clears all values from the store', () async {
await sut.clear(); await sut.clear();
verify(() => mockDriftStoreRepo.deleteAll()).called(1); verify(() => mockDriftStoreRepo.deleteAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.legacyAccessToken), isNull);
expect(sut.tryGet(StoreKey.advancedTroubleshooting), isNull); expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), isNull);
expect(sut.tryGet(StoreKey.version), isNull); expect(sut.tryGet(StoreKey.legacyVersion), isNull);
}); });
}); });
} }
@@ -4,8 +4,8 @@ import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.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/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart';
@@ -36,7 +36,6 @@ class _AbortCallbackWrapper {
class _MockAbortCallbackWrapper extends Mock implements _AbortCallbackWrapper {} class _MockAbortCallbackWrapper extends Mock implements _AbortCallbackWrapper {}
void main() { void main() {
late SyncStreamService sut; late SyncStreamService sut;
late SyncStreamRepository mockSyncStreamRepo; late SyncStreamRepository mockSyncStreamRepo;
@@ -51,6 +50,7 @@ void main() {
late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback; late Future<void> Function(List<SyncEvent>, Function(), Function()) handleEventsCallback;
late _MockAbortCallbackWrapper mockAbortCallbackWrapper; late _MockAbortCallbackWrapper mockAbortCallbackWrapper;
late _MockAbortCallbackWrapper mockResetCallbackWrapper; late _MockAbortCallbackWrapper mockResetCallbackWrapper;
late MockAppMetadataRepository mockAppMetadataRepository;
late Drift db; late Drift db;
late bool hasManageMediaPermission; late bool hasManageMediaPermission;
@@ -59,6 +59,8 @@ void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.android; debugDefaultTargetPlatformOverride = TargetPlatform.android;
registerFallbackValue(LocalAssetStub.image1); registerFallbackValue(LocalAssetStub.image1);
registerFallbackValue(const SemVer(major: 2, minor: 5, patch: 0)); registerFallbackValue(const SemVer(major: 2, minor: 5, patch: 0));
registerFallbackValue(AppMetadataKey.syncMigrationStatus);
registerFallbackValue(const <String>[]);
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db)); await StoreService.init(storeRepository: DriftStoreRepository(db));
@@ -84,6 +86,7 @@ void main() {
mockApi = MockApiService(); mockApi = MockApiService();
mockServerApi = MockServerApi(); mockServerApi = MockServerApi();
mockSyncMigrationRepo = MockSyncMigrationRepository(); mockSyncMigrationRepo = MockSyncMigrationRepository();
mockAppMetadataRepository = MockAppMetadataRepository();
when(() => mockAbortCallbackWrapper()).thenReturn(false); when(() => mockAbortCallbackWrapper()).thenReturn(false);
@@ -159,6 +162,7 @@ void main() {
permissionRepository: mockPermissionRepo, permissionRepository: mockPermissionRepo,
api: mockApi, api: mockApi,
syncMigrationRepository: mockSyncMigrationRepo, syncMigrationRepository: mockSyncMigrationRepo,
appMetadataRepository: mockAppMetadataRepository,
); );
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {}); when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {});
@@ -172,7 +176,11 @@ void main() {
return ids; return ids;
}); });
when(() => mockAssetMediaRepo.restoreAssetsFromTrash(any())).thenAnswer((_) async => []); when(() => mockAssetMediaRepo.restoreAssetsFromTrash(any())).thenAnswer((_) async => []);
await Store.put(StoreKey.manageLocalMediaAndroid, false); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false);
when(
() => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus),
).thenAnswer((_) async => const <String>[]);
when(() => mockAppMetadataRepository.set<List<String>, List<String>>(any(), any())).thenAnswer((_) async {});
}); });
Future<void> simulateEvents(List<SyncEvent> events) async { Future<void> simulateEvents(List<SyncEvent> events) async {
@@ -243,6 +251,7 @@ void main() {
cancellation: cancellation, cancellation: cancellation,
api: mockApi, api: mockApi,
syncMigrationRepository: mockSyncMigrationRepo, syncMigrationRepository: mockSyncMigrationRepo,
appMetadataRepository: mockAppMetadataRepository,
); );
await sut.sync(); await sut.sync();
@@ -283,6 +292,7 @@ void main() {
cancellation: cancellation, cancellation: cancellation,
api: mockApi, api: mockApi,
syncMigrationRepository: mockSyncMigrationRepo, syncMigrationRepository: mockSyncMigrationRepo,
appMetadataRepository: mockAppMetadataRepository,
); );
await sut.sync(); await sut.sync();
@@ -394,12 +404,12 @@ void main() {
group("SyncStreamService - remote trash & restore", () { group("SyncStreamService - remote trash & restore", () {
setUp(() async { setUp(() async {
await Store.put(StoreKey.manageLocalMediaAndroid, true); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => true);
hasManageMediaPermission = true; hasManageMediaPermission = true;
}); });
tearDown(() async { tearDown(() async {
await Store.put(StoreKey.manageLocalMediaAndroid, false); when(() => mockAppMetadataRepository.get(AppMetadataKey.manageLocalMediaAndroid)).thenAnswer((_) async => false);
hasManageMediaPermission = false; hasManageMediaPermission = false;
}); });
@@ -552,7 +562,9 @@ void main() {
group('SyncStreamService - Sync Migration', () { group('SyncStreamService - Sync Migration', () {
test('ensure that <2.5.0 migrations run', () async { test('ensure that <2.5.0 migrations run', () async {
await Store.put(StoreKey.syncMigrationStatus, "[]"); when(
() => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus),
).thenAnswer((_) async => const <String>[]);
when( when(
() => mockServerApi.getServerVersion(), () => mockServerApi.getServerVersion(),
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null)); ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null));
@@ -580,7 +592,9 @@ void main() {
); );
}); });
test('ensure that >=2.5.0 migrations run', () async { test('ensure that >=2.5.0 migrations run', () async {
await Store.put(StoreKey.syncMigrationStatus, "[]"); when(
() => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus),
).thenAnswer((_) async => const <String>[]);
when( when(
() => mockServerApi.getServerVersion(), () => mockServerApi.getServerVersion(),
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null)); ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null));
@@ -606,10 +620,9 @@ void main() {
}); });
test('ensure that migrations do not re-run', () async { test('ensure that migrations do not re-run', () async {
await Store.put( when(
StoreKey.syncMigrationStatus, () => mockAppMetadataRepository.get(AppMetadataKey.syncMigrationStatus),
'["${SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name}"]', ).thenAnswer((_) async => [SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name]);
);
when( when(
() => mockServerApi.getServerVersion(), () => mockServerApi.getServerVersion(),
@@ -1,78 +1,62 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import '../../fixtures/user.stub.dart'; import '../../fixtures/user.stub.dart';
import '../../infrastructure/repository.mock.dart'; import '../../infrastructure/repository.mock.dart';
import '../service.mock.dart';
void main() { void main() {
late UserService sut; late UserService sut;
late UserApiRepository mockUserApiRepo; late UserApiRepository mockUserApiRepo;
late StoreService mockStoreService; late DriftAuthUserRepository mockAuthUserRepo;
setUp(() { setUp(() {
mockUserApiRepo = MockUserApiRepository(); mockUserApiRepo = MockUserApiRepository();
mockStoreService = MockStoreService(); mockAuthUserRepo = MockDriftAuthUserRepository();
sut = UserService(userApiRepository: mockUserApiRepo, storeService: mockStoreService); sut = UserService(userApiRepository: mockUserApiRepo, authUserRepository: mockAuthUserRepo);
registerFallbackValue(UserStub.admin); registerFallbackValue(UserStub.admin);
when(() => mockStoreService.get(StoreKey.currentUser)).thenReturn(UserStub.admin); when(() => mockAuthUserRepo.get()).thenAnswer((_) async => UserStub.admin);
when(() => mockStoreService.tryGet(StoreKey.currentUser)).thenReturn(UserStub.admin); when(() => mockAuthUserRepo.upsert(any())).thenAnswer((_) async => UserStub.admin);
});
group('getMyUser', () {
test('should return user from store', () {
final result = sut.getMyUser();
expect(result, UserStub.admin);
});
test('should handle user not found scenario', () {
when(() => mockStoreService.get(StoreKey.currentUser)).thenThrow(Exception('User not found'));
expect(() => sut.getMyUser(), throwsA(isA<Exception>()));
});
}); });
group('tryGetMyUser', () { group('tryGetMyUser', () {
test('should return user from store', () { test('should return the current user from the auth user repository', () async {
final result = sut.tryGetMyUser(); final result = await sut.tryGetMyUser();
expect(result, UserStub.admin); expect(result, UserStub.admin);
}); });
test('should return null if user not found', () { test('should return null if no user is logged in', () async {
when(() => mockStoreService.tryGet(StoreKey.currentUser)).thenReturn(null); when(() => mockAuthUserRepo.get()).thenAnswer((_) async => null);
final result = sut.tryGetMyUser(); final result = await sut.tryGetMyUser();
expect(result, isNull); expect(result, isNull);
}); });
}); });
group('watchMyUser', () { group('watchMyUser', () {
test('should return user stream from store', () { test('should return the current user stream from the auth user repository', () {
when(() => mockStoreService.watch(StoreKey.currentUser)).thenAnswer((_) => Stream.value(UserStub.admin)); when(() => mockAuthUserRepo.watch()).thenAnswer((_) => Stream.value(UserStub.admin));
final result = sut.watchMyUser(); final result = sut.watchMyUser();
expect(result, emits(UserStub.admin)); expect(result, emits(UserStub.admin));
}); });
test('should return an empty stream if user not found', () { test('should return an empty stream if no user is logged in', () {
when(() => mockStoreService.watch(StoreKey.currentUser)).thenAnswer((_) => const Stream.empty()); when(() => mockAuthUserRepo.watch()).thenAnswer((_) => const Stream.empty());
final result = sut.watchMyUser(); final result = sut.watchMyUser();
expect(result, emitsInOrder([])); expect(result, emitsInOrder([]));
}); });
}); });
group('refreshMyUser', () { group('refreshMyUser', () {
test('should return user from api and store it', () async { test('should return user from api and persist it', () async {
when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin); when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => UserStub.admin);
when(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).thenAnswer((_) async => true);
final result = await sut.refreshMyUser(); final result = await sut.refreshMyUser();
verify(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)).called(1); verify(() => mockAuthUserRepo.upsert(UserStub.admin)).called(1);
expect(result, UserStub.admin); expect(result, UserStub.admin);
}); });
@@ -80,7 +64,7 @@ void main() {
when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => null); when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => null);
final result = await sut.refreshMyUser(); final result = await sut.refreshMyUser();
verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin)); verifyNever(() => mockAuthUserRepo.upsert(any()));
expect(result, isNull); expect(result, isNull);
}); });
}); });
@@ -88,29 +72,26 @@ void main() {
group('createProfileImage', () { group('createProfileImage', () {
test('should return profile image path', () async { test('should return profile image path', () async {
const profileImagePath = 'profile.jpg'; const profileImagePath = 'profile.jpg';
final updatedUser = UserStub.admin;
when( when(
() => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)),
).thenAnswer((_) async => profileImagePath); ).thenAnswer((_) async => profileImagePath);
when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true);
final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); final result = await sut.createProfileImage(profileImagePath, Uint8List(0));
verify(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).called(1); verify(() => mockAuthUserRepo.upsert(UserStub.admin)).called(1);
expect(result, profileImagePath); expect(result, profileImagePath);
}); });
test('should return null if profile image creation fails', () async { test('should return null if profile image creation fails', () async {
const profileImagePath = 'profile.jpg'; const profileImagePath = 'profile.jpg';
final updatedUser = UserStub.admin;
when( when(
() => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)), () => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)),
).thenThrow(Exception('Failed to create profile image')); ).thenThrow(Exception('Failed to create profile image'));
final result = await sut.createProfileImage(profileImagePath, Uint8List(0)); final result = await sut.createProfileImage(profileImagePath, Uint8List(0));
verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser)); verifyNever(() => mockAuthUserRepo.upsert(any()));
expect(result, isNull); expect(result, isNull);
}); });
}); });
+12
View File
@@ -33,6 +33,9 @@ import 'schema_v26.dart' as v26;
import 'schema_v27.dart' as v27; import 'schema_v27.dart' as v27;
import 'schema_v28.dart' as v28; import 'schema_v28.dart' as v28;
import 'schema_v29.dart' as v29; import 'schema_v29.dart' as v29;
import 'schema_v30.dart' as v30;
import 'schema_v31.dart' as v31;
import 'schema_v32.dart' as v32;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@@ -96,6 +99,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v28.DatabaseAtV28(db); return v28.DatabaseAtV28(db);
case 29: case 29:
return v29.DatabaseAtV29(db); return v29.DatabaseAtV29(db);
case 30:
return v30.DatabaseAtV30(db);
case 31:
return v31.DatabaseAtV31(db);
case 32:
return v32.DatabaseAtV32(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
@@ -131,5 +140,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
27, 27,
28, 28,
29, 29,
30,
31,
32,
]; ];
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+34 -4
View File
@@ -1,13 +1,11 @@
// dart format width=80
// ignore_for_file: unused_local_variable, unused_import
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_dev/api/migrations_native.dart'; import 'package:drift_dev/api/migrations_native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'generated/schema.dart'; import 'generated/schema.dart';
import 'generated/schema_v1.dart' as v1; import 'generated/schema_v31.dart' as v31;
import 'generated/schema_v2.dart' as v2;
void main() { void main() {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
@@ -35,4 +33,36 @@ void main() {
}); });
} }
}); });
group('data migrations', () {
test('v31->v32 backfills the migration', () async {
final schema = await verifier.schemaAt(31);
final oldDb = v31.DatabaseAtV31(schema.newConnection());
await oldDb.into(oldDb.storeEntity).insert(v31.StoreEntityCompanion.insert(id: 0, intValue: const Value(28)));
await oldDb.close();
final db = Drift(schema.newConnection());
await verifier.migrateAndValidate(db, 32);
final cursor = await (db.appMetadataEntity.select()..where((tbl) => tbl.key.equals(AppMetadataKey.version.name)))
.map((row) => row.value)
.getSingleOrNull();
expect(cursor, '28');
await db.close();
});
test('v31->v32 writes no row when the legacy store has none', () async {
final schema = await verifier.schemaAt(31);
final db = Drift(schema.newConnection());
await verifier.migrateAndValidate(db, 32);
final rows = await db.appMetadataEntity.select().get();
expect(rows, isEmpty);
await db.close();
});
});
} }
@@ -4,24 +4,20 @@ import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import '../../fixtures/user.stub.dart';
const _kTestAccessToken = "#TestToken"; const _kTestAccessToken = "#TestToken";
const _kTestVersion = 10; const _kTestVersion = 10;
const _kTestAdvancedTroubleshooting = false; const _kTestAdvancedTroubleshooting = false;
final _kTestUser = UserStub.admin;
Future<void> _populateStore(Drift db) async { Future<void> _populateStore(Drift db) async {
await db.batch((batch) async { await db.batch((batch) async {
batch.insert( batch.insert(
db.storeEntity, db.storeEntity,
StoreEntityCompanion( StoreEntityCompanion(
id: Value(StoreKey.advancedTroubleshooting.id), id: Value(StoreKey.legacyAdvancedTroubleshooting.id),
intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0), intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0),
stringValue: const Value(null), stringValue: const Value(null),
), ),
@@ -29,7 +25,7 @@ Future<void> _populateStore(Drift db) async {
batch.insert( batch.insert(
db.storeEntity, db.storeEntity,
StoreEntityCompanion( StoreEntityCompanion(
id: Value(StoreKey.accessToken.id), id: Value(StoreKey.legacyAccessToken.id),
intValue: const Value(null), intValue: const Value(null),
stringValue: const Value(_kTestAccessToken), stringValue: const Value(_kTestAccessToken),
), ),
@@ -37,7 +33,7 @@ Future<void> _populateStore(Drift db) async {
batch.insert( batch.insert(
db.storeEntity, db.storeEntity,
StoreEntityCompanion( StoreEntityCompanion(
id: Value(StoreKey.version.id), id: Value(StoreKey.legacyVersion.id),
intValue: const Value(_kTestVersion), intValue: const Value(_kTestVersion),
stringValue: const Value(null), stringValue: const Value(null),
), ),
@@ -60,36 +56,28 @@ void main() {
group('Store Repository converters:', () { group('Store Repository converters:', () {
test('converts int', () async { test('converts int', () async {
int? version = await sut.tryGet(StoreKey.version); int? version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, isNull); expect(version, isNull);
await sut.upsert(StoreKey.version, _kTestVersion); await sut.upsert(StoreKey.legacyVersion, _kTestVersion);
version = await sut.tryGet(StoreKey.version); version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, _kTestVersion); expect(version, _kTestVersion);
}); });
test('converts string', () async { test('converts string', () async {
String? accessToken = await sut.tryGet(StoreKey.accessToken); String? accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
expect(accessToken, isNull); expect(accessToken, isNull);
await sut.upsert(StoreKey.accessToken, _kTestAccessToken); await sut.upsert(StoreKey.legacyAccessToken, _kTestAccessToken);
accessToken = await sut.tryGet(StoreKey.accessToken); accessToken = await sut.tryGet(StoreKey.legacyAccessToken);
expect(accessToken, _kTestAccessToken); expect(accessToken, _kTestAccessToken);
}); });
test('converts bool', () async { test('converts bool', () async {
bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, isNull); expect(advancedTroubleshooting, isNull);
await sut.upsert(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting); await sut.upsert(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting);
advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting); expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting);
}); });
test('converts user', () async {
UserDto? user = await sut.tryGet(StoreKey.currentUser);
expect(user, isNull);
await sut.upsert(StoreKey.currentUser, _kTestUser);
user = await sut.tryGet(StoreKey.currentUser);
expect(user, _kTestUser);
});
}); });
group('Store Repository Deletes:', () { group('Store Repository Deletes:', () {
@@ -98,10 +86,10 @@ void main() {
}); });
test('delete()', () async { test('delete()', () async {
bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, isFalse); expect(advancedTroubleshooting, isFalse);
await sut.delete(StoreKey.advancedTroubleshooting); await sut.delete(StoreKey.legacyAdvancedTroubleshooting);
advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting); advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting);
expect(advancedTroubleshooting, isNull); expect(advancedTroubleshooting, isNull);
}); });
@@ -119,10 +107,10 @@ void main() {
}); });
test('upsert()', () async { test('upsert()', () async {
int? version = await sut.tryGet(StoreKey.version); int? version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, _kTestVersion); expect(version, _kTestVersion);
await sut.upsert(StoreKey.version, _kTestVersion + 10); await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10);
version = await sut.tryGet(StoreKey.version); version = await sut.tryGet(StoreKey.legacyVersion);
expect(version, _kTestVersion + 10); expect(version, _kTestVersion + 10);
}); });
}); });
@@ -133,10 +121,10 @@ void main() {
}); });
test('watch()', () async { test('watch()', () async {
final stream = sut.watch(StoreKey.version); final stream = sut.watch(StoreKey.legacyVersion);
unawaited(expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10]))); unawaited(expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10])));
await pumpEventQueue(); await pumpEventQueue();
await sut.upsert(StoreKey.version, _kTestVersion + 10); await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10);
}); });
test('watchAll()', () async { test('watchAll()', () async {
@@ -146,19 +134,19 @@ void main() {
stream, stream,
emitsInOrder([ emitsInOrder([
[ [
const StoreDto<Object>(StoreKey.version, _kTestVersion), const StoreDto<Object>(StoreKey.legacyVersion, _kTestVersion),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting), const StoreDto<Object>(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting),
], ],
[ [
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10), const StoreDto<Object>(StoreKey.legacyVersion, _kTestVersion + 10),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), const StoreDto<Object>(StoreKey.legacyAccessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting), const StoreDto<Object>(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting),
], ],
]), ]),
), ),
); );
await sut.upsert(StoreKey.version, _kTestVersion + 10); await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10);
}); });
}); });
} }
@@ -1,3 +1,4 @@
import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
@@ -22,6 +23,8 @@ class MockDriftStoreRepository extends Mock implements DriftStoreRepository {}
class MockSettingsRepository extends Mock implements SettingsRepository {} class MockSettingsRepository extends Mock implements SettingsRepository {}
class MockAppMetadataRepository extends Mock implements AppMetadataRepository {}
class MockLogRepository extends Mock implements LogRepository {} class MockLogRepository extends Mock implements LogRepository {}
class MockSyncStreamRepository extends Mock implements SyncStreamRepository {} class MockSyncStreamRepository extends Mock implements SyncStreamRepository {}
@@ -48,6 +51,8 @@ class MockSyncMigrationRepository extends Mock implements SyncMigrationRepositor
class MockUserRepository extends Mock implements UserRepository {} class MockUserRepository extends Mock implements UserRepository {}
class MockDriftAuthUserRepository extends Mock implements DriftAuthUserRepository {}
class MockPartnerRepository extends Mock implements PartnerRepository {} class MockPartnerRepository extends Mock implements PartnerRepository {}
// API Repos // API Repos
@@ -0,0 +1,90 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/app_metadata_key.dart';
import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/app_metadata.repository.dart';
import '../repository_context.dart';
void main() {
late MediumRepositoryContext ctx;
late AppMetadataRepository sut;
setUpAll(() {
ctx = MediumRepositoryContext();
sut = AppMetadataRepository(ctx.db);
});
tearDownAll(() async {
await ctx.dispose();
});
setUp(() async {
await ctx.db.delete(ctx.db.appMetadataEntity).go();
});
group('get', () {
test('a stored NULL value column resolves to the key default', () async {
await ctx.db
.into(ctx.db.appMetadataEntity)
.insert(
AppMetadataEntityCompanion.insert(
key: AppMetadataKey.manageLocalMediaAndroid.name,
value: const .new(null),
updatedAt: .new(DateTime.now()),
),
);
expect(await sut.get(.manageLocalMediaAndroid), false);
});
});
group('defaults', () {
test('falls back to the key default when the value is absent', () async {
expect(await sut.get(.version), kCurrentVersion);
expect(await sut.get(.syncMigrationStatus), const <String>[]);
expect(await sut.get(.manageLocalMediaAndroid), false);
});
test('a stored value takes precedence over the default', () async {
await sut.set(.version, 5);
await sut.set(.syncMigrationStatus, const ['task']);
await sut.set(.manageLocalMediaAndroid, true);
expect(await sut.get(.version), 5);
expect(await sut.get(.syncMigrationStatus), const ['task']);
expect(await sut.get(.manageLocalMediaAndroid), true);
});
});
group('set', () {
test('round-trips int, List and bool values to their typed form', () async {
await sut.set(.version, 42);
await sut.set(.syncMigrationStatus, const ['task']);
await sut.set(.manageLocalMediaAndroid, true);
expect(await sut.get(.version), 42);
expect(await sut.get(.syncMigrationStatus), const ['task']);
expect(await sut.get(.manageLocalMediaAndroid), true);
});
test('overwrites the existing value and keeps a single row per key', () async {
await sut.set(.version, 1);
await sut.set(.version, 2);
expect(await sut.get(.version), 2);
expect(await ctx.db.select(ctx.db.appMetadataEntity).get(), hasLength(1));
});
});
group('cache-less reads', () {
test('observes a value mutated directly in the DB', () async {
await sut.set(.version, 10);
await (ctx.db.update(ctx.db.appMetadataEntity)..where((r) => r.key.equals(AppMetadataKey.version.name))).write(
AppMetadataEntityCompanion(value: .new(AppMetadataKey.version.encode(99))),
);
expect(await sut.get(.version), 99);
});
});
}
@@ -456,7 +456,7 @@ void main() {
test('does not update when longitude does not match', () async { test('does not update when longitude does not match', () async {
final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); final remoteAsset = await ctx.newRemoteAsset(ownerId: userId);
final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: (-74.006).toOption()); final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: .fromNullable((-74.006)));
final localAsset = await ctx.newLocalAsset( final localAsset = await ctx.newLocalAsset(
checksumOption: const Option.none(), checksumOption: const Option.none(),
iCloudId: cloudIdAsset.cloudId, iCloudId: cloudIdAsset.cloudId,
@@ -0,0 +1,118 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import '../repository_context.dart';
void main() {
late MediumRepositoryContext ctx;
late SessionRepository sut;
setUpAll(() async {
ctx = MediumRepositoryContext();
sut = await SessionRepository.ensureInitialized(ctx.db);
});
tearDownAll(() async {
await ctx.dispose();
});
setUp(() async {
await ctx.db.delete(ctx.db.sessionEntity).go();
await SessionRepository.instance.refresh();
});
group('defaults', () {
test('session returns null fields when DB is empty', () {
expect(sut.session.serverUrl, isNull);
expect(sut.session.accessToken, isNull);
expect(sut.session.serverEndpoint, isNull);
});
});
group('write', () {
test('persists a value and reflects it in the composed view', () async {
await sut.write(.serverEndpoint, 'https://demo.immich.app/api');
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
});
test('persists across keys independently', () async {
await sut.write(.serverUrl, 'https://demo.immich.app');
await sut.write(.accessToken, 'token-123');
expect(sut.session.serverUrl, 'https://demo.immich.app');
expect(sut.session.accessToken, 'token-123');
expect(sut.session.serverEndpoint, isNull);
});
});
group('null values', () {
test('a stored NULL value column decodes to null on refresh', () async {
await ctx.db
.into(ctx.db.sessionEntity)
.insert(
SessionEntityCompanion.insert(
key: SessionKey.accessToken.name,
value: const .new(null),
updatedAt: .new(DateTime.now()),
),
);
await SessionRepository.instance.refresh();
expect(sut.session.accessToken, isNull);
});
});
group('sync', () {
test('picks up rows that were inserted directly into the DB', () async {
await ctx.db
.into(ctx.db.sessionEntity)
.insert(
SessionEntityCompanion.insert(
key: SessionKey.serverEndpoint.name,
value: const .new('https://demo.immich.app/api'),
updatedAt: .new(DateTime.now()),
),
);
expect(sut.session.serverEndpoint, isNull);
await SessionRepository.instance.refresh();
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
});
test('drops cached values for rows that were deleted out from under the repo', () async {
await sut.write(.serverEndpoint, 'https://demo.immich.app/api');
await ctx.db.delete(ctx.db.sessionEntity).go();
expect(sut.session.serverEndpoint, 'https://demo.immich.app/api');
await SessionRepository.instance.refresh();
expect(sut.session.serverEndpoint, isNull);
});
test('skips rows whose key is unknown to SessionKey', () async {
await ctx.db
.into(ctx.db.sessionEntity)
.insert(
SessionEntityCompanion.insert(
key: 'session.unknown.future-key',
value: const .new('unknown'),
updatedAt: .new(DateTime.now()),
),
);
await SessionRepository.instance.refresh();
expect(sut.session.serverEndpoint, isNull);
});
});
group('watch', () {
test('watchSession emits the new value after a write', () async {
final expectation = expectLater(
sut.watch().map((s) => s.serverEndpoint),
emitsThrough('https://demo.immich.app/api/watch'),
);
await sut.write(SessionKey.serverEndpoint, 'https://demo.immich.app/api/watch');
await expectation;
});
});
}
@@ -1,4 +1,3 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
@@ -61,6 +60,39 @@ void main() {
}); });
}); });
group('null values', () {
test('writing null to a nullable key clears the row and reverts the cache to null', () async {
await sut.write(SettingsKey.networkPreferredWifiName, 'home-wifi');
expect(sut.appConfig.network.preferredWifiName, 'home-wifi');
expect(await ctx.db.select(ctx.db.settingsEntity).get(), hasLength(1));
await sut.write(SettingsKey.networkPreferredWifiName, null);
expect(await ctx.db.select(ctx.db.settingsEntity).get(), isEmpty);
expect(sut.appConfig.network.preferredWifiName, isNull);
});
test('writing null to an already-null key is a no-op', () async {
await sut.write(SettingsKey.networkPreferredWifiName, null);
expect(await ctx.db.select(ctx.db.settingsEntity).get(), isEmpty);
});
test('a stored NULL value column decodes to null on refresh', () async {
await ctx.db
.into(ctx.db.settingsEntity)
.insert(
SettingsEntityCompanion.insert(
key: SettingsKey.networkPreferredWifiName.name,
value: const .new(null),
updatedAt: .new(DateTime.now()),
),
);
await SettingsRepository.instance.refresh();
expect(sut.appConfig.network.preferredWifiName, isNull);
});
});
group('delete', () {}); group('delete', () {});
group('sync', () { group('sync', () {
@@ -70,8 +102,8 @@ void main() {
.insert( .insert(
SettingsEntityCompanion.insert( SettingsEntityCompanion.insert(
key: SettingsKey.themeMode.name, key: SettingsKey.themeMode.name,
value: ThemeMode.dark.name, value: .new(ThemeMode.dark.name),
updatedAt: Value(DateTime.now()), updatedAt: .new(DateTime.now()),
), ),
); );
@@ -98,8 +130,8 @@ void main() {
.insert( .insert(
SettingsEntityCompanion.insert( SettingsEntityCompanion.insert(
key: 'app-config.unknown.future-key', key: 'app-config.unknown.future-key',
value: 'whatever', value: const .new('unknown'),
updatedAt: Value(DateTime.now()), updatedAt: .new(DateTime.now()),
), ),
); );
@@ -110,13 +142,13 @@ void main() {
group('watch', () { group('watch', () {
test('watchAppConfig emits the new value after a write', () async { test('watchAppConfig emits the new value after a write', () async {
final expectation = expectLater(sut.watchConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark)); final expectation = expectLater(sut.watch().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark));
await sut.write(SettingsKey.themeMode, ThemeMode.dark); await sut.write(SettingsKey.themeMode, ThemeMode.dark);
await expectation; await expectation;
}); });
test('watchConfig emits the new value after a write', () async { test('watchConfig emits the new value after a write', () async {
final expectation = expectLater(sut.watchConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning)); final expectation = expectLater(sut.watch().map((c) => c.logLevel), emitsThrough(LogLevel.warning));
await sut.write(SettingsKey.logLevel, LogLevel.warning); await sut.write(SettingsKey.logLevel, LogLevel.warning);
await expectation; await expectation;
}); });
@@ -60,7 +60,7 @@ void main() {
when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => true); when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => true);
when(() => assetService.watchAsset(any())).thenAnswer((_) => const Stream.empty()); when(() => assetService.watchAsset(any())).thenAnswer((_) => const Stream.empty());
when(() => assetService.getExif(any())).thenAnswer((_) async => null); when(() => assetService.getExif(any())).thenAnswer((_) async => null);
when(() => userService.tryGetMyUser()).thenReturn(_user); when(() => userService.tryGetMyUser()).thenAnswer((_) async => _user);
when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty()); when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty());
container = ProviderContainer( container = ProviderContainer(
-3
View File
@@ -1,10 +1,7 @@
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/network.service.dart'; import 'package:immich_mobile/services/network.service.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
class MockApiService extends Mock implements ApiService {} class MockApiService extends Mock implements ApiService {}
class MockNetworkService extends Mock implements NetworkService {} class MockNetworkService extends Mock implements NetworkService {}
class MockAppSettingService extends Mock implements AppSettingsService {}
@@ -2,7 +2,6 @@ import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@@ -28,6 +27,7 @@ void main() {
late MockAssetMediaRepository assetMediaRepository; late MockAssetMediaRepository assetMediaRepository;
late MockDownloadRepository downloadRepository; late MockDownloadRepository downloadRepository;
late MockTagService tagService; late MockTagService tagService;
late MockAppMetadataRepository appMetadataRepository;
late Drift db; late Drift db;
@@ -55,6 +55,7 @@ void main() {
assetMediaRepository = MockAssetMediaRepository(); assetMediaRepository = MockAssetMediaRepository();
downloadRepository = MockDownloadRepository(); downloadRepository = MockDownloadRepository();
tagService = MockTagService(); tagService = MockTagService();
appMetadataRepository = MockAppMetadataRepository();
sut = ActionService( sut = ActionService(
assetApiRepository, assetApiRepository,
@@ -66,7 +67,10 @@ void main() {
assetMediaRepository, assetMediaRepository,
downloadRepository, downloadRepository,
tagService, tagService,
appMetadataRepository,
); );
when(() => appMetadataRepository.get(.manageLocalMediaAndroid)).thenAnswer((_) async => false);
}); });
tearDown(() async { tearDown(() async {
@@ -144,7 +148,7 @@ void main() {
group('ActionService.deleteLocal', () { group('ActionService.deleteLocal', () {
test('routes deleted ids to trashed repository when Android trash handling is enabled', () async { test('routes deleted ids to trashed repository when Android trash handling is enabled', () async {
await Store.put(StoreKey.manageLocalMediaAndroid, true); when(() => appMetadataRepository.get(.manageLocalMediaAndroid)).thenAnswer((_) async => true);
const ids = ['a', 'b']; const ids = ['a', 'b'];
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids);
@@ -159,7 +163,7 @@ void main() {
}); });
test('deletes locally when Android trash handling is disabled', () async { test('deletes locally when Android trash handling is disabled', () async {
await Store.put(StoreKey.manageLocalMediaAndroid, false); when(() => appMetadataRepository.get(.manageLocalMediaAndroid)).thenAnswer((_) async => false);
const ids = ['c']; const ids = ['c'];
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids);
@@ -174,7 +178,7 @@ void main() {
}); });
test('short-circuits when nothing was deleted', () async { test('short-circuits when nothing was deleted', () async {
await Store.put(StoreKey.manageLocalMediaAndroid, true); when(() => appMetadataRepository.get(.manageLocalMediaAndroid)).thenAnswer((_) async => true);
const ids = ['x']; const ids = ['x'];
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => <String>[]); when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => <String>[]);
@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/auth.service.dart';
@@ -44,6 +45,7 @@ void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db)); await StoreService.init(storeRepository: DriftStoreRepository(db));
await SessionRepository.ensureInitialized(db);
}); });
tearDownAll(() async { tearDownAll(() async {
@@ -7,10 +7,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.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/session.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:immich_mobile/services/background_upload.service.dart';
@@ -39,8 +41,8 @@ void main() {
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db)); await StoreService.init(storeRepository: DriftStoreRepository(db));
await SettingsRepository.ensureInitialized(db); await SettingsRepository.ensureInitialized(db);
await SessionRepository.ensureInitialized(db);
await Store.put(StoreKey.serverEndpoint, 'http://test-server.com'); await SessionRepository.instance.write(SessionKey.serverEndpoint, 'https://demo.immich.app');
await Store.put(StoreKey.deviceId, 'test-device-id'); await Store.put(StoreKey.deviceId, 'test-device-id');
}); });
+4 -2
View File
@@ -5,10 +5,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/session.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/session.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import '../test_utils.dart'; import '../test_utils.dart';
@@ -25,7 +26,8 @@ class PresentationContext {
if (_db == null) { if (_db == null) {
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false); await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false);
await StoreService.I.put(StoreKey.serverEndpoint, serverEndpoint); await SessionRepository.ensureInitialized(db);
await SessionRepository.instance.write(SessionKey.serverEndpoint, serverEndpoint);
_db = db; _db = db;
} }
return const PresentationContext._(); return const PresentationContext._();

Some files were not shown because too many files have changed in this diff Show More