mirror of
https://github.com/immich-app/immich.git
synced 2025-12-20 17:25:35 +03:00
refactor(server): stacks (#11453)
* refactor: stacks * mobile: get it built * chore: feedback * fix: sync and duplicates * mobile: remove old stack reference * chore: add primary asset id * revert change to asset entity * mobile: refactor mobile api * mobile: sync stack info after creating stack * mobile: update timeline after deleting stack * server: update asset updatedAt when stack is deleted * mobile: simplify action * mobile: rename to match dto property * fix: web test --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/asset_stack.service.dart';
|
||||
import 'package:immich_mobile/services/stack.service.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||
@@ -344,11 +344,9 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
if (!selectionEnabledHook.value || selection.value.length < 2) {
|
||||
return;
|
||||
}
|
||||
final parent = selection.value.elementAt(0);
|
||||
selection.value.remove(parent);
|
||||
await ref.read(assetStackServiceProvider).updateStack(
|
||||
parent,
|
||||
childrenToAdd: selection.value.toList(),
|
||||
|
||||
await ref.read(stackServiceProvider).createStack(
|
||||
selection.value.map((e) => e.remoteId!).toList(),
|
||||
);
|
||||
} finally {
|
||||
processing.value = false;
|
||||
|
||||
@@ -107,16 +107,16 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
right: 8,
|
||||
child: Row(
|
||||
children: [
|
||||
if (asset.stackChildrenCount > 1)
|
||||
if (asset.stackCount > 1)
|
||||
Text(
|
||||
"${asset.stackChildrenCount}",
|
||||
"${asset.stackCount}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (asset.stackChildrenCount > 1)
|
||||
if (asset.stackCount > 1)
|
||||
const SizedBox(
|
||||
width: 3,
|
||||
),
|
||||
@@ -208,7 +208,7 @@ class ThumbnailImage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
if (!asset.isImage) buildVideoIcon(),
|
||||
if (asset.stackChildrenCount > 0) buildStackIcon(),
|
||||
if (asset.stackCount > 0) buildStackIcon(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'package:immich_mobile/providers/album/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/services/asset_stack.service.dart';
|
||||
import 'package:immich_mobile/services/stack.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||
@@ -49,11 +49,10 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
|
||||
|
||||
final stack = showStack && asset.stackChildrenCount > 0
|
||||
final stackItems = showStack && asset.stackCount > 0
|
||||
? ref.watch(assetStackStateProvider(asset))
|
||||
: <Asset>[];
|
||||
final stackElements = showStack ? [asset, ...stack] : <Asset>[];
|
||||
bool isParent = stackIndex == -1 || stackIndex == 0;
|
||||
bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null;
|
||||
final navStack = AutoRouter.of(context).stackData;
|
||||
final isTrashEnabled =
|
||||
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
|
||||
@@ -76,7 +75,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
{asset},
|
||||
force: force,
|
||||
);
|
||||
if (isDeleted && isParent) {
|
||||
if (isDeleted && isStackPrimaryAsset) {
|
||||
// Workaround for asset remaining in the gallery
|
||||
renderList.deleteAsset(asset);
|
||||
|
||||
@@ -98,7 +97,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
final isDeleted = await onDelete(false);
|
||||
if (isDeleted) {
|
||||
// Can only trash assets stored in server. Local assets are always permanently removed for now
|
||||
if (context.mounted && asset.isRemote && isParent) {
|
||||
if (context.mounted && asset.isRemote && isStackPrimaryAsset) {
|
||||
ImmichToast.show(
|
||||
durationInSecond: 1,
|
||||
context: context,
|
||||
@@ -127,6 +126,16 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
unStack() async {
|
||||
if (asset.stackId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(stackServiceProvider)
|
||||
.deleteStack(asset.stackId!, [asset, ...stackItems]);
|
||||
}
|
||||
|
||||
void showStackActionItems() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
@@ -138,74 +147,13 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isParent)
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.bookmark_border_outlined,
|
||||
size: 24,
|
||||
),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(assetStackServiceProvider)
|
||||
.updateStackParent(
|
||||
asset,
|
||||
stackElements.elementAt(stackIndex),
|
||||
);
|
||||
ctx.pop();
|
||||
context.maybePop();
|
||||
},
|
||||
title: const Text(
|
||||
"viewer_stack_use_as_main_asset",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.copy_all_outlined,
|
||||
size: 24,
|
||||
),
|
||||
onTap: () async {
|
||||
if (isParent) {
|
||||
await ref
|
||||
.read(assetStackServiceProvider)
|
||||
.updateStackParent(
|
||||
asset,
|
||||
stackElements
|
||||
.elementAt(1), // Next asset as parent
|
||||
);
|
||||
// Remove itself from stack
|
||||
await ref.read(assetStackServiceProvider).updateStack(
|
||||
stackElements.elementAt(1),
|
||||
childrenToRemove: [asset],
|
||||
);
|
||||
ctx.pop();
|
||||
context.maybePop();
|
||||
} else {
|
||||
await ref.read(assetStackServiceProvider).updateStack(
|
||||
asset,
|
||||
childrenToRemove: [
|
||||
stackElements.elementAt(stackIndex),
|
||||
],
|
||||
);
|
||||
removeAssetFromStack();
|
||||
ctx.pop();
|
||||
}
|
||||
},
|
||||
title: const Text(
|
||||
"viewer_remove_from_stack",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).tr(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.filter_none_outlined,
|
||||
size: 18,
|
||||
),
|
||||
onTap: () async {
|
||||
await ref.read(assetStackServiceProvider).updateStack(
|
||||
asset,
|
||||
childrenToRemove: stack,
|
||||
);
|
||||
await unStack();
|
||||
ctx.pop();
|
||||
context.maybePop();
|
||||
},
|
||||
@@ -255,7 +203,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
|
||||
handleArchive() {
|
||||
ref.read(assetProvider.notifier).toggleArchive([asset]);
|
||||
if (isParent) {
|
||||
if (isStackPrimaryAsset) {
|
||||
context.maybePop();
|
||||
return;
|
||||
}
|
||||
@@ -346,7 +294,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
tooltip: 'control_bottom_app_bar_archive'.tr(),
|
||||
): (_) => handleArchive(),
|
||||
},
|
||||
if (isOwner && stack.isNotEmpty)
|
||||
if (isOwner && asset.stackCount > 0)
|
||||
{
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.burst_mode_outlined),
|
||||
|
||||
Reference in New Issue
Block a user