From d6f2ca6aaa91ac1eea1d750be448f28cfeb9bfc3 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 3 Apr 2023 16:43:46 -0500 Subject: [PATCH] feat(mobile): improved logging page experience (#2158) * feat(mobile): improve logging page * Use new API for share file * removed unused code * Better safe area on the home screen * Added preparing share dialog to home screen --- mobile/lib/modules/home/views/home_page.dart | 43 ++-- mobile/lib/routing/router.dart | 48 +++-- mobile/lib/routing/router.gr.dart | 48 +++++ .../services/immich_logger.service.dart | 10 +- mobile/lib/shared/services/share.service.dart | 1 - mobile/lib/shared/services/sync.service.dart | 1 - .../lib/shared/views/app_log_detail_page.dart | 190 ++++++++++++++++++ mobile/lib/shared/views/app_log_page.dart | 7 + 8 files changed, 313 insertions(+), 35 deletions(-) create mode 100644 mobile/lib/shared/views/app_log_detail_page.dart diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index b01d1b73d9..2bc8c04aa4 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -27,6 +27,7 @@ import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/services/share.service.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:immich_mobile/shared/ui/share_dialog.dart'; class HomePage extends HookConsumerWidget { const HomePage({Key? key}) : super(key: key); @@ -81,7 +82,19 @@ class HomePage extends HookConsumerWidget { } void onShareAssets() { - ref.watch(shareServiceProvider).shareAssets(selection.value.toList()); + showDialog( + context: context, + builder: (BuildContext buildContext) { + ref + .watch(shareServiceProvider) + .shareAssets(selection.value.toList()) + .then((_) => Navigator.of(buildContext).pop()); + return const ShareDialog(); + }, + barrierDismissible: false, + ); + + // ref.watch(shareServiceProvider).shareAssets(selection.value.toList()); selectionEnabledHook.value = false; } @@ -244,6 +257,7 @@ class HomePage extends HookConsumerWidget { return SafeArea( top: true, + bottom: false, child: Stack( children: [ ref.watch(assetProvider).renderList == null || @@ -261,17 +275,14 @@ class HomePage extends HookConsumerWidget { onRefresh: refreshAssets, ), if (selectionEnabledHook.value) - SafeArea( - bottom: true, - child: ControlBottomAppBar( - onShare: onShareAssets, - onFavorite: onFavoriteAssets, - onDelete: onDelete, - onAddToAlbum: onAddToAlbum, - albums: albums, - sharedAlbums: sharedAlbums, - onCreateNewAlbum: onCreateNewAlbum, - ), + ControlBottomAppBar( + onShare: onShareAssets, + onFavorite: onFavoriteAssets, + onDelete: onDelete, + onAddToAlbum: onAddToAlbum, + albums: albums, + sharedAlbums: sharedAlbums, + onCreateNewAlbum: onCreateNewAlbum, ), ], ), @@ -279,9 +290,11 @@ class HomePage extends HookConsumerWidget { } return Scaffold( - appBar: HomePageAppBar( - onPopBack: reloadAllAsset, - ), + appBar: !selectionEnabledHook.value + ? HomePageAppBar( + onPopBack: reloadAllAsset, + ) + : null, drawer: const ProfileDrawer(), body: buildBody(), ); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 389fc5c900..d4f2b8a799 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -34,8 +34,10 @@ import 'package:immich_mobile/routing/duplicate_guard.dart'; import 'package:immich_mobile/routing/gallery_permission_guard.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/album.dart'; +import 'package:immich_mobile/shared/models/logger_message.model.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:immich_mobile/shared/views/app_log_detail_page.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/tab_controller_page.dart'; @@ -47,8 +49,12 @@ part 'router.gr.dart'; replaceInRouteName: 'Page,Route', routes: [ AutoRoute(page: SplashScreenPage, initial: true), - AutoRoute(page: PermissionOnboardingPage, guards: [AuthGuard, DuplicateGuard]), - AutoRoute(page: LoginPage, + AutoRoute( + page: PermissionOnboardingPage, + guards: [AuthGuard, DuplicateGuard], + ), + AutoRoute( + page: LoginPage, guards: [ DuplicateGuard, ], @@ -65,7 +71,10 @@ part 'router.gr.dart'; ], transitionsBuilder: TransitionsBuilders.fadeIn, ), - AutoRoute(page: GalleryViewerPage, guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard]), + AutoRoute( + page: GalleryViewerPage, + guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard], + ), AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: BackupControllerPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: SearchResultPage, guards: [AuthGuard, DuplicateGuard]), @@ -75,7 +84,10 @@ part 'router.gr.dart'; AutoRoute(page: FavoritesPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: AllVideosPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: AllMotionPhotosPage, guards: [AuthGuard, DuplicateGuard]), - AutoRoute(page: RecentlyAddedPage, guards: [AuthGuard, DuplicateGuard],), + AutoRoute( + page: RecentlyAddedPage, + guards: [AuthGuard, DuplicateGuard], + ), CustomRoute( page: AssetSelectionPage, guards: [AuthGuard, DuplicateGuard], @@ -92,14 +104,18 @@ part 'router.gr.dart'; guards: [AuthGuard, DuplicateGuard], transitionsBuilder: TransitionsBuilders.slideBottom, ), - AutoRoute(page: BackupAlbumSelectionPage, guards: [AuthGuard, DuplicateGuard]), + AutoRoute( + page: BackupAlbumSelectionPage, + guards: [AuthGuard, DuplicateGuard], + ), AutoRoute(page: AlbumPreviewPage, guards: [AuthGuard, DuplicateGuard]), CustomRoute( page: FailedBackupStatusPage, guards: [AuthGuard, DuplicateGuard], transitionsBuilder: TransitionsBuilders.slideBottom, ), - AutoRoute(page: SettingsPage, + AutoRoute( + page: SettingsPage, guards: [ AuthGuard, DuplicateGuard, @@ -109,6 +125,9 @@ part 'router.gr.dart'; page: AppLogPage, transitionsBuilder: TransitionsBuilders.slideBottom, ), + AutoRoute( + page: AppLogDetailPage, + ), ], ) class AppRouter extends _$AppRouter { @@ -116,14 +135,19 @@ class AppRouter extends _$AppRouter { final ApiService _apiService; AppRouter( - this._apiService, + this._apiService, GalleryPermissionNotifier galleryPermissionNotifier, - ) : super( - authGuard: AuthGuard(_apiService), + ) : super( + authGuard: AuthGuard(_apiService), duplicateGuard: DuplicateGuard(), - galleryPermissionGuard: GalleryPermissionGuard(galleryPermissionNotifier), + galleryPermissionGuard: + GalleryPermissionGuard(galleryPermissionNotifier), ); } -final appRouterProvider = - Provider((ref) => AppRouter(ref.watch(apiServiceProvider), ref.watch(galleryPermissionNotifier.notifier))); +final appRouterProvider = Provider( + (ref) => AppRouter( + ref.watch(apiServiceProvider), + ref.watch(galleryPermissionNotifier.notifier), + ), +); diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 02b7ce5452..d3ca49dd4f 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -230,6 +230,16 @@ class _$AppRouter extends RootStackRouter { barrierDismissible: false, ); }, + AppLogDetailRoute.name: (routeData) { + final args = routeData.argsAs(); + return MaterialPageX( + routeData: routeData, + child: AppLogDetailPage( + key: args.key, + logMessage: args.logMessage, + ), + ); + }, HomeRoute.name: (routeData) { return MaterialPageX( routeData: routeData, @@ -485,6 +495,10 @@ class _$AppRouter extends RootStackRouter { AppLogRoute.name, path: '/app-log-page', ), + RouteConfig( + AppLogDetailRoute.name, + path: '/app-log-detail-page', + ), ]; } @@ -974,6 +988,40 @@ class AppLogRoute extends PageRouteInfo { static const String name = 'AppLogRoute'; } +/// generated route for +/// [AppLogDetailPage] +class AppLogDetailRoute extends PageRouteInfo { + AppLogDetailRoute({ + Key? key, + required LoggerMessage logMessage, + }) : super( + AppLogDetailRoute.name, + path: '/app-log-detail-page', + args: AppLogDetailRouteArgs( + key: key, + logMessage: logMessage, + ), + ); + + static const String name = 'AppLogDetailRoute'; +} + +class AppLogDetailRouteArgs { + const AppLogDetailRouteArgs({ + this.key, + required this.logMessage, + }); + + final Key? key; + + final LoggerMessage logMessage; + + @override + String toString() { + return 'AppLogDetailRouteArgs{key: $key, logMessage: $logMessage}'; + } +} + /// generated route for /// [HomePage] class HomeRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/services/immich_logger.service.dart b/mobile/lib/shared/services/immich_logger.service.dart index 4987e66572..ebcf828f3f 100644 --- a/mobile/lib/shared/services/immich_logger.service.dart +++ b/mobile/lib/shared/services/immich_logger.service.dart @@ -102,15 +102,13 @@ class ImmichLogger { } // Share file - // ignore: deprecated_member_use - await Share.shareFiles( - [filePath], + await Share.shareXFiles( + [XFile(filePath)], subject: "Immich logs $dateTime", sharePositionOrigin: Rect.zero, + ).then( + (value) => logFile.delete(), ); - - // Clean up temp file - await logFile.delete(); } /// Flush pending log messages to persistent storage diff --git a/mobile/lib/shared/services/share.service.dart b/mobile/lib/shared/services/share.service.dart index 36d74ff9d8..df8f138fb5 100644 --- a/mobile/lib/shared/services/share.service.dart +++ b/mobile/lib/shared/services/share.service.dart @@ -36,7 +36,6 @@ class ShareService { } }); - // ignore: deprecated_member_use Share.shareXFiles( await Future.wait(downloadedXFiles), sharePositionOrigin: Rect.zero, diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index 97a2d54a67..ac01d05f5a 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -369,7 +369,6 @@ class SyncService { List onDevice, [ Set? excludedAssets, ]) async { - _log.info("Syncing ${onDevice.length} albums from device: $onDevice"); onDevice.sort((a, b) => a.id.compareTo(b.id)); final List inDb = await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll(); diff --git a/mobile/lib/shared/views/app_log_detail_page.dart b/mobile/lib/shared/views/app_log_detail_page.dart new file mode 100644 index 0000000000..212787aa6b --- /dev/null +++ b/mobile/lib/shared/views/app_log_detail_page.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/models/logger_message.model.dart'; +import 'package:flutter/services.dart'; + +class AppLogDetailPage extends HookConsumerWidget { + const AppLogDetailPage({super.key, required this.logMessage}); + + final LoggerMessage logMessage; + + @override + Widget build(BuildContext context, WidgetRef ref) { + var isDarkMode = Theme.of(context).brightness == Brightness.dark; + + buildStackMessage(String stackTrace) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "STACK TRACES", + style: TextStyle( + fontSize: 12.0, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: stackTrace)) + .then((_) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Copied to clipboard")), + ); + }); + }, + icon: Icon( + Icons.copy, + size: 16.0, + color: Theme.of(context).primaryColor, + ), + ) + ], + ), + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + borderRadius: BorderRadius.circular(15.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SelectableText( + stackTrace, + style: const TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.bold, + fontFamily: "Inconsolata", + ), + ), + ), + ), + ], + ), + ); + } + + buildLogMessage(String message) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "MESSAGE", + style: TextStyle( + fontSize: 12.0, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: message)).then((_) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Copied to clipboard")), + ); + }); + }, + icon: Icon( + Icons.copy, + size: 16.0, + color: Theme.of(context).primaryColor, + ), + ) + ], + ), + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + borderRadius: BorderRadius.circular(15.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SelectableText( + message, + style: const TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.bold, + fontFamily: "Inconsolata", + ), + ), + ), + ), + ], + ), + ); + } + + buildLogContext1(String context1) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "FROM", + style: TextStyle( + fontSize: 12.0, + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + borderRadius: BorderRadius.circular(15.0), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SelectableText( + context1.toString(), + style: const TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.bold, + fontFamily: "Inconsolata", + ), + ), + ), + ), + ], + ), + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text("Log Detail"), + ), + body: SafeArea( + child: ListView( + children: [ + buildLogMessage(logMessage.message), + if (logMessage.context1 != null) + buildLogContext1(logMessage.context1.toString()), + if (logMessage.context2 != null) + buildStackMessage(logMessage.context2.toString()) + ], + ), + ), + ); + } +} diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart index 5505ff3937..3d1fbfee71 100644 --- a/mobile/lib/shared/views/app_log_page.dart +++ b/mobile/lib/shared/views/app_log_page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/logger_message.model.dart'; import 'package:immich_mobile/shared/services/immich_logger.service.dart'; import 'package:intl/intl.dart'; @@ -123,6 +124,12 @@ class AppLogPage extends HookConsumerWidget { itemBuilder: (context, index) { var logMessage = logMessages.value[index]; return ListTile( + onTap: () => AutoRouter.of(context).push( + AppLogDetailRoute( + logMessage: logMessage, + ), + ), + trailing: const Icon(Icons.arrow_forward_ios_rounded), visualDensity: VisualDensity.compact, dense: true, tileColor: getTileColor(logMessage.level),