mirror of
https://github.com/immich-app/immich.git
synced 2025-08-30 23:02:39 -04:00
feat: show remainder assets info (#21114)
* feat: show remainder assets info * pr feedback
This commit is contained in:
parent
66c657ca8a
commit
ab2849781a
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,5 @@ class ThumbhashImageRequest extends ImageRequest {
|
||||
}
|
||||
|
||||
@override
|
||||
void _onCancelled() {
|
||||
if (!kReleaseMode) {
|
||||
debugPrint('Thumbhash request $requestId for $thumbhash was cancelled');
|
||||
}
|
||||
}
|
||||
void _onCancelled() {}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
90
mobile/lib/pages/backup/drift_backup_asset_detail.page.dart
Normal file
90
mobile/lib/pages/backup/drift_backup_asset_detail.page.dart
Normal 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()));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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: '/'),
|
||||
|
@ -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> {
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user