1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

refactor(mobile): migrate all Hive boxes to Isar database (#2036)

This commit is contained in:
Fynn Petersen-Frey 2023-03-23 02:36:44 +01:00 committed by GitHub
parent 0616a66b05
commit eccde8fa07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 448 additions and 383 deletions

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
@ -35,9 +34,7 @@ class ImmichTestHelper {
} }
static Future<void> loadApp(WidgetTester tester) async { static Future<void> loadApp(WidgetTester tester) async {
// Clear all data from Hive await EasyLocalization.ensureInitialized();
await Hive.deleteFromDisk();
await app.openBoxes();
// Clear all data from Isar (reuse existing instance if available) // Clear all data from Isar (reuse existing instance if available)
final db = Isar.getInstance() ?? await app.loadDb(); final db = Isar.getInstance() ?? await app.loadDb();
await Store.clear(); await Store.clear();
@ -65,12 +62,13 @@ void immichWidgetTest(
} }
Future<void> pumpUntilFound( Future<void> pumpUntilFound(
WidgetTester tester, WidgetTester tester,
Finder finder, { Finder finder, {
Duration timeout = const Duration(seconds: 120), Duration timeout = const Duration(seconds: 120),
}) async { }) async {
bool found = false; bool found = false;
final timer = Timer(timeout, () => throw TimeoutException("Pump until has timed out")); final timer =
Timer(timeout, () => throw TimeoutException("Pump until has timed out"));
while (found != true) { while (found != true) {
await tester.pump(); await tester.pump();
found = tester.any(finder); found = tester.any(finder);

View File

@ -25,6 +25,7 @@ import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart'; import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/app_state.provider.dart';
@ -42,35 +43,23 @@ import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'constants/hive_box.dart';
void main() async { void main() async {
await initApp(); WidgetsFlutterBinding.ensureInitialized();
final db = await loadDb(); final db = await loadDb();
await initApp();
await migrateHiveToStoreIfNecessary(); await migrateHiveToStoreIfNecessary();
await migrateJsonCacheIfNecessary(); await migrateJsonCacheIfNecessary();
runApp(getMainWidget(db)); runApp(getMainWidget(db));
} }
Future<void> openBoxes() async {
await Future.wait([
Hive.openBox<ImmichLoggerMessage>(immichLoggerBox),
Hive.openBox(userInfoBox),
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
Hive.openBox(hiveGithubReleaseInfoBox),
Hive.openBox(userSettingInfoBox),
EasyLocalization.ensureInitialized(),
]);
}
Future<void> initApp() async { Future<void> initApp() async {
await Hive.initFlutter(); await Hive.initFlutter();
Hive.registerAdapter(HiveSavedLoginInfoAdapter()); Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter()); Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
Hive.registerAdapter(ImmichLoggerMessageAdapter()); Hive.registerAdapter(ImmichLoggerMessageAdapter());
await EasyLocalization.ensureInitialized();
await openBoxes();
if (kReleaseMode && Platform.isAndroid) { if (kReleaseMode && Platform.isAndroid) {
try { try {
@ -82,7 +71,7 @@ Future<void> initApp() async {
} }
// Initialize Immich Logger Service // Initialize Immich Logger Service
ImmichLogger().init(); ImmichLogger();
var log = Logger("ImmichErrorLogger"); var log = Logger("ImmichErrorLogger");
@ -108,6 +97,7 @@ Future<Isar> loadDb() async {
UserSchema, UserSchema,
BackupAlbumSchema, BackupAlbumSchema,
DuplicatedAssetSchema, DuplicatedAssetSchema,
LoggerMessageSchema,
], ],
directory: dir.path, directory: dir.path,
maxSizeMiB: 256, maxSizeMiB: 256,
@ -174,6 +164,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive"); debugPrint("[APP STATE] inactive");
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive; ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
ImmichLogger().flush();
ref.watch(websocketProvider.notifier).disconnect(); ref.watch(websocketProvider.notifier).disconnect();
ref.watch(backupProvider.notifier).cancelBackup(); ref.watch(backupProvider.notifier).cancelBackup();

View File

@ -265,7 +265,7 @@ class AlbumService {
Future<bool> deleteAlbum(Album album) async { Future<bool> deleteAlbum(Album album) async {
try { try {
final userId = Store.get<User>(StoreKey.currentUser)!.isarId; final userId = Store.get(StoreKey.currentUser).isarId;
if (album.owner.value?.isarId == userId) { if (album.owner.value?.isarId == userId) {
await _apiService.albumApi.deleteAlbum(album.remoteId!); await _apiService.albumApi.deleteAlbum(album.remoteId!);
} }

View File

@ -2,10 +2,9 @@ import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -21,7 +20,6 @@ class AlbumThumbnailListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
var cardSize = 68.0; var cardSize = 68.0;
var isDarkMode = Theme.of(context).brightness == Brightness.dark; var isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -50,7 +48,9 @@ class AlbumThumbnailListTile extends StatelessWidget {
album, album,
type: ThumbnailFormat.JPEG, type: ThumbnailFormat.JPEG,
), ),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, httpHeaders: {
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
},
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG), cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
errorWidget: (context, url, error) => errorWidget: (context, url, error) =>
const Icon(Icons.image_not_supported_outlined), const Icon(Icons.image_not_supported_outlined),

View File

@ -4,16 +4,15 @@ import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/services/asset.service.dart'; import 'package:immich_mobile/shared/services/asset.service.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
@ -47,7 +46,6 @@ class GalleryViewerPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox);
final settings = ref.watch(appSettingsServiceProvider); final settings = ref.watch(appSettingsServiceProvider);
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
@ -57,7 +55,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final isPlayingMotionVideo = useState(false); final isPlayingMotionVideo = useState(false);
final isPlayingVideo = useState(false); final isPlayingVideo = useState(false);
late Offset localPosition; late Offset localPosition;
final authToken = 'Bearer ${box.get(accessTokenKey)}'; final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
showAppBar.addListener(() { showAppBar.addListener(() {
// Change to and from immersive mode, hiding navigation and app bar // Change to and from immersive mode, hiding navigation and app bar

View File

@ -1,13 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@ -54,17 +53,15 @@ class VideoViewerPage extends HookConsumerWidget {
} }
final downloadAssetStatus = final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus; ref.watch(imageViewerStateProvider).downloadAssetStatus;
final box = Hive.box(userInfoBox);
final String jwtToken = box.get(accessTokenKey);
final String videoUrl = isMotionVideo final String videoUrl = isMotionVideo
? '${box.get(serverEndpointKey)}/asset/file/${asset.livePhotoVideoId}' ? '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.livePhotoVideoId}'
: '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}'; : '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}';
return Stack( return Stack(
children: [ children: [
VideoThumbnailPlayer( VideoThumbnailPlayer(
url: videoUrl, url: videoUrl,
jwtToken: jwtToken, jwtToken: Store.get(StoreKey.accessToken),
isMotionVideo: isMotionVideo, isMotionVideo: isMotionVideo,
onVideoEnded: onVideoEnded, onVideoEnded: onVideoEnded,
onPaused: onPaused, onPaused: onPaused,

View File

@ -8,16 +8,13 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/main.dart'; import 'package:immich_mobile/main.dart';
import 'package:immich_mobile/modules/backup/background_service/localization.dart'; import 'package:immich_mobile/modules/backup/background_service/localization.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart'; import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/shared/services/api.service.dart';
@ -317,7 +314,6 @@ class BackgroundService {
debugPrint(error.toString()); debugPrint(error.toString());
return false; return false;
} finally { } finally {
await Hive.close();
releaseLock(); releaseLock();
} }
case "systemStop": case "systemStop":
@ -332,17 +328,9 @@ class BackgroundService {
Future<bool> _onAssetsChanged() async { Future<bool> _onAssetsChanged() async {
final Isar db = await loadDb(); final Isar db = await loadDb();
await Hive.initFlutter();
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
await Future.wait([
Hive.openBox(userInfoBox),
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
Hive.openBox(userSettingInfoBox),
]);
ApiService apiService = ApiService(); ApiService apiService = ApiService();
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey)); apiService.setAccessToken(Store.get(StoreKey.accessToken));
BackupService backupService = BackupService(apiService, db); BackupService backupService = BackupService(apiService, db);
AppSettingsService settingsService = AppSettingsService(); AppSettingsService settingsService = AppSettingsService();
@ -387,7 +375,7 @@ class BackgroundService {
db.backupAlbums.deleteAllSync(toDelete); db.backupAlbums.deleteAllSync(toDelete);
db.backupAlbums.putAllSync(toUpsert); db.backupAlbums.putAllSync(toUpsert);
}); });
} else if (Store.get(StoreKey.backupFailedSince) == null) { } else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
Store.put(StoreKey.backupFailedSince, DateTime.now()); Store.put(StoreKey.backupFailedSince, DateTime.now());
return false; return false;
} }
@ -529,7 +517,7 @@ class BackgroundService {
} else if (value == 5) { } else if (value == 5) {
return false; return false;
} }
final DateTime? failedSince = Store.get(StoreKey.backupFailedSince); final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince);
if (failedSince == null) { if (failedSince == null) {
return false; return false;
} }

View File

@ -1,9 +1,7 @@
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
@ -42,9 +40,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInPercentage: 0, progressInPercentage: 0,
cancelToken: CancellationToken(), cancelToken: CancellationToken(),
backgroundBackup: false, backgroundBackup: false,
backupRequireWifi: true, backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
backupRequireCharging: false, backupRequireCharging:
backupTriggerDelay: 5000, Store.get(StoreKey.backupRequireCharging, false),
backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000),
serverInfo: ServerInfoResponseDto( serverInfo: ServerInfoResponseDto(
diskAvailable: "0", diskAvailable: "0",
diskAvailableRaw: 0, diskAvailableRaw: 0,
@ -163,14 +162,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
triggerMaxDelay: state.backupTriggerDelay * 10, triggerMaxDelay: state.backupTriggerDelay * 10,
); );
if (success) { if (success) {
await Future.wait([ await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi);
Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi), await Store.put(
Store.put( StoreKey.backupRequireCharging,
StoreKey.backupRequireCharging, state.backupRequireCharging,
state.backupRequireCharging, );
), await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay),
]);
} else { } else {
state = state.copyWith( state = state.copyWith(
backgroundBackup: wasEnabled, backgroundBackup: wasEnabled,
@ -544,7 +541,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Future<void> _resumeBackup() async { Future<void> _resumeBackup() async {
// Check if user is login // Check if user is login
final accessKey = Hive.box(userInfoBox).get(accessTokenKey); final accessKey = Store.tryGet(StoreKey.accessToken);
// User has been logged out return // User has been logged out return
if (accessKey == null || !_authState.isAuthenticated) { if (accessKey == null || !_authState.isAuthenticated) {
@ -603,9 +600,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
backupProgress: BackUpProgressEnum.inBackground, backupProgress: BackUpProgressEnum.inBackground,
selectedBackupAlbums: selectedAlbums, selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums, excludedBackupAlbums: excludedAlbums,
backupRequireWifi: Store.get(StoreKey.backupRequireWifi),
backupRequireCharging: Store.get(StoreKey.backupRequireCharging),
backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay),
); );
// assumes the background service is currently running // assumes the background service is currently running
// if true, waits until it has stopped to start the backup // if true, waits until it has stopped to start the backup

View File

@ -5,13 +5,12 @@ import 'dart:io';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/shared/services/api.service.dart';
@ -38,7 +37,7 @@ class BackupService {
BackupService(this._apiService, this._db); BackupService(this._apiService, this._db);
Future<List<String>?> getDeviceBackupAsset() async { Future<List<String>?> getDeviceBackupAsset() async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey); final String deviceId = Store.get(StoreKey.deviceId);
try { try {
return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId); return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
@ -173,7 +172,7 @@ class BackupService {
} }
final Set<String> existing = {}; final Set<String> existing = {};
try { try {
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); final String deviceId = Store.get(StoreKey.deviceId);
final CheckExistingAssetsResponseDto? duplicates = final CheckExistingAssetsResponseDto? duplicates =
await _apiService.assetApi.checkExistingAssets( await _apiService.assetApi.checkExistingAssets(
CheckExistingAssetsDto( CheckExistingAssetsDto(
@ -204,8 +203,8 @@ class BackupService {
Function(CurrentUploadAsset) setCurrentUploadAssetCb, Function(CurrentUploadAsset) setCurrentUploadAssetCb,
Function(ErrorUploadAsset) errorCb, Function(ErrorUploadAsset) errorCb,
) async { ) async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey); final String deviceId = Store.get(StoreKey.deviceId);
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey); final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
File? file; File? file;
bool anyErrors = false; bool anyErrors = false;
final List<String> duplicatedAssetIds = []; final List<String> duplicatedAssetIds = [];
@ -236,15 +235,14 @@ class BackupService {
), ),
); );
var box = Hive.box(userInfoBox);
var req = MultipartRequest( var req = MultipartRequest(
'POST', 'POST',
Uri.parse('$savedEndpoint/asset/upload'), Uri.parse('$savedEndpoint/asset/upload'),
onProgress: ((bytes, totalBytes) => onProgress: ((bytes, totalBytes) =>
uploadProgressCb(bytes, totalBytes)), uploadProgressCb(bytes, totalBytes)),
); );
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}"; req.headers["Authorization"] =
"Bearer ${Store.get(StoreKey.accessToken)}";
req.fields['deviceAssetId'] = entity.id; req.fields['deviceAssetId'] = entity.id;
req.fields['deviceId'] = deviceId; req.fields['deviceId'] = deviceId;

View File

@ -2,9 +2,7 @@ import 'dart:math';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@ -12,6 +10,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/shared/models/server_info_state.model.dart'; import 'package:immich_mobile/shared/models/server_info_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/transparent_image.dart'; import 'package:immich_mobile/shared/ui/transparent_image.dart';
@ -47,7 +46,7 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
}, },
); );
} else { } else {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); final String? endpoint = Store.get(StoreKey.serverEndpoint);
var dummy = Random().nextInt(1024); var dummy = Random().nextInt(1024);
return InkWell( return InkWell(
onTap: () { onTap: () {

View File

@ -1,14 +1,13 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/transparent_image.dart'; import 'package:immich_mobile/shared/ui/transparent_image.dart';
@ -19,7 +18,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey); final String endpoint = Store.get(StoreKey.serverEndpoint);
AuthenticationState authState = ref.watch(authenticationProvider); AuthenticationState authState = ref.watch(authenticationProvider);
final uploadProfileImageStatus = final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status; ref.watch(uploadProfileImageProvider).status;

View File

@ -2,12 +2,9 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart'; import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
@ -91,11 +88,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
try { try {
await Future.wait([ await Future.wait([
_apiService.authenticationApi.logout(), _apiService.authenticationApi.logout(),
Hive.box(userInfoBox).delete(accessTokenKey),
Store.delete(StoreKey.assetETag), Store.delete(StoreKey.assetETag),
Store.delete(StoreKey.userRemoteId), Store.delete(StoreKey.userRemoteId),
Store.delete(StoreKey.currentUser), Store.delete(StoreKey.currentUser),
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey) Store.delete(StoreKey.accessToken),
]); ]);
state = state.copyWith(isAuthenticated: false); state = state.copyWith(isAuthenticated: false);
@ -157,14 +153,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
} }
if (userResponseDto != null) { if (userResponseDto != null) {
var userInfoHiveBox = await Hive.openBox(userInfoBox);
var deviceInfo = await _deviceInfoService.getDeviceInfo(); var deviceInfo = await _deviceInfoService.getDeviceInfo();
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
userInfoHiveBox.put(accessTokenKey, accessToken);
Store.put(StoreKey.deviceId, deviceInfo["deviceId"]); Store.put(StoreKey.deviceId, deviceInfo["deviceId"]);
Store.put(StoreKey.deviceIdHash, fastHash(deviceInfo["deviceId"])); Store.put(StoreKey.deviceIdHash, fastHash(deviceInfo["deviceId"]));
Store.put(StoreKey.userRemoteId, userResponseDto.id); Store.put(StoreKey.userRemoteId, userResponseDto.id);
Store.put(StoreKey.currentUser, User.fromDto(userResponseDto)); Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken);
state = state.copyWith( state = state.copyWith(
isAuthenticated: true, isAuthenticated: true,
@ -178,17 +173,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
deviceId: deviceInfo["deviceId"], deviceId: deviceInfo["deviceId"],
deviceType: deviceInfo["deviceType"], deviceType: deviceInfo["deviceType"],
); );
// Save login info to local storage
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
savedLoginInfoKey,
HiveSavedLoginInfo(
email: "",
password: "",
serverUrl: serverUrl,
accessToken: accessToken,
),
);
} }
// Register device info // Register device info

View File

@ -1,14 +1,12 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/oauth.provider.dart'; import 'package:immich_mobile/modules/login/providers/oauth.provider.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@ -63,8 +61,7 @@ class LoginForm extends HookConsumerWidget {
try { try {
isLoadingServer.value = true; isLoadingServer.value = true;
final endpoint = final endpoint = await apiService.resolveAndSetEndpoint(serverUrl);
await apiService.resolveAndSetEndpoint(serverUrl);
final loginConfig = await apiService.oAuthApi.generateConfig( final loginConfig = await apiService.oAuthApi.generateConfig(
OAuthConfigDto(redirectUri: serverUrl), OAuthConfigDto(redirectUri: serverUrl),
@ -104,15 +101,10 @@ class LoginForm extends HookConsumerWidget {
useEffect( useEffect(
() { () {
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox) final serverUrl = Store.tryGet(StoreKey.serverUrl);
.get(savedLoginInfoKey); if (serverUrl != null) {
serverEndpointController.text = serverUrl;
if (loginInfo != null) {
usernameController.text = loginInfo.email;
passwordController.text = loginInfo.password;
serverEndpointController.text = loginInfo.serverUrl;
} }
return null; return null;
}, },
[], [],
@ -133,11 +125,11 @@ class LoginForm extends HookConsumerWidget {
try { try {
final isAuthenticated = final isAuthenticated =
await ref.read(authenticationProvider.notifier).login( await ref.read(authenticationProvider.notifier).login(
usernameController.text, usernameController.text,
passwordController.text, passwordController.text,
serverEndpointController.text.trim(), serverEndpointController.text.trim(),
); );
if (isAuthenticated) { if (isAuthenticated) {
// Resume backup (if enable) then navigate // Resume backup (if enable) then navigate
if (ref.read(authenticationProvider).shouldChangePassword && if (ref.read(authenticationProvider).shouldChangePassword &&
@ -283,61 +275,61 @@ class LoginForm extends HookConsumerWidget {
onSubmit: login, onSubmit: login,
), ),
// Note: This used to have an AnimatedSwitcher, but was removed // Note: This used to have an AnimatedSwitcher, but was removed
// because of https://github.com/flutter/flutter/issues/120874 // because of https://github.com/flutter/flutter/issues/120874
isLoading.value isLoading.value
? const Padding( ? const Padding(
padding: EdgeInsets.only(top: 18.0), padding: EdgeInsets.only(top: 18.0),
child: SizedBox( child: SizedBox(
width: 24, width: 24,
height: 24, height: 24,
child: FittedBox( child: FittedBox(
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
),
), ),
), ),
), )
) : Column(
: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ const SizedBox(height: 18),
const SizedBox(height: 18), LoginButton(onPressed: login),
LoginButton(onPressed: login), if (isOauthEnable.value) ...[
if (isOauthEnable.value) ...[ Padding(
Padding( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 16.0,
horizontal: 16.0, ),
child: Divider(
color:
Brightness.dark == Theme.of(context).brightness
? Colors.white
: Colors.black,
),
), ),
child: Divider( OAuthLoginButton(
color: serverEndpointController: serverEndpointController,
Brightness.dark == Theme.of(context).brightness buttonLabel: oAuthButtonLabel.value,
? Colors.white isLoading: isLoading,
: Colors.black, onPressed: oAuthLogin,
), ),
), ],
OAuthLoginButton(
serverEndpointController: serverEndpointController,
buttonLabel: oAuthButtonLabel.value,
isLoading: isLoading,
onPressed: oAuthLogin,
),
], ],
], ),
), const SizedBox(height: 12),
const SizedBox(height: 12), TextButton.icon(
TextButton.icon( icon: const Icon(Icons.arrow_back),
icon: const Icon(Icons.arrow_back), onPressed: () => serverEndpoint.value = null,
onPressed: () => serverEndpoint.value = null, label: const Text('Back'),
label: const Text('Back'), ),
),
], ],
), ),
); );
} }
final serverSelectionOrLogin = serverEndpoint.value == null
? buildSelectServer() final serverSelectionOrLogin =
: buildLogin(); serverEndpoint.value == null ? buildSelectServer() : buildLogin();
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@ -545,7 +537,6 @@ class OAuthLoginButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon( return ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor.withAlpha(230), backgroundColor: Theme.of(context).primaryColor.withAlpha(230),

View File

@ -1,7 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/constants/hive_box.dart';
class ThumbnailWithInfo extends StatelessWidget { class ThumbnailWithInfo extends StatelessWidget {
const ThumbnailWithInfo({ const ThumbnailWithInfo({
@ -19,7 +18,6 @@ class ThumbnailWithInfo extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
var isDarkMode = Theme.of(context).brightness == Brightness.dark; var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var textAndIconColor = isDarkMode ? Colors.grey[100] : Colors.grey[700]; var textAndIconColor = isDarkMode ? Colors.grey[100] : Colors.grey[700];
return GestureDetector( return GestureDetector(
@ -51,7 +49,8 @@ class ThumbnailWithInfo extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
imageUrl: imageUrl!, imageUrl: imageUrl!,
httpHeaders: { httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}" "Authorization":
"Bearer ${Store.get(StoreKey.accessToken)}"
}, },
errorWidget: (context, url, error) => errorWidget: (context, url, error) =>
const Icon(Icons.image_not_supported_outlined), const Icon(Icons.image_not_supported_outlined),

View File

@ -1,15 +1,14 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/ui/search_bar.dart'; import 'package:immich_mobile/modules/search/ui/search_bar.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/utils/capitalize_first_letter.dart'; import 'package:immich_mobile/utils/capitalize_first_letter.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -22,7 +21,6 @@ class SearchPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox);
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation = AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
ref.watch(getCuratedLocationProvider); ref.watch(getCuratedLocationProvider);
@ -64,7 +62,7 @@ class SearchPage extends HookConsumerWidget {
itemBuilder: ((context, index) { itemBuilder: ((context, index) {
var locationInfo = curatedLocations[index]; var locationInfo = curatedLocations[index];
var thumbnailRequestUrl = var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/thumbnail/${locationInfo.id}'; '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${locationInfo.id}';
return ThumbnailWithInfo( return ThumbnailWithInfo(
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,
textInfo: locationInfo.city, textInfo: locationInfo.city,
@ -113,7 +111,7 @@ class SearchPage extends HookConsumerWidget {
itemBuilder: ((context, index) { itemBuilder: ((context, index) {
var curatedObjectInfo = objects[index]; var curatedObjectInfo = objects[index];
var thumbnailRequestUrl = var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/thumbnail/${curatedObjectInfo.id}'; '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/${curatedObjectInfo.id}';
return ThumbnailWithInfo( return ThumbnailWithInfo(
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,

View File

@ -1,59 +1,63 @@
import 'package:hive_flutter/hive_flutter.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/constants/hive_box.dart';
enum AppSettingsEnum<T> { enum AppSettingsEnum<T> {
loadPreview<bool>("loadPreview", true), loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true),
loadOriginal<bool>("loadOriginal", false), loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
themeMode<String>("themeMode", "system"), // "light","dark","system" themeMode<String>(
tilesPerRow<int>("tilesPerRow", 4), StoreKey.themeMode,
dynamicLayout<bool>("dynamicLayout", false), "themeMode",
groupAssetsBy<int>("groupBy", 0), "system",
), // "light","dark","system"
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false),
groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0),
uploadErrorNotificationGracePeriod<int>( uploadErrorNotificationGracePeriod<int>(
StoreKey.uploadErrorNotificationGracePeriod,
"uploadErrorNotificationGracePeriod", "uploadErrorNotificationGracePeriod",
2, 2,
), ),
backgroundBackupTotalProgress<bool>("backgroundBackupTotalProgress", true), backgroundBackupTotalProgress<bool>(
backgroundBackupSingleProgress<bool>("backgroundBackupSingleProgress", false), StoreKey.backgroundBackupTotalProgress,
storageIndicator<bool>("storageIndicator", true), "backgroundBackupTotalProgress",
thumbnailCacheSize<int>("thumbnailCacheSize", 10000), true,
imageCacheSize<int>("imageCacheSize", 350), ),
albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200), backgroundBackupSingleProgress<bool>(
useExperimentalAssetGrid<bool>("useExperimentalAssetGrid", false), StoreKey.backgroundBackupSingleProgress,
selectedAlbumSortOrder<int>("selectedAlbumSortOrder", 0); "backgroundBackupSingleProgress",
false,
),
storageIndicator<bool>(StoreKey.storageIndicator, "storageIndicator", true),
thumbnailCacheSize<int>(
StoreKey.thumbnailCacheSize,
"thumbnailCacheSize",
10000,
),
imageCacheSize<int>(StoreKey.imageCacheSize, "imageCacheSize", 350),
albumThumbnailCacheSize<int>(
StoreKey.albumThumbnailCacheSize,
"albumThumbnailCacheSize",
200,
),
selectedAlbumSortOrder<int>(
StoreKey.selectedAlbumSortOrder,
"selectedAlbumSortOrder",
0,
),
;
const AppSettingsEnum(this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
final StoreKey<T> storeKey;
final String hiveKey; final String hiveKey;
final T defaultValue; final T defaultValue;
} }
class AppSettingsService { class AppSettingsService {
late final Box hiveBox; T getSetting<T>(AppSettingsEnum<T> setting) {
return Store.get(setting.storeKey, setting.defaultValue);
AppSettingsService() {
hiveBox = Hive.box(userSettingInfoBox);
} }
T getSetting<T>(AppSettingsEnum<T> settingType) { void setSetting<T>(AppSettingsEnum<T> setting, T value) {
if (!hiveBox.containsKey(settingType.hiveKey)) { Store.put(setting.storeKey, value);
return _setDefault(settingType);
}
var result = hiveBox.get(settingType.hiveKey);
if (result is! T) {
return _setDefault(settingType);
}
return result;
}
setSetting<T>(AppSettingsEnum<T> settingType, T value) {
hiveBox.put(settingType.hiveKey, value);
}
T _setDefault<T>(AppSettingsEnum<T> settingType) {
hiveBox.put(settingType.hiveKey, settingType.defaultValue);
return settingType.defaultValue;
} }
} }

View File

@ -13,7 +13,6 @@ class AuthGuard extends AutoRouteGuard {
void onNavigation(NavigationResolver resolver, StackRouter router) async { void onNavigation(NavigationResolver resolver, StackRouter router) async {
try { try {
var res = await _apiService.authenticationApi.validateAccessToken(); var res = await _apiService.authenticationApi.validateAccessToken();
if (res != null && res.authStatus) { if (res != null && res.authStatus) {
resolver.next(true); resolver.next(true);
} else { } else {

View File

@ -1,6 +1,5 @@
import 'package:immich_mobile/shared/models/exif_info.dart'; import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/utils/hash.dart'; import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -40,7 +39,7 @@ class Asset {
width = local.width, width = local.width,
fileName = local.title!, fileName = local.title!,
deviceId = Store.get(StoreKey.deviceIdHash), deviceId = Store.get(StoreKey.deviceIdHash),
ownerId = Store.get<User>(StoreKey.currentUser)!.isarId, ownerId = Store.get(StoreKey.currentUser).isarId,
fileModifiedAt = local.modifiedDateTime.toUtc(), fileModifiedAt = local.modifiedDateTime.toUtc(),
updatedAt = local.modifiedDateTime.toUtc(), updatedAt = local.modifiedDateTime.toUtc(),
isFavorite = local.isFavorite, isFavorite = local.isFavorite,

View File

@ -0,0 +1,48 @@
// ignore_for_file: constant_identifier_names
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
part 'logger_message.model.g.dart';
@Collection(inheritance: false)
class LoggerMessage {
Id id = Isar.autoIncrement;
String message;
@Enumerated(EnumType.ordinal)
LogLevel level = LogLevel.INFO;
DateTime createdAt;
String? context1;
String? context2;
LoggerMessage({
required this.message,
required this.level,
required this.createdAt,
required this.context1,
required this.context2,
});
@override
String toString() {
return 'InAppLoggerMessage(message: $message, level: $level, createdAt: $createdAt)';
}
}
/// Log levels according to dart logging [Level]
enum LogLevel {
ALL,
FINEST,
FINER,
FINE,
CONFIG,
INFO,
WARNING,
SEVERE,
SHOUT,
OFF,
}
extension LevelExtension on Level {
LogLevel toLogLevel() => LogLevel.values[Level.LEVELS.indexOf(this)];
}

Binary file not shown.

View File

@ -1,7 +1,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'dart:convert';
part 'store.g.dart'; part 'store.g.dart';
@ -26,12 +25,21 @@ class Store {
return _db.writeTxn(() => _db.storeValues.clear()); return _db.writeTxn(() => _db.storeValues.clear());
} }
/// Returns the stored value for the given key, or the default value if null /// Returns the stored value for the given key or if null the [defaultValue]
static T? get<T>(StoreKey key, [T? defaultValue]) => /// Throws a [StoreKeyNotFoundException] if both are null
_cache[key.id] ?? defaultValue; static T get<T>(StoreKey<T> key, [T? defaultValue]) {
final value = _cache[key.id] ?? defaultValue;
if (value == null) {
throw StoreKeyNotFoundException(key);
}
return value;
}
/// Returns the stored value for the given key (possibly null)
static T? tryGet<T>(StoreKey<T> key) => _cache[key.id];
/// Stores the value synchronously in the cache and asynchronously in the DB /// Stores the value synchronously in the cache and asynchronously in the DB
static Future<void> put<T>(StoreKey key, T value) { static Future<void> put<T>(StoreKey<T> key, T value) {
_cache[key.id] = value; _cache[key.id] = value;
return _db.writeTxn( return _db.writeTxn(
() async => _db.storeValues.put(await StoreValue._of(value, key)), () async => _db.storeValues.put(await StoreValue._of(value, key)),
@ -39,7 +47,7 @@ class Store {
} }
/// Removes the value synchronously from the cache and asynchronously from the DB /// Removes the value synchronously from the cache and asynchronously from the DB
static Future<void> delete(StoreKey key) { static Future<void> delete<T>(StoreKey<T> key) {
_cache[key.id] = null; _cache[key.id] = null;
return _db.writeTxn(() => _db.storeValues.delete(key.id)); return _db.writeTxn(() => _db.storeValues.delete(key.id));
} }
@ -58,7 +66,8 @@ class Store {
static void _onChangeListener(List<StoreValue>? data) { static void _onChangeListener(List<StoreValue>? data) {
if (data != null) { if (data != null) {
for (StoreValue value in data) { for (StoreValue value in data) {
_cache[value.id] = value._extract(StoreKey.values[value.id]); _cache[value.id] =
value._extract(StoreKey.values.firstWhere((e) => e.id == value.id));
} }
} }
} }
@ -72,76 +81,113 @@ class StoreValue {
int? intValue; int? intValue;
String? strValue; String? strValue;
dynamic _extract(StoreKey key) { T? _extract<T>(StoreKey<T> key) {
switch (key.type) { switch (key.type) {
case int: case int:
return key.fromDb == null return intValue as T?;
? intValue
: key.fromDb!.call(Store._db, intValue!);
case bool: case bool:
return intValue == null ? null : intValue! == 1; return intValue == null ? null : (intValue! == 1) as T;
case DateTime: case DateTime:
return intValue == null return intValue == null
? null ? null
: DateTime.fromMicrosecondsSinceEpoch(intValue!); : DateTime.fromMicrosecondsSinceEpoch(intValue!) as T;
case String: case String:
return key.fromJson != null return strValue as T?;
? key.fromJson!.call(json.decode(strValue!)) default:
: strValue; if (key.fromDb != null) {
return key.fromDb!.call(Store._db, intValue!);
}
} }
throw TypeError();
} }
static Future<StoreValue> _of(dynamic value, StoreKey key) async { static Future<StoreValue> _of<T>(T? value, StoreKey<T> key) async {
int? i; int? i;
String? s; String? s;
switch (key.type) { switch (key.type) {
case int: case int:
i = (key.toDb == null ? value : await key.toDb!.call(Store._db, value)); i = value as int?;
break; break;
case bool: case bool:
i = value == null ? null : (value ? 1 : 0); i = value == null ? null : (value == true ? 1 : 0);
break; break;
case DateTime: case DateTime:
i = value == null ? null : (value as DateTime).microsecondsSinceEpoch; i = value == null ? null : (value as DateTime).microsecondsSinceEpoch;
break; break;
case String: case String:
s = key.fromJson == null ? value : json.encode(value.toJson()); s = value as String?;
break; break;
default:
if (key.toDb != null) {
i = await key.toDb!.call(Store._db, value);
break;
}
throw TypeError();
} }
return StoreValue(key.id, intValue: i, strValue: s); return StoreValue(key.id, intValue: i, strValue: s);
} }
} }
class StoreKeyNotFoundException implements Exception {
final StoreKey key;
StoreKeyNotFoundException(this.key);
@override
String toString() => "Key '${key.name}' not found in Store";
}
/// Key for each possible value in the `Store`. /// Key for each possible value in the `Store`.
/// Defines the data type (int, String, JSON) for each value /// Defines the data type for each value
enum StoreKey { enum StoreKey<T> {
userRemoteId(0), userRemoteId<String>(0, type: String),
assetETag(1), assetETag<String>(1, type: String),
currentUser(2, type: int, fromDb: _getUser, toDb: _toUser), currentUser<User>(2, type: User, fromDb: _getUser, toDb: _toUser),
deviceIdHash(3, type: int), deviceIdHash<int>(3, type: int),
deviceId(4), deviceId<String>(4, type: String),
backupFailedSince(5, type: DateTime), backupFailedSince<DateTime>(5, type: DateTime),
backupRequireWifi(6, type: bool), backupRequireWifi<bool>(6, type: bool),
backupRequireCharging(7, type: bool), backupRequireCharging<bool>(7, type: bool),
backupTriggerDelay(8, type: int); backupTriggerDelay<int>(8, type: int),
githubReleaseInfo<String>(9, type: String),
serverUrl<String>(10, type: String),
accessToken<String>(11, type: String),
serverEndpoint<String>(12, type: String),
// user settings from [AppSettingsEnum] below:
loadPreview<bool>(100, type: bool),
loadOriginal<bool>(101, type: bool),
themeMode<String>(102, type: String),
tilesPerRow<int>(103, type: int),
dynamicLayout<bool>(104, type: bool),
groupAssetsBy<int>(105, type: int),
uploadErrorNotificationGracePeriod<int>(106, type: int),
backgroundBackupTotalProgress<bool>(107, type: bool),
backgroundBackupSingleProgress<bool>(108, type: bool),
storageIndicator<bool>(109, type: bool),
thumbnailCacheSize<int>(110, type: int),
imageCacheSize<int>(111, type: int),
albumThumbnailCacheSize<int>(112, type: int),
selectedAlbumSortOrder<int>(113, type: int),
;
const StoreKey( const StoreKey(
this.id, { this.id, {
this.type = String, required this.type,
this.fromDb, this.fromDb,
this.toDb, this.toDb,
// ignore: unused_element
this.fromJson,
}); });
final int id; final int id;
final Type type; final Type type;
final dynamic Function(Isar, int)? fromDb; final T? Function<T>(Isar, int)? fromDb;
final Future<int> Function(Isar, dynamic)? toDb; final Future<int> Function<T>(Isar, T)? toDb;
final Function(dynamic)? fromJson;
} }
User? _getUser(Isar db, int i) => db.users.getSync(i); T? _getUser<T>(Isar db, int i) {
Future<int> _toUser(Isar db, dynamic u) { final User? u = db.users.getSync(i);
User user = (u as User); return u as T?;
return db.users.put(user); }
Future<int> _toUser<T>(Isar db, T u) {
if (u is User) {
return db.users.put(u);
}
throw TypeError();
} }

View File

@ -1,10 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -13,10 +12,10 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
final log = Logger('ReleaseInfoNotifier'); final log = Logger('ReleaseInfoNotifier');
void checkGithubReleaseInfo() async { void checkGithubReleaseInfo() async {
final Client client = Client(); final Client client = Client();
var box = Hive.box(hiveGithubReleaseInfoBox);
try { try {
String? localReleaseVersion = box.get(githubReleaseInfoKey); final String? localReleaseVersion =
Store.tryGet(StoreKey.githubReleaseInfo);
final res = await client.get( final res = await client.get(
Uri.parse( Uri.parse(
"https://api.github.com/repos/immich-app/immich/releases/latest", "https://api.github.com/repos/immich-app/immich/releases/latest",
@ -48,9 +47,7 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
} }
void acknowledgeNewVersion() { void acknowledgeNewVersion() {
var box = Hive.box(hiveGithubReleaseInfoBox); Store.put(StoreKey.githubReleaseInfo, state);
box.put(githubReleaseInfoKey, state);
VersionAnnouncementOverlayController.appLoader.hide(); VersionAnnouncementOverlayController.appLoader.hide();
} }
} }

View File

@ -1,11 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@ -58,9 +57,9 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
var authenticationState = ref.read(authenticationProvider); var authenticationState = ref.read(authenticationProvider);
if (authenticationState.isAuthenticated) { if (authenticationState.isAuthenticated) {
var accessToken = Hive.box(userInfoBox).get(accessTokenKey); final accessToken = Store.get(StoreKey.accessToken);
try { try {
var endpoint = Uri.parse(Hive.box(userInfoBox).get(serverEndpointKey)); final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
debugPrint("Attempting to connect to websocket"); debugPrint("Attempting to connect to websocket");
// Configure socket transports must be specified // Configure socket transports must be specified

View File

@ -1,8 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
@ -19,13 +18,9 @@ class ApiService {
late DeviceInfoApi deviceInfoApi; late DeviceInfoApi deviceInfoApi;
ApiService() { ApiService() {
if (Hive.isBoxOpen(userInfoBox)) { final endpoint = Store.tryGet(StoreKey.serverEndpoint);
final endpoint = Hive.box(userInfoBox).get(serverEndpointKey) as String?; if (endpoint != null && endpoint.isNotEmpty) {
if (endpoint != null && endpoint.isNotEmpty) { setEndpoint(endpoint);
setEndpoint(endpoint);
}
} else {
debugPrint("Cannot init ApiServer endpoint, userInfoBox not open yet.");
} }
} }
String? _authToken; String? _authToken;
@ -49,7 +44,7 @@ class ApiService {
setEndpoint(endpoint); setEndpoint(endpoint);
// Save in hivebox for next startup // Save in hivebox for next startup
Hive.box(userInfoBox).put(serverEndpointKey, endpoint); Store.put(StoreKey.serverEndpoint, endpoint);
return endpoint; return endpoint;
} }

View File

@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/exif_info.dart'; import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/shared/services/api.service.dart';
@ -44,7 +43,7 @@ class AssetService {
.where() .where()
.remoteIdIsNotNull() .remoteIdIsNotNull()
.filter() .filter()
.ownerIdEqualTo(Store.get<User>(StoreKey.currentUser)!.isarId) .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.count(); .count();
final List<AssetResponseDto>? dtos = final List<AssetResponseDto>? dtos =
await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0); await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0);
@ -63,7 +62,7 @@ class AssetService {
required bool hasCache, required bool hasCache,
}) async { }) async {
try { try {
final etag = hasCache ? Store.get(StoreKey.assetETag) : null; final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null;
final Pair<List<AssetResponseDto>, String?>? remote = final Pair<List<AssetResponseDto>, String?>? remote =
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag); await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
if (remote == null) { if (remote == null) {

View File

@ -1,15 +1,15 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive/hive.dart'; import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:isar/isar.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
/// [ImmichLogger] is a custom logger that is built on top of the [logging] package. /// [ImmichLogger] is a custom logger that is built on top of the [logging] package.
/// The logs are written to a Hive box and onto console, using `debugPrint` method. /// The logs are written to the database and onto console, using `debugPrint` method.
/// ///
/// The logs are deleted when exceeding the `maxLogEntries` (default 200) property /// The logs are deleted when exceeding the `maxLogEntries` (default 200) property
/// in the class. /// in the class.
@ -17,48 +17,61 @@ import 'package:share_plus/share_plus.dart';
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog /// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
/// and generate a csv file. /// and generate a csv file.
class ImmichLogger { class ImmichLogger {
static final ImmichLogger _instance = ImmichLogger._internal();
final maxLogEntries = 200; final maxLogEntries = 200;
final Box<ImmichLoggerMessage> _box = Hive.box(immichLoggerBox); final Isar _db = Isar.getInstance()!;
final List<LoggerMessage> _msgBuffer = [];
Timer? _timer;
List<ImmichLoggerMessage> get messages => factory ImmichLogger() => _instance;
_box.values.toList().reversed.toList();
ImmichLogger() { ImmichLogger._internal() {
_removeOverflowMessages(); _removeOverflowMessages();
}
init() {
Logger.root.level = Level.INFO; Logger.root.level = Level.INFO;
Logger.root.onRecord.listen(_writeLogToHiveBox); Logger.root.onRecord.listen(_writeLogToDatabase);
} }
_removeOverflowMessages() { List<LoggerMessage> get messages {
if (_box.length > maxLogEntries) { final inDb =
var numberOfEntryToBeDeleted = _box.length - maxLogEntries; _db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync();
for (var i = 0; i < numberOfEntryToBeDeleted; i++) { return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb;
_box.deleteAt(0); }
}
void _removeOverflowMessages() {
final msgCount = _db.loggerMessages.countSync();
if (msgCount > maxLogEntries) {
final numberOfEntryToBeDeleted = msgCount - maxLogEntries;
_db.loggerMessages.where().limit(numberOfEntryToBeDeleted).deleteAll();
} }
} }
_writeLogToHiveBox(LogRecord record) { void _writeLogToDatabase(LogRecord record) {
final Box<ImmichLoggerMessage> box = Hive.box(immichLoggerBox);
var formattedMessage = record.message;
debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
box.add( final lm = LoggerMessage(
ImmichLoggerMessage( message: record.message,
message: formattedMessage, level: record.level.toLogLevel(),
level: record.level.name, createdAt: record.time,
createdAt: record.time, context1: record.loggerName,
context1: record.loggerName, context2: record.stackTrace?.toString(),
context2: record.stackTrace?.toString(),
),
); );
_msgBuffer.add(lm);
// delayed batch writing to database: increases performance when logging
// messages in quick succession and reduces NAND wear
_timer ??= Timer(const Duration(seconds: 5), _flushBufferToDatabase);
}
void _flushBufferToDatabase() {
_timer = null;
_db.writeTxnSync(() => _db.loggerMessages.putAllSync(_msgBuffer));
_msgBuffer.clear();
} }
void clearLogs() { void clearLogs() {
_box.clear(); _timer?.cancel();
_timer = null;
_msgBuffer.clear();
_db.writeTxn(() => _db.loggerMessages.clear());
} }
Future<void> shareLogs() async { Future<void> shareLogs() async {
@ -93,4 +106,12 @@ class ImmichLogger {
// Clean up temp file // Clean up temp file
await logFile.delete(); await logFile.delete();
} }
/// Flush pending log messages to persistent storage
void flush() {
if (_timer != null) {
_timer!.cancel();
_flushBufferToDatabase();
}
}
} }

View File

@ -241,7 +241,7 @@ class SyncService {
} }
if (album.shared || dto.shared) { if (album.shared || dto.shared) {
final userId = Store.get<User>(StoreKey.currentUser)!.isarId; final userId = Store.get(StoreKey.currentUser).isarId;
final foreign = final foreign =
await album.assets.filter().not().ownerIdEqualTo(userId).findAll(); await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
existing.addAll(foreign); existing.addAll(foreign);

View File

@ -42,7 +42,7 @@ class UserService {
if (self) { if (self) {
return _db.users.where().findAll(); return _db.users.where().findAll();
} }
final int userId = Store.get<User>(StoreKey.currentUser)!.isarId; final int userId = Store.get(StoreKey.currentUser).isarId;
return _db.users.where().isarIdNotEqualTo(userId).findAll(); return _db.users.where().isarIdNotEqualTo(userId).findAll();
} }

View File

@ -1,9 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -84,7 +83,7 @@ class ImmichImage extends StatelessWidget {
}, },
); );
} }
final String? token = Hive.box(userInfoBox).get(accessTokenKey); final String? token = Store.get(StoreKey.accessToken);
final String thumbnailRequestUrl = getThumbnailUrl(asset); final String thumbnailRequestUrl = getThumbnailUrl(asset);
return CachedNetworkImage( return CachedNetworkImage(
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,

View File

@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.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/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/services/immich_logger.service.dart'; import 'package:immich_mobile/shared/services/immich_logger.service.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -31,29 +32,29 @@ class AppLogPage extends HookConsumerWidget {
); );
} }
Widget buildLeadingIcon(String level) { Widget buildLeadingIcon(LogLevel level) {
switch (level) { switch (level) {
case "INFO": case LogLevel.INFO:
return colorStatusIndicator(Theme.of(context).primaryColor); return colorStatusIndicator(Theme.of(context).primaryColor);
case "SEVERE": case LogLevel.SEVERE:
return colorStatusIndicator(Colors.redAccent); return colorStatusIndicator(Colors.redAccent);
case "WARNING": case LogLevel.WARNING:
return colorStatusIndicator(Colors.orangeAccent); return colorStatusIndicator(Colors.orangeAccent);
default: default:
return colorStatusIndicator(Colors.grey); return colorStatusIndicator(Colors.grey);
} }
} }
getTileColor(String level) { getTileColor(LogLevel level) {
switch (level) { switch (level) {
case "INFO": case LogLevel.INFO:
return Colors.transparent; return Colors.transparent;
case "SEVERE": case LogLevel.SEVERE:
return Theme.of(context).brightness == Brightness.dark return Theme.of(context).brightness == Brightness.dark
? Colors.redAccent.withOpacity(0.25) ? Colors.redAccent.withOpacity(0.25)
: Colors.redAccent.withOpacity(0.075); : Colors.redAccent.withOpacity(0.075);
case "WARNING": case LogLevel.WARNING:
return Theme.of(context).brightness == Brightness.dark return Theme.of(context).brightness == Brightness.dark
? Colors.orangeAccent.withOpacity(0.25) ? Colors.orangeAccent.withOpacity(0.25)
: Colors.orangeAccent.withOpacity(0.075); : Colors.orangeAccent.withOpacity(0.075);

View File

@ -1,14 +1,12 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.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' hide Store;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart';
class SplashScreenPage extends HookConsumerWidget { class SplashScreenPage extends HookConsumerWidget {
@ -17,23 +15,23 @@ class SplashScreenPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final apiService = ref.watch(apiServiceProvider); final apiService = ref.watch(apiServiceProvider);
HiveSavedLoginInfo? loginInfo = final serverUrl = Store.tryGet(StoreKey.serverUrl);
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey); final accessToken = Store.tryGet(StoreKey.accessToken);
void performLoggingIn() async { void performLoggingIn() async {
bool isSuccess = false; bool isSuccess = false;
if (loginInfo != null) { if (accessToken != null && serverUrl != null) {
try { try {
// Resolve API server endpoint from user provided serverUrl // Resolve API server endpoint from user provided serverUrl
await apiService.resolveAndSetEndpoint(loginInfo.serverUrl); await apiService.resolveAndSetEndpoint(serverUrl);
} catch (e) { } catch (e) {
// okay, try to continue anyway if offline // okay, try to continue anyway if offline
} }
isSuccess = isSuccess =
await ref.read(authenticationProvider.notifier).setSuccessLoginInfo( await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
accessToken: loginInfo.accessToken, accessToken: accessToken,
serverUrl: loginInfo.serverUrl, serverUrl: serverUrl,
); );
} }
if (isSuccess) { if (isSuccess) {
@ -51,7 +49,7 @@ class SplashScreenPage extends HookConsumerWidget {
useEffect( useEffect(
() { () {
if (loginInfo != null) { if (serverUrl != null && accessToken != null) {
performLoggingIn(); performLoggingIn();
} else { } else {
AutoRouter.of(context).replace(const LoginRoute()); AutoRouter.of(context).replace(const LoginRoute());

View File

@ -1,10 +1,8 @@
import 'package:hive/hive.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import '../constants/hive_box.dart';
String getThumbnailUrl( String getThumbnailUrl(
final Asset asset, { final Asset asset, {
ThumbnailFormat type = ThumbnailFormat.WEBP, ThumbnailFormat type = ThumbnailFormat.WEBP,
@ -48,8 +46,7 @@ String getAlbumThumbNailCacheKey(
} }
String getImageUrl(final Asset asset) { String getImageUrl(final Asset asset) {
final box = Hive.box(userInfoBox); return '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}?isThumb=false';
return '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}?isThumb=false';
} }
String getImageCacheKey(final Asset asset) { String getImageCacheKey(final Asset asset) {
@ -60,7 +57,5 @@ String _getThumbnailUrl(
final String id, { final String id, {
ThumbnailFormat type = ThumbnailFormat.WEBP, ThumbnailFormat type = ThumbnailFormat.WEBP,
}) { }) {
final box = Hive.box(userInfoBox); return '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$id?format=${type.value}';
return '${box.get(serverEndpointKey)}/asset/thumbnail/$id?format=${type.value}';
} }

View File

@ -1,5 +1,7 @@
// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
@ -8,6 +10,9 @@ import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/services/asset_cache.service.dart'; import 'package:immich_mobile/shared/services/asset_cache.service.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
@ -23,11 +28,37 @@ Future<void> migrateHiveToStoreIfNecessary() async {
duplicatedAssetsBox, duplicatedAssetsBox,
_migrateDuplicatedAssetsBox, _migrateDuplicatedAssetsBox,
); );
await _migrateHiveBoxIfNecessary(
hiveGithubReleaseInfoBox,
_migrateReleaseInfoBox,
);
await _migrateHiveBoxIfNecessary(hiveLoginInfoBox, _migrateLoginInfoBox);
await _migrateHiveBoxIfNecessary(
immichLoggerBox,
(Box<ImmichLoggerMessage> box) => box.deleteFromDisk(),
);
await _migrateHiveBoxIfNecessary(userSettingInfoBox, _migrateAppSettingsBox);
}
FutureOr<void> _migrateReleaseInfoBox(Box box) =>
_migrateKey(box, githubReleaseInfoKey, StoreKey.githubReleaseInfo);
Future<void> _migrateLoginInfoBox(Box<HiveSavedLoginInfo> box) async {
final HiveSavedLoginInfo? info = box.get(savedLoginInfoKey);
if (info != null) {
await Store.put(StoreKey.serverUrl, info.serverUrl);
await Store.put(StoreKey.accessToken, info.accessToken);
}
} }
Future<void> _migrateHiveUserInfoBox(Box box) async { Future<void> _migrateHiveUserInfoBox(Box box) async {
await _migrateKey(box, userIdKey, StoreKey.userRemoteId); await _migrateKey(box, userIdKey, StoreKey.userRemoteId);
await _migrateKey(box, assetEtagKey, StoreKey.assetETag); await _migrateKey(box, assetEtagKey, StoreKey.assetETag);
if (Store.tryGet(StoreKey.deviceId) == null) {
await _migrateKey(box, deviceIdKey, StoreKey.deviceId);
}
await _migrateKey(box, serverEndpointKey, StoreKey.serverEndpoint);
} }
Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async { Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async {
@ -35,16 +66,15 @@ Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async {
await _migrateKey(box, backupRequireWifi, StoreKey.backupRequireWifi); await _migrateKey(box, backupRequireWifi, StoreKey.backupRequireWifi);
await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging); await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging);
await _migrateKey(box, backupTriggerDelay, StoreKey.backupTriggerDelay); await _migrateKey(box, backupTriggerDelay, StoreKey.backupTriggerDelay);
return box.deleteFromDisk();
} }
Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async { FutureOr<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
final HiveBackupAlbums? infos = box.get(backupInfoKey); final HiveBackupAlbums? infos = box.get(backupInfoKey);
if (infos != null) { if (infos != null) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
List<BackupAlbum> albums = []; List<BackupAlbum> albums = [];
for (int i = 0; i < infos.selectedAlbumIds.length; i++) { for (int i = 0; i < infos.selectedAlbumIds.length; i++) {
final album = BackupAlbum( final album = BackupAlbum(
@ -62,48 +92,49 @@ Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async {
); );
albums.add(album); albums.add(album);
} }
await db.writeTxn(() => db.backupAlbums.putAll(albums)); return db.writeTxn(() => db.backupAlbums.putAll(albums));
} else {
debugPrint("_migrateBackupInfoBox deletes empty box");
} }
return box.deleteFromDisk();
} }
Future<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) async { FutureOr<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
final HiveDuplicatedAssets? duplicatedAssets = box.get(duplicatedAssetsKey); final HiveDuplicatedAssets? duplicatedAssets = box.get(duplicatedAssetsKey);
if (duplicatedAssets != null) { if (duplicatedAssets != null) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds
.map((id) => DuplicatedAsset(id)) .map((id) => DuplicatedAsset(id))
.toList(); .toList();
await db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds)); return db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds));
} else { }
debugPrint("_migrateDuplicatedAssetsBox deletes empty box"); }
Future<void> _migrateAppSettingsBox(Box box) async {
for (AppSettingsEnum s in AppSettingsEnum.values) {
await _migrateKey(box, s.hiveKey, s.storeKey);
} }
return box.deleteFromDisk();
} }
Future<void> _migrateHiveBoxIfNecessary<T>( Future<void> _migrateHiveBoxIfNecessary<T>(
String boxName, String boxName,
Future<void> Function(Box<T>) migrate, FutureOr<void> Function(Box<T>) migrate,
) async { ) async {
try { try {
if (await Hive.boxExists(boxName)) { if (await Hive.boxExists(boxName)) {
await migrate(await Hive.openBox<T>(boxName)); final box = await Hive.openBox<T>(boxName);
await migrate(box);
await box.deleteFromDisk();
} }
} catch (e) { } catch (e) {
debugPrint("Error while migrating $boxName $e"); debugPrint("Error while migrating $boxName $e");
} }
} }
_migrateKey(Box box, String hiveKey, StoreKey key) async { FutureOr<void> _migrateKey<T>(Box box, String hiveKey, StoreKey<T> key) {
final String? value = box.get(hiveKey); final T? value = box.get(hiveKey);
if (value != null) { if (value != null) {
await Store.put(key, value); return Store.put(key, value);
await box.delete(hiveKey);
} }
} }