Browse Source

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
Alex 2 years ago
parent
commit
d6f2ca6aaa

+ 28 - 15
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(),
     );

+ 36 - 12
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>[
     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<AssetSelectionPageResult?>(
       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),
+  ),
+);

+ 48 - 0
mobile/lib/routing/router.gr.dart

@@ -230,6 +230,16 @@ class _$AppRouter extends RootStackRouter {
         barrierDismissible: false,
       );
     },
+    AppLogDetailRoute.name: (routeData) {
+      final args = routeData.argsAs<AppLogDetailRouteArgs>();
+      return MaterialPageX<dynamic>(
+        routeData: routeData,
+        child: AppLogDetailPage(
+          key: args.key,
+          logMessage: args.logMessage,
+        ),
+      );
+    },
     HomeRoute.name: (routeData) {
       return MaterialPageX<dynamic>(
         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<void> {
   static const String name = 'AppLogRoute';
 }
 
+/// generated route for
+/// [AppLogDetailPage]
+class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> {
+  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<void> {

+ 4 - 6
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

+ 0 - 1
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,

+ 0 - 1
mobile/lib/shared/services/sync.service.dart

@@ -369,7 +369,6 @@ class SyncService {
     List<AssetPathEntity> onDevice, [
     Set<String>? excludedAssets,
   ]) async {
-    _log.info("Syncing ${onDevice.length} albums from device: $onDevice");
     onDevice.sort((a, b) => a.id.compareTo(b.id));
     final List<Album> inDb =
         await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll();

+ 190 - 0
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())
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 7 - 0
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),