feat: show remainder assets info (#21114)

* feat: show remainder assets info

* pr feedback
This commit is contained in:
Alex 2025-08-21 12:18:31 -05:00 committed by GitHub
parent 66c657ca8a
commit ab2849781a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 195 additions and 45 deletions

View File

@ -16,10 +16,6 @@ class LocalImageRequest extends ImageRequest {
return null;
}
Stopwatch? stopwatch;
if (!kReleaseMode) {
stopwatch = Stopwatch()..start();
}
final Map<String, int> 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<void> _onCancelled() {
if (!kReleaseMode) {
debugPrint('Local image request $requestId for $localId of size $width x $height was cancelled');
}
return thumbnailApi.cancelImageRequest(requestId);
}
}

View File

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

View File

@ -17,9 +17,5 @@ class ThumbhashImageRequest extends ImageRequest {
}
@override
void _onCancelled() {
if (!kReleaseMode) {
debugPrint('Thumbhash request $requestId for $thumbhash was cancelled');
}
}
void _onCancelled() {}
}

View File

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

View File

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

View File

@ -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<List<LocalAsset>> result = ref.watch(driftBackupCandidateProvider);
return Scaffold(
appBar: AppBar(title: Text('backup_controller_page_remainder'.t(context: context))),
body: result.when(
data: (List<LocalAsset> 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()));
},
),
);
}
}

View File

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

View File

@ -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<DriftBackupState> {
super.dispose();
}
}
final driftBackupCandidateProvider = FutureProvider.autoDispose<List<LocalAsset>>((ref) async {
final user = ref.watch(currentUserProvider);
if (user == null) {
return [];
}
return ref.read(backupRepositoryProvider).getCandidates(user.id);
});
final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family<List<LocalAlbum>, String>((
ref,
assetId,
) {
return ref.read(backupRepositoryProvider).getSourceAlbums(assetId);
});

View File

@ -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: '/'),

View File

@ -796,6 +796,22 @@ class DriftBackupAlbumSelectionRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [DriftBackupAssetDetailPage]
class DriftBackupAssetDetailRoute extends PageRouteInfo<void> {
const DriftBackupAssetDetailRoute({List<PageRouteInfo>? 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<void> {

View File

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