1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00: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:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:integration_test/integration_test.dart';
import 'package:isar/isar.dart';
@ -35,9 +34,7 @@ class ImmichTestHelper {
}
static Future<void> loadApp(WidgetTester tester) async {
// Clear all data from Hive
await Hive.deleteFromDisk();
await app.openBoxes();
await EasyLocalization.ensureInitialized();
// Clear all data from Isar (reuse existing instance if available)
final db = Isar.getInstance() ?? await app.loadDb();
await Store.clear();
@ -65,12 +62,13 @@ void immichWidgetTest(
}
Future<void> pumpUntilFound(
WidgetTester tester,
Finder finder, {
Duration timeout = const Duration(seconds: 120),
}) async {
WidgetTester tester,
Finder finder, {
Duration timeout = const Duration(seconds: 120),
}) async {
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) {
await tester.pump();
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/exif_info.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/user.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:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'constants/hive_box.dart';
void main() async {
await initApp();
WidgetsFlutterBinding.ensureInitialized();
final db = await loadDb();
await initApp();
await migrateHiveToStoreIfNecessary();
await migrateJsonCacheIfNecessary();
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 {
await Hive.initFlutter();
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
Hive.registerAdapter(ImmichLoggerMessageAdapter());
await openBoxes();
await EasyLocalization.ensureInitialized();
if (kReleaseMode && Platform.isAndroid) {
try {
@ -82,7 +71,7 @@ Future<void> initApp() async {
}
// Initialize Immich Logger Service
ImmichLogger().init();
ImmichLogger();
var log = Logger("ImmichErrorLogger");
@ -108,6 +97,7 @@ Future<Isar> loadDb() async {
UserSchema,
BackupAlbumSchema,
DuplicatedAssetSchema,
LoggerMessageSchema,
],
directory: dir.path,
maxSizeMiB: 256,
@ -174,6 +164,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
case AppLifecycleState.inactive:
debugPrint("[APP STATE] inactive");
ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
ImmichLogger().flush();
ref.watch(websocketProvider.notifier).disconnect();
ref.watch(backupProvider.notifier).cancelBackup();

View file

@ -265,7 +265,7 @@ class AlbumService {
Future<bool> deleteAlbum(Album album) async {
try {
final userId = Store.get<User>(StoreKey.currentUser)!.isarId;
final userId = Store.get(StoreKey.currentUser).isarId;
if (album.owner.value?.isarId == userId) {
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:easy_localization/easy_localization.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/shared/models/album.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
@ -21,7 +20,6 @@ class AlbumThumbnailListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
var cardSize = 68.0;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -50,7 +48,9 @@ class AlbumThumbnailListTile extends StatelessWidget {
album,
type: ThumbnailFormat.JPEG,
),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
httpHeaders: {
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
},
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
errorWidget: (context, url, error) =>
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:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
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/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/top_control_app_bar.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/shared/models/store.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/settings/providers/app_settings.provider.dart';
@ -47,7 +46,6 @@ class GalleryViewerPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox);
final settings = ref.watch(appSettingsServiceProvider);
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
@ -57,7 +55,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final isPlayingMotionVideo = useState(false);
final isPlayingVideo = useState(false);
late Offset localPosition;
final authToken = 'Bearer ${box.get(accessTokenKey)}';
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
showAppBar.addListener(() {
// Change to and from immersive mode, hiding navigation and app bar

View file

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

View file

@ -8,16 +8,13 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.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/modules/backup/background_service/localization.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/error_upload_asset.model.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/shared/models/store.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
@ -317,7 +314,6 @@ class BackgroundService {
debugPrint(error.toString());
return false;
} finally {
await Hive.close();
releaseLock();
}
case "systemStop":
@ -332,17 +328,9 @@ class BackgroundService {
Future<bool> _onAssetsChanged() async {
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.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
apiService.setAccessToken(Store.get(StoreKey.accessToken));
BackupService backupService = BackupService(apiService, db);
AppSettingsService settingsService = AppSettingsService();
@ -387,7 +375,7 @@ class BackgroundService {
db.backupAlbums.deleteAllSync(toDelete);
db.backupAlbums.putAllSync(toUpsert);
});
} else if (Store.get(StoreKey.backupFailedSince) == null) {
} else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
Store.put(StoreKey.backupFailedSince, DateTime.now());
return false;
}
@ -529,7 +517,7 @@ class BackgroundService {
} else if (value == 5) {
return false;
}
final DateTime? failedSince = Store.get(StoreKey.backupFailedSince);
final DateTime? failedSince = Store.tryGet(StoreKey.backupFailedSince);
if (failedSince == null) {
return false;
}

View file

@ -1,9 +1,7 @@
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.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/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
@ -42,9 +40,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInPercentage: 0,
cancelToken: CancellationToken(),
backgroundBackup: false,
backupRequireWifi: true,
backupRequireCharging: false,
backupTriggerDelay: 5000,
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
backupRequireCharging:
Store.get(StoreKey.backupRequireCharging, false),
backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay, 5000),
serverInfo: ServerInfoResponseDto(
diskAvailable: "0",
diskAvailableRaw: 0,
@ -163,14 +162,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
triggerMaxDelay: state.backupTriggerDelay * 10,
);
if (success) {
await Future.wait([
Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi),
Store.put(
StoreKey.backupRequireCharging,
state.backupRequireCharging,
),
Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay),
]);
await Store.put(StoreKey.backupRequireWifi, state.backupRequireWifi);
await Store.put(
StoreKey.backupRequireCharging,
state.backupRequireCharging,
);
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
} else {
state = state.copyWith(
backgroundBackup: wasEnabled,
@ -544,7 +541,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Future<void> _resumeBackup() async {
// Check if user is login
final accessKey = Hive.box(userInfoBox).get(accessTokenKey);
final accessKey = Store.tryGet(StoreKey.accessToken);
// User has been logged out return
if (accessKey == null || !_authState.isAuthenticated) {
@ -603,9 +600,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
backupProgress: BackUpProgressEnum.inBackground,
selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums,
backupRequireWifi: Store.get(StoreKey.backupRequireWifi),
backupRequireCharging: Store.get(StoreKey.backupRequireCharging),
backupTriggerDelay: Store.get(StoreKey.backupTriggerDelay),
);
// assumes the background service is currently running
// 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:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.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/current_upload_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/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
@ -38,7 +37,7 @@ class BackupService {
BackupService(this._apiService, this._db);
Future<List<String>?> getDeviceBackupAsset() async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
final String deviceId = Store.get(StoreKey.deviceId);
try {
return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
@ -173,7 +172,7 @@ class BackupService {
}
final Set<String> existing = {};
try {
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
final String deviceId = Store.get(StoreKey.deviceId);
final CheckExistingAssetsResponseDto? duplicates =
await _apiService.assetApi.checkExistingAssets(
CheckExistingAssetsDto(
@ -204,8 +203,8 @@ class BackupService {
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
Function(ErrorUploadAsset) errorCb,
) async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
final String deviceId = Store.get(StoreKey.deviceId);
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
File? file;
bool anyErrors = false;
final List<String> duplicatedAssetIds = [];
@ -236,15 +235,14 @@ class BackupService {
),
);
var box = Hive.box(userInfoBox);
var req = MultipartRequest(
'POST',
Uri.parse('$savedEndpoint/asset/upload'),
onProgress: ((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['deviceId'] = deviceId;

View file

@ -2,9 +2,7 @@ import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.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/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/shared/models/server_info_state.model.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/ui/transparent_image.dart';
@ -47,7 +46,7 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
},
);
} else {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
final String? endpoint = Store.get(StoreKey.serverEndpoint);
var dummy = Random().nextInt(1024);
return InkWell(
onTap: () {

View file

@ -1,14 +1,13 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.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/login/models/authentication_state.model.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/transparent_image.dart';
@ -19,7 +18,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
@override
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);
final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status;

View file

@ -2,12 +2,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.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/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/shared/models/user.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
@ -91,11 +88,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
try {
await Future.wait([
_apiService.authenticationApi.logout(),
Hive.box(userInfoBox).delete(accessTokenKey),
Store.delete(StoreKey.assetETag),
Store.delete(StoreKey.userRemoteId),
Store.delete(StoreKey.currentUser),
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey)
Store.delete(StoreKey.accessToken),
]);
state = state.copyWith(isAuthenticated: false);
@ -157,14 +153,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
}
if (userResponseDto != null) {
var userInfoHiveBox = await Hive.openBox(userInfoBox);
var deviceInfo = await _deviceInfoService.getDeviceInfo();
userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]);
userInfoHiveBox.put(accessTokenKey, accessToken);
Store.put(StoreKey.deviceId, deviceInfo["deviceId"]);
Store.put(StoreKey.deviceIdHash, fastHash(deviceInfo["deviceId"]));
Store.put(StoreKey.userRemoteId, userResponseDto.id);
Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken);
state = state.copyWith(
isAuthenticated: true,
@ -178,17 +173,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
deviceId: deviceInfo["deviceId"],
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import 'package:immich_mobile/shared/models/exif_info.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:isar/isar.dart';
import 'package:openapi/api.dart';
@ -40,7 +39,7 @@ class Asset {
width = local.width,
fileName = local.title!,
deviceId = Store.get(StoreKey.deviceIdHash),
ownerId = Store.get<User>(StoreKey.currentUser)!.isarId,
ownerId = Store.get(StoreKey.currentUser).isarId,
fileModifiedAt = local.modifiedDateTime.toUtc(),
updatedAt = local.modifiedDateTime.toUtc(),
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:immich_mobile/shared/models/user.dart';
import 'package:isar/isar.dart';
import 'dart:convert';
part 'store.g.dart';
@ -26,12 +25,21 @@ class Store {
return _db.writeTxn(() => _db.storeValues.clear());
}
/// Returns the stored value for the given key, or the default value if null
static T? get<T>(StoreKey key, [T? defaultValue]) =>
_cache[key.id] ?? defaultValue;
/// Returns the stored value for the given key or if null the [defaultValue]
/// Throws a [StoreKeyNotFoundException] if both are null
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
static Future<void> put<T>(StoreKey key, T value) {
static Future<void> put<T>(StoreKey<T> key, T value) {
_cache[key.id] = value;
return _db.writeTxn(
() 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
static Future<void> delete(StoreKey key) {
static Future<void> delete<T>(StoreKey<T> key) {
_cache[key.id] = null;
return _db.writeTxn(() => _db.storeValues.delete(key.id));
}
@ -58,7 +66,8 @@ class Store {
static void _onChangeListener(List<StoreValue>? data) {
if (data != null) {
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;
String? strValue;
dynamic _extract(StoreKey key) {
T? _extract<T>(StoreKey<T> key) {
switch (key.type) {
case int:
return key.fromDb == null
? intValue
: key.fromDb!.call(Store._db, intValue!);
return intValue as T?;
case bool:
return intValue == null ? null : intValue! == 1;
return intValue == null ? null : (intValue! == 1) as T;
case DateTime:
return intValue == null
? null
: DateTime.fromMicrosecondsSinceEpoch(intValue!);
: DateTime.fromMicrosecondsSinceEpoch(intValue!) as T;
case String:
return key.fromJson != null
? key.fromJson!.call(json.decode(strValue!))
: strValue;
return strValue as T?;
default:
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;
String? s;
switch (key.type) {
case int:
i = (key.toDb == null ? value : await key.toDb!.call(Store._db, value));
i = value as int?;
break;
case bool:
i = value == null ? null : (value ? 1 : 0);
i = value == null ? null : (value == true ? 1 : 0);
break;
case DateTime:
i = value == null ? null : (value as DateTime).microsecondsSinceEpoch;
break;
case String:
s = key.fromJson == null ? value : json.encode(value.toJson());
s = value as String?;
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);
}
}
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`.
/// Defines the data type (int, String, JSON) for each value
enum StoreKey {
userRemoteId(0),
assetETag(1),
currentUser(2, type: int, fromDb: _getUser, toDb: _toUser),
deviceIdHash(3, type: int),
deviceId(4),
backupFailedSince(5, type: DateTime),
backupRequireWifi(6, type: bool),
backupRequireCharging(7, type: bool),
backupTriggerDelay(8, type: int);
/// Defines the data type for each value
enum StoreKey<T> {
userRemoteId<String>(0, type: String),
assetETag<String>(1, type: String),
currentUser<User>(2, type: User, fromDb: _getUser, toDb: _toUser),
deviceIdHash<int>(3, type: int),
deviceId<String>(4, type: String),
backupFailedSince<DateTime>(5, type: DateTime),
backupRequireWifi<bool>(6, type: bool),
backupRequireCharging<bool>(7, type: bool),
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(
this.id, {
this.type = String,
required this.type,
this.fromDb,
this.toDb,
// ignore: unused_element
this.fromJson,
});
final int id;
final Type type;
final dynamic Function(Isar, int)? fromDb;
final Future<int> Function(Isar, dynamic)? toDb;
final Function(dynamic)? fromJson;
final T? Function<T>(Isar, int)? fromDb;
final Future<int> Function<T>(Isar, T)? toDb;
}
User? _getUser(Isar db, int i) => db.users.getSync(i);
Future<int> _toUser(Isar db, dynamic u) {
User user = (u as User);
return db.users.put(user);
T? _getUser<T>(Isar db, int i) {
final User? u = db.users.getSync(i);
return u as T?;
}
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 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:logging/logging.dart';
@ -13,10 +12,10 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
final log = Logger('ReleaseInfoNotifier');
void checkGithubReleaseInfo() async {
final Client client = Client();
var box = Hive.box(hiveGithubReleaseInfoBox);
try {
String? localReleaseVersion = box.get(githubReleaseInfoKey);
final String? localReleaseVersion =
Store.tryGet(StoreKey.githubReleaseInfo);
final res = await client.get(
Uri.parse(
"https://api.github.com/repos/immich-app/immich/releases/latest",
@ -48,9 +47,7 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
}
void acknowledgeNewVersion() {
var box = Hive.box(hiveGithubReleaseInfoBox);
box.put(githubReleaseInfoKey, state);
Store.put(StoreKey.githubReleaseInfo, state);
VersionAnnouncementOverlayController.appLoader.hide();
}
}

View file

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

View file

@ -1,8 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:openapi/api.dart';
import 'package:http/http.dart';
@ -19,13 +18,9 @@ class ApiService {
late DeviceInfoApi deviceInfoApi;
ApiService() {
if (Hive.isBoxOpen(userInfoBox)) {
final endpoint = Hive.box(userInfoBox).get(serverEndpointKey) as String?;
if (endpoint != null && endpoint.isNotEmpty) {
setEndpoint(endpoint);
}
} else {
debugPrint("Cannot init ApiServer endpoint, userInfoBox not open yet.");
final endpoint = Store.tryGet(StoreKey.serverEndpoint);
if (endpoint != null && endpoint.isNotEmpty) {
setEndpoint(endpoint);
}
}
String? _authToken;
@ -49,7 +44,7 @@ class ApiService {
setEndpoint(endpoint);
// Save in hivebox for next startup
Hive.box(userInfoBox).put(serverEndpointKey, endpoint);
Store.put(StoreKey.serverEndpoint, 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/exif_info.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/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
@ -44,7 +43,7 @@ class AssetService {
.where()
.remoteIdIsNotNull()
.filter()
.ownerIdEqualTo(Store.get<User>(StoreKey.currentUser)!.isarId)
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.count();
final List<AssetResponseDto>? dtos =
await _getRemoteAssets(hasCache: numOwnedRemoteAssets > 0);
@ -63,7 +62,7 @@ class AssetService {
required bool hasCache,
}) async {
try {
final etag = hasCache ? Store.get(StoreKey.assetETag) : null;
final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null;
final Pair<List<AssetResponseDto>, String?>? remote =
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
if (remote == null) {

View file

@ -1,15 +1,15 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
/// [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
/// 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
/// and generate a csv file.
class ImmichLogger {
static final ImmichLogger _instance = ImmichLogger._internal();
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 =>
_box.values.toList().reversed.toList();
factory ImmichLogger() => _instance;
ImmichLogger() {
ImmichLogger._internal() {
_removeOverflowMessages();
}
init() {
Logger.root.level = Level.INFO;
Logger.root.onRecord.listen(_writeLogToHiveBox);
Logger.root.onRecord.listen(_writeLogToDatabase);
}
_removeOverflowMessages() {
if (_box.length > maxLogEntries) {
var numberOfEntryToBeDeleted = _box.length - maxLogEntries;
for (var i = 0; i < numberOfEntryToBeDeleted; i++) {
_box.deleteAt(0);
}
List<LoggerMessage> get messages {
final inDb =
_db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync();
return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb;
}
void _removeOverflowMessages() {
final msgCount = _db.loggerMessages.countSync();
if (msgCount > maxLogEntries) {
final numberOfEntryToBeDeleted = msgCount - maxLogEntries;
_db.loggerMessages.where().limit(numberOfEntryToBeDeleted).deleteAll();
}
}
_writeLogToHiveBox(LogRecord record) {
final Box<ImmichLoggerMessage> box = Hive.box(immichLoggerBox);
var formattedMessage = record.message;
void _writeLogToDatabase(LogRecord record) {
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
box.add(
ImmichLoggerMessage(
message: formattedMessage,
level: record.level.name,
createdAt: record.time,
context1: record.loggerName,
context2: record.stackTrace?.toString(),
),
final lm = LoggerMessage(
message: record.message,
level: record.level.toLogLevel(),
createdAt: record.time,
context1: record.loggerName,
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() {
_box.clear();
_timer?.cancel();
_timer = null;
_msgBuffer.clear();
_db.writeTxn(() => _db.loggerMessages.clear());
}
Future<void> shareLogs() async {
@ -93,4 +106,12 @@ class ImmichLogger {
// Clean up temp file
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) {
final userId = Store.get<User>(StoreKey.currentUser)!.isarId;
final userId = Store.get(StoreKey.currentUser).isarId;
final foreign =
await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
existing.addAll(foreign);

View file

@ -42,7 +42,7 @@ class UserService {
if (self) {
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();
}

View file

@ -1,9 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.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/store.dart';
import 'package:immich_mobile/utils/image_url_builder.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);
return CachedNetworkImage(
imageUrl: thumbnailRequestUrl,

View file

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

View file

@ -1,14 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
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/login/models/hive_saved_login_info.model.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/routing/router.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
class SplashScreenPage extends HookConsumerWidget {
@ -17,23 +15,23 @@ class SplashScreenPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiService = ref.watch(apiServiceProvider);
HiveSavedLoginInfo? loginInfo =
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
final serverUrl = Store.tryGet(StoreKey.serverUrl);
final accessToken = Store.tryGet(StoreKey.accessToken);
void performLoggingIn() async {
bool isSuccess = false;
if (loginInfo != null) {
if (accessToken != null && serverUrl != null) {
try {
// Resolve API server endpoint from user provided serverUrl
await apiService.resolveAndSetEndpoint(loginInfo.serverUrl);
await apiService.resolveAndSetEndpoint(serverUrl);
} catch (e) {
// okay, try to continue anyway if offline
}
isSuccess =
await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
accessToken: loginInfo.accessToken,
serverUrl: loginInfo.serverUrl,
accessToken: accessToken,
serverUrl: serverUrl,
);
}
if (isSuccess) {
@ -51,7 +49,7 @@ class SplashScreenPage extends HookConsumerWidget {
useEffect(
() {
if (loginInfo != null) {
if (serverUrl != null && accessToken != null) {
performLoggingIn();
} else {
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/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:openapi/api.dart';
import '../constants/hive_box.dart';
String getThumbnailUrl(
final Asset asset, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
@ -48,8 +46,7 @@ String getAlbumThumbNailCacheKey(
}
String getImageUrl(final Asset asset) {
final box = Hive.box(userInfoBox);
return '${box.get(serverEndpointKey)}/asset/file/${asset.remoteId}?isThumb=false';
return '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}?isThumb=false';
}
String getImageCacheKey(final Asset asset) {
@ -60,7 +57,5 @@ String _getThumbnailUrl(
final String id, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
final box = Hive.box(userInfoBox);
return '${box.get(serverEndpointKey)}/asset/thumbnail/$id?format=${type.value}';
return '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$id?format=${type.value}';
}

View file

@ -1,5 +1,7 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:hive/hive.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/hive_backup_albums.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/services/asset_cache.service.dart';
import 'package:isar/isar.dart';
@ -23,11 +28,37 @@ Future<void> migrateHiveToStoreIfNecessary() async {
duplicatedAssetsBox,
_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 {
await _migrateKey(box, userIdKey, StoreKey.userRemoteId);
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 {
@ -35,16 +66,15 @@ Future<void> _migrateHiveBackgroundBackupInfoBox(Box box) async {
await _migrateKey(box, backupRequireWifi, StoreKey.backupRequireWifi);
await _migrateKey(box, backupRequireCharging, StoreKey.backupRequireCharging);
await _migrateKey(box, backupTriggerDelay, StoreKey.backupTriggerDelay);
return box.deleteFromDisk();
}
Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
FutureOr<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) {
final HiveBackupAlbums? infos = box.get(backupInfoKey);
if (infos != null) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
List<BackupAlbum> albums = [];
for (int i = 0; i < infos.selectedAlbumIds.length; i++) {
final album = BackupAlbum(
@ -62,48 +92,49 @@ Future<void> _migrateBackupInfoBox(Box<HiveBackupAlbums> box) async {
);
albums.add(album);
}
await db.writeTxn(() => db.backupAlbums.putAll(albums));
} else {
debugPrint("_migrateBackupInfoBox deletes empty box");
return db.writeTxn(() => db.backupAlbums.putAll(albums));
}
return box.deleteFromDisk();
}
Future<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) async {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
FutureOr<void> _migrateDuplicatedAssetsBox(Box<HiveDuplicatedAssets> box) {
final HiveDuplicatedAssets? duplicatedAssets = box.get(duplicatedAssetsKey);
if (duplicatedAssets != null) {
final Isar? db = Isar.getInstance();
if (db == null) {
throw Exception("_migrateBackupInfoBox could not load database");
}
final duplicatedAssetIds = duplicatedAssets.duplicatedAssetIds
.map((id) => DuplicatedAsset(id))
.toList();
await db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds));
} else {
debugPrint("_migrateDuplicatedAssetsBox deletes empty box");
return db.writeTxn(() => db.duplicatedAssets.putAll(duplicatedAssetIds));
}
}
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>(
String boxName,
Future<void> Function(Box<T>) migrate,
FutureOr<void> Function(Box<T>) migrate,
) async {
try {
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) {
debugPrint("Error while migrating $boxName $e");
}
}
_migrateKey(Box box, String hiveKey, StoreKey key) async {
final String? value = box.get(hiveKey);
FutureOr<void> _migrateKey<T>(Box box, String hiveKey, StoreKey<T> key) {
final T? value = box.get(hiveKey);
if (value != null) {
await Store.put(key, value);
await box.delete(hiveKey);
return Store.put(key, value);
}
}