diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml
index f3f254e4be..bfbc7bd2e2 100644
--- a/.github/workflows/build-mobile.yml
+++ b/.github/workflows/build-mobile.yml
@@ -116,7 +116,6 @@ jobs:
~/.gradle/wrapper
~/.android/sdk
mobile/android/.gradle
- mobile/.dart_tool
key: build-mobile-gradle-${{ runner.os }}-main
- name: Setup Android SDK
@@ -189,7 +188,6 @@ jobs:
~/.gradle/wrapper
~/.android/sdk
mobile/android/.gradle
- mobile/.dart_tool
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
build-sign-ios:
diff --git a/docs/docs/install/synology.md b/docs/docs/install/synology.md
index b86561dbbf..de96886caa 100644
--- a/docs/docs/install/synology.md
+++ b/docs/docs/install/synology.md
@@ -52,7 +52,7 @@ Scroll to the bottom of the "**Details**" section and find the `IP Address` list
## Step 4 - Configure Firewall Settings
-Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS.
+Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS to allow communication between the Immich containers.
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
@@ -74,6 +74,7 @@ Read the [Post Installation](/install/post-install.mdx) steps and [upgrade instr
Updating Immich using Container Manager
+
Check the post installation and upgrade instructions at the links above before proceeding with this section.
## Step 1. Backup
@@ -110,7 +111,7 @@ Go to **Project**, select **Action** then **Build**. This will download, unpack,
## Step 5. Update firewall rule
-The default behavior is to automatically start the containers once installed. If `immich_server` runs for a few seconds and then stops, it may be because the firewall rule no longer matches the server IP address.
+Without a fixed subnet, the default behavior is to automatically start the containers once installed. If `immich_server` runs for a few seconds and then stops, it may be because the firewall rule no longer matches the server IP address.
Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address.

@@ -123,4 +124,67 @@ In this example, the IP addresses mismatch and the firewall rule needs to be edi

+To prevent future firewall issues, you may set a fixed subnet. [See Set Fixed Subnet](#set-fixed-subnet) for instructions.
+
+
+
+
+ Set Fixed Subnet
+
+Docker by default assigns dynamic subnets to bridge networks which can change when rebuilding containers and can cause firewall rules to break. To avoid this, define a fixed subnet in your `docker-compose.yml`:
+
+## Step 1. Determine current subnet
+
+Go to the **Container** section. Click on `immich_server` and scroll down on **General** to find the IP address.
+
+
+## Step 2. Add network configuration
+
+Add the following network configuration at the end of your `docker-compose.yml` file:
+
+```yaml
+networks:
+ immich-network:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.20.0.0/16
+ gateway: 172.20.0.1
+```
+
+If your docker container is running on a different subnet then update accordingly.
+
+## Step 3. Add network to each service
+
+Add the network to each service (immich-server, immich-machine-learning, redis, database):
+
+```yaml
+services:
+ immich-server:
+ # other config options
+ networks:
+ - immich-network
+
+ immich-machine-learning:
+ # other config options
+ networks:
+ - immich-network
+
+ redis:
+ # other config options
+ networks:
+ - immich-network
+
+ database:
+ # other config options
+ networks:
+ - immich-network
+```
+
+Save your changes. Synology will ask if you want to save changes only or rebuild containers. Select rebuild containers.
+
+## Step 4. Update Firewall Rules, if necessary
+
+If your firewall rules were not already set for this subnet, the firewall rules will need to be updated. See [Step 4 - Configure Firewall Settings](#step-4---configure-firewall-settings).
+
diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart
index e52e8a0a92..63281e49da 100644
--- a/mobile/lib/domain/models/store.model.dart
+++ b/mobile/lib/domain/models/store.model.dart
@@ -4,25 +4,15 @@ import 'package:immich_mobile/domain/models/user.model.dart';
/// Defines the data type for each value
enum StoreKey {
version._(0),
- assetETag._(1),
currentUser._(2),
- deviceIdHash._(3),
deviceId._(4),
- backupFailedSince._(5),
- backupRequireWifi._(6),
backupRequireCharging._(7),
backupTriggerDelay._(8),
serverUrl._(10),
accessToken._(11),
serverEndpoint._(12),
- autoBackup._(13),
- backgroundBackup._(14),
- sslClientCertData._(15),
- sslClientPasswd._(16),
- uploadErrorNotificationGracePeriod._(106),
selectedAlbumSortOrder._(113),
advancedTroubleshooting._(114),
- selfSignedCert._(120),
selectedAlbumSortReverse._(123),
enableHapticFeedback._(126),
customHeaders._(127),
@@ -38,7 +28,6 @@ enum StoreKey {
// Read-only Mode settings
readonlyModeEnabled._(138),
albumGridView._(140),
- loadOriginal._(101),
// Experimental stuff
enableBackup._(1003),
diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart
new file mode 100644
index 0000000000..1713718967
--- /dev/null
+++ b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/enums.dart';
+import 'package:immich_mobile/domain/models/events.model.dart';
+import 'package:immich_mobile/domain/utils/event_stream.dart';
+import 'package:immich_mobile/extensions/translate_extensions.dart';
+import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
+import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
+import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
+import 'package:immich_mobile/widgets/common/immich_toast.dart';
+
+class RestoreActionButton extends ConsumerWidget {
+ final ActionSource source;
+ final bool iconOnly;
+ final bool menuItem;
+
+ const RestoreActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
+
+ void _onTap(BuildContext context, WidgetRef ref) async {
+ if (!context.mounted) {
+ return;
+ }
+
+ final result = await ref.read(actionProvider.notifier).restoreTrash(source);
+ ref.read(multiSelectProvider.notifier).reset();
+
+ if (source == ActionSource.viewer) {
+ EventStream.shared.emit(const ViewerReloadAssetEvent());
+ }
+
+ final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()});
+
+ if (context.mounted) {
+ ImmichToast.show(
+ context: context,
+ msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context),
+ gravity: ToastGravity.BOTTOM,
+ toastType: result.success ? ToastType.success : ToastType.error,
+ );
+ }
+ }
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ return BaseActionButton(
+ iconData: Icons.history_rounded,
+ label: 'restore'.t(context: context),
+ iconOnly: iconOnly,
+ menuItem: menuItem,
+ onPressed: () => _onTap(context, ref),
+ maxWidth: 100.0,
+ );
+ }
+}
diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart
index ff09d15496..21401f37e5 100644
--- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart
+++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart
@@ -2,15 +2,18 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
+import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
+import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
+import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@@ -33,19 +36,24 @@ class ViewerBottomBar extends ConsumerWidget {
final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails));
final isInLockedView = ref.watch(inLockedViewProvider);
final serverInfo = ref.watch(serverInfoProvider);
+ final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
final originalTheme = context.themeData;
final actions = [
- const ShareActionButton(source: ActionSource.viewer),
+ if (isInTrash && isOwner && asset.hasRemote)
+ const RestoreActionButton(source: ActionSource.viewer)
+ else
+ const ShareActionButton(source: ActionSource.viewer),
if (!isInLockedView) ...[
- if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
- // edit sync was added in 2.6.0
- if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0))
- const EditImageActionButton(),
- if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
-
+ if (!isInTrash) ...[
+ if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
+ // edit sync was added in 2.6.0
+ if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0))
+ const EditImageActionButton(),
+ if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
+ ],
if (isOwner) ...[
asset.isLocalOnly
? const DeleteLocalActionButton(source: ActionSource.viewer)
diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart
index 825d9e7bc8..4c2a110fde 100644
--- a/mobile/lib/providers/auth.provider.dart
+++ b/mobile/lib/providers/auth.provider.dart
@@ -11,12 +11,11 @@ import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
+import 'package:immich_mobile/services/background_upload.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/services/secure_storage.service.dart';
-import 'package:immich_mobile/services/background_upload.service.dart';
import 'package:immich_mobile/services/widget.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
-import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -144,7 +143,6 @@ class AuthNotifier extends StateNotifier {
// Due to the flow of the code, this will always happen on first login
user = serverUser;
await Store.put(StoreKey.deviceId, deviceId);
- await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
}
} on ApiException catch (error, stackTrace) {
if (error.code == 401) {
diff --git a/mobile/lib/routing/auth_guard.dart b/mobile/lib/routing/auth_guard.dart
index eaa821c0eb..2fc27be4f4 100644
--- a/mobile/lib/routing/auth_guard.dart
+++ b/mobile/lib/routing/auth_guard.dart
@@ -15,37 +15,62 @@ class AuthGuard extends AutoRouteGuard {
final ApiService _apiService;
final AuthService _authService;
final _log = Logger("AuthGuard");
+ bool _validateInFlight = false;
AuthGuard(this._apiService, this._authService);
@override
- void onNavigation(NavigationResolver resolver, StackRouter router) async {
- resolver.next(true);
-
+ void onNavigation(NavigationResolver resolver, StackRouter router) {
+ // Synchronously check for the access token. auto_route awaits async
+ // guards, so we keep this function fully sync and validate the token in
+ // the background — otherwise a slow validateAccessToken() request would
+ // block the route transition for as long as the OS-level HTTP timeout.
try {
- // Look in the store for an access token
Store.get(StoreKey.accessToken);
-
- // Validate the access token with the server
- final res = await _apiService.authenticationApi.validateAccessToken();
- if (res == null || res.authStatus != true) {
- // If the access token is invalid, take user back to login
- _log.fine('User token is invalid. Redirecting to login');
- unawaited(router.replaceAll([const LoginRoute()]).then((_) => _authService.clearLocalData()));
- }
} on StoreKeyNotFoundException catch (_) {
- // If there is no access token, take us to the login page
_log.warning('No access token in the store.');
+ resolver.next(false);
unawaited(router.replaceAll([const LoginRoute()]));
return;
+ }
+
+ resolver.next(true);
+ unawaited(_validateAccessTokenInBackground(router));
+ }
+
+ Future _validateAccessTokenInBackground(StackRouter router) async {
+ if (_validateInFlight) {
+ return;
+ }
+ final token = Store.tryGet(StoreKey.accessToken);
+ if (token == null) {
+ return;
+ }
+ _validateInFlight = true;
+ try {
+ final res = await _apiService.authenticationApi.validateAccessToken();
+ if (res == null || res.authStatus != true) {
+ // Token may have changed during validation (user logged out + logged in
+ // again); only act if it still applies to the current session.
+ if (Store.tryGet(StoreKey.accessToken) != token) {
+ return;
+ }
+ _log.fine('User token is invalid. Redirecting to login');
+ await router.replaceAll([const LoginRoute()]);
+ await _authService.clearLocalData();
+ }
} on ApiException catch (e) {
- // On an unauthorized request, take us to the login page
- if (e.code == HttpStatus.unauthorized) {
- _log.warning("Unauthorized access token.");
- unawaited(router.replaceAll([const LoginRoute()]).then((_) => _authService.clearLocalData()));
+ if (e.code != HttpStatus.unauthorized) {
return;
}
+ if (Store.tryGet(StoreKey.accessToken) != token) {
+ return;
+ }
+ _log.warning("Unauthorized access token.");
+ await router.replaceAll([const LoginRoute()]);
+ await _authService.clearLocalData();
} catch (e) {
- // Otherwise, this is not fatal, but we still log the warning
_log.warning('Error validating access token from server: $e');
+ } finally {
+ _validateInFlight = false;
}
}
}
diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart
index 38d3e028cb..1b9a38bc19 100644
--- a/mobile/lib/services/app_settings.service.dart
+++ b/mobile/lib/services/app_settings.service.dart
@@ -2,15 +2,9 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum {
- uploadErrorNotificationGracePeriod(
- StoreKey.uploadErrorNotificationGracePeriod,
- "uploadErrorNotificationGracePeriod",
- 2,
- ),
selectedAlbumSortOrder(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
advancedTroubleshooting(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid(StoreKey.manageLocalMediaAndroid, null, false),
- allowSelfSignedSSLCert(StoreKey.selfSignedCert, null, false),
selectedAlbumSortReverse(StoreKey.selectedAlbumSortReverse, null, true),
enableHapticFeedback(StoreKey.enableHapticFeedback, null, true),
syncAlbums(StoreKey.syncAlbums, null, false),
diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart
index 667681e579..1b5eaab715 100644
--- a/mobile/lib/services/auth.service.dart
+++ b/mobile/lib/services/auth.service.dart
@@ -123,7 +123,6 @@ class AuthService {
_authRepository.clearLocalData(),
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
- Store.delete(StoreKey.assetETag),
Store.delete(StoreKey.autoEndpointSwitching),
Store.delete(StoreKey.preferredWifiName),
Store.delete(StoreKey.localEndpoint),
diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart
index d527f3a59e..c38c536136 100644
--- a/mobile/lib/utils/action_button.utils.dart
+++ b/mobile/lib/utils/action_button.utils.dart
@@ -21,6 +21,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f
import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
+import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
@@ -81,6 +82,7 @@ enum ActionButtonType {
moveToLockFolder,
removeFromLockFolder,
removeFromAlbum,
+ restoreTrash,
trash,
deleteLocal,
deletePermanent,
@@ -112,7 +114,13 @@ enum ActionButtonType {
context.isOwner && //
!context.isInLockedView && //
context.asset.hasRemote && //
- context.isTrashEnabled,
+ context.isTrashEnabled && //
+ context.timelineOrigin != TimelineOrigin.trash,
+ ActionButtonType.restoreTrash =>
+ context.isOwner && //
+ !context.isInLockedView && //
+ context.asset.hasRemote && //
+ context.timelineOrigin == TimelineOrigin.trash,
ActionButtonType.deletePermanent =>
context.isOwner && //
context.asset.hasRemote && //
@@ -201,6 +209,11 @@ enum ActionButtonType {
),
ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
+ ActionButtonType.restoreTrash => RestoreActionButton(
+ source: context.source,
+ iconOnly: iconOnly,
+ menuItem: menuItem,
+ ),
ActionButtonType.deletePermanent => DeletePermanentActionButton(
source: context.source,
iconOnly: iconOnly,
@@ -292,6 +305,7 @@ enum ActionButtonType {
ActionButtonType.moveToLockFolder => 10,
ActionButtonType.deleteLocal => 10,
ActionButtonType.delete => 10,
+ ActionButtonType.restoreTrash => 10,
// 90: advancedInfo
ActionButtonType.advancedInfo => 90,
// 1: others
@@ -309,6 +323,7 @@ class ActionButtonBuilder {
ActionButtonType.delete,
ActionButtonType.archive,
ActionButtonType.unarchive,
+ ActionButtonType.restoreTrash,
};
static List build(ActionButtonContext context) {
diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart
index 18a9749a71..46120bb218 100644
--- a/mobile/lib/widgets/settings/notification_setting.dart
+++ b/mobile/lib/widgets/settings/notification_setting.dart
@@ -3,10 +3,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart';
-import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
-import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -16,9 +13,6 @@ class NotificationSetting extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final permissionService = ref.watch(notificationPermissionProvider);
-
- final sliderValue = useAppSettingsState(AppSettingsEnum.uploadErrorNotificationGracePeriod);
-
final hasPermission = permissionService == PermissionStatus.granted;
openAppNotificationSettings(BuildContext ctx) {
@@ -41,8 +35,6 @@ class NotificationSetting extends HookConsumerWidget {
);
}
- final String formattedValue = _formatSliderValue(sliderValue.value.toDouble());
-
final notificationSettings = [
if (!hasPermission)
SettingsButtonListTile(
@@ -57,32 +49,8 @@ class NotificationSetting extends HookConsumerWidget {
}
}),
),
- SettingsSliderListTile(
- enabled: hasPermission,
- valueNotifier: sliderValue,
- text: 'setting_notifications_notify_failures_grace_period'.tr(namedArgs: {'duration': formattedValue}),
- maxValue: 5.0,
- noDivisons: 5,
- label: formattedValue,
- ),
];
return SettingsSubPageScaffold(settings: notificationSettings);
}
}
-
-String _formatSliderValue(double v) {
- if (v == 0.0) {
- return 'setting_notifications_notify_immediately'.tr();
- } else if (v == 1.0) {
- return 'setting_notifications_notify_minutes'.tr(namedArgs: {'count': '30'});
- } else if (v == 2.0) {
- return 'setting_notifications_notify_hours'.tr(namedArgs: {'count': '2'});
- } else if (v == 3.0) {
- return 'setting_notifications_notify_hours'.tr(namedArgs: {'count': '8'});
- } else if (v == 4.0) {
- return 'setting_notifications_notify_hours'.tr(namedArgs: {'count': '24'});
- } else {
- return 'setting_notifications_notify_never'.tr();
- }
-}
diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart
index 9f6a30eefe..0a55f8d5c7 100644
--- a/mobile/test/domain/services/store_service_test.dart
+++ b/mobile/test/domain/services/store_service_test.dart
@@ -9,9 +9,8 @@ import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart';
const _kAccessToken = '#ThisIsAToken';
-const _kBackgroundBackup = false;
+const _kEnableBackup = false;
const _kVersion = 2;
-final _kBackupFailedSince = DateTime.utc(2023);
void main() {
late StoreService sut;
@@ -24,15 +23,13 @@ void main() {
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
registerFallbackValue(StoreKey.accessToken);
registerFallbackValue(StoreKey.backupTriggerDelay);
- registerFallbackValue(StoreKey.backgroundBackup);
- registerFallbackValue(StoreKey.backupFailedSince);
+ registerFallbackValue(StoreKey.enableBackup);
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
(_) async => [
const StoreDto(StoreKey.accessToken, _kAccessToken),
- const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
+ const StoreDto(StoreKey.enableBackup, _kEnableBackup),
const StoreDto(StoreKey.version, _kVersion),
- StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
],
);
when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
@@ -49,9 +46,8 @@ void main() {
test('Populates the internal cache on init', () {
verify(() => mockDriftStoreRepo.getAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
- expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup);
+ expect(sut.tryGet(StoreKey.enableBackup), _kEnableBackup);
expect(sut.tryGet(StoreKey.version), _kVersion);
- expect(sut.tryGet(StoreKey.backupFailedSince), _kBackupFailedSince);
// Other keys should be null
expect(sut.tryGet(StoreKey.currentUser), isNull);
});
@@ -151,9 +147,8 @@ void main() {
await sut.clear();
verify(() => mockDriftStoreRepo.deleteAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), isNull);
- expect(sut.tryGet(StoreKey.backgroundBackup), isNull);
+ expect(sut.tryGet(StoreKey.enableBackup), isNull);
expect(sut.tryGet(StoreKey.version), isNull);
- expect(sut.tryGet(StoreKey.backupFailedSince), isNull);
});
});
}
diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart
index 806cde9b75..672776b226 100644
--- a/mobile/test/infrastructure/repositories/store_repository_test.dart
+++ b/mobile/test/infrastructure/repositories/store_repository_test.dart
@@ -12,9 +12,8 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'
import '../../fixtures/user.stub.dart';
const _kTestAccessToken = "#TestToken";
-final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45);
const _kTestVersion = 10;
-const _kTestBackupRequireWifi = false;
+const _kTestBackupRequireCharging = false;
final _kTestUser = UserStub.admin;
Future _populateStore(Drift db) async {
@@ -22,16 +21,8 @@ Future _populateStore(Drift db) async {
batch.insert(
db.storeEntity,
StoreEntityCompanion(
- id: Value(StoreKey.backupRequireWifi.id),
- intValue: const Value(_kTestBackupRequireWifi ? 1 : 0),
- stringValue: const Value(null),
- ),
- );
- batch.insert(
- db.storeEntity,
- StoreEntityCompanion(
- id: Value(StoreKey.backupFailedSince.id),
- intValue: Value(_kTestBackupFailed.millisecondsSinceEpoch),
+ id: Value(StoreKey.backupRequireCharging.id),
+ intValue: const Value(_kTestBackupRequireCharging ? 1 : 0),
stringValue: const Value(null),
),
);
@@ -84,20 +75,12 @@ void main() {
expect(accessToken, _kTestAccessToken);
});
- test('converts datetime', () async {
- DateTime? backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
- expect(backupFailedSince, isNull);
- await sut.upsert(StoreKey.backupFailedSince, _kTestBackupFailed);
- backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
- expect(backupFailedSince, _kTestBackupFailed);
- });
-
test('converts bool', () async {
- bool? backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
- expect(backupRequireWifi, isNull);
- await sut.upsert(StoreKey.backupRequireWifi, _kTestBackupRequireWifi);
- backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
- expect(backupRequireWifi, _kTestBackupRequireWifi);
+ bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
+ expect(backupRequireCharging, isNull);
+ await sut.upsert(StoreKey.backupRequireCharging, _kTestBackupRequireCharging);
+ backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
+ expect(backupRequireCharging, _kTestBackupRequireCharging);
});
test('converts user', () async {
@@ -115,11 +98,11 @@ void main() {
});
test('delete()', () async {
- bool? backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
- expect(backupRequireWifi, isFalse);
- await sut.delete(StoreKey.backupRequireWifi);
- backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
- expect(backupRequireWifi, isNull);
+ bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
+ expect(backupRequireCharging, isFalse);
+ await sut.delete(StoreKey.backupRequireCharging);
+ backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging);
+ expect(backupRequireCharging, isNull);
});
test('deleteAll()', () async {
@@ -164,14 +147,12 @@ void main() {
emitsInOrder([
[
const StoreDto