mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat(mobile) Add in app logging to show app's log information (#1014)
This commit is contained in:
parent
fb3b36a569
commit
024177515d
20 changed files with 486 additions and 86 deletions
|
@ -120,6 +120,7 @@
|
||||||
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
"profile_drawer_settings": "Settings",
|
"profile_drawer_settings": "Settings",
|
||||||
"profile_drawer_sign_out": "Sign Out",
|
"profile_drawer_sign_out": "Sign Out",
|
||||||
|
"profile_drawer_app_logs": "Logs",
|
||||||
"search_bar_hint": "Search your photos",
|
"search_bar_hint": "Search your photos",
|
||||||
"search_page_no_objects": "No Objects Info Available",
|
"search_page_no_objects": "No Objects Info Available",
|
||||||
"search_page_no_places": "No Places Info Available",
|
"search_page_no_places": "No Places Info Available",
|
||||||
|
|
BIN
mobile/fonts/Inconsolata-Regular.ttf
Normal file
BIN
mobile/fonts/Inconsolata-Regular.ttf
Normal file
Binary file not shown.
|
@ -30,3 +30,6 @@ const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
|
||||||
// Duplicate asset
|
// Duplicate asset
|
||||||
const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box
|
const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box
|
||||||
const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1
|
const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1
|
||||||
|
|
||||||
|
// In app logger
|
||||||
|
const String immichLoggerBox = "immichInAppLogger"; // Box
|
|
@ -16,11 +16,13 @@ import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.d
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/release_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/release_info.provider.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/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/immich_logger.service.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
|
||||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||||
|
@ -31,8 +33,10 @@ void main() async {
|
||||||
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||||
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
||||||
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
|
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
|
||||||
|
Hive.registerAdapter(ImmichLoggerMessageAdapter());
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
|
Hive.openBox<ImmichLoggerMessage>(immichLoggerBox),
|
||||||
Hive.openBox(userInfoBox),
|
Hive.openBox(userInfoBox),
|
||||||
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
||||||
Hive.openBox(hiveGithubReleaseInfoBox),
|
Hive.openBox(hiveGithubReleaseInfoBox),
|
||||||
|
@ -58,6 +62,9 @@ void main() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Immich Logger Service
|
||||||
|
ImmichLogger().init();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
EasyLocalization(
|
EasyLocalization(
|
||||||
supportedLocales: locales,
|
supportedLocales: locales,
|
||||||
|
|
|
@ -349,7 +349,6 @@ class BackgroundService {
|
||||||
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||||
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ApiService apiService = ApiService();
|
ApiService apiService = ApiService();
|
||||||
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
||||||
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
|
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.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/constants/hive_box.dart';
|
||||||
|
@ -18,6 +17,7 @@ import 'package:immich_mobile/modules/login/models/authentication_state.model.da
|
||||||
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/providers/app_state.provider.dart';
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
getBackupInfo();
|
getBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final log = Logger('BackupNotifier');
|
||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final ServerInfoService _serverInfoService;
|
final ServerInfoService _serverInfoService;
|
||||||
final AuthenticationState _authState;
|
final AuthenticationState _authState;
|
||||||
|
@ -218,13 +219,16 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (backupAlbumInfo == null) {
|
if (backupAlbumInfo == null) {
|
||||||
debugPrint("[ERROR] getting Hive backup album infomation");
|
log.severe(
|
||||||
|
"backupAlbumInfo == null",
|
||||||
|
"Failed to get Hive backup album information",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First time backup - set isAll album is the default one for backup.
|
// First time backup - set isAll album is the default one for backup.
|
||||||
if (backupAlbumInfo.selectedAlbumIds.isEmpty) {
|
if (backupAlbumInfo.selectedAlbumIds.isEmpty) {
|
||||||
debugPrint("First time backup setup recent album as default");
|
log.info("First time backup; setup 'Recent(s)' album as default");
|
||||||
|
|
||||||
// Get album that contains all assets
|
// Get album that contains all assets
|
||||||
var list = await PhotoManager.getAssetPathList(
|
var list = await PhotoManager.getAssetPathList(
|
||||||
|
@ -286,8 +290,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
selectedBackupAlbums: selectedAlbums,
|
selectedBackupAlbums: selectedAlbums,
|
||||||
excludedBackupAlbums: excludedAlbums,
|
excludedBackupAlbums: excludedAlbums,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
debugPrint("[ERROR] Failed to generate album from id $e");
|
log.severe("Failed to generate album from id", e, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +342,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (allUniqueAssets.isEmpty) {
|
if (allUniqueAssets.isEmpty) {
|
||||||
debugPrint("No Asset On Device");
|
log.info("Not found albums or assets on the device to backup");
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
allAssetsInDatabase: allAssetsInDatabase,
|
allAssetsInDatabase: allAssetsInDatabase,
|
||||||
|
@ -412,7 +416,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
await PhotoManager.clearFileCache();
|
await PhotoManager.clearFileCache();
|
||||||
|
|
||||||
if (state.allUniqueAssets.isEmpty) {
|
if (state.allUniqueAssets.isEmpty) {
|
||||||
debugPrint("No Asset On Device - Abort Backup Process");
|
log.info("No Asset On Device - Abort Backup Process");
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -530,7 +534,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
|
|
||||||
// User has been logged out return
|
// User has been logged out return
|
||||||
if (accessKey == null || !_authState.isAuthenticated) {
|
if (accessKey == null || !_authState.isAuthenticated) {
|
||||||
debugPrint("[resumeBackup] not authenticated - abort");
|
log.info("[_resumeBackup] not authenticated - abort");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,17 +543,17 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
_authState.deviceInfo.isAutoBackup) {
|
_authState.deviceInfo.isAutoBackup) {
|
||||||
// check if backup is alreayd in process - then return
|
// check if backup is alreayd in process - then return
|
||||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||||
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
log.info("[_resumeBackup] Backup is already in progress - abort");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.backupProgress == BackUpProgressEnum.inBackground) {
|
if (state.backupProgress == BackUpProgressEnum.inBackground) {
|
||||||
debugPrint("[resumeBackup] Background backup is running - abort");
|
log.info("[_resumeBackup] Background backup is running - abort");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run backup
|
// Run backup
|
||||||
debugPrint("[resumeBackup] Start back up");
|
log.info("[_resumeBackup] Start back up");
|
||||||
await startBackupProcess();
|
await startBackupProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,7 +569,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
||||||
final bool hasLock = await _backgroundService.acquireLock();
|
final bool hasLock = await _backgroundService.acquireLock();
|
||||||
if (!hasLock) {
|
if (!hasLock) {
|
||||||
debugPrint("WARNING [resumeBackup] failed to acquireLock");
|
log.warning("WARNING [resumeBackup] failed to acquireLock");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
|
@ -612,7 +616,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]);
|
AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]);
|
||||||
result.add(a.copyWith(lastBackup: times[i]));
|
result.add(a.copyWith(lastBackup: times[i]));
|
||||||
} on StateError {
|
} on StateError {
|
||||||
debugPrint("[_updateAlbumBackupTime] failed to find album in state");
|
log.severe(
|
||||||
|
"[_updateAlbumBackupTime] failed to find album in state",
|
||||||
|
"State Error",
|
||||||
|
StackTrace.current,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -631,21 +639,29 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
|
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
log.info("[_notifyBackgroundServiceCanRun] failed to close box");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (Hive.isBoxOpen(duplicatedAssetsBox)) {
|
if (Hive.isBoxOpen(duplicatedAssetsBox)) {
|
||||||
await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close();
|
await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error, stackTrace) {
|
||||||
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
log.severe(
|
||||||
|
"[_notifyBackgroundServiceCanRun] failed to close box",
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
|
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
|
||||||
await Hive.box(backgroundBackupInfoBox).close();
|
await Hive.box(backgroundBackupInfoBox).close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error, stackTrace) {
|
||||||
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
|
log.severe(
|
||||||
|
"[_notifyBackgroundServiceCanRun] failed to close box",
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_backgroundService.releaseLock();
|
_backgroundService.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ 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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.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/backup/providers/backup.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
|
||||||
class ProfileDrawer extends HookConsumerWidget {
|
class ProfileDrawer extends HookConsumerWidget {
|
||||||
|
@ -70,6 +70,30 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildAppLogButton() {
|
||||||
|
return ListTile(
|
||||||
|
horizontalTitleGap: 0,
|
||||||
|
leading: SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
child: Icon(
|
||||||
|
Icons.assignment_outlined,
|
||||||
|
color: Theme.of(context).textTheme.labelMedium?.color,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"profile_drawer_app_logs",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context).push(const AppLogRoute());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
@ -80,6 +104,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
const ProfileDrawerHeader(),
|
const ProfileDrawerHeader(),
|
||||||
buildSettingButton(),
|
buildSettingButton(),
|
||||||
|
buildAppLogButton(),
|
||||||
buildSignoutButton(),
|
buildSignoutButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,14 +1,65 @@
|
||||||
|
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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/login/ui/login_form.dart';
|
import 'package:immich_mobile/modules/login/ui/login_form.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
class LoginPage extends HookConsumerWidget {
|
class LoginPage extends HookConsumerWidget {
|
||||||
const LoginPage({Key? key}) : super(key: key);
|
const LoginPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return const Scaffold(
|
final appVersion = useState('0.0.0');
|
||||||
body: LoginForm(),
|
|
||||||
|
getAppInfo() async {
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
appVersion.value = packageInfo.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
getAppInfo();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: const LoginForm(),
|
||||||
|
bottomNavigationBar: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'v${appVersion.value}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: "Inconsolata",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(' '),
|
||||||
|
GestureDetector(
|
||||||
|
child: Text(
|
||||||
|
'Logs',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: "Inconsolata",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
AutoRouter.of(context).push(const AppLogRoute());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,34 @@
|
||||||
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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/library_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/views/change_password_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
|
import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
import 'package:immich_mobile/modules/album/views/create_album_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/album/views/library_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart';
|
import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
|
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
|
||||||
import 'package:immich_mobile/modules/album/views/sharing_page.dart';
|
import 'package:immich_mobile/modules/album/views/sharing_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/views/change_password_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
|
||||||
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/views/app_log_page.dart';
|
||||||
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
import 'package:immich_mobile/shared/views/splash_screen.dart';
|
||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
@ -80,6 +81,10 @@ part 'router.gr.dart';
|
||||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
),
|
),
|
||||||
AutoRoute(page: SettingsPage, guards: [AuthGuard]),
|
AutoRoute(page: SettingsPage, guards: [AuthGuard]),
|
||||||
|
CustomRoute(
|
||||||
|
page: AppLogPage,
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
|
|
@ -142,6 +142,14 @@ class _$AppRouter extends RootStackRouter {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData, child: const SettingsPage());
|
routeData: routeData, child: const SettingsPage());
|
||||||
},
|
},
|
||||||
|
AppLogRoute.name: (routeData) {
|
||||||
|
return CustomPage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const AppLogPage(),
|
||||||
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
|
opaque: true,
|
||||||
|
barrierDismissible: false);
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData, child: const HomePage());
|
routeData: routeData, child: const HomePage());
|
||||||
|
@ -218,7 +226,8 @@ class _$AppRouter extends RootStackRouter {
|
||||||
RouteConfig(FailedBackupStatusRoute.name,
|
RouteConfig(FailedBackupStatusRoute.name,
|
||||||
path: '/failed-backup-status-page', guards: [authGuard]),
|
path: '/failed-backup-status-page', guards: [authGuard]),
|
||||||
RouteConfig(SettingsRoute.name,
|
RouteConfig(SettingsRoute.name,
|
||||||
path: '/settings-page', guards: [authGuard])
|
path: '/settings-page', guards: [authGuard]),
|
||||||
|
RouteConfig(AppLogRoute.name, path: '/app-log-page')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,6 +569,14 @@ class SettingsRoute extends PageRouteInfo<void> {
|
||||||
static const String name = 'SettingsRoute';
|
static const String name = 'SettingsRoute';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AppLogPage]
|
||||||
|
class AppLogRoute extends PageRouteInfo<void> {
|
||||||
|
const AppLogRoute() : super(AppLogRoute.name, path: '/app-log-page');
|
||||||
|
|
||||||
|
static const String name = 'AppLogRoute';
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
|
34
mobile/lib/shared/models/immich_logger_message.model.dart
Normal file
34
mobile/lib/shared/models/immich_logger_message.model.dart
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
|
part 'immich_logger_message.model.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 3)
|
||||||
|
class ImmichLoggerMessage {
|
||||||
|
@HiveField(0)
|
||||||
|
String message;
|
||||||
|
|
||||||
|
@HiveField(1, defaultValue: "INFO")
|
||||||
|
String level;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
String? context1;
|
||||||
|
|
||||||
|
@HiveField(4)
|
||||||
|
String? context2;
|
||||||
|
|
||||||
|
ImmichLoggerMessage({
|
||||||
|
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)';
|
||||||
|
}
|
||||||
|
}
|
BIN
mobile/lib/shared/models/immich_logger_message.model.g.dart
Normal file
BIN
mobile/lib/shared/models/immich_logger_message.model.g.dart
Normal file
Binary file not shown.
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:hive/hive.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/constants/hive_box.dart';
|
||||||
|
@ -10,13 +9,14 @@ import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class AssetNotifier extends StateNotifier<List<Asset>> {
|
class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||||
final AssetService _assetService;
|
final AssetService _assetService;
|
||||||
final AssetCacheService _assetCacheService;
|
final AssetCacheService _assetCacheService;
|
||||||
|
final log = Logger('AssetNotifier');
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||||
bool _getAllAssetInProgress = false;
|
bool _getAllAssetInProgress = false;
|
||||||
bool _deleteInProgress = false;
|
bool _deleteInProgress = false;
|
||||||
|
@ -41,7 +41,7 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||||
final remoteTask = _assetService.getRemoteAssets();
|
final remoteTask = _assetService.getRemoteAssets();
|
||||||
if (isCacheValid && state.isEmpty) {
|
if (isCacheValid && state.isEmpty) {
|
||||||
state = await _assetCacheService.get();
|
state = await _assetCacheService.get();
|
||||||
debugPrint(
|
log.info(
|
||||||
"Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms",
|
"Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms",
|
||||||
);
|
);
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
|
@ -52,25 +52,25 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||||
final List<Asset> currentLocal = state.slice(0, remoteBegin);
|
final List<Asset> currentLocal = state.slice(0, remoteBegin);
|
||||||
List<Asset>? newRemote = await remoteTask;
|
List<Asset>? newRemote = await remoteTask;
|
||||||
List<Asset>? newLocal = await localTask;
|
List<Asset>? newLocal = await localTask;
|
||||||
debugPrint("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
if (newRemote == null &&
|
if (newRemote == null &&
|
||||||
(newLocal == null || currentLocal.equals(newLocal))) {
|
(newLocal == null || currentLocal.equals(newLocal))) {
|
||||||
debugPrint("state is already up-to-date");
|
log.info("state is already up-to-date");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newRemote ??= state.slice(remoteBegin);
|
newRemote ??= state.slice(remoteBegin);
|
||||||
newLocal ??= [];
|
newLocal ??= [];
|
||||||
state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote);
|
state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote);
|
||||||
debugPrint("Combining assets: ${stopwatch.elapsedMilliseconds}ms");
|
log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
} finally {
|
} finally {
|
||||||
_getAllAssetInProgress = false;
|
_getAllAssetInProgress = false;
|
||||||
}
|
}
|
||||||
debugPrint("[getAllAsset] setting new asset state");
|
log.info("setting new asset state");
|
||||||
|
|
||||||
stopwatch.reset();
|
stopwatch.reset();
|
||||||
_cacheState();
|
_cacheState();
|
||||||
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Asset> _combineLocalAndRemoteAssets({
|
List<Asset> _combineLocalAndRemoteAssets({
|
||||||
|
@ -155,8 +155,8 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||||
if (local.isNotEmpty) {
|
if (local.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
return await PhotoManager.editor.deleteWithIds(local);
|
return await PhotoManager.editor.deleteWithIds(local);
|
||||||
} catch (e) {
|
} catch (e, stack) {
|
||||||
debugPrint("Delete asset from device failed: $e");
|
log.severe("Failed to delete asset from device", e, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -6,10 +6,11 @@ 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/constants/hive_box.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';
|
||||||
|
|
||||||
class ReleaseInfoNotifier extends StateNotifier<String> {
|
class ReleaseInfoNotifier extends StateNotifier<String> {
|
||||||
ReleaseInfoNotifier() : super("");
|
ReleaseInfoNotifier() : super("");
|
||||||
|
final log = Logger('ReleaseInfoNotifier');
|
||||||
void checkGithubReleaseInfo() async {
|
void checkGithubReleaseInfo() async {
|
||||||
final Client client = Client();
|
final Client client = Client();
|
||||||
var box = Hive.box(hiveGithubReleaseInfoBox);
|
var box = Hive.box(hiveGithubReleaseInfoBox);
|
||||||
|
@ -28,9 +29,6 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
|
||||||
String latestTagVersion = data["tag_name"];
|
String latestTagVersion = data["tag_name"];
|
||||||
state = latestTagVersion;
|
state = latestTagVersion;
|
||||||
|
|
||||||
debugPrint("Local release version $localReleaseVersion");
|
|
||||||
debugPrint("Remote release veresion $latestTagVersion");
|
|
||||||
|
|
||||||
if (localReleaseVersion == null && latestTagVersion.isNotEmpty) {
|
if (localReleaseVersion == null && latestTagVersion.isNotEmpty) {
|
||||||
VersionAnnouncementOverlayController.appLoader.show();
|
VersionAnnouncementOverlayController.appLoader.show();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -6,23 +6,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.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/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:socket_io_client/socket_io_client.dart';
|
import 'package:socket_io_client/socket_io_client.dart';
|
||||||
|
|
||||||
class WebscoketState {
|
class WebsocketState {
|
||||||
final Socket? socket;
|
final Socket? socket;
|
||||||
final bool isConnected;
|
final bool isConnected;
|
||||||
|
|
||||||
WebscoketState({
|
WebsocketState({
|
||||||
this.socket,
|
this.socket,
|
||||||
required this.isConnected,
|
required this.isConnected,
|
||||||
});
|
});
|
||||||
|
|
||||||
WebscoketState copyWith({
|
WebsocketState copyWith({
|
||||||
Socket? socket,
|
Socket? socket,
|
||||||
bool? isConnected,
|
bool? isConnected,
|
||||||
}) {
|
}) {
|
||||||
return WebscoketState(
|
return WebsocketState(
|
||||||
socket: socket ?? this.socket,
|
socket: socket ?? this.socket,
|
||||||
isConnected: isConnected ?? this.isConnected,
|
isConnected: isConnected ?? this.isConnected,
|
||||||
);
|
);
|
||||||
|
@ -30,13 +31,13 @@ class WebscoketState {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'WebscoketState(socket: $socket, isConnected: $isConnected)';
|
'WebsocketState(socket: $socket, isConnected: $isConnected)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is WebscoketState &&
|
return other is WebsocketState &&
|
||||||
other.socket == socket &&
|
other.socket == socket &&
|
||||||
other.isConnected == isConnected;
|
other.isConnected == isConnected;
|
||||||
}
|
}
|
||||||
|
@ -45,12 +46,11 @@ class WebscoketState {
|
||||||
int get hashCode => socket.hashCode ^ isConnected.hashCode;
|
int get hashCode => socket.hashCode ^ isConnected.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
WebsocketNotifier(this.ref)
|
WebsocketNotifier(this.ref)
|
||||||
: super(WebscoketState(socket: null, isConnected: false)) {
|
: super(WebsocketState(socket: null, isConnected: false));
|
||||||
debugPrint("Init websocket instance");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
final log = Logger('WebsocketNotifier');
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
@ -60,8 +60,8 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||||
var accessToken = Hive.box(userInfoBox).get(accessTokenKey);
|
var accessToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
var endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
var endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
try {
|
try {
|
||||||
debugPrint("[WEBSOCKET] Attempting to connect to ws");
|
log.info("Attempting to connect to websocket");
|
||||||
// Configure socket transports must be sepecified
|
// Configure socket transports must be specified
|
||||||
Socket socket = io(
|
Socket socket = io(
|
||||||
endpoint.toString().replaceAll('/api', ''),
|
endpoint.toString().replaceAll('/api', ''),
|
||||||
OptionBuilder()
|
OptionBuilder()
|
||||||
|
@ -76,18 +76,18 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
socket.onConnect((_) {
|
socket.onConnect((_) {
|
||||||
debugPrint("[WEBSOCKET] Established Websocket Connection");
|
log.info("Established Websocket Connection");
|
||||||
state = WebscoketState(isConnected: true, socket: socket);
|
state = WebsocketState(isConnected: true, socket: socket);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.onDisconnect((_) {
|
socket.onDisconnect((_) {
|
||||||
debugPrint("[WEBSOCKET] Disconnect to Websocket Connection");
|
log.info("Disconnect to Websocket Connection");
|
||||||
state = WebscoketState(isConnected: false, socket: null);
|
state = WebsocketState(isConnected: false, socket: null);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', (errorMessage) {
|
socket.on('error', (errorMessage) {
|
||||||
debugPrint("Webcoket Error - $errorMessage");
|
log.severe("Websocket Error - $errorMessage");
|
||||||
state = WebscoketState(isConnected: false, socket: null);
|
state = WebsocketState(isConnected: false, socket: null);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('on_upload_success', (data) {
|
socket.on('on_upload_success', (data) {
|
||||||
|
@ -105,21 +105,22 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
debugPrint("[WEBSOCKET] Attempting to disconnect");
|
log.info("Attempting to disconnect from websocket");
|
||||||
|
|
||||||
var socket = state.socket?.disconnect();
|
var socket = state.socket?.disconnect();
|
||||||
|
|
||||||
if (socket?.disconnected == true) {
|
if (socket?.disconnected == true) {
|
||||||
state = WebscoketState(isConnected: false, socket: null);
|
state = WebsocketState(isConnected: false, socket: null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopListenToEvent(String eventName) {
|
stopListenToEvent(String eventName) {
|
||||||
debugPrint("[Websocket] Stop listening to event $eventName");
|
log.info("Stop listening to event $eventName");
|
||||||
state.socket?.off(eventName);
|
state.socket?.off(eventName);
|
||||||
}
|
}
|
||||||
|
|
||||||
listenUploadEvent() {
|
listenUploadEvent() {
|
||||||
debugPrint("[Websocket] Start listening to event on_upload_success");
|
log.info("Start listening to event on_upload_success");
|
||||||
state.socket?.on('on_upload_success', (data) {
|
state.socket?.on('on_upload_success', (data) {
|
||||||
var jsonString = jsonDecode(data.toString());
|
var jsonString = jsonDecode(data.toString());
|
||||||
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString);
|
||||||
|
@ -132,6 +133,6 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final websocketProvider =
|
final websocketProvider =
|
||||||
StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
|
StateNotifierProvider<WebsocketNotifier, WebsocketState>((ref) {
|
||||||
return WebsocketNotifier(ref);
|
return WebsocketNotifier(ref);
|
||||||
});
|
});
|
||||||
|
|
87
mobile/lib/shared/services/immich_logger.service.dart
Normal file
87
mobile/lib/shared/services/immich_logger.service.dart
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
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: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 deleted when exceeding the `maxLogEntries` (default 200) property
|
||||||
|
/// in the class.
|
||||||
|
///
|
||||||
|
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||||
|
/// and generate a csv file.
|
||||||
|
class ImmichLogger {
|
||||||
|
final maxLogEntries = 200;
|
||||||
|
final Box<ImmichLoggerMessage> _box = Hive.box(immichLoggerBox);
|
||||||
|
|
||||||
|
List<ImmichLoggerMessage> get messages =>
|
||||||
|
_box.values.toList().reversed.toList();
|
||||||
|
|
||||||
|
ImmichLogger() {
|
||||||
|
_removeOverflowMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Logger.root.level = Level.INFO;
|
||||||
|
Logger.root.onRecord.listen(_writeLogToHiveBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeOverflowMessages() {
|
||||||
|
if (_box.length > maxLogEntries) {
|
||||||
|
var numberOfEntryToBeDeleted = _box.length - maxLogEntries;
|
||||||
|
for (var i = 0; i < numberOfEntryToBeDeleted; i++) {
|
||||||
|
_box.deleteAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeLogToHiveBox(LogRecord record) {
|
||||||
|
final Box<ImmichLoggerMessage> box = Hive.box(immichLoggerBox);
|
||||||
|
var formattedMessage = record.message;
|
||||||
|
|
||||||
|
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(), // Something more useful here? (e.g. stacktrace - I cannot get it to format nicely though)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearLogs() {
|
||||||
|
_box.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
shareLogs() async {
|
||||||
|
var tempDir = await getTemporaryDirectory();
|
||||||
|
var filePath = '${tempDir.path}/${DateTime.now().toIso8601String()}.csv';
|
||||||
|
var logFile = await File(filePath).create();
|
||||||
|
// Write header
|
||||||
|
logFile.writeAsStringSync("created_at,context_1,context_2,message,type\n");
|
||||||
|
|
||||||
|
// Write messages
|
||||||
|
for (var message in messages) {
|
||||||
|
logFile.writeAsStringSync(
|
||||||
|
"${message.createdAt},${message.context1 ?? ""},${message.context2 ?? ""},${message.message},${message.level.toString()}\n",
|
||||||
|
mode: FileMode.append,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share file
|
||||||
|
Share.shareFiles(
|
||||||
|
[filePath],
|
||||||
|
subject: "Immich logs ${DateTime.now().toIso8601String()}",
|
||||||
|
sharePositionOrigin: Rect.zero,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
153
mobile/lib/shared/views/app_log_page.dart
Normal file
153
mobile/lib/shared/views/app_log_page.dart
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
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/services/immich_logger.service.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class AppLogPage extends HookConsumerWidget {
|
||||||
|
const AppLogPage({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final immichLogger = ImmichLogger();
|
||||||
|
final logMessages = useState(immichLogger.messages);
|
||||||
|
|
||||||
|
Widget buildLeadingIcon(String level) {
|
||||||
|
switch (level) {
|
||||||
|
case "INFO":
|
||||||
|
return Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case "SEVERE":
|
||||||
|
return Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case "WARNING":
|
||||||
|
return Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orangeAccent,
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTileColor(String level) {
|
||||||
|
switch (level) {
|
||||||
|
case "INFO":
|
||||||
|
return Colors.transparent;
|
||||||
|
case "SEVERE":
|
||||||
|
return Colors.redAccent.withOpacity(0.075);
|
||||||
|
case "WARNING":
|
||||||
|
return Colors.orangeAccent.withOpacity(0.075);
|
||||||
|
default:
|
||||||
|
return Theme.of(context).primaryColor.withOpacity(0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
"Logs - ${logMessages.value.length}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
scrolledUnderElevation: 1,
|
||||||
|
elevation: 2,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.delete_outline_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
semanticLabel: "Clear logs",
|
||||||
|
size: 20.0,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
immichLogger.clearLogs();
|
||||||
|
logMessages.value = [];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.share_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
semanticLabel: "Share logs",
|
||||||
|
size: 20.0,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
immichLogger.shareLogs();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).pop();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_new_rounded,
|
||||||
|
size: 20.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: ListView.separated(
|
||||||
|
separatorBuilder: (context, index) {
|
||||||
|
return Divider(
|
||||||
|
height: 0,
|
||||||
|
color: Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Colors.white70
|
||||||
|
: Colors.grey[500],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: logMessages.value.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
var logMessage = logMessages.value[index];
|
||||||
|
return ListTile(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
dense: true,
|
||||||
|
tileColor: getTileColor(logMessage.level),
|
||||||
|
minLeadingWidth: 10,
|
||||||
|
title: Text(
|
||||||
|
logMessage.message,
|
||||||
|
style: const TextStyle(fontSize: 14.0, fontFamily: "Inconsolata"),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"[${logMessage.context1}] Logged on ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: buildLeadingIcon(logMessage.level),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -266,7 +266,7 @@ packages:
|
||||||
name: ffi
|
name: ffi
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "2.0.1"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -554,12 +554,12 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: logging
|
name: logging
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.1.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -629,7 +629,7 @@ packages:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.2"
|
version: "1.4.3+1"
|
||||||
package_info_plus_linux:
|
package_info_plus_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -664,7 +664,7 @@ packages:
|
||||||
name: package_info_plus_windows
|
name: package_info_plus_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "2.1.0"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -699,7 +699,7 @@ packages:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.6"
|
version: "2.1.7"
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -720,7 +720,7 @@ packages:
|
||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.1.3"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -998,14 +998,14 @@ packages:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2+1"
|
version: "2.2.0+3"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1+1"
|
version: "2.4.0+2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1257,7 +1257,7 @@ packages:
|
||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.2"
|
version: "2.7.0"
|
||||||
wkt_parser:
|
wkt_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -47,6 +47,7 @@ dependencies:
|
||||||
|
|
||||||
# easy to remove packages:
|
# easy to remove packages:
|
||||||
image_picker: ^0.8.5+3 # only used to select user profile image from system gallery -> we can simply select an image from within immich?
|
image_picker: ^0.8.5+3 # only used to select user profile image from system gallery -> we can simply select an image from within immich?
|
||||||
|
logging: ^1.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -71,7 +72,9 @@ flutter:
|
||||||
- family: SnowburstOne
|
- family: SnowburstOne
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/SnowburstOne.ttf
|
- asset: fonts/SnowburstOne.ttf
|
||||||
|
- family: Inconsolata
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/Inconsolata-Regular.ttf
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
image_path_android: "assets/immich-logo-no-outline.png"
|
image_path_android: "assets/immich-logo-no-outline.png"
|
||||||
image_path_ios: "assets/immich-logo-no-outline.png"
|
image_path_ios: "assets/immich-logo-no-outline.png"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { UserEntity } from '@app/database/entities/user.entity';
|
import { UserEntity } from '@app/database/entities/user.entity';
|
||||||
import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
import { newUserRepositoryMock } from '../../../test/test-utils';
|
import { newUserRepositoryMock } from '../../../test/test-utils';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { IUserRepository } from './user-repository';
|
import { IUserRepository } from './user-repository';
|
||||||
|
|
Loading…
Reference in a new issue