1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat(mobile) Add in app logging to show app's log information (#1014)

This commit is contained in:
Alex 2022-11-27 14:34:19 -06:00 committed by GitHub
parent fb3b36a569
commit 024177515d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 486 additions and 86 deletions

View file

@ -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",

Binary file not shown.

View file

@ -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

View file

@ -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,

View file

@ -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));

View file

@ -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();
} }

View file

@ -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(),
], ],
), ),

View file

@ -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());
},
),
],
),
),
),
); );
} }
} }

View file

@ -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 {

View file

@ -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> {

View 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)';
}
}

View file

@ -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 [];

View file

@ -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;

View file

@ -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);
}); });

View 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,
);
}
}

View 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),
);
},
),
);
}
}

View file

@ -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:

View file

@ -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"

View file

@ -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';