mirror of
https://github.com/immich-app/immich.git
synced 2026-06-12 19:11:52 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cba557ecd | |||
| 254eecb73e | |||
| f33b64e7dc |
@@ -1063,6 +1063,8 @@
|
||||
"error_title": "Error - Something went wrong",
|
||||
"error_while_navigating": "Error while navigating to asset",
|
||||
"errors": {
|
||||
"add_to_album_no_permission": "Cannot add assets you don't own",
|
||||
"add_to_album_not_found": "Some assets could not be found",
|
||||
"cannot_navigate_next_asset": "Cannot navigate to the next asset",
|
||||
"cannot_navigate_previous_asset": "Cannot navigate to previous asset",
|
||||
"cant_apply_changes": "Can't apply changes",
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dar
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:openapi/api.dart' show BulkIdErrorReason;
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// Categorizes a heterogeneous asset selection into the candidates that can
|
||||
@@ -176,12 +177,15 @@ class RemoteAlbumService {
|
||||
return _repository.getAssets(albumId);
|
||||
}
|
||||
|
||||
Future<int> addAssets({required String albumId, required List<String> assetIds}) async {
|
||||
Future<({int added, Map<BulkIdErrorReason, int> failureReasons})> addAssets({
|
||||
required String albumId,
|
||||
required List<String> assetIds,
|
||||
}) async {
|
||||
final album = await _albumApiRepository.addAssets(albumId, assetIds);
|
||||
|
||||
await _repository.addAssets(albumId, album.added);
|
||||
|
||||
return album.added.length;
|
||||
return (added: album.added.length, failureReasons: album.failureReasons);
|
||||
}
|
||||
|
||||
/// !TODO The name here is not clear as we have addAssets method above,
|
||||
@@ -197,7 +201,7 @@ class RemoteAlbumService {
|
||||
}) async {
|
||||
int addedCount = 0;
|
||||
if (candidates.remoteAssetIds.isNotEmpty) {
|
||||
addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds);
|
||||
addedCount += (await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds)).added;
|
||||
}
|
||||
if (candidates.localAssetsToUpload.isNotEmpty) {
|
||||
addedCount += await _uploadAndAddLocals(
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.d
|
||||
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/utils/album_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
@@ -154,17 +155,10 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.count == 0) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
} else {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
|
||||
);
|
||||
final (msg, toastType) = resolveAlbumAddToast(result, album.name, context);
|
||||
ImmichToast.show(context: context, msg: msg, toastType: toastType);
|
||||
|
||||
if (result.count > 0) {
|
||||
// Refresh the "Appears in" list on the asset's info panel.
|
||||
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (addedCount != remoteAssets.length) {
|
||||
if (addedCount.added != remoteAssets.length) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: 'add_to_album_bottom_sheet_already_exists'.t(args: {"album": album.name}),
|
||||
|
||||
@@ -28,6 +28,7 @@ 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/album_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class GeneralBottomSheet extends ConsumerStatefulWidget {
|
||||
@@ -72,12 +73,8 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
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}),
|
||||
);
|
||||
final (msg, toastType) = resolveAlbumAddToast(result, album.name, context);
|
||||
ImmichToast.show(context: context, msg: msg, toastType: toastType);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.d
|
||||
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/server_info.provider.dart';
|
||||
import 'package:immich_mobile/utils/album_toast.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -71,12 +72,8 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
return;
|
||||
}
|
||||
|
||||
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}),
|
||||
);
|
||||
final (msg, toastType) = resolveAlbumAddToast(result, album.name, context);
|
||||
ImmichToast.show(context: context, msg: msg, toastType: toastType);
|
||||
}
|
||||
|
||||
Future<void> onKeyboardExpand() {
|
||||
|
||||
@@ -34,14 +34,24 @@ final actionProvider = NotifierProvider<ActionNotifier, void>(ActionNotifier.new
|
||||
|
||||
class ActionResult {
|
||||
final int count;
|
||||
final Map<BulkIdErrorReason, int> failureReasons;
|
||||
final bool success;
|
||||
final String? error;
|
||||
final List<String> remoteAssetIds;
|
||||
|
||||
const ActionResult({required this.count, required this.success, this.error, this.remoteAssetIds = const []});
|
||||
const ActionResult({
|
||||
required this.count,
|
||||
this.failureReasons = const {},
|
||||
required this.success,
|
||||
this.error,
|
||||
this.remoteAssetIds = const [],
|
||||
});
|
||||
|
||||
int get duplicate => failureReasons[BulkIdErrorReason.duplicate] ?? 0;
|
||||
|
||||
@override
|
||||
String toString() => 'ActionResult(count: $count, success: $success, error: $error, remoteAssetIds: $remoteAssetIds)';
|
||||
String toString() =>
|
||||
'ActionResult(count: $count, failureReasons: $failureReasons, success: $success, error: $error, remoteAssetIds: $remoteAssetIds)';
|
||||
}
|
||||
|
||||
class ActionNotifier extends Notifier<void> {
|
||||
@@ -389,9 +399,12 @@ class ActionNotifier extends Notifier<void> {
|
||||
final albumNotifier = ref.read(remoteAlbumProvider.notifier);
|
||||
|
||||
int addedRemote = 0;
|
||||
Map<BulkIdErrorReason, int> remoteFailures = {};
|
||||
if (remoteIds.isNotEmpty) {
|
||||
try {
|
||||
addedRemote = await albumNotifier.addAssets(album.id, remoteIds);
|
||||
final result = await albumNotifier.addAssets(album.id, remoteIds);
|
||||
addedRemote = result.added;
|
||||
remoteFailures = result.failureReasons;
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to add assets to album ${album.id}', error, stack);
|
||||
return ActionResult(count: 0, success: false, error: error.toString());
|
||||
@@ -405,7 +418,7 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
|
||||
if (localAssets.isEmpty) {
|
||||
return ActionResult(count: addedRemote, success: true);
|
||||
return ActionResult(count: addedRemote, failureReasons: remoteFailures, success: true);
|
||||
}
|
||||
|
||||
final uploadResult = await upload(
|
||||
@@ -418,6 +431,7 @@ class ActionNotifier extends Notifier<void> {
|
||||
|
||||
return ActionResult(
|
||||
count: addedRemote + uploadResult.count,
|
||||
failureReasons: remoteFailures,
|
||||
success: uploadResult.success,
|
||||
error: uploadResult.error,
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' show BulkIdErrorReason;
|
||||
|
||||
class RemoteAlbumState {
|
||||
final List<RemoteAlbum> albums;
|
||||
@@ -201,12 +202,15 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
|
||||
return _remoteAlbumService.getAssets(albumId);
|
||||
}
|
||||
|
||||
Future<int> addAssets(String albumId, List<String> assetIds) async {
|
||||
final added = await _remoteAlbumService.addAssets(albumId: albumId, assetIds: assetIds);
|
||||
if (added > 0) {
|
||||
Future<({int added, Map<BulkIdErrorReason, int> failureReasons})> addAssets(
|
||||
String albumId,
|
||||
List<String> assetIds,
|
||||
) async {
|
||||
final result = await _remoteAlbumService.addAssets(albumId: albumId, assetIds: assetIds);
|
||||
if (result.added > 0) {
|
||||
await _refreshAlbumInState(albumId);
|
||||
}
|
||||
return added;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Links a freshly-uploaded local asset to an album using its new remote ID,
|
||||
|
||||
@@ -48,7 +48,7 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
return (removed: removed, failed: failed);
|
||||
}
|
||||
|
||||
Future<({List<String> added, List<String> failed})> addAssets(
|
||||
Future<({List<String> added, Map<BulkIdErrorReason, int> failureReasons})> addAssets(
|
||||
String albumId,
|
||||
Iterable<String> assetIds, {
|
||||
Future<void>? abortTrigger,
|
||||
@@ -56,16 +56,18 @@ class DriftAlbumApiRepository extends ApiRepository {
|
||||
final response = await checkNull(
|
||||
_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()), abortTrigger: abortTrigger),
|
||||
);
|
||||
final List<String> added = [], failed = [];
|
||||
final List<String> added = [];
|
||||
final Map<BulkIdErrorReason, int> failureReasons = {};
|
||||
for (final dto in response) {
|
||||
if (dto.success) {
|
||||
added.add(dto.id);
|
||||
} else {
|
||||
failed.add(dto.id);
|
||||
final reason = dto.error.orElse(null) ?? BulkIdErrorReason.unknown;
|
||||
failureReasons[reason] = (failureReasons[reason] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return (added: added, failed: failed);
|
||||
return (added: added, failureReasons: failureReasons);
|
||||
}
|
||||
|
||||
Future<RemoteAlbum> updateAlbum(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
// ignore: import_rule_openapi
|
||||
import 'package:openapi/api.dart' show BulkIdErrorReason;
|
||||
|
||||
/// Maps an [ActionResult] from an add-to-album operation to the appropriate
|
||||
/// toast message and type, mirroring the web's priority order:
|
||||
/// 1. Some added (with or without duplicates) → success/conflicts message
|
||||
/// 2. All duplicates → "already in album"
|
||||
/// 3. No permission → specific error
|
||||
/// 4. Not found → specific error
|
||||
/// 5. Anything else → generic error
|
||||
(String msg, ToastType type) resolveAlbumAddToast(ActionResult result, String albumName, BuildContext context) {
|
||||
final duplicate = result.failureReasons[BulkIdErrorReason.duplicate] ?? 0;
|
||||
|
||||
if (result.count > 0 && duplicate == 0) {
|
||||
return ('add_to_album_bottom_sheet_added'.t(context: context, args: {'album': albumName}), ToastType.info);
|
||||
}
|
||||
|
||||
if (result.count > 0 && duplicate > 0) {
|
||||
return (
|
||||
'home_page_add_to_album_conflicts'.t(
|
||||
context: context,
|
||||
args: {'added': result.count, 'album': albumName, 'failed': duplicate},
|
||||
),
|
||||
ToastType.info,
|
||||
);
|
||||
}
|
||||
|
||||
if (duplicate > 0) {
|
||||
return ('add_to_album_bottom_sheet_already_exists'.t(context: context, args: {'album': albumName}), ToastType.info);
|
||||
}
|
||||
|
||||
if ((result.failureReasons[BulkIdErrorReason.noPermission] ?? 0) > 0) {
|
||||
return ('errors.add_to_album_no_permission'.t(context: context), ToastType.error);
|
||||
}
|
||||
|
||||
if ((result.failureReasons[BulkIdErrorReason.notFound] ?? 0) > 0) {
|
||||
return ('errors.add_to_album_not_found'.t(context: context), ToastType.error);
|
||||
}
|
||||
|
||||
return ('scaffold_body_error_occurred'.t(context: context), ToastType.error);
|
||||
}
|
||||
Reference in New Issue
Block a user