mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
refactor(mobile): download button in new timeline (#20010)
* download button * minor improvements
This commit is contained in:
parent
fafb88d31c
commit
261818ddd9
@ -782,6 +782,7 @@
|
||||
"documentation": "Documentation",
|
||||
"done": "Done",
|
||||
"download": "Download",
|
||||
"download_action_prompt": "Downloading {count} assets",
|
||||
"download_canceled": "Download canceled",
|
||||
"download_complete": "Download complete",
|
||||
"download_enqueue": "Download enqueued",
|
||||
|
@ -1,16 +1,54 @@
|
||||
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/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.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 {
|
||||
const DownloadActionButton({super.key});
|
||||
final ActionSource source;
|
||||
|
||||
const DownloadActionButton({super.key, required this.source});
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await ref.read(actionProvider.notifier).downloadAll(source);
|
||||
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) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.download,
|
||||
label: "download".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
if (asset.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.viewer),
|
||||
const ArchiveActionButton(source: ActionSource.viewer),
|
||||
if (!asset.hasLocal) const DownloadActionButton(),
|
||||
if (!asset.hasLocal)
|
||||
const DownloadActionButton(source: ActionSource.viewer),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.viewer)
|
||||
: const DeletePermanentActionButton(source: ActionSource.viewer),
|
||||
|
@ -38,7 +38,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const DownloadActionButton(),
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(
|
||||
|
@ -38,7 +38,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const UnFavoriteActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const DownloadActionButton(),
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(
|
||||
|
@ -38,7 +38,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const DownloadActionButton(),
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(
|
||||
|
@ -18,7 +18,7 @@ class LockedFolderBottomSheet extends ConsumerWidget {
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
DownloadActionButton(),
|
||||
DownloadActionButton(source: ActionSource.timeline),
|
||||
DeletePermanentActionButton(source: ActionSource.timeline),
|
||||
RemoveFromLockFolderActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
|
@ -16,7 +16,7 @@ class PartnerDetailBottomSheet extends ConsumerWidget {
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
DownloadActionButton(),
|
||||
DownloadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ArchiveActionButton(source: ActionSource.timeline),
|
||||
const FavoriteActionButton(source: ActionSource.timeline),
|
||||
const DownloadActionButton(),
|
||||
const DownloadActionButton(source: ActionSource.timeline),
|
||||
isTrashEnable
|
||||
? const TrashActionButton(source: ActionSource.timeline)
|
||||
: const DeletePermanentActionButton(
|
||||
|
@ -41,7 +41,8 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
|
||||
List<String> _getRemoteIdsForSource(ActionSource source) {
|
||||
return _getIdsForSource<RemoteAsset>(source)
|
||||
return _getAssets(source)
|
||||
.whereType<RemoteAsset>()
|
||||
.toIds()
|
||||
.toList(growable: false);
|
||||
}
|
||||
@ -63,7 +64,8 @@ class ActionNotifier extends Notifier<void> {
|
||||
|
||||
List<String> _getOwnedRemoteIdsForSource(ActionSource source) {
|
||||
final ownerId = ref.read(currentUserProvider)?.id;
|
||||
return _getIdsForSource<RemoteAsset>(source)
|
||||
return _getAssets(source)
|
||||
.whereType<RemoteAsset>()
|
||||
.ownedAssets(ownerId)
|
||||
.toIds()
|
||||
.toList(growable: false);
|
||||
@ -331,6 +333,24 @@ 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;
|
||||
return ActionResult(count: enqueueCount, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to download assets', error, stack);
|
||||
return ActionResult(
|
||||
count: assets.length,
|
||||
success: false,
|
||||
error: error.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on Iterable<RemoteAsset> {
|
||||
|
@ -1,10 +1,28 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/download.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
|
||||
|
||||
class DownloadRepository {
|
||||
static final _downloader = FileDownloader();
|
||||
static final _dummyTask = DownloadTask(
|
||||
taskId: 'dummy',
|
||||
url: '',
|
||||
filename: 'dummy',
|
||||
group: '',
|
||||
updates: Updates.statusAndProgress,
|
||||
);
|
||||
static final _dummyMetadata = {'part': LivePhotosPart.image, 'id': ''};
|
||||
|
||||
void Function(TaskStatusUpdate)? onImageDownloadStatus;
|
||||
|
||||
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
|
||||
@ -14,19 +32,19 @@ class DownloadRepository {
|
||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||
|
||||
DownloadRepository() {
|
||||
FileDownloader().registerCallbacks(
|
||||
_downloader.registerCallbacks(
|
||||
group: downloadGroupImage,
|
||||
taskStatusCallback: (update) => onImageDownloadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
|
||||
FileDownloader().registerCallbacks(
|
||||
_downloader.registerCallbacks(
|
||||
group: downloadGroupVideo,
|
||||
taskStatusCallback: (update) => onVideoDownloadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
|
||||
FileDownloader().registerCallbacks(
|
||||
_downloader.registerCallbacks(
|
||||
group: downloadGroupLivePhoto,
|
||||
taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
@ -34,25 +52,87 @@ class DownloadRepository {
|
||||
}
|
||||
|
||||
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
|
||||
return FileDownloader().enqueueAll(tasks);
|
||||
return _downloader.enqueueAll(tasks);
|
||||
}
|
||||
|
||||
Future<void> deleteAllTrackingRecords() {
|
||||
return FileDownloader().database.deleteAllRecords();
|
||||
return _downloader.database.deleteAllRecords();
|
||||
}
|
||||
|
||||
Future<bool> cancel(String id) {
|
||||
return FileDownloader().cancelTaskWithId(id);
|
||||
return _downloader.cancelTaskWithId(id);
|
||||
}
|
||||
|
||||
Future<List<TaskRecord>> getLiveVideoTasks() {
|
||||
return FileDownloader().database.allRecordsWithStatus(
|
||||
TaskStatus.complete,
|
||||
group: downloadGroupLivePhoto,
|
||||
);
|
||||
return _downloader.database.allRecordsWithStatus(
|
||||
TaskStatus.complete,
|
||||
group: downloadGroupLivePhoto,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteRecordsWithIds(List<String> ids) {
|
||||
return FileDownloader().database.deleteRecordsWithIds(ids);
|
||||
return _downloader.database.deleteRecordsWithIds(ids);
|
||||
}
|
||||
|
||||
Future<List<bool>> downloadAllAssets(List<RemoteAsset> assets) async {
|
||||
if (assets.isEmpty) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
|
||||
final length = Platform.isAndroid ? assets.length : assets.length * 2;
|
||||
final tasks = List.filled(length, _dummyTask);
|
||||
int taskIndex = 0;
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
for (final asset in assets) {
|
||||
if (!asset.isRemoteOnly) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final id = asset.id;
|
||||
final livePhotoVideoId = asset.livePhotoVideoId;
|
||||
final isVideo = asset.isVideo;
|
||||
final url = getOriginalUrlForRemoteId(id);
|
||||
|
||||
if (Platform.isAndroid || livePhotoVideoId == null || isVideo) {
|
||||
tasks[taskIndex++] = DownloadTask(
|
||||
taskId: id,
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: asset.name,
|
||||
updates: Updates.statusAndProgress,
|
||||
group: isVideo ? downloadGroupVideo : downloadGroupImage,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
_dummyMetadata['part'] = LivePhotosPart.image;
|
||||
_dummyMetadata['id'] = id;
|
||||
tasks[taskIndex++] = DownloadTask(
|
||||
taskId: id,
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: asset.name,
|
||||
updates: Updates.statusAndProgress,
|
||||
group: downloadGroupLivePhoto,
|
||||
metaData: json.encode(_dummyMetadata),
|
||||
);
|
||||
|
||||
_dummyMetadata['part'] = LivePhotosPart.video;
|
||||
tasks[taskIndex++] = DownloadTask(
|
||||
taskId: livePhotoVideoId,
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: asset.name
|
||||
.toUpperCase()
|
||||
.replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'),
|
||||
updates: Updates.statusAndProgress,
|
||||
group: downloadGroupLivePhoto,
|
||||
metaData: json.encode(_dummyMetadata),
|
||||
);
|
||||
}
|
||||
if (taskIndex == 0) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
return _downloader.enqueueAll(tasks.slice(0, taskIndex));
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/repositories/download.repository.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
@ -23,6 +25,7 @@ final actionServiceProvider = Provider<ActionService>(
|
||||
ref.watch(driftAlbumApiRepositoryProvider),
|
||||
ref.watch(remoteAlbumRepository),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
ref.watch(downloadRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@ -33,6 +36,7 @@ class ActionService {
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final DownloadRepository _downloadRepository;
|
||||
|
||||
const ActionService(
|
||||
this._assetApiRepository,
|
||||
@ -41,6 +45,7 @@ class ActionService {
|
||||
this._albumApiRepository,
|
||||
this._remoteAlbumRepository,
|
||||
this._assetMediaRepository,
|
||||
this._downloadRepository,
|
||||
);
|
||||
|
||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||
@ -191,4 +196,8 @@ class ActionService {
|
||||
Future<int> shareAssets(List<BaseAsset> assets) {
|
||||
return _assetMediaRepository.shareAssets(assets);
|
||||
}
|
||||
|
||||
Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
|
||||
return _downloadRepository.downloadAllAssets(assets);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user