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",
|
"documentation": "Documentation",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"download_action_prompt": "Downloading {count} assets",
|
||||||
"download_canceled": "Download canceled",
|
"download_canceled": "Download canceled",
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
"download_enqueue": "Download enqueued",
|
"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:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.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/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 {
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.download,
|
iconData: Icons.download,
|
||||||
label: "download".t(context: context),
|
label: "download".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,8 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
if (asset.hasRemote) ...[
|
if (asset.hasRemote) ...[
|
||||||
const ShareLinkActionButton(source: ActionSource.viewer),
|
const ShareLinkActionButton(source: ActionSource.viewer),
|
||||||
const ArchiveActionButton(source: ActionSource.viewer),
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
if (!asset.hasLocal) const DownloadActionButton(),
|
if (!asset.hasLocal)
|
||||||
|
const DownloadActionButton(source: ActionSource.viewer),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.viewer)
|
? const TrashActionButton(source: ActionSource.viewer)
|
||||||
: const DeletePermanentActionButton(source: ActionSource.viewer),
|
: const DeletePermanentActionButton(source: ActionSource.viewer),
|
||||||
|
@ -38,7 +38,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnArchiveActionButton(source: ActionSource.timeline),
|
const UnArchiveActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(
|
: const DeletePermanentActionButton(
|
||||||
|
@ -38,7 +38,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const UnFavoriteActionButton(source: ActionSource.timeline),
|
const UnFavoriteActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(
|
: const DeletePermanentActionButton(
|
||||||
|
@ -38,7 +38,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(
|
: const DeletePermanentActionButton(
|
||||||
|
@ -18,7 +18,7 @@ class LockedFolderBottomSheet extends ConsumerWidget {
|
|||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
ShareActionButton(source: ActionSource.timeline),
|
ShareActionButton(source: ActionSource.timeline),
|
||||||
DownloadActionButton(),
|
DownloadActionButton(source: ActionSource.timeline),
|
||||||
DeletePermanentActionButton(source: ActionSource.timeline),
|
DeletePermanentActionButton(source: ActionSource.timeline),
|
||||||
RemoveFromLockFolderActionButton(source: ActionSource.timeline),
|
RemoveFromLockFolderActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
|
@ -16,7 +16,7 @@ class PartnerDetailBottomSheet extends ConsumerWidget {
|
|||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
ShareActionButton(source: ActionSource.timeline),
|
ShareActionButton(source: ActionSource.timeline),
|
||||||
DownloadActionButton(),
|
DownloadActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
|||||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||||
const ArchiveActionButton(source: ActionSource.timeline),
|
const ArchiveActionButton(source: ActionSource.timeline),
|
||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(),
|
const DownloadActionButton(source: ActionSource.timeline),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton(source: ActionSource.timeline)
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(
|
: const DeletePermanentActionButton(
|
||||||
|
@ -41,7 +41,8 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> _getRemoteIdsForSource(ActionSource source) {
|
List<String> _getRemoteIdsForSource(ActionSource source) {
|
||||||
return _getIdsForSource<RemoteAsset>(source)
|
return _getAssets(source)
|
||||||
|
.whereType<RemoteAsset>()
|
||||||
.toIds()
|
.toIds()
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
@ -63,7 +64,8 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
|
|
||||||
List<String> _getOwnedRemoteIdsForSource(ActionSource source) {
|
List<String> _getOwnedRemoteIdsForSource(ActionSource source) {
|
||||||
final ownerId = ref.read(currentUserProvider)?.id;
|
final ownerId = ref.read(currentUserProvider)?.id;
|
||||||
return _getIdsForSource<RemoteAsset>(source)
|
return _getAssets(source)
|
||||||
|
.whereType<RemoteAsset>()
|
||||||
.ownedAssets(ownerId)
|
.ownedAssets(ownerId)
|
||||||
.toIds()
|
.toIds()
|
||||||
.toList(growable: false);
|
.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> {
|
extension on Iterable<RemoteAsset> {
|
||||||
|
@ -1,10 +1,28 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/download.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
|
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
|
||||||
|
|
||||||
class 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)? onImageDownloadStatus;
|
||||||
|
|
||||||
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
|
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
|
||||||
@ -14,19 +32,19 @@ class DownloadRepository {
|
|||||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||||
|
|
||||||
DownloadRepository() {
|
DownloadRepository() {
|
||||||
FileDownloader().registerCallbacks(
|
_downloader.registerCallbacks(
|
||||||
group: downloadGroupImage,
|
group: downloadGroupImage,
|
||||||
taskStatusCallback: (update) => onImageDownloadStatus?.call(update),
|
taskStatusCallback: (update) => onImageDownloadStatus?.call(update),
|
||||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||||
);
|
);
|
||||||
|
|
||||||
FileDownloader().registerCallbacks(
|
_downloader.registerCallbacks(
|
||||||
group: downloadGroupVideo,
|
group: downloadGroupVideo,
|
||||||
taskStatusCallback: (update) => onVideoDownloadStatus?.call(update),
|
taskStatusCallback: (update) => onVideoDownloadStatus?.call(update),
|
||||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||||
);
|
);
|
||||||
|
|
||||||
FileDownloader().registerCallbacks(
|
_downloader.registerCallbacks(
|
||||||
group: downloadGroupLivePhoto,
|
group: downloadGroupLivePhoto,
|
||||||
taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update),
|
taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update),
|
||||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||||
@ -34,25 +52,87 @@ class DownloadRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
|
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
|
||||||
return FileDownloader().enqueueAll(tasks);
|
return _downloader.enqueueAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAllTrackingRecords() {
|
Future<void> deleteAllTrackingRecords() {
|
||||||
return FileDownloader().database.deleteAllRecords();
|
return _downloader.database.deleteAllRecords();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> cancel(String id) {
|
Future<bool> cancel(String id) {
|
||||||
return FileDownloader().cancelTaskWithId(id);
|
return _downloader.cancelTaskWithId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TaskRecord>> getLiveVideoTasks() {
|
Future<List<TaskRecord>> getLiveVideoTasks() {
|
||||||
return FileDownloader().database.allRecordsWithStatus(
|
return _downloader.database.allRecordsWithStatus(
|
||||||
TaskStatus.complete,
|
TaskStatus.complete,
|
||||||
group: downloadGroupLivePhoto,
|
group: downloadGroupLivePhoto,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteRecordsWithIds(List<String> ids) {
|
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:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
@ -23,6 +25,7 @@ final actionServiceProvider = Provider<ActionService>(
|
|||||||
ref.watch(driftAlbumApiRepositoryProvider),
|
ref.watch(driftAlbumApiRepositoryProvider),
|
||||||
ref.watch(remoteAlbumRepository),
|
ref.watch(remoteAlbumRepository),
|
||||||
ref.watch(assetMediaRepositoryProvider),
|
ref.watch(assetMediaRepositoryProvider),
|
||||||
|
ref.watch(downloadRepositoryProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -33,6 +36,7 @@ class ActionService {
|
|||||||
final DriftAlbumApiRepository _albumApiRepository;
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||||
final AssetMediaRepository _assetMediaRepository;
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
|
final DownloadRepository _downloadRepository;
|
||||||
|
|
||||||
const ActionService(
|
const ActionService(
|
||||||
this._assetApiRepository,
|
this._assetApiRepository,
|
||||||
@ -41,6 +45,7 @@ class ActionService {
|
|||||||
this._albumApiRepository,
|
this._albumApiRepository,
|
||||||
this._remoteAlbumRepository,
|
this._remoteAlbumRepository,
|
||||||
this._assetMediaRepository,
|
this._assetMediaRepository,
|
||||||
|
this._downloadRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||||
@ -191,4 +196,8 @@ class ActionService {
|
|||||||
Future<int> shareAssets(List<BaseAsset> assets) {
|
Future<int> shareAssets(List<BaseAsset> assets) {
|
||||||
return _assetMediaRepository.shareAssets(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