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:
Jason Rasmussen 2024-08-19 13:37:15 -04:00 committed by GitHub
parent ca52cbace1
commit 8338657eaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 2321 additions and 1152 deletions

View file

@ -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 });
}

View file

@ -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} />

View file

@ -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"

View file

@ -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>

View file

@ -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);
}

View file

@ -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>