mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix: download feedback (#22178)
* fix: download feedback * chore: use FAB for asset viewer as well
This commit is contained in:
parent
642065f506
commit
33d76fb386
10 changed files with 185 additions and 42 deletions
57
mobile/lib/presentation/pages/download_info.page.dart
Normal file
57
mobile/lib/presentation/pages/download_info.page.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/pages/common/download_panel.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DownloadInfoPage extends ConsumerWidget {
|
||||
const DownloadInfoPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList();
|
||||
|
||||
onCancelDownload(String id) {
|
||||
ref.watch(downloadStateProvider.notifier).cancelDownload(id);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("download".t(context: context)),
|
||||
actions: [],
|
||||
),
|
||||
body: ListView.builder(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = tasks[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: DownloadTaskTile(
|
||||
progress: task.value.progress,
|
||||
fileName: task.value.fileName,
|
||||
status: task.value.status,
|
||||
onCancelDownload: () => onCancelDownload(task.key),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
persistentFooterButtons: [
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
tasks.map((e) => e.key).forEach(onCancelDownload);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(side: BorderSide(color: context.colorScheme.primary)),
|
||||
child: Text(
|
||||
'clear_all'.t(context: context),
|
||||
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +1,45 @@
|
|||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class DownloadActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool menuItem;
|
||||
const DownloadActionButton({super.key, required this.source, this.menuItem = false});
|
||||
|
||||
const DownloadActionButton({super.key, required this.source});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await ref.read(actionProvider.notifier).downloadAll(source);
|
||||
try {
|
||||
await ref.read(actionProvider.notifier).downloadAll(source);
|
||||
|
||||
Future.delayed(const Duration(seconds: 1), () async {
|
||||
await backgroundSyncManager.syncLocal();
|
||||
await backgroundSyncManager.hashAssets();
|
||||
});
|
||||
} finally {
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'scaffold_body_error_occurred'.t(context: context),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
} else if (result.count > 0) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'download_action_prompt'.t(context: context, args: {'count': result.count.toString()}),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final backgroundManager = ref.watch(backgroundSyncProvider);
|
||||
|
||||
return BaseActionButton(
|
||||
iconData: Icons.download,
|
||||
maxWidth: 95,
|
||||
label: "download".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref, backgroundManager),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class DownloadStatusFloatingButton extends ConsumerWidget {
|
||||
const DownloadStatusFloatingButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final shouldShow = ref.watch(downloadStateProvider.select((state) => state.showProgress));
|
||||
final itemCount = ref.watch(downloadStateProvider.select((state) => state.taskProgress.length));
|
||||
final isDownloading = ref
|
||||
.watch(downloadStateProvider.select((state) => state.taskProgress))
|
||||
.values
|
||||
.where((element) => element.progress != 1)
|
||||
.isNotEmpty;
|
||||
|
||||
return shouldShow
|
||||
? Badge.count(
|
||||
count: itemCount,
|
||||
textColor: context.colorScheme.onPrimary,
|
||||
backgroundColor: context.colorScheme.primary,
|
||||
child: FloatingActionButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
side: BorderSide(color: context.colorScheme.outlineVariant, width: 1),
|
||||
),
|
||||
backgroundColor: context.isDarkTheme
|
||||
? context.colorScheme.surfaceContainer
|
||||
: context.colorScheme.surfaceBright,
|
||||
elevation: 2,
|
||||
onPressed: () {
|
||||
context.pushRoute(const DownloadInfoRoute());
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
isDownloading
|
||||
? Icon(Icons.downloading_rounded, color: context.colorScheme.primary, size: 28)
|
||||
: Icon(
|
||||
Icons.download_done,
|
||||
color: context.isDarkTheme ? Colors.green[200] : Colors.green[400],
|
||||
size: 28,
|
||||
),
|
||||
if (isDownloading)
|
||||
const SizedBox(
|
||||
height: 31,
|
||||
width: 31,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
backgroundColor: Colors.transparent,
|
||||
value: null, // Indeterminate progress
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
|
|||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
|
|
@ -649,7 +650,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
appBar: const ViewerTopAppBar(),
|
||||
extendBody: true,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: PhotoViewGallery.builder(
|
||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||
body: Stack(
|
||||
children: [
|
||||
PhotoViewGallery.builder(
|
||||
gaplessPlayback: true,
|
||||
loadingBuilder: _placeholderBuilder,
|
||||
pageController: pageController,
|
||||
|
|
@ -664,6 +668,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||
enablePanAlways: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: showingBottomSheet
|
||||
? const SizedBox.shrink()
|
||||
: const Column(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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/download_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';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
|
|
@ -56,6 +57,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||
|
||||
final actions = <Widget>[
|
||||
if (asset.hasRemote) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
||||
if (album != null && album.isActivityEnabled && album.isShared)
|
||||
IconButton(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
|
|||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
|
|
@ -55,6 +56,7 @@ class Timeline extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||
body: LayoutBuilder(
|
||||
builder: (_, constraints) => ProviderScope(
|
||||
overrides: [
|
||||
|
|
|
|||
|
|
@ -356,7 +356,6 @@ class ActionNotifier extends Notifier<void> {
|
|||
|
||||
Future<ActionResult> downloadAll(ActionSource source) async {
|
||||
final assets = _getAssets(source).whereType<RemoteAsset>().toList(growable: false);
|
||||
|
||||
try {
|
||||
final didEnqueue = await _service.downloadAll(assets);
|
||||
final enqueueCount = didEnqueue.where((e) => e).length;
|
||||
|
|
|
|||
|
|
@ -90,7 +90,11 @@ class DownloadRepository {
|
|||
final isVideo = asset.isVideo;
|
||||
final url = getOriginalUrlForRemoteId(id);
|
||||
|
||||
if (Platform.isAndroid || livePhotoVideoId == null || isVideo) {
|
||||
// on iOS it cannot link the image, check if the filename has .MP extension
|
||||
// to avoid downloading the video part
|
||||
final isAndroidMotionPhoto = asset.name.contains(".MP");
|
||||
|
||||
if (Platform.isAndroid || livePhotoVideoId == null || isVideo || isAndroidMotionPhoto) {
|
||||
tasks[taskIndex++] = DownloadTask(
|
||||
taskId: id,
|
||||
url: url,
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
|||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/download_info.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||
import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart';
|
||||
|
|
@ -345,6 +346,7 @@ class AppRouter extends RootStackRouter {
|
|||
AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
// required to handle all deeplinks in deep_link.service.dart
|
||||
// auto_route_library#1722
|
||||
RedirectRoute(path: '*', redirectTo: '/'),
|
||||
|
|
|
|||
|
|
@ -688,6 +688,22 @@ class CropImageRouteArgs {
|
|||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [DownloadInfoPage]
|
||||
class DownloadInfoRoute extends PageRouteInfo<void> {
|
||||
const DownloadInfoRoute({List<PageRouteInfo>? children})
|
||||
: super(DownloadInfoRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'DownloadInfoRoute';
|
||||
|
||||
static PageInfo page = PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const DownloadInfoPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [DriftActivitiesPage]
|
||||
class DriftActivitiesRoute extends PageRouteInfo<void> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue