diff --git a/mobile/lib/infrastructure/loaders/local_image_request.dart b/mobile/lib/infrastructure/loaders/local_image_request.dart index 915b2b66be..7a1b3d8957 100644 --- a/mobile/lib/infrastructure/loaders/local_image_request.dart +++ b/mobile/lib/infrastructure/loaders/local_image_request.dart @@ -16,10 +16,6 @@ class LocalImageRequest extends ImageRequest { return null; } - Stopwatch? stopwatch; - if (!kReleaseMode) { - stopwatch = Stopwatch()..start(); - } final Map info = await thumbnailApi.requestImage( localId, requestId: requestId, @@ -27,19 +23,13 @@ class LocalImageRequest extends ImageRequest { height: height, isVideo: assetType == AssetType.video, ); - if (!kReleaseMode) { - stopwatch!.stop(); - debugPrint('Local request $requestId took ${stopwatch.elapsedMilliseconds}ms for $localId of $width x $height'); - } + final frame = await _fromPlatformImage(info); return frame == null ? null : ImageInfo(image: frame.image, scale: scale); } @override Future _onCancelled() { - if (!kReleaseMode) { - debugPrint('Local image request $requestId for $localId of size $width x $height was cancelled'); - } return thumbnailApi.cancelImageRequest(requestId); } } diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index 2051e3eaa1..ffb7312850 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -24,18 +24,11 @@ class RemoteImageRequest extends ImageRequest { } try { - Stopwatch? stopwatch; - if (!kReleaseMode) { - stopwatch = Stopwatch()..start(); - } final buffer = await _downloadImage(uri); if (buffer == null) { return null; } - if (!kReleaseMode) { - stopwatch!.stop(); - debugPrint('Remote image download $requestId took ${stopwatch.elapsedMilliseconds}ms for $uri'); - } + return await _decodeBuffer(buffer, decode, scale); } catch (e) { if (_isCancelled) { @@ -139,8 +132,5 @@ class RemoteImageRequest extends ImageRequest { void _onCancelled() { _request?.abort(); _request = null; - if (!kReleaseMode) { - debugPrint('Cancelled remote image request $requestId for $uri'); - } } } diff --git a/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart b/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart index 083c0b2a96..a876020984 100644 --- a/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart +++ b/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart @@ -17,9 +17,5 @@ class ThumbhashImageRequest extends ImageRequest { } @override - void _onCancelled() { - if (!kReleaseMode) { - debugPrint('Thumbhash request $requestId for $thumbhash was cancelled'); - } - } + void _onCancelled() {} } diff --git a/mobile/lib/infrastructure/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart index a40e145a13..d98067d5fd 100644 --- a/mobile/lib/infrastructure/repositories/backup.repository.dart +++ b/mobile/lib/infrastructure/repositories/backup.repository.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; @@ -135,4 +137,22 @@ class DriftBackupRepository extends DriftDatabaseRepository { return query.map((localAsset) => localAsset.toDto()).get(); } + + FutureOr> getSourceAlbums(String localAssetId) { + final query = _db.localAlbumEntity.select() + ..where( + (lae) => + existsQuery( + _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.albumId]) + ..where( + _db.localAlbumAssetEntity.albumId.equalsExp(lae.id) & + _db.localAlbumAssetEntity.assetId.equals(localAssetId), + ), + ) & + lae.backupSelection.equalsValue(BackupSelection.selected), + ) + ..orderBy([(lae) => OrderingTerm.asc(lae.name)]); + return query.map((localAlbum) => localAlbum.toDto()).get(); + } } diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 27deba27bf..b125c35908 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -249,6 +249,7 @@ class _RemainderCard extends ConsumerWidget { title: "backup_controller_page_remainder".tr(), subtitle: "backup_controller_page_remainder_sub".tr(), info: remainderCount.toString(), + onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()), ); } } diff --git a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart new file mode 100644 index 0000000000..f361261e34 --- /dev/null +++ b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart @@ -0,0 +1,90 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +@RoutePage() +class DriftBackupAssetDetailPage extends ConsumerWidget { + const DriftBackupAssetDetailPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + AsyncValue> result = ref.watch(driftBackupCandidateProvider); + return Scaffold( + appBar: AppBar(title: Text('backup_controller_page_remainder'.t(context: context))), + body: result.when( + data: (List candidates) { + return ListView.separated( + padding: const EdgeInsets.only(top: 16.0), + separatorBuilder: (context, index) => Divider(color: context.colorScheme.outlineVariant), + itemCount: candidates.length, + itemBuilder: (context, index) { + final asset = candidates[index]; + final albumsAsyncValue = ref.watch(driftCandidateBackupAlbumInfoProvider(asset.id)); + return LargeLeadingTile( + title: Text( + asset.name, + style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500, fontSize: 16), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + asset.createdAt.toString(), + style: TextStyle(fontSize: 13.0, color: context.colorScheme.onSurfaceSecondary), + ), + Text( + asset.checksum ?? "N/A", + style: TextStyle(fontSize: 13.0, color: context.colorScheme.onSurfaceSecondary), + overflow: TextOverflow.ellipsis, + ), + albumsAsyncValue.when( + data: (albums) { + if (albums.isEmpty) { + return const SizedBox.shrink(); + } + return Text( + albums.map((a) => a.name).join(', '), + style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), + overflow: TextOverflow.ellipsis, + ); + }, + error: (error, stackTrace) => + Text('Error: $error', style: TextStyle(color: context.colorScheme.error)), + loading: () => const SizedBox(height: 16, width: 16, child: CircularProgressIndicator.adaptive()), + ), + ], + ), + leading: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Thumbnail(asset: asset, size: const Size(64, 64), fit: BoxFit.cover), + ), + trailing: const Padding(padding: EdgeInsets.only(right: 24, left: 8), child: Icon(Icons.image_search)), + onTap: () async { + await context.maybePop(); + await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()])); + EventStream.shared.emit(ScrollToDateEvent(asset.createdAt)); + }, + ); + }, + ); + }, + error: (Object error, StackTrace stackTrace) { + return Center(child: Text('Error: $error')); + }, + loading: () { + return const SizedBox(height: 48, width: 48, child: Center(child: CircularProgressIndicator.adaptive())); + }, + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart index dace3d53a3..411e279460 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; @@ -67,7 +68,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { EventStream.shared.emit(ScrollToDateEvent(asset.createdAt)); }, icon: const Icon(Icons.image_search), - tooltip: 'view_in_timeline', + tooltip: 'view_in_timeline'.t(context: context), ), if (asset.hasRemote && isOwner && !asset.isFavorite) const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index 748a099049..1228783737 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -8,6 +8,10 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/upload.service.dart'; import 'package:logging/logging.dart'; @@ -356,3 +360,19 @@ class DriftBackupNotifier extends StateNotifier { super.dispose(); } } + +final driftBackupCandidateProvider = FutureProvider.autoDispose>((ref) async { + final user = ref.watch(currentUserProvider); + if (user == null) { + return []; + } + + return ref.read(backupRepositoryProvider).getCandidates(user.id); +}); + +final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family, String>(( + ref, + assetId, +) { + return ref.read(backupRepositoryProvider).getSourceAlbums(assetId); +}); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 22bcd11aa4..b289cc3225 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -28,6 +28,7 @@ import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_album_selection.page.dart'; +import 'package:immich_mobile/pages/backup/drift_backup_asset_detail.page.dart'; import 'package:immich_mobile/pages/backup/drift_backup_options.page.dart'; import 'package:immich_mobile/pages/backup/drift_upload_detail.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; @@ -341,6 +342,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftCropImageRoute.page), AutoRoute(page: DriftFilterImageRoute.page), AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 30b09b47ce..84f2685ab5 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -796,6 +796,22 @@ class DriftBackupAlbumSelectionRoute extends PageRouteInfo { ); } +/// generated route for +/// [DriftBackupAssetDetailPage] +class DriftBackupAssetDetailRoute extends PageRouteInfo { + const DriftBackupAssetDetailRoute({List? children}) + : super(DriftBackupAssetDetailRoute.name, initialChildren: children); + + static const String name = 'DriftBackupAssetDetailRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const DriftBackupAssetDetailPage(); + }, + ); +} + /// generated route for /// [DriftBackupOptionsPage] class DriftBackupOptionsRoute extends PageRouteInfo { diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index 767d94bbf9..6e34e89938 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -2,12 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; class BackupInfoCard extends StatelessWidget { final String title; final String subtitle; final String info; - const BackupInfoCard({super.key, required this.title, required this.subtitle, required this.info}); + final VoidCallback? onTap; + const BackupInfoCard({super.key, required this.title, required this.subtitle, required this.info, this.onTap}); @override Widget build(BuildContext context) { @@ -20,24 +22,46 @@ class BackupInfoCard extends StatelessWidget { ), elevation: 0, borderOnForeground: false, - child: ListTile( - minVerticalPadding: 18, - isThreeLine: true, - title: Text(title, style: context.textTheme.titleMedium), - subtitle: Padding( - padding: const EdgeInsets.only(top: 4.0, right: 18.0), - child: Text( - subtitle, - style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), + child: Column( + children: [ + ListTile( + minVerticalPadding: 18, + isThreeLine: true, + title: Text(title, style: context.textTheme.titleMedium), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4.0, right: 18.0), + child: Text( + subtitle, + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(info, style: context.textTheme.titleLarge), + Text("backup_info_card_assets", style: context.textTheme.labelLarge).tr(), + ], + ), ), - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(info, style: context.textTheme.titleLarge), - Text("backup_info_card_assets", style: context.textTheme.labelLarge).tr(), + + if (onTap != null) ...[ + const Divider(height: 0), + ListTile( + enableFeedback: true, + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 0.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + ), + onTap: onTap, + title: Text( + "view_details".t(context: context), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), + ), + trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant), + ), ], - ), + ], ), ); }