mirror of
https://github.com/immich-app/immich.git
synced 2026-05-20 23:02:32 -04:00
refactor
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -142,13 +143,18 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(album.id, [latest.remoteId!]);
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addedCount == 0) {
|
||||
if (!result.success) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.count == 0) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}),
|
||||
@@ -159,7 +165,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
|
||||
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||
// Refresh the "Appears in" list on the asset's info panel.
|
||||
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
@@ -22,11 +23,12 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/utils/add_to_album.utils.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class GeneralBottomSheet extends ConsumerStatefulWidget {
|
||||
final double? minChildSize;
|
||||
@@ -60,11 +62,20 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
);
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets.toList(growable: false);
|
||||
if (selectedAssets.isEmpty) {
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await addSelectedAssetsToAlbum(context, ref, album, selectedAssets);
|
||||
if (!result.success) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.count == 0
|
||||
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
|
||||
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
@@ -7,8 +8,8 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/utils/add_to_album.utils.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class LocalAlbumBottomSheet extends ConsumerStatefulWidget {
|
||||
const LocalAlbumBottomSheet({super.key});
|
||||
@@ -34,12 +35,24 @@ class _LocalAlbumBottomSheetState extends ConsumerState<LocalAlbumBottomSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = ref.read(multiSelectProvider).selectedAssets.toList(growable: false);
|
||||
if (selectedAssets.isEmpty) {
|
||||
Future<void> addToAlbum(RemoteAlbum album) async {
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await addSelectedAssetsToAlbum(context, ref, album, selectedAssets);
|
||||
|
||||
if (!result.success) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.count == 0
|
||||
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
|
||||
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
@@ -58,7 +71,7 @@ class _LocalAlbumBottomSheetState extends ConsumerState<LocalAlbumBottomSheet> {
|
||||
],
|
||||
slivers: [
|
||||
const AddToAlbumHeader(),
|
||||
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+13
-18
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||
@@ -21,7 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -57,28 +56,24 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId;
|
||||
|
||||
Future<void> addAssetsToAlbum(RemoteAlbum album) async {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
if (selectedAssets.isEmpty) {
|
||||
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final addedCount = await ref
|
||||
.read(remoteAlbumProvider.notifier)
|
||||
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
|
||||
|
||||
if (addedCount != selectedAssets.length) {
|
||||
if (!result.success) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
|
||||
msg: 'scaffold_body_error_occurred'.t(context: context),
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.count == 0
|
||||
? 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name})
|
||||
: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
|
||||
@@ -5,12 +5,15 @@ import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
|
||||
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
|
||||
@@ -371,6 +374,52 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> addToAlbum(ActionSource source, RemoteAlbum album) async {
|
||||
final selected = _getAssets(source).toList(growable: false);
|
||||
if (selected.isEmpty) {
|
||||
return const ActionResult(count: 0, success: true);
|
||||
}
|
||||
|
||||
final candidates = RemoteAlbumService.categorizeCandidates(selected);
|
||||
final remoteIds = candidates.remoteAssetIds;
|
||||
final localAssets = candidates.localAssetsToUpload;
|
||||
final albumNotifier = ref.read(remoteAlbumProvider.notifier);
|
||||
|
||||
// Clear multi-select so the timeline tiles can render upload progress overlays.
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
int addedRemote = 0;
|
||||
if (remoteIds.isNotEmpty) {
|
||||
try {
|
||||
addedRemote = await albumNotifier.addAssets(album.id, remoteIds);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to add assets to album ${album.id}', error, stack);
|
||||
return ActionResult(count: 0, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (localAssets.isEmpty) {
|
||||
return ActionResult(count: addedRemote, success: true);
|
||||
}
|
||||
|
||||
final uploadResult = await upload(
|
||||
source,
|
||||
assets: localAssets,
|
||||
onAssetUploaded: (asset, remoteId) async {
|
||||
final added = await albumNotifier.linkUploadedAssetToAlbum(album.id, asset, remoteId);
|
||||
if (added == 0) {
|
||||
throw StateError('Uploaded asset was not added to album ${album.id}');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return ActionResult(
|
||||
count: addedRemote + uploadResult.count,
|
||||
success: uploadResult.success,
|
||||
error: uploadResult.error,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
try {
|
||||
@@ -537,16 +586,19 @@ class ActionNotifier extends Notifier<void> {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await Future.wait(postUploadTasks);
|
||||
final successfulCount = uploadedAssetIds.difference(failedAssetIds).length;
|
||||
final isSuccess = successfulCount == assetsToUpload.length && failedAssetIds.isEmpty;
|
||||
final successCount = uploadedAssetIds.difference(failedAssetIds).length;
|
||||
final isSuccess = successCount == assetsToUpload.length && failedAssetIds.isEmpty;
|
||||
|
||||
return ActionResult(
|
||||
count: successfulCount,
|
||||
count: successCount,
|
||||
success: isSuccess,
|
||||
error: isSuccess ? null : 'Failed to upload ${assetsToUpload.length - successfulCount} assets',
|
||||
error: isSuccess ? null : 'Failed to upload ${assetsToUpload.length - successCount} assets',
|
||||
);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed manually upload assets', error, stack);
|
||||
|
||||
return ActionResult(
|
||||
count: uploadedAssetIds.difference(failedAssetIds).length,
|
||||
success: false,
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
/// Adds [selectedAssets] to [album], uploading any local assets through the
|
||||
/// manual upload flow (so the timeline thumbnails show progress) and linking
|
||||
/// each one to the album as its upload finishes.
|
||||
Future<void> addSelectedAssetsToAlbum(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
RemoteAlbum album,
|
||||
List<BaseAsset> selectedAssets,
|
||||
) async {
|
||||
if (selectedAssets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final candidates = RemoteAlbumService.categorizeCandidates(selectedAssets);
|
||||
final remoteIds = candidates.remoteAssetIds;
|
||||
final localAssets = candidates.localAssetsToUpload;
|
||||
|
||||
// Capture notifiers up front: the WidgetRef is tied to the calling widget
|
||||
// and may be disposed (e.g., when the bottom sheet closes) before the
|
||||
// background upload callbacks fire.
|
||||
final albumNotifier = ref.read(remoteAlbumProvider.notifier);
|
||||
final actionNotifier = ref.read(actionProvider.notifier);
|
||||
|
||||
// Clear multi-select so the timeline tiles can render upload progress overlays.
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
int addedRemote = 0;
|
||||
if (remoteIds.isNotEmpty) {
|
||||
try {
|
||||
addedRemote = await albumNotifier.addAssets(album.id, remoteIds);
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (localAssets.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: addedRemote == 0
|
||||
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
|
||||
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await actionNotifier.upload(
|
||||
ActionSource.timeline,
|
||||
assets: localAssets,
|
||||
onAssetUploaded: (asset, remoteId) async {
|
||||
final added = await albumNotifier.linkUploadedAssetToAlbum(album.id, asset, remoteId);
|
||||
if (added == 0) {
|
||||
throw StateError('Uploaded asset was not added to album ${album.id}');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user