mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00: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:
parent
ca52cbace1
commit
8338657eaa
63 changed files with 2321 additions and 1152 deletions
|
|
@ -1,17 +1,17 @@
|
|||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { unstackAssets } from '$lib/utils/asset-utils';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { deleteStack } from '$lib/utils/asset-utils';
|
||||
import type { StackResponseDto } from '@immich/sdk';
|
||||
import { mdiImageMinusOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { OnAction } from './action';
|
||||
|
||||
export let stackedAssets: AssetResponseDto[];
|
||||
export let stack: StackResponseDto;
|
||||
export let onAction: OnAction;
|
||||
|
||||
const handleUnstack = async () => {
|
||||
const unstackedAssets = await unstackAssets(stackedAssets);
|
||||
const unstackedAssets = await deleteStack([stack.id]);
|
||||
if (unstackedAssets) {
|
||||
onAction({ type: AssetAction.UNSTACK, assets: unstackedAssets });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,13 @@
|
|||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { getAssetJobName, getSharedLink } from '$lib/utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { AssetJobName, AssetTypeEnum, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetTypeEnum,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
type StackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import {
|
||||
mdiAlertOutline,
|
||||
mdiCogRefreshOutline,
|
||||
|
|
@ -37,10 +43,9 @@
|
|||
|
||||
export let asset: AssetResponseDto;
|
||||
export let album: AlbumResponseDto | null = null;
|
||||
export let stackedAssets: AssetResponseDto[];
|
||||
export let stack: StackResponseDto | null = null;
|
||||
export let showDetailButton: boolean;
|
||||
export let showSlideshow = false;
|
||||
export let hasStackChildren = false;
|
||||
export let onZoomImage: () => void;
|
||||
export let onCopyImage: () => void;
|
||||
export let onAction: OnAction;
|
||||
|
|
@ -136,8 +141,8 @@
|
|||
{/if}
|
||||
|
||||
{#if isOwner}
|
||||
{#if hasStackChildren}
|
||||
<UnstackAction {stackedAssets} {onAction} />
|
||||
{#if stack}
|
||||
<UnstackAction {stack} {onAction} />
|
||||
{/if}
|
||||
{#if album}
|
||||
<SetAlbumCoverAction {asset} {album} />
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@
|
|||
type ActivityResponseDto,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
getStack,
|
||||
type StackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { mdiImageBrokenVariant } from '@mdi/js';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
|
|
@ -74,7 +76,6 @@
|
|||
}>();
|
||||
|
||||
let appearsInAlbums: AlbumResponseDto[] = [];
|
||||
let stackedAssets: AssetResponseDto[] = [];
|
||||
let shouldPlayMotionPhoto = false;
|
||||
let sharedLink = getSharedLink();
|
||||
let enableDetailPanel = asset.hasMetadata;
|
||||
|
|
@ -92,22 +93,28 @@
|
|||
|
||||
$: isFullScreen = fullscreenElement !== null;
|
||||
|
||||
$: {
|
||||
if (asset.stackCount && asset.stack) {
|
||||
stackedAssets = asset.stack;
|
||||
stackedAssets = [...stackedAssets, asset].sort(
|
||||
(a, b) => new Date(b.fileCreatedAt).getTime() - new Date(a.fileCreatedAt).getTime(),
|
||||
);
|
||||
let stack: StackResponseDto | null = null;
|
||||
|
||||
// if its a stack, add the next stack image in addition to the next asset
|
||||
if (asset.stackCount > 1) {
|
||||
preloadAssets.push(stackedAssets[1]);
|
||||
}
|
||||
const refreshStack = async () => {
|
||||
if (isSharedLink()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stackedAssets.map((a) => a.id).includes(asset.id)) {
|
||||
stackedAssets = [];
|
||||
if (asset.stack) {
|
||||
stack = await getStack({ id: asset.stack.id });
|
||||
}
|
||||
|
||||
if (!stack?.assets.some(({ id }) => id === asset.id)) {
|
||||
stack = null;
|
||||
}
|
||||
|
||||
if (stack && stack?.assets.length > 1) {
|
||||
preloadAssets.push(stack.assets[1]);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (asset) {
|
||||
handlePromiseError(refreshStack());
|
||||
}
|
||||
|
||||
$: {
|
||||
|
|
@ -215,15 +222,6 @@
|
|||
if (!sharedLink) {
|
||||
await handleGetAllAlbums();
|
||||
}
|
||||
|
||||
if (asset.stackCount && asset.stack) {
|
||||
stackedAssets = asset.stack;
|
||||
stackedAssets = [...stackedAssets, asset].sort(
|
||||
(a, b) => new Date(a.fileCreatedAt).getTime() - new Date(b.fileCreatedAt).getTime(),
|
||||
);
|
||||
} else {
|
||||
stackedAssets = [];
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
|
@ -392,8 +390,10 @@
|
|||
await handleGetAllAlbums();
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetAction.UNSTACK: {
|
||||
await closeViewer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -420,10 +420,9 @@
|
|||
<AssetViewerNavBar
|
||||
{asset}
|
||||
{album}
|
||||
{stackedAssets}
|
||||
{stack}
|
||||
showDetailButton={enableDetailPanel}
|
||||
showSlideshow={!!assetStore}
|
||||
hasStackChildren={stackedAssets.length > 0}
|
||||
onZoomImage={zoomToggle}
|
||||
onCopyImage={copyImage}
|
||||
onAction={handleAction}
|
||||
|
|
@ -568,7 +567,8 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if stackedAssets.length > 0 && withStacked}
|
||||
{#if stack && withStacked}
|
||||
{@const stackedAssets = stack.assets}
|
||||
<div
|
||||
id="stack-slideshow"
|
||||
class="z-[1002] flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 overflow-x-auto horizontal-scrollbar"
|
||||
|
|
|
|||
|
|
@ -170,14 +170,14 @@
|
|||
|
||||
<!-- Stacked asset -->
|
||||
|
||||
{#if asset.stackCount && showStackedIcon}
|
||||
{#if asset.stack && showStackedIcon}
|
||||
<div
|
||||
class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == undefined
|
||||
? 'top-0 right-0'
|
||||
: 'top-7 right-1'} z-20 flex place-items-center gap-1 text-xs font-medium text-white"
|
||||
>
|
||||
<span class="pr-2 pt-2 flex place-items-center gap-1">
|
||||
<p>{asset.stackCount.toLocaleString($locale)}</p>
|
||||
<p>{asset.stack.assetCount.toLocaleString($locale)}</p>
|
||||
<Icon path={mdiCameraBurst} size="24" />
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
|
||||
import { stackAssets, unstackAssets } from '$lib/utils/asset-utils';
|
||||
import { stackAssets, deleteStack } from '$lib/utils/asset-utils';
|
||||
import type { OnStack, OnUnstack } from '$lib/utils/actions';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
|
|
@ -30,8 +30,7 @@
|
|||
if (!stack) {
|
||||
return;
|
||||
}
|
||||
const assets = [selectedAssets[0], ...stack];
|
||||
const unstackedAssets = await unstackAssets(assets);
|
||||
const unstackedAssets = await deleteStack([stack.id]);
|
||||
if (unstackedAssets) {
|
||||
onUnstack?.(unstackedAssets);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
$: isFromExternalLibrary = !!asset.libraryId;
|
||||
$: assetData = JSON.stringify(asset, null, 2);
|
||||
$: stackCount = asset.stackCount;
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
@ -55,17 +54,17 @@
|
|||
{isSelected ? $t('keep') : $t('to_trash')}
|
||||
</div>
|
||||
|
||||
<!-- EXTERNAL LIBRARY / STACK COUNT CHIP-->
|
||||
<!-- EXTERNAL LIBRARY / STACK COUNT CHIP -->
|
||||
<div class="absolute top-2 right-3">
|
||||
{#if isFromExternalLibrary}
|
||||
<div class="bg-immich-primary/90 px-2 py-1 rounded-xl text-xs text-white">
|
||||
{$t('external')}
|
||||
</div>
|
||||
{/if}
|
||||
{#if stackCount != null && stackCount != 0}
|
||||
{#if asset.stack?.assetCount}
|
||||
<div class="bg-immich-primary/90 px-2 py-1 my-0.5 rounded-xl text-xs text-white">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="mr-1">{stackCount}</div>
|
||||
<div class="mr-1">{asset.stack.assetCount}</div>
|
||||
<Icon path={mdiImageMultipleOutline} size="18" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue