mirror of
https://github.com/immich-app/immich.git
synced 2025-07-08 02:34:12 -04:00
Merge branch 'main' into service_worker_appstatic
This commit is contained in:
commit
56cb8316a1
@ -13,6 +13,9 @@ import {
|
|||||||
mdiTrashCan,
|
mdiTrashCan,
|
||||||
mdiWeb,
|
mdiWeb,
|
||||||
mdiWrap,
|
mdiWrap,
|
||||||
|
mdiCloudKeyOutline,
|
||||||
|
mdiRegex,
|
||||||
|
mdiCodeJson,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -23,6 +26,30 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
|
|||||||
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
|
{
|
||||||
|
icon: mdiRegex,
|
||||||
|
iconColor: 'purple',
|
||||||
|
title: 'Zitadel Actions are cursed',
|
||||||
|
description:
|
||||||
|
"Zitadel is cursed because its custom scripting feature is executed with a JS engine that doesn't support regex named capture groups.",
|
||||||
|
link: {
|
||||||
|
url: 'https://github.com/dop251/goja',
|
||||||
|
text: 'Go JS engine',
|
||||||
|
},
|
||||||
|
date: new Date(2025, 5, 4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiCloudKeyOutline,
|
||||||
|
iconColor: '#0078d4',
|
||||||
|
title: 'Entra is cursed',
|
||||||
|
description:
|
||||||
|
"Microsoft Entra supports PKCE, but doesn't include it in its OpenID discovery document. This leads to clients thinking PKCE isn't available.",
|
||||||
|
link: {
|
||||||
|
url: 'https://github.com/immich-app/immich/pull/18725',
|
||||||
|
text: '#18725',
|
||||||
|
},
|
||||||
|
date: new Date(2025, 4, 30),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: mdiCrop,
|
icon: mdiCrop,
|
||||||
iconColor: 'tomato',
|
iconColor: 'tomato',
|
||||||
@ -35,6 +62,17 @@ const items: Item[] = [
|
|||||||
},
|
},
|
||||||
date: new Date(2025, 4, 5),
|
date: new Date(2025, 4, 5),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: mdiCodeJson,
|
||||||
|
iconColor: 'yellow',
|
||||||
|
title: 'YAML whitespace is cursed',
|
||||||
|
description: 'YAML whitespaces are often handled in unintuitive ways.',
|
||||||
|
link: {
|
||||||
|
url: 'https://github.com/immich-app/immich/pull/17309',
|
||||||
|
text: '#17309',
|
||||||
|
},
|
||||||
|
date: new Date(2025, 3, 1),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: mdiMicrosoftWindows,
|
icon: mdiMicrosoftWindows,
|
||||||
iconColor: '#357EC7',
|
iconColor: '#357EC7',
|
||||||
|
@ -28,8 +28,10 @@ services:
|
|||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'auth-server:host-gateway'
|
- 'auth-server:host-gateway'
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
redis:
|
||||||
- database
|
condition: service_started
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- 2285:2285
|
- 2285:2285
|
||||||
|
|
||||||
@ -45,3 +47,9 @@ services:
|
|||||||
POSTGRES_DB: immich
|
POSTGRES_DB: immich
|
||||||
ports:
|
ports:
|
||||||
- 5435:5432
|
- 5435:5432
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U postgres -d immich']
|
||||||
|
interval: 1s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
start_period: 10s
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
import { UserMetadataKey } from 'src/enum';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`INSERT INTO user_metadata SELECT id, ${UserMetadataKey.ONBOARDING}, '{"isOnboarded": true}' FROM users
|
||||||
|
ON CONFLICT ("userId", key) DO NOTHING
|
||||||
|
`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DELETE FROM user_metadata WHERE key = ${UserMetadataKey.ONBOARDING}`.execute(db);
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { AssetAction } from '$lib/constants';
|
import type { AssetAction } from '$lib/constants';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import type { AlbumResponseDto } from '@immich/sdk';
|
import type { AlbumResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
type ActionMap = {
|
type ActionMap = {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { shortcut } from '$lib/actions/shortcut';
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { downloadFile } from '$lib/utils/asset-utils';
|
import { downloadFile } from '$lib/utils/asset-utils';
|
||||||
import { getAssetInfo } from '@immich/sdk';
|
import { getAssetInfo } from '@immich/sdk';
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { AssetVisibility, updateAssets } from '@immich/sdk';
|
import { AssetVisibility, updateAssets } from '@immich/sdk';
|
||||||
import { mdiLockOpenVariantOutline, mdiLockOutline } from '@mdi/js';
|
import { mdiLockOpenVariantOutline, mdiLockOutline } from '@mdi/js';
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { isShowDetail } from '$lib/stores/preferences.store';
|
import { isShowDetail } from '$lib/stores/preferences.store';
|
||||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
|
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
|
||||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||||
import { castManager } from '$lib/managers/cast-manager.svelte';
|
import { castManager } from '$lib/managers/cast-manager.svelte';
|
||||||
import { photoViewerImgElement, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
import { thumbhash } from '$lib/actions/thumbhash';
|
import { thumbhash } from '$lib/actions/thumbhash';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { moveFocus } from '$lib/utils/focus-util';
|
import { moveFocus } from '$lib/utils/focus-util';
|
||||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||||
@ -231,7 +231,7 @@
|
|||||||
{#if (!loaded || thumbError) && asset.thumbhash}
|
{#if (!loaded || thumbError) && asset.thumbhash}
|
||||||
<canvas
|
<canvas
|
||||||
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
|
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
|
||||||
class="absolute object-cover"
|
class="absolute object-cover z-1"
|
||||||
style:width="{width}px"
|
style:width="{width}px"
|
||||||
style:height="{height}px"
|
style:height="{height}px"
|
||||||
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
|
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
|
||||||
|
@ -27,7 +27,8 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { type TimelineAsset, type Viewport } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
||||||
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { moveFocus } from '$lib/utils/focus-util';
|
import { moveFocus } from '$lib/utils/focus-util';
|
||||||
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { cancelMultiselect, selectAllAssets } from '$lib/utils/asset-utils';
|
import { cancelMultiselect, selectAllAssets } from '$lib/utils/asset-utils';
|
||||||
import { Button, IconButton } from '@immich/ui';
|
import { Button, IconButton } from '@immich/ui';
|
||||||
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import {
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
type AssetBucket,
|
import type { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
assetSnapshot,
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
assetsSnapshot,
|
import { assetSnapshot, assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
type AssetStore,
|
|
||||||
isSelectingAllAssets,
|
|
||||||
type TimelineAsset,
|
|
||||||
} from '$lib/stores/assets-store.svelte';
|
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
|
||||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||||
import { fly, scale } from 'svelte/transition';
|
import { fly, scale } from 'svelte/transition';
|
||||||
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||||
|
|
||||||
|
import type { AssetBucket } from '$lib/managers/timeline-manager/asset-bucket.svelte';
|
||||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
|
|
||||||
|
@ -18,17 +18,15 @@
|
|||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import {
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
AssetBucket,
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
assetsSnapshot,
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
AssetStore,
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
isSelectingAllAssets,
|
|
||||||
type TimelineAsset,
|
|
||||||
} from '$lib/stores/assets-store.svelte';
|
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import type { AssetBucket } from '$lib/managers/timeline-manager/asset-bucket.svelte';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { mdiClose } from '@mdi/js';
|
import { mdiClose } from '@mdi/js';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import type { AssetStore, LiteBucket } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import type { LiteBucket } from '$lib/managers/timeline-manager/types';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { getTabbable } from '$lib/utils/focus-util';
|
import { getTabbable } from '$lib/utils/focus-util';
|
||||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
||||||
|
60
web/src/lib/managers/timeline-manager/add-context.svelte.ts
Normal file
60
web/src/lib/managers/timeline-manager/add-context.svelte.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { TimelinePlainDate } from '$lib/utils/timeline-util';
|
||||||
|
import { AssetOrder } from '@immich/sdk';
|
||||||
|
import type { AssetBucket } from './asset-bucket.svelte';
|
||||||
|
import type { AssetDateGroup } from './asset-date-group.svelte';
|
||||||
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
|
export class AddContext {
|
||||||
|
#lookupCache: {
|
||||||
|
[year: number]: { [month: number]: { [day: number]: AssetDateGroup } };
|
||||||
|
} = {};
|
||||||
|
unprocessedAssets: TimelineAsset[] = [];
|
||||||
|
changedDateGroups = new Set<AssetDateGroup>();
|
||||||
|
newDateGroups = new Set<AssetDateGroup>();
|
||||||
|
|
||||||
|
getDateGroup({ year, month, day }: TimelinePlainDate): AssetDateGroup | undefined {
|
||||||
|
return this.#lookupCache[year]?.[month]?.[day];
|
||||||
|
}
|
||||||
|
|
||||||
|
setDateGroup(dateGroup: AssetDateGroup, { year, month, day }: TimelinePlainDate) {
|
||||||
|
if (!this.#lookupCache[year]) {
|
||||||
|
this.#lookupCache[year] = {};
|
||||||
|
}
|
||||||
|
if (!this.#lookupCache[year][month]) {
|
||||||
|
this.#lookupCache[year][month] = {};
|
||||||
|
}
|
||||||
|
this.#lookupCache[year][month][day] = dateGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
get existingDateGroups() {
|
||||||
|
return this.changedDateGroups.difference(this.newDateGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
get updatedBuckets() {
|
||||||
|
const updated = new Set<AssetBucket>();
|
||||||
|
for (const group of this.changedDateGroups) {
|
||||||
|
updated.add(group.bucket);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bucketsWithNewDateGroups() {
|
||||||
|
const updated = new Set<AssetBucket>();
|
||||||
|
for (const group of this.newDateGroups) {
|
||||||
|
updated.add(group.bucket);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(bucket: AssetBucket, sortOrder: AssetOrder = AssetOrder.Desc) {
|
||||||
|
for (const group of this.changedDateGroups) {
|
||||||
|
group.sortAssets(sortOrder);
|
||||||
|
}
|
||||||
|
for (const group of this.newDateGroups) {
|
||||||
|
group.sortAssets(sortOrder);
|
||||||
|
}
|
||||||
|
if (this.newDateGroups.size > 0) {
|
||||||
|
bucket.sortDateGroups();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
361
web/src/lib/managers/timeline-manager/asset-bucket.svelte.ts
Normal file
361
web/src/lib/managers/timeline-manager/asset-bucket.svelte.ts
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import {
|
||||||
|
formatBucketTitle,
|
||||||
|
formatGroupTitle,
|
||||||
|
fromLocalDateTimeToObject,
|
||||||
|
fromTimelinePlainDate,
|
||||||
|
fromTimelinePlainDateTime,
|
||||||
|
fromTimelinePlainYearMonth,
|
||||||
|
type TimelinePlainDateTime,
|
||||||
|
type TimelinePlainYearMonth,
|
||||||
|
} from '$lib/utils/timeline-util';
|
||||||
|
import { AssetOrder, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { AddContext } from './add-context.svelte';
|
||||||
|
import { AssetDateGroup } from './asset-date-group.svelte';
|
||||||
|
import type { AssetStore } from './asset-store.svelte';
|
||||||
|
import { IntersectingAsset } from './intersecting-asset.svelte';
|
||||||
|
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
||||||
|
|
||||||
|
export class AssetBucket {
|
||||||
|
#intersecting: boolean = $state(false);
|
||||||
|
actuallyIntersecting: boolean = $state(false);
|
||||||
|
isLoaded: boolean = $state(false);
|
||||||
|
dateGroups: AssetDateGroup[] = $state([]);
|
||||||
|
readonly store: AssetStore;
|
||||||
|
|
||||||
|
#bucketHeight: number = $state(0);
|
||||||
|
#top: number = $state(0);
|
||||||
|
|
||||||
|
#initialCount: number = 0;
|
||||||
|
#sortOrder: AssetOrder = AssetOrder.Desc;
|
||||||
|
percent: number = $state(0);
|
||||||
|
|
||||||
|
bucketCount: number = $derived(
|
||||||
|
this.isLoaded
|
||||||
|
? this.dateGroups.reduce((accumulator, g) => accumulator + g.intersectingAssets.length, 0)
|
||||||
|
: this.#initialCount,
|
||||||
|
);
|
||||||
|
loader: CancellableTask | undefined;
|
||||||
|
isBucketHeightActual: boolean = $state(false);
|
||||||
|
|
||||||
|
readonly bucketDateFormatted: string;
|
||||||
|
readonly yearMonth: TimelinePlainYearMonth;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
store: AssetStore,
|
||||||
|
yearMonth: TimelinePlainYearMonth,
|
||||||
|
initialCount: number,
|
||||||
|
order: AssetOrder = AssetOrder.Desc,
|
||||||
|
) {
|
||||||
|
this.store = store;
|
||||||
|
this.#initialCount = initialCount;
|
||||||
|
this.#sortOrder = order;
|
||||||
|
|
||||||
|
this.yearMonth = yearMonth;
|
||||||
|
this.bucketDateFormatted = formatBucketTitle(fromTimelinePlainYearMonth(yearMonth));
|
||||||
|
|
||||||
|
this.loader = new CancellableTask(
|
||||||
|
() => {
|
||||||
|
this.isLoaded = true;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.dateGroups = [];
|
||||||
|
this.isLoaded = false;
|
||||||
|
},
|
||||||
|
this.#handleLoadError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set intersecting(newValue: boolean) {
|
||||||
|
const old = this.#intersecting;
|
||||||
|
if (old === newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#intersecting = newValue;
|
||||||
|
if (newValue) {
|
||||||
|
void this.store.loadBucket(this.yearMonth);
|
||||||
|
} else {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get intersecting() {
|
||||||
|
return this.#intersecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastDateGroup() {
|
||||||
|
return this.dateGroups.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstAsset() {
|
||||||
|
return this.dateGroups[0]?.getFirstAsset();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssets() {
|
||||||
|
// eslint-disable-next-line unicorn/no-array-reduce
|
||||||
|
return this.dateGroups.reduce(
|
||||||
|
(accumulator: TimelineAsset[], g: AssetDateGroup) => accumulator.concat(g.getAssets()),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sortDateGroups() {
|
||||||
|
if (this.#sortOrder === AssetOrder.Asc) {
|
||||||
|
return this.dateGroups.sort((a, b) => a.day - b.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dateGroups.sort((a, b) => b.day - a.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||||
|
if (ids.size === 0) {
|
||||||
|
return {
|
||||||
|
moveAssets: [] as MoveAsset[],
|
||||||
|
processedIds: new Set<string>(),
|
||||||
|
unprocessedIds: ids,
|
||||||
|
changedGeometry: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { dateGroups } = this;
|
||||||
|
let combinedChangedGeometry = false;
|
||||||
|
let idsToProcess = new Set(ids);
|
||||||
|
const idsProcessed = new Set<string>();
|
||||||
|
const combinedMoveAssets: MoveAsset[][] = [];
|
||||||
|
let index = dateGroups.length;
|
||||||
|
while (index--) {
|
||||||
|
if (idsToProcess.size > 0) {
|
||||||
|
const group = dateGroups[index];
|
||||||
|
const { moveAssets, processedIds, changedGeometry } = group.runAssetOperation(ids, operation);
|
||||||
|
if (moveAssets.length > 0) {
|
||||||
|
combinedMoveAssets.push(moveAssets);
|
||||||
|
}
|
||||||
|
idsToProcess = idsToProcess.difference(processedIds);
|
||||||
|
for (const id of processedIds) {
|
||||||
|
idsProcessed.add(id);
|
||||||
|
}
|
||||||
|
combinedChangedGeometry = combinedChangedGeometry || changedGeometry;
|
||||||
|
if (group.intersectingAssets.length === 0) {
|
||||||
|
dateGroups.splice(index, 1);
|
||||||
|
combinedChangedGeometry = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
moveAssets: combinedMoveAssets.flat(),
|
||||||
|
unprocessedIds: idsToProcess,
|
||||||
|
processedIds: idsProcessed,
|
||||||
|
changedGeometry: combinedChangedGeometry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
addAssets(bucketAssets: TimeBucketAssetResponseDto) {
|
||||||
|
const addContext = new AddContext();
|
||||||
|
const people: string[] = [];
|
||||||
|
for (let i = 0; i < bucketAssets.id.length; i++) {
|
||||||
|
const timelineAsset: TimelineAsset = {
|
||||||
|
city: bucketAssets.city[i],
|
||||||
|
country: bucketAssets.country[i],
|
||||||
|
duration: bucketAssets.duration[i],
|
||||||
|
id: bucketAssets.id[i],
|
||||||
|
visibility: bucketAssets.visibility[i],
|
||||||
|
isFavorite: bucketAssets.isFavorite[i],
|
||||||
|
isImage: bucketAssets.isImage[i],
|
||||||
|
isTrashed: bucketAssets.isTrashed[i],
|
||||||
|
isVideo: !bucketAssets.isImage[i],
|
||||||
|
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
||||||
|
localDateTime: fromLocalDateTimeToObject(bucketAssets.localDateTime[i]),
|
||||||
|
ownerId: bucketAssets.ownerId[i],
|
||||||
|
people,
|
||||||
|
projectionType: bucketAssets.projectionType[i],
|
||||||
|
ratio: bucketAssets.ratio[i],
|
||||||
|
stack: bucketAssets.stack?.[i]
|
||||||
|
? {
|
||||||
|
id: bucketAssets.stack[i]![0],
|
||||||
|
primaryAssetId: bucketAssets.id[i],
|
||||||
|
assetCount: Number.parseInt(bucketAssets.stack[i]![1]),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
thumbhash: bucketAssets.thumbhash[i],
|
||||||
|
};
|
||||||
|
this.addTimelineAsset(timelineAsset, addContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of addContext.existingDateGroups) {
|
||||||
|
group.sortAssets(this.#sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addContext.newDateGroups.size > 0) {
|
||||||
|
this.sortDateGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
addContext.sort(this, this.#sortOrder);
|
||||||
|
|
||||||
|
return addContext.unprocessedAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) {
|
||||||
|
const { localDateTime } = timelineAsset;
|
||||||
|
|
||||||
|
const { year, month } = this.yearMonth;
|
||||||
|
if (month !== localDateTime.month || year !== localDateTime.year) {
|
||||||
|
addContext.unprocessedAssets.push(timelineAsset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dateGroup = addContext.getDateGroup(localDateTime) || this.findDateGroupByDay(localDateTime.day);
|
||||||
|
if (dateGroup) {
|
||||||
|
addContext.setDateGroup(dateGroup, localDateTime);
|
||||||
|
} else {
|
||||||
|
const groupTitle = formatGroupTitle(fromTimelinePlainDate(localDateTime));
|
||||||
|
dateGroup = new AssetDateGroup(this, this.dateGroups.length, localDateTime.day, groupTitle);
|
||||||
|
this.dateGroups.push(dateGroup);
|
||||||
|
addContext.setDateGroup(dateGroup, localDateTime);
|
||||||
|
addContext.newDateGroups.add(dateGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersectingAsset = new IntersectingAsset(dateGroup, timelineAsset);
|
||||||
|
dateGroup.intersectingAssets.push(intersectingAsset);
|
||||||
|
addContext.changedDateGroups.add(dateGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomDateGroup() {
|
||||||
|
const random = Math.floor(Math.random() * this.dateGroups.length);
|
||||||
|
return this.dateGroups[random];
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomAsset() {
|
||||||
|
return this.getRandomDateGroup()?.getRandomAsset()?.asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewId() {
|
||||||
|
const { year, month } = this.yearMonth;
|
||||||
|
return year + '-' + month;
|
||||||
|
}
|
||||||
|
|
||||||
|
set bucketHeight(height: number) {
|
||||||
|
if (this.#bucketHeight === height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { store, percent } = this;
|
||||||
|
const index = store.buckets.indexOf(this);
|
||||||
|
const bucketHeightDelta = height - this.#bucketHeight;
|
||||||
|
this.#bucketHeight = height;
|
||||||
|
const prevBucket = store.buckets[index - 1];
|
||||||
|
if (prevBucket) {
|
||||||
|
const newTop = prevBucket.#top + prevBucket.#bucketHeight;
|
||||||
|
if (this.#top !== newTop) {
|
||||||
|
this.#top = newTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let cursor = index + 1; cursor < store.buckets.length; cursor++) {
|
||||||
|
const bucket = this.store.buckets[cursor];
|
||||||
|
const newTop = bucket.#top + bucketHeightDelta;
|
||||||
|
if (bucket.#top !== newTop) {
|
||||||
|
bucket.#top = newTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (store.topIntersectingBucket) {
|
||||||
|
const currentIndex = store.buckets.indexOf(store.topIntersectingBucket);
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
if (index < currentIndex) {
|
||||||
|
store.scrollCompensation = {
|
||||||
|
heightDelta: bucketHeightDelta,
|
||||||
|
scrollTop: undefined,
|
||||||
|
bucket: this,
|
||||||
|
};
|
||||||
|
} else if (percent > 0) {
|
||||||
|
const top = this.top + height * percent;
|
||||||
|
store.scrollCompensation = {
|
||||||
|
heightDelta: undefined,
|
||||||
|
scrollTop: top,
|
||||||
|
bucket: this,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get bucketHeight() {
|
||||||
|
return this.#bucketHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
get top(): number {
|
||||||
|
return this.#top + this.store.topSectionHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleLoadError(error: unknown) {
|
||||||
|
const _$t = get(t);
|
||||||
|
handleError(error, _$t('errors.failed_to_load_assets'));
|
||||||
|
}
|
||||||
|
|
||||||
|
findDateGroupForAsset(asset: TimelineAsset) {
|
||||||
|
for (const group of this.dateGroups) {
|
||||||
|
if (group.intersectingAssets.some((IntersectingAsset) => IntersectingAsset.id === asset.id)) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findDateGroupByDay(day: number) {
|
||||||
|
return this.dateGroups.find((group) => group.day === day);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAssetAbsolutePosition(assetId: string) {
|
||||||
|
this.store.clearDeferredLayout(this);
|
||||||
|
for (const group of this.dateGroups) {
|
||||||
|
const intersectingAsset = group.intersectingAssets.find((asset) => asset.id === assetId);
|
||||||
|
if (intersectingAsset) {
|
||||||
|
if (!intersectingAsset.position) {
|
||||||
|
console.warn('No position for asset');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return this.top + group.top + intersectingAsset.position.top + this.store.headerHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*assetsIterator(options?: { startDateGroup?: AssetDateGroup; startAsset?: TimelineAsset; direction?: Direction }) {
|
||||||
|
const direction = options?.direction ?? 'earlier';
|
||||||
|
let { startAsset } = options ?? {};
|
||||||
|
const isEarlier = direction === 'earlier';
|
||||||
|
let groupIndex = options?.startDateGroup
|
||||||
|
? this.dateGroups.indexOf(options.startDateGroup)
|
||||||
|
: isEarlier
|
||||||
|
? 0
|
||||||
|
: this.dateGroups.length - 1;
|
||||||
|
|
||||||
|
while (groupIndex >= 0 && groupIndex < this.dateGroups.length) {
|
||||||
|
const group = this.dateGroups[groupIndex];
|
||||||
|
yield* group.assetsIterator({ startAsset, direction });
|
||||||
|
startAsset = undefined;
|
||||||
|
groupIndex += isEarlier ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findAssetById(assetDescriptor: AssetDescriptor) {
|
||||||
|
return this.assetsIterator().find((asset) => asset.id === assetDescriptor.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
findClosest(target: TimelinePlainDateTime) {
|
||||||
|
const targetDate = fromTimelinePlainDateTime(target);
|
||||||
|
let closest = undefined;
|
||||||
|
let smallestDiff = Infinity;
|
||||||
|
for (const current of this.assetsIterator()) {
|
||||||
|
const currentAssetDate = fromTimelinePlainDateTime(current.localDateTime);
|
||||||
|
const diff = Math.abs(targetDate.diff(currentAssetDate).as('milliseconds'));
|
||||||
|
if (diff < smallestDiff) {
|
||||||
|
smallestDiff = diff;
|
||||||
|
closest = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.loader?.cancel();
|
||||||
|
}
|
||||||
|
}
|
162
web/src/lib/managers/timeline-manager/asset-date-group.svelte.ts
Normal file
162
web/src/lib/managers/timeline-manager/asset-date-group.svelte.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
|
||||||
|
import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
|
||||||
|
import { plainDateTimeCompare } from '$lib/utils/timeline-util';
|
||||||
|
import { AssetOrder } from '@immich/sdk';
|
||||||
|
import type { AssetBucket } from './asset-bucket.svelte';
|
||||||
|
import { IntersectingAsset } from './intersecting-asset.svelte';
|
||||||
|
import type { AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
|
||||||
|
|
||||||
|
export class AssetDateGroup {
|
||||||
|
readonly bucket: AssetBucket;
|
||||||
|
readonly index: number;
|
||||||
|
readonly groupTitle: string;
|
||||||
|
readonly day: number;
|
||||||
|
intersectingAssets: IntersectingAsset[] = $state([]);
|
||||||
|
|
||||||
|
height = $state(0);
|
||||||
|
width = $state(0);
|
||||||
|
intersecting = $derived.by(() => this.intersectingAssets.some((asset) => asset.intersecting));
|
||||||
|
|
||||||
|
#top: number = $state(0);
|
||||||
|
#left: number = $state(0);
|
||||||
|
#row = $state(0);
|
||||||
|
#col = $state(0);
|
||||||
|
#deferredLayout = false;
|
||||||
|
|
||||||
|
constructor(bucket: AssetBucket, index: number, day: number, groupTitle: string) {
|
||||||
|
this.index = index;
|
||||||
|
this.bucket = bucket;
|
||||||
|
this.day = day;
|
||||||
|
this.groupTitle = groupTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
get top() {
|
||||||
|
return this.#top;
|
||||||
|
}
|
||||||
|
|
||||||
|
set top(value: number) {
|
||||||
|
this.#top = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get left() {
|
||||||
|
return this.#left;
|
||||||
|
}
|
||||||
|
|
||||||
|
set left(value: number) {
|
||||||
|
this.#left = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get row() {
|
||||||
|
return this.#row;
|
||||||
|
}
|
||||||
|
|
||||||
|
set row(value: number) {
|
||||||
|
this.#row = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get col() {
|
||||||
|
return this.#col;
|
||||||
|
}
|
||||||
|
|
||||||
|
set col(value: number) {
|
||||||
|
this.#col = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get deferredLayout() {
|
||||||
|
return this.#deferredLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
set deferredLayout(value: boolean) {
|
||||||
|
this.#deferredLayout = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
|
||||||
|
const sortFn = plainDateTimeCompare.bind(undefined, sortOrder === AssetOrder.Asc);
|
||||||
|
this.intersectingAssets.sort((a, b) => sortFn(a.asset.localDateTime, b.asset.localDateTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstAsset() {
|
||||||
|
return this.intersectingAssets[0]?.asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomAsset() {
|
||||||
|
const random = Math.floor(Math.random() * this.intersectingAssets.length);
|
||||||
|
return this.intersectingAssets[random];
|
||||||
|
}
|
||||||
|
|
||||||
|
*assetsIterator(options: { startAsset?: TimelineAsset; direction?: Direction } = {}) {
|
||||||
|
const isEarlier = (options?.direction ?? 'earlier') === 'earlier';
|
||||||
|
let assetIndex = options?.startAsset
|
||||||
|
? this.intersectingAssets.findIndex((intersectingAsset) => intersectingAsset.asset.id === options.startAsset!.id)
|
||||||
|
: isEarlier
|
||||||
|
? 0
|
||||||
|
: this.intersectingAssets.length - 1;
|
||||||
|
|
||||||
|
while (assetIndex >= 0 && assetIndex < this.intersectingAssets.length) {
|
||||||
|
const intersectingAsset = this.intersectingAssets[assetIndex];
|
||||||
|
yield intersectingAsset.asset;
|
||||||
|
assetIndex += isEarlier ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssets() {
|
||||||
|
return this.intersectingAssets.map((intersectingasset) => intersectingasset.asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||||
|
if (ids.size === 0) {
|
||||||
|
return {
|
||||||
|
moveAssets: [] as MoveAsset[],
|
||||||
|
processedIds: new Set<string>(),
|
||||||
|
unprocessedIds: ids,
|
||||||
|
changedGeometry: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const unprocessedIds = new Set<string>(ids);
|
||||||
|
const processedIds = new Set<string>();
|
||||||
|
const moveAssets: MoveAsset[] = [];
|
||||||
|
let changedGeometry = false;
|
||||||
|
for (const assetId of unprocessedIds) {
|
||||||
|
const index = this.intersectingAssets.findIndex((ia) => ia.id == assetId);
|
||||||
|
if (index === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = this.intersectingAssets[index].asset!;
|
||||||
|
const oldTime = { ...asset.localDateTime };
|
||||||
|
let { remove } = operation(asset);
|
||||||
|
const newTime = asset.localDateTime;
|
||||||
|
if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) {
|
||||||
|
const { year, month, day } = newTime;
|
||||||
|
remove = true;
|
||||||
|
moveAssets.push({ asset, date: { year, month, day } });
|
||||||
|
}
|
||||||
|
unprocessedIds.delete(assetId);
|
||||||
|
processedIds.add(assetId);
|
||||||
|
if (remove || this.bucket.store.isExcluded(asset)) {
|
||||||
|
this.intersectingAssets.splice(index, 1);
|
||||||
|
changedGeometry = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { moveAssets, processedIds, unprocessedIds, changedGeometry };
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(options: CommonLayoutOptions, noDefer: boolean) {
|
||||||
|
if (!noDefer && !this.bucket.intersecting) {
|
||||||
|
this.#deferredLayout = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const assets = this.intersectingAssets.map((intersetingAsset) => intersetingAsset.asset!);
|
||||||
|
const geometry = getJustifiedLayoutFromAssets(assets, options);
|
||||||
|
this.width = geometry.containerWidth;
|
||||||
|
this.height = assets.length === 0 ? 0 : geometry.containerHeight;
|
||||||
|
for (let i = 0; i < this.intersectingAssets.length; i++) {
|
||||||
|
const position = getPosition(geometry, i);
|
||||||
|
this.intersectingAssets[i].position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get absoluteDateGroupTop() {
|
||||||
|
return this.bucket.top + this.#top;
|
||||||
|
}
|
||||||
|
}
|
934
web/src/lib/managers/timeline-manager/asset-store.svelte.ts
Normal file
934
web/src/lib/managers/timeline-manager/asset-store.svelte.ts
Normal file
@ -0,0 +1,934 @@
|
|||||||
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
|
import {
|
||||||
|
plainDateTimeCompare,
|
||||||
|
toISOLocalDateTime,
|
||||||
|
toTimelineAsset,
|
||||||
|
type TimelinePlainDate,
|
||||||
|
type TimelinePlainDateTime,
|
||||||
|
type TimelinePlainYearMonth,
|
||||||
|
} from '$lib/utils/timeline-util';
|
||||||
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
|
import { getAssetInfo, getTimeBucket, getTimeBuckets } from '@immich/sdk';
|
||||||
|
import { clamp, debounce, isEqual, throttle } from 'lodash-es';
|
||||||
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
|
import type { Unsubscriber } from 'svelte/store';
|
||||||
|
import { AddContext } from './add-context.svelte';
|
||||||
|
import { AssetBucket } from './asset-bucket.svelte';
|
||||||
|
import { AssetDateGroup } from './asset-date-group.svelte';
|
||||||
|
import type {
|
||||||
|
AssetDescriptor,
|
||||||
|
AssetOperation,
|
||||||
|
AssetStoreLayoutOptions,
|
||||||
|
AssetStoreOptions,
|
||||||
|
Direction,
|
||||||
|
LiteBucket,
|
||||||
|
PendingChange,
|
||||||
|
TimelineAsset,
|
||||||
|
UpdateGeometryOptions,
|
||||||
|
Viewport,
|
||||||
|
} from './types';
|
||||||
|
import { isMismatched, updateObject } from './utils.svelte';
|
||||||
|
|
||||||
|
const {
|
||||||
|
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||||
|
} = TUNABLES;
|
||||||
|
|
||||||
|
export class AssetStore {
|
||||||
|
isInitialized = $state(false);
|
||||||
|
buckets: AssetBucket[] = $state([]);
|
||||||
|
topSectionHeight = $state(0);
|
||||||
|
timelineHeight = $derived(
|
||||||
|
this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0) + this.topSectionHeight,
|
||||||
|
);
|
||||||
|
count = $derived(this.buckets.reduce((accumulator, b) => accumulator + b.bucketCount, 0));
|
||||||
|
|
||||||
|
albumAssets: Set<string> = new SvelteSet();
|
||||||
|
|
||||||
|
scrubberBuckets: LiteBucket[] = $state([]);
|
||||||
|
scrubberTimelineHeight: number = $state(0);
|
||||||
|
|
||||||
|
topIntersectingBucket: AssetBucket | undefined = $state();
|
||||||
|
|
||||||
|
visibleWindow = $derived.by(() => ({
|
||||||
|
top: this.#scrollTop,
|
||||||
|
bottom: this.#scrollTop + this.viewportHeight,
|
||||||
|
}));
|
||||||
|
|
||||||
|
initTask = new CancellableTask(
|
||||||
|
() => {
|
||||||
|
this.isInitialized = true;
|
||||||
|
if (this.#options.albumId || this.#options.personId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.connect();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.disconnect();
|
||||||
|
this.isInitialized = false;
|
||||||
|
},
|
||||||
|
() => void 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
static #INIT_OPTIONS = {};
|
||||||
|
#viewportHeight = $state(0);
|
||||||
|
#viewportWidth = $state(0);
|
||||||
|
#scrollTop = $state(0);
|
||||||
|
#pendingChanges: PendingChange[] = [];
|
||||||
|
#unsubscribers: Unsubscriber[] = [];
|
||||||
|
|
||||||
|
#rowHeight = $state(235);
|
||||||
|
#headerHeight = $state(48);
|
||||||
|
#gap = $state(12);
|
||||||
|
|
||||||
|
#options: AssetStoreOptions = AssetStore.#INIT_OPTIONS;
|
||||||
|
|
||||||
|
#scrolling = $state(false);
|
||||||
|
#suspendTransitions = $state(false);
|
||||||
|
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
|
||||||
|
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
|
||||||
|
scrollCompensation: {
|
||||||
|
heightDelta: number | undefined;
|
||||||
|
scrollTop: number | undefined;
|
||||||
|
bucket: AssetBucket | undefined;
|
||||||
|
} = $state({
|
||||||
|
heightDelta: 0,
|
||||||
|
scrollTop: 0,
|
||||||
|
bucket: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: AssetStoreLayoutOptions) {
|
||||||
|
let changed = false;
|
||||||
|
changed ||= this.#setHeaderHeight(headerHeight);
|
||||||
|
changed ||= this.#setGap(gap);
|
||||||
|
changed ||= this.#setRowHeight(rowHeight);
|
||||||
|
if (changed) {
|
||||||
|
this.refreshLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setHeaderHeight(value: number) {
|
||||||
|
if (this.#headerHeight == value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.#headerHeight = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerHeight() {
|
||||||
|
return this.#headerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setGap(value: number) {
|
||||||
|
if (this.#gap == value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.#gap = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get gap() {
|
||||||
|
return this.#gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setRowHeight(value: number) {
|
||||||
|
if (this.#rowHeight == value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.#rowHeight = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rowHeight() {
|
||||||
|
return this.#rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
set scrolling(value: boolean) {
|
||||||
|
this.#scrolling = value;
|
||||||
|
if (value) {
|
||||||
|
this.suspendTransitions = true;
|
||||||
|
this.#resetScrolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get scrolling() {
|
||||||
|
return this.#scrolling;
|
||||||
|
}
|
||||||
|
|
||||||
|
set suspendTransitions(value: boolean) {
|
||||||
|
this.#suspendTransitions = value;
|
||||||
|
if (value) {
|
||||||
|
this.#resetSuspendTransitions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get suspendTransitions() {
|
||||||
|
return this.#suspendTransitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
set viewportWidth(value: number) {
|
||||||
|
const changed = value !== this.#viewportWidth;
|
||||||
|
this.#viewportWidth = value;
|
||||||
|
this.suspendTransitions = true;
|
||||||
|
void this.#updateViewportGeometry(changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewportWidth() {
|
||||||
|
return this.#viewportWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
set viewportHeight(value: number) {
|
||||||
|
this.#viewportHeight = value;
|
||||||
|
this.#suspendTransitions = true;
|
||||||
|
void this.#updateViewportGeometry(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewportHeight() {
|
||||||
|
return this.#viewportHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async *assetsIterator(options?: {
|
||||||
|
startBucket?: AssetBucket;
|
||||||
|
startDateGroup?: AssetDateGroup;
|
||||||
|
startAsset?: TimelineAsset;
|
||||||
|
direction?: Direction;
|
||||||
|
}) {
|
||||||
|
const direction = options?.direction ?? 'earlier';
|
||||||
|
let { startDateGroup, startAsset } = options ?? {};
|
||||||
|
for (const bucket of this.bucketsIterator({ direction, startBucket: options?.startBucket })) {
|
||||||
|
await this.loadBucket(bucket.yearMonth, { cancelable: false });
|
||||||
|
yield* bucket.assetsIterator({ startDateGroup, startAsset, direction });
|
||||||
|
startDateGroup = startAsset = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*bucketsIterator(options?: { direction?: Direction; startBucket?: AssetBucket }) {
|
||||||
|
const isEarlier = options?.direction === 'earlier';
|
||||||
|
let startIndex = options?.startBucket
|
||||||
|
? this.buckets.indexOf(options.startBucket)
|
||||||
|
: isEarlier
|
||||||
|
? 0
|
||||||
|
: this.buckets.length - 1;
|
||||||
|
|
||||||
|
while (startIndex >= 0 && startIndex < this.buckets.length) {
|
||||||
|
yield this.buckets[startIndex];
|
||||||
|
startIndex += isEarlier ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#addPendingChanges(...changes: PendingChange[]) {
|
||||||
|
this.#pendingChanges.push(...changes);
|
||||||
|
this.#processPendingChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.#unsubscribers.push(
|
||||||
|
websocketEvents.on('on_upload_success', (asset) =>
|
||||||
|
this.#addPendingChanges({ type: 'add', values: [toTimelineAsset(asset)] }),
|
||||||
|
),
|
||||||
|
websocketEvents.on('on_asset_trash', (ids) => this.#addPendingChanges({ type: 'trash', values: ids })),
|
||||||
|
websocketEvents.on('on_asset_update', (asset) =>
|
||||||
|
this.#addPendingChanges({ type: 'update', values: [toTimelineAsset(asset)] }),
|
||||||
|
),
|
||||||
|
websocketEvents.on('on_asset_delete', (id: string) => this.#addPendingChanges({ type: 'delete', values: [id] })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
for (const unsubscribe of this.#unsubscribers) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
this.#unsubscribers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
#getPendingChangeBatches() {
|
||||||
|
const batch: {
|
||||||
|
add: TimelineAsset[];
|
||||||
|
update: TimelineAsset[];
|
||||||
|
remove: string[];
|
||||||
|
} = {
|
||||||
|
add: [],
|
||||||
|
update: [],
|
||||||
|
remove: [],
|
||||||
|
};
|
||||||
|
for (const { type, values } of this.#pendingChanges) {
|
||||||
|
switch (type) {
|
||||||
|
case 'add': {
|
||||||
|
batch.add.push(...values);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
batch.update.push(...values);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'delete':
|
||||||
|
case 'trash': {
|
||||||
|
batch.remove.push(...values);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return batch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#findBucketForAsset(id: string) {
|
||||||
|
for (const bucket of this.buckets) {
|
||||||
|
const asset = bucket.findAssetById({ id });
|
||||||
|
if (asset) {
|
||||||
|
return { bucket, asset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#findBucketForDate(targetYearMonth: TimelinePlainYearMonth) {
|
||||||
|
for (const bucket of this.buckets) {
|
||||||
|
const { year, month } = bucket.yearMonth;
|
||||||
|
if (month === targetYearMonth.month && year === targetYearMonth.year) {
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSlidingWindow(scrollTop: number) {
|
||||||
|
if (this.#scrollTop !== scrollTop) {
|
||||||
|
this.#scrollTop = scrollTop;
|
||||||
|
this.updateIntersections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScrollCompensation() {
|
||||||
|
this.scrollCompensation = {
|
||||||
|
heightDelta: undefined,
|
||||||
|
scrollTop: undefined,
|
||||||
|
bucket: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateIntersections() {
|
||||||
|
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let topIntersectingBucket = undefined;
|
||||||
|
for (const bucket of this.buckets) {
|
||||||
|
this.#updateIntersection(bucket);
|
||||||
|
if (!topIntersectingBucket && bucket.actuallyIntersecting) {
|
||||||
|
topIntersectingBucket = bucket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (topIntersectingBucket !== undefined && this.topIntersectingBucket !== topIntersectingBucket) {
|
||||||
|
this.topIntersectingBucket = topIntersectingBucket;
|
||||||
|
}
|
||||||
|
for (const bucket of this.buckets) {
|
||||||
|
if (bucket === this.topIntersectingBucket) {
|
||||||
|
this.topIntersectingBucket.percent = clamp(
|
||||||
|
(this.visibleWindow.top - this.topIntersectingBucket.top) / this.topIntersectingBucket.bucketHeight,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
bucket.percent = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#calculateIntersecting(bucket: AssetBucket, expandTop: number, expandBottom: number) {
|
||||||
|
const bucketTop = bucket.top;
|
||||||
|
const bucketBottom = bucketTop + bucket.bucketHeight;
|
||||||
|
const topWindow = this.visibleWindow.top - expandTop;
|
||||||
|
const bottomWindow = this.visibleWindow.bottom + expandBottom;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(bucketTop >= topWindow && bucketTop < bottomWindow) ||
|
||||||
|
(bucketBottom >= topWindow && bucketBottom < bottomWindow) ||
|
||||||
|
(bucketTop < topWindow && bucketBottom >= bottomWindow)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDeferredLayout(bucket: AssetBucket) {
|
||||||
|
const hasDeferred = bucket.dateGroups.some((group) => group.deferredLayout);
|
||||||
|
if (hasDeferred) {
|
||||||
|
this.#updateGeometry(bucket, { invalidateHeight: true, noDefer: true });
|
||||||
|
for (const group of bucket.dateGroups) {
|
||||||
|
group.deferredLayout = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateIntersection(bucket: AssetBucket) {
|
||||||
|
const actuallyIntersecting = this.#calculateIntersecting(bucket, 0, 0);
|
||||||
|
let preIntersecting = false;
|
||||||
|
if (!actuallyIntersecting) {
|
||||||
|
preIntersecting = this.#calculateIntersecting(bucket, INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM);
|
||||||
|
}
|
||||||
|
bucket.intersecting = actuallyIntersecting || preIntersecting;
|
||||||
|
bucket.actuallyIntersecting = actuallyIntersecting;
|
||||||
|
if (preIntersecting || actuallyIntersecting) {
|
||||||
|
this.clearDeferredLayout(bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#processPendingChanges = throttle(() => {
|
||||||
|
const { add, update, remove } = this.#getPendingChangeBatches();
|
||||||
|
if (add.length > 0) {
|
||||||
|
this.addAssets(add);
|
||||||
|
}
|
||||||
|
if (update.length > 0) {
|
||||||
|
this.updateAssets(update);
|
||||||
|
}
|
||||||
|
if (remove.length > 0) {
|
||||||
|
this.removeAssets(remove);
|
||||||
|
}
|
||||||
|
this.#pendingChanges = [];
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
async #initializeTimeBuckets() {
|
||||||
|
const timebuckets = await getTimeBuckets({
|
||||||
|
...this.#options,
|
||||||
|
key: authManager.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.buckets = timebuckets.map((bucket) => {
|
||||||
|
const date = new Date(bucket.timeBucket);
|
||||||
|
return new AssetBucket(
|
||||||
|
this,
|
||||||
|
{ year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 },
|
||||||
|
bucket.count,
|
||||||
|
this.#options.order,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.albumAssets.clear();
|
||||||
|
this.#updateViewportGeometry(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOptions(options: AssetStoreOptions) {
|
||||||
|
if (options.deferInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.#options !== AssetStore.#INIT_OPTIONS && isEqual(this.#options, options)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.initTask.reset();
|
||||||
|
await this.#init(options);
|
||||||
|
this.#updateViewportGeometry(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #init(options: AssetStoreOptions) {
|
||||||
|
this.isInitialized = false;
|
||||||
|
this.buckets = [];
|
||||||
|
this.albumAssets.clear();
|
||||||
|
await this.initTask.execute(async () => {
|
||||||
|
this.#options = options;
|
||||||
|
await this.#initializeTimeBuckets();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.disconnect();
|
||||||
|
this.isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateViewport(viewport: Viewport) {
|
||||||
|
if (viewport.height === 0 && viewport.width === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.viewportHeight === viewport.height && this.viewportWidth === viewport.width) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.initTask.executed) {
|
||||||
|
await (this.initTask.loading ? this.initTask.waitUntilCompletion() : this.#init(this.#options));
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedWidth = viewport.width !== this.viewportWidth;
|
||||||
|
this.viewportHeight = viewport.height;
|
||||||
|
this.viewportWidth = viewport.width;
|
||||||
|
this.#updateViewportGeometry(changedWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateViewportGeometry(changedWidth: boolean) {
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.viewportWidth === 0 || this.viewportHeight === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const bucket of this.buckets) {
|
||||||
|
this.#updateGeometry(bucket, { invalidateHeight: changedWidth });
|
||||||
|
}
|
||||||
|
this.updateIntersections();
|
||||||
|
this.#createScrubBuckets();
|
||||||
|
}
|
||||||
|
|
||||||
|
#createScrubBuckets() {
|
||||||
|
this.scrubberBuckets = this.buckets.map((bucket) => ({
|
||||||
|
assetCount: bucket.bucketCount,
|
||||||
|
year: bucket.yearMonth.year,
|
||||||
|
month: bucket.yearMonth.month,
|
||||||
|
bucketDateFormattted: bucket.bucketDateFormatted,
|
||||||
|
bucketHeight: bucket.bucketHeight,
|
||||||
|
}));
|
||||||
|
this.scrubberTimelineHeight = this.timelineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
createLayoutOptions() {
|
||||||
|
const viewportWidth = this.viewportWidth;
|
||||||
|
|
||||||
|
return {
|
||||||
|
spacing: 2,
|
||||||
|
heightTolerance: 0.15,
|
||||||
|
rowHeight: this.#rowHeight,
|
||||||
|
rowWidth: Math.floor(viewportWidth),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateGeometry(bucket: AssetBucket, options: UpdateGeometryOptions) {
|
||||||
|
const { invalidateHeight, noDefer = false } = options;
|
||||||
|
if (invalidateHeight) {
|
||||||
|
bucket.isBucketHeightActual = false;
|
||||||
|
}
|
||||||
|
if (!bucket.isLoaded) {
|
||||||
|
const viewportWidth = this.viewportWidth;
|
||||||
|
if (!bucket.isBucketHeightActual) {
|
||||||
|
const unwrappedWidth = (3 / 2) * bucket.bucketCount * this.#rowHeight * (7 / 10);
|
||||||
|
const rows = Math.ceil(unwrappedWidth / viewportWidth);
|
||||||
|
const height = 51 + Math.max(1, rows) * this.#rowHeight;
|
||||||
|
bucket.bucketHeight = height;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#layoutBucket(bucket, noDefer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#layoutBucket(bucket: AssetBucket, noDefer: boolean = false) {
|
||||||
|
let cummulativeHeight = 0;
|
||||||
|
let cummulativeWidth = 0;
|
||||||
|
let lastRowHeight = 0;
|
||||||
|
let lastRow = 0;
|
||||||
|
|
||||||
|
let dateGroupRow = 0;
|
||||||
|
let dateGroupCol = 0;
|
||||||
|
|
||||||
|
const rowSpaceRemaining: number[] = Array.from({ length: bucket.dateGroups.length });
|
||||||
|
rowSpaceRemaining.fill(this.viewportWidth, 0, bucket.dateGroups.length);
|
||||||
|
const options = this.createLayoutOptions();
|
||||||
|
for (const assetGroup of bucket.dateGroups) {
|
||||||
|
assetGroup.layout(options, noDefer);
|
||||||
|
rowSpaceRemaining[dateGroupRow] -= assetGroup.width - 1;
|
||||||
|
if (dateGroupCol > 0) {
|
||||||
|
rowSpaceRemaining[dateGroupRow] -= this.gap;
|
||||||
|
}
|
||||||
|
if (rowSpaceRemaining[dateGroupRow] >= 0) {
|
||||||
|
assetGroup.row = dateGroupRow;
|
||||||
|
assetGroup.col = dateGroupCol;
|
||||||
|
assetGroup.left = cummulativeWidth;
|
||||||
|
assetGroup.top = cummulativeHeight;
|
||||||
|
|
||||||
|
dateGroupCol++;
|
||||||
|
|
||||||
|
cummulativeWidth += assetGroup.width + this.gap;
|
||||||
|
} else {
|
||||||
|
cummulativeWidth = 0;
|
||||||
|
dateGroupRow++;
|
||||||
|
dateGroupCol = 0;
|
||||||
|
assetGroup.row = dateGroupRow;
|
||||||
|
assetGroup.col = dateGroupCol;
|
||||||
|
assetGroup.left = cummulativeWidth;
|
||||||
|
|
||||||
|
rowSpaceRemaining[dateGroupRow] -= assetGroup.width;
|
||||||
|
dateGroupCol++;
|
||||||
|
cummulativeHeight += lastRowHeight;
|
||||||
|
assetGroup.top = cummulativeHeight;
|
||||||
|
cummulativeWidth += assetGroup.width + this.gap;
|
||||||
|
lastRow = assetGroup.row - 1;
|
||||||
|
}
|
||||||
|
lastRowHeight = assetGroup.height + this.headerHeight;
|
||||||
|
}
|
||||||
|
if (lastRow === 0 || lastRow !== bucket.lastDateGroup?.row) {
|
||||||
|
cummulativeHeight += lastRowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.bucketHeight = cummulativeHeight;
|
||||||
|
bucket.isBucketHeightActual = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadBucket(yearMonth: TimelinePlainYearMonth, options?: { cancelable: boolean }): Promise<void> {
|
||||||
|
let cancelable = true;
|
||||||
|
if (options) {
|
||||||
|
cancelable = options.cancelable;
|
||||||
|
}
|
||||||
|
const bucket = this.getBucketByDate(yearMonth);
|
||||||
|
if (!bucket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucket.loader?.executed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await bucket.loader?.execute(async (signal: AbortSignal) => {
|
||||||
|
if (bucket.getFirstAsset()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timeBucket = toISOLocalDateTime(bucket.yearMonth);
|
||||||
|
const key = authManager.key;
|
||||||
|
const bucketResponse = await getTimeBucket(
|
||||||
|
{
|
||||||
|
...this.#options,
|
||||||
|
timeBucket,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
if (bucketResponse) {
|
||||||
|
if (this.#options.timelineAlbumId) {
|
||||||
|
const albumAssets = await getTimeBucket(
|
||||||
|
{
|
||||||
|
albumId: this.#options.timelineAlbumId,
|
||||||
|
timeBucket,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
for (const id of albumAssets.id) {
|
||||||
|
this.albumAssets.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unprocessedAssets = bucket.addAssets(bucketResponse);
|
||||||
|
if (unprocessedAssets.length > 0) {
|
||||||
|
console.error(
|
||||||
|
`Warning: getTimeBucket API returning assets not in requested month: ${bucket.yearMonth.month}, ${JSON.stringify(
|
||||||
|
unprocessedAssets.map((unprocessed) => ({
|
||||||
|
id: unprocessed.id,
|
||||||
|
localDateTime: unprocessed.localDateTime,
|
||||||
|
})),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.#layoutBucket(bucket);
|
||||||
|
}
|
||||||
|
}, cancelable);
|
||||||
|
if (result === 'LOADED') {
|
||||||
|
this.#updateIntersection(bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addAssets(assets: TimelineAsset[]) {
|
||||||
|
const assetsToUpdate: TimelineAsset[] = [];
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
if (this.isExcluded(asset)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assetsToUpdate.push(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notUpdated = this.updateAssets(assetsToUpdate);
|
||||||
|
this.#addAssetsToBuckets([...notUpdated]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#addAssetsToBuckets(assets: TimelineAsset[]) {
|
||||||
|
if (assets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addContext = new AddContext();
|
||||||
|
const updatedBuckets = new Set<AssetBucket>();
|
||||||
|
const bucketCount = this.buckets.length;
|
||||||
|
for (const asset of assets) {
|
||||||
|
let bucket = this.getBucketByDate(asset.localDateTime);
|
||||||
|
|
||||||
|
if (!bucket) {
|
||||||
|
bucket = new AssetBucket(this, asset.localDateTime, 1, this.#options.order);
|
||||||
|
bucket.isLoaded = true;
|
||||||
|
this.buckets.push(bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.addTimelineAsset(asset, addContext);
|
||||||
|
updatedBuckets.add(bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.buckets.length !== bucketCount) {
|
||||||
|
this.buckets.sort((a, b) => {
|
||||||
|
return a.yearMonth.year === b.yearMonth.year
|
||||||
|
? b.yearMonth.month - a.yearMonth.month
|
||||||
|
: b.yearMonth.year - a.yearMonth.year;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of addContext.existingDateGroups) {
|
||||||
|
group.sortAssets(this.#options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bucket of addContext.bucketsWithNewDateGroups) {
|
||||||
|
bucket.sortDateGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bucket of addContext.updatedBuckets) {
|
||||||
|
bucket.sortDateGroups();
|
||||||
|
this.#updateGeometry(bucket, { invalidateHeight: true });
|
||||||
|
}
|
||||||
|
this.updateIntersections();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBucketByDate(targetYearMonth: TimelinePlainYearMonth): AssetBucket | undefined {
|
||||||
|
return this.buckets.find(
|
||||||
|
(bucket) => bucket.yearMonth.year === targetYearMonth.year && bucket.yearMonth.month === targetYearMonth.month,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findBucketForAsset(id: string) {
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
await this.initTask.waitUntilCompletion();
|
||||||
|
}
|
||||||
|
let { bucket } = this.#findBucketForAsset(id) ?? {};
|
||||||
|
if (bucket) {
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
const asset = toTimelineAsset(await getAssetInfo({ id, key: authManager.key }));
|
||||||
|
if (!asset || this.isExcluded(asset)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bucket = await this.#loadBucketAtTime(asset.localDateTime, { cancelable: false });
|
||||||
|
if (bucket?.findAssetById({ id })) {
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #loadBucketAtTime(yearMonth: TimelinePlainYearMonth, options?: { cancelable: boolean }) {
|
||||||
|
await this.loadBucket(yearMonth, options);
|
||||||
|
return this.getBucketByDate(yearMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBucketIndexByAssetId(assetId: string) {
|
||||||
|
const bucketInfo = this.#findBucketForAsset(assetId);
|
||||||
|
return bucketInfo?.bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRandomBucket() {
|
||||||
|
const random = Math.floor(Math.random() * this.buckets.length);
|
||||||
|
const bucket = this.buckets[random];
|
||||||
|
await this.loadBucket(bucket.yearMonth, { cancelable: false });
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRandomAsset() {
|
||||||
|
const bucket = await this.getRandomBucket();
|
||||||
|
return bucket?.getRandomAsset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#runAssetOperation(ids: Set<string>, operation: AssetOperation) {
|
||||||
|
if (ids.size === 0) {
|
||||||
|
return { processedIds: new Set(), unprocessedIds: ids, changedGeometry: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedBuckets = new Set<AssetBucket>();
|
||||||
|
let idsToProcess = new Set(ids);
|
||||||
|
const idsProcessed = new Set<string>();
|
||||||
|
const combinedMoveAssets: { asset: TimelineAsset; date: TimelinePlainDate }[][] = [];
|
||||||
|
for (const bucket of this.buckets) {
|
||||||
|
if (idsToProcess.size > 0) {
|
||||||
|
const { moveAssets, processedIds, changedGeometry } = bucket.runAssetOperation(idsToProcess, operation);
|
||||||
|
if (moveAssets.length > 0) {
|
||||||
|
combinedMoveAssets.push(moveAssets);
|
||||||
|
}
|
||||||
|
idsToProcess = idsToProcess.difference(processedIds);
|
||||||
|
for (const id of processedIds) {
|
||||||
|
idsProcessed.add(id);
|
||||||
|
}
|
||||||
|
if (changedGeometry) {
|
||||||
|
changedBuckets.add(bucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (combinedMoveAssets.length > 0) {
|
||||||
|
this.#addAssetsToBuckets(combinedMoveAssets.flat().map((a) => a.asset));
|
||||||
|
}
|
||||||
|
const changedGeometry = changedBuckets.size > 0;
|
||||||
|
for (const bucket of changedBuckets) {
|
||||||
|
this.#updateGeometry(bucket, { invalidateHeight: true });
|
||||||
|
}
|
||||||
|
if (changedGeometry) {
|
||||||
|
this.updateIntersections();
|
||||||
|
}
|
||||||
|
return { unprocessedIds: idsToProcess, processedIds: idsProcessed, changedGeometry };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAssetOperation(ids: string[], operation: AssetOperation) {
|
||||||
|
this.#runAssetOperation(new Set(ids), operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAssets(assets: TimelineAsset[]) {
|
||||||
|
const lookup = new Map<string, TimelineAsset>(assets.map((asset) => [asset.id, asset]));
|
||||||
|
const { unprocessedIds } = this.#runAssetOperation(new Set(lookup.keys()), (asset) => {
|
||||||
|
updateObject(asset, lookup.get(asset.id));
|
||||||
|
return { remove: false };
|
||||||
|
});
|
||||||
|
return unprocessedIds.values().map((id) => lookup.get(id)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAssets(ids: string[]) {
|
||||||
|
const { unprocessedIds } = this.#runAssetOperation(new Set(ids), () => {
|
||||||
|
return { remove: true };
|
||||||
|
});
|
||||||
|
return [...unprocessedIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshLayout() {
|
||||||
|
for (const bucket of this.buckets) {
|
||||||
|
this.#updateGeometry(bucket, { invalidateHeight: true });
|
||||||
|
}
|
||||||
|
this.updateIntersections();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstAsset(): TimelineAsset | undefined {
|
||||||
|
return this.buckets[0]?.getFirstAsset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLaterAsset(
|
||||||
|
assetDescriptor: AssetDescriptor,
|
||||||
|
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||||
|
): Promise<TimelineAsset | undefined> {
|
||||||
|
return await this.#getAssetWithOffset(assetDescriptor, interval, 'later');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEarlierAsset(
|
||||||
|
assetDescriptor: AssetDescriptor,
|
||||||
|
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||||
|
): Promise<TimelineAsset | undefined> {
|
||||||
|
return await this.#getAssetWithOffset(assetDescriptor, interval, 'earlier');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getClosestAssetToDate(dateTime: TimelinePlainDateTime) {
|
||||||
|
const bucket = this.#findBucketForDate(dateTime);
|
||||||
|
if (!bucket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.loadBucket(dateTime, { cancelable: false });
|
||||||
|
const asset = bucket.findClosest(dateTime);
|
||||||
|
if (asset) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
for await (const asset of this.assetsIterator({ startBucket: bucket })) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveRange(start: AssetDescriptor, end: AssetDescriptor) {
|
||||||
|
let { asset: startAsset, bucket: startBucket } = this.#findBucketForAsset(start.id) ?? {};
|
||||||
|
if (!startBucket || !startAsset) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let { asset: endAsset, bucket: endBucket } = this.#findBucketForAsset(end.id) ?? {};
|
||||||
|
if (!endBucket || !endAsset) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let direction: Direction = 'earlier';
|
||||||
|
if (plainDateTimeCompare(true, startAsset.localDateTime, endAsset.localDateTime) < 0) {
|
||||||
|
[startAsset, endAsset] = [endAsset, startAsset];
|
||||||
|
[startBucket, endBucket] = [endBucket, startBucket];
|
||||||
|
direction = 'earlier';
|
||||||
|
}
|
||||||
|
|
||||||
|
const range: TimelineAsset[] = [];
|
||||||
|
const startDateGroup = startBucket.findDateGroupForAsset(startAsset);
|
||||||
|
for await (const targetAsset of this.assetsIterator({
|
||||||
|
startBucket,
|
||||||
|
startDateGroup,
|
||||||
|
startAsset,
|
||||||
|
direction,
|
||||||
|
})) {
|
||||||
|
range.push(targetAsset);
|
||||||
|
if (targetAsset.id === endAsset.id) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getAssetWithOffset(
|
||||||
|
assetDescriptor: AssetDescriptor,
|
||||||
|
interval: 'asset' | 'day' | 'month' | 'year' = 'asset',
|
||||||
|
direction: Direction,
|
||||||
|
): Promise<TimelineAsset | undefined> {
|
||||||
|
const { asset, bucket } = this.#findBucketForAsset(assetDescriptor.id) ?? {};
|
||||||
|
if (!bucket || !asset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (interval) {
|
||||||
|
case 'asset': {
|
||||||
|
return this.#getAssetByAssetOffset(asset, bucket, direction);
|
||||||
|
}
|
||||||
|
case 'day': {
|
||||||
|
return this.#getAssetByDayOffset(asset, bucket, direction);
|
||||||
|
}
|
||||||
|
case 'month': {
|
||||||
|
return this.#getAssetByMonthOffset(bucket, direction);
|
||||||
|
}
|
||||||
|
case 'year': {
|
||||||
|
return this.#getAssetByYearOffset(bucket, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getAssetByAssetOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) {
|
||||||
|
const dateGroup = bucket.findDateGroupForAsset(asset);
|
||||||
|
for await (const targetAsset of this.assetsIterator({
|
||||||
|
startBucket: bucket,
|
||||||
|
startDateGroup: dateGroup,
|
||||||
|
startAsset: asset,
|
||||||
|
direction,
|
||||||
|
})) {
|
||||||
|
if (asset.id === targetAsset.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return targetAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getAssetByDayOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) {
|
||||||
|
const dateGroup = bucket.findDateGroupForAsset(asset);
|
||||||
|
for await (const targetAsset of this.assetsIterator({
|
||||||
|
startBucket: bucket,
|
||||||
|
startDateGroup: dateGroup,
|
||||||
|
startAsset: asset,
|
||||||
|
direction,
|
||||||
|
})) {
|
||||||
|
if (targetAsset.localDateTime.day !== asset.localDateTime.day) {
|
||||||
|
return targetAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getAssetByMonthOffset(bucket: AssetBucket, direction: Direction) {
|
||||||
|
for (const targetBucket of this.bucketsIterator({ startBucket: bucket, direction })) {
|
||||||
|
if (targetBucket.yearMonth.month !== bucket.yearMonth.month) {
|
||||||
|
for await (const targetAsset of this.assetsIterator({ startBucket: targetBucket, direction })) {
|
||||||
|
return targetAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getAssetByYearOffset(bucket: AssetBucket, direction: Direction) {
|
||||||
|
for (const targetBucket of this.bucketsIterator({ startBucket: bucket, direction })) {
|
||||||
|
if (targetBucket.yearMonth.year !== bucket.yearMonth.year) {
|
||||||
|
for await (const targetAsset of this.assetsIterator({ startBucket: targetBucket, direction })) {
|
||||||
|
return targetAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isExcluded(asset: TimelineAsset) {
|
||||||
|
return (
|
||||||
|
isMismatched(this.#options.visibility, asset.visibility) ||
|
||||||
|
isMismatched(this.#options.isFavorite, asset.isFavorite) ||
|
||||||
|
isMismatched(this.#options.isTrashed, asset.isTrashed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import type { CommonPosition } from '$lib/utils/layout-utils';
|
||||||
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
|
import type { AssetDateGroup } from './asset-date-group.svelte';
|
||||||
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||||
|
} = TUNABLES;
|
||||||
|
|
||||||
|
export class IntersectingAsset {
|
||||||
|
readonly #group: AssetDateGroup;
|
||||||
|
|
||||||
|
intersecting = $derived.by(() => {
|
||||||
|
if (!this.position) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = this.#group.bucket.store;
|
||||||
|
|
||||||
|
const scrollCompensation = store.scrollCompensation;
|
||||||
|
const scrollCompensationHeightDelta = scrollCompensation?.heightDelta ?? 0;
|
||||||
|
|
||||||
|
const topWindow =
|
||||||
|
store.visibleWindow.top - store.headerHeight - INTERSECTION_EXPAND_TOP + scrollCompensationHeightDelta;
|
||||||
|
const bottomWindow =
|
||||||
|
store.visibleWindow.bottom + store.headerHeight + INTERSECTION_EXPAND_BOTTOM + scrollCompensationHeightDelta;
|
||||||
|
const positionTop = this.#group.absoluteDateGroupTop + this.position.top;
|
||||||
|
const positionBottom = positionTop + this.position.height;
|
||||||
|
|
||||||
|
const intersecting =
|
||||||
|
(positionTop >= topWindow && positionTop < bottomWindow) ||
|
||||||
|
(positionBottom >= topWindow && positionBottom < bottomWindow) ||
|
||||||
|
(positionTop < topWindow && positionBottom >= bottomWindow);
|
||||||
|
return intersecting;
|
||||||
|
});
|
||||||
|
|
||||||
|
position: CommonPosition | undefined = $state();
|
||||||
|
asset: TimelineAsset = <TimelineAsset>$state();
|
||||||
|
id: string | undefined = $derived(this.asset?.id);
|
||||||
|
|
||||||
|
constructor(group: AssetDateGroup, asset: TimelineAsset) {
|
||||||
|
this.#group = group;
|
||||||
|
this.asset = asset;
|
||||||
|
}
|
||||||
|
}
|
93
web/src/lib/managers/timeline-manager/types.ts
Normal file
93
web/src/lib/managers/timeline-manager/types.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import type { TimelinePlainDate, TimelinePlainDateTime } from '$lib/utils/timeline-util';
|
||||||
|
import type { AssetStackResponseDto, AssetVisibility } from '@immich/sdk';
|
||||||
|
|
||||||
|
export type AssetApiGetTimeBucketsRequest = Parameters<typeof import('@immich/sdk').getTimeBuckets>[0];
|
||||||
|
|
||||||
|
export type AssetStoreOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'> & {
|
||||||
|
timelineAlbumId?: string;
|
||||||
|
deferInit?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetDescriptor = { id: string };
|
||||||
|
|
||||||
|
export type Direction = 'earlier' | 'later';
|
||||||
|
|
||||||
|
export type TimelineAsset = {
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
ratio: number;
|
||||||
|
thumbhash: string | null;
|
||||||
|
localDateTime: TimelinePlainDateTime;
|
||||||
|
visibility: AssetVisibility;
|
||||||
|
isFavorite: boolean;
|
||||||
|
isTrashed: boolean;
|
||||||
|
isVideo: boolean;
|
||||||
|
isImage: boolean;
|
||||||
|
stack: AssetStackResponseDto | null;
|
||||||
|
duration: string | null;
|
||||||
|
projectionType: string | null;
|
||||||
|
livePhotoVideoId: string | null;
|
||||||
|
city: string | null;
|
||||||
|
country: string | null;
|
||||||
|
people: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetOperation = (asset: TimelineAsset) => { remove: boolean };
|
||||||
|
|
||||||
|
export type MoveAsset = { asset: TimelineAsset; date: TimelinePlainDate };
|
||||||
|
|
||||||
|
export interface Viewport {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ViewportXY = Viewport & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AddAsset {
|
||||||
|
type: 'add';
|
||||||
|
values: TimelineAsset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateAsset {
|
||||||
|
type: 'update';
|
||||||
|
values: TimelineAsset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteAsset {
|
||||||
|
type: 'delete';
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrashAssets {
|
||||||
|
type: 'trash';
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateStackAssets {
|
||||||
|
type: 'update_stack_assets';
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets | UpdateStackAssets;
|
||||||
|
|
||||||
|
export type LiteBucket = {
|
||||||
|
bucketHeight: number;
|
||||||
|
assetCount: number;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
bucketDateFormattted: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetStoreLayoutOptions = {
|
||||||
|
rowHeight?: number;
|
||||||
|
headerHeight?: number;
|
||||||
|
gap?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UpdateGeometryOptions {
|
||||||
|
invalidateHeight: boolean;
|
||||||
|
noDefer?: boolean;
|
||||||
|
}
|
34
web/src/lib/managers/timeline-manager/utils.svelte.ts
Normal file
34
web/src/lib/managers/timeline-manager/utils.svelte.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function updateObject(target: any, source: any): boolean {
|
||||||
|
if (!target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let updated = false;
|
||||||
|
for (const key in source) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(source, key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '__proto__' || key === 'constructor') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const isDate = target[key] instanceof Date;
|
||||||
|
if (typeof target[key] === 'object' && !isDate) {
|
||||||
|
updated = updated || updateObject(target[key], source[key]);
|
||||||
|
} else {
|
||||||
|
if (target[key] !== source[key]) {
|
||||||
|
target[key] = source[key];
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMismatched<T>(option: T | undefined, value: T): boolean {
|
||||||
|
return option === undefined ? false : option !== value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assetSnapshot = (asset: TimelineAsset): TimelineAsset => $state.snapshot(asset);
|
||||||
|
export const assetsSnapshot = (assets: TimelineAsset[]) => assets.map((asset) => $state.snapshot(asset));
|
@ -1,4 +1,4 @@
|
|||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk';
|
import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { type AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
import { type AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
||||||
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { readonly, writable } from 'svelte/store';
|
import { readonly, writable } from 'svelte/store';
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||||
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { AbortError } from '$lib/utils';
|
import { AbortError } from '$lib/utils';
|
||||||
import { fromLocalDateTimeToObject } from '$lib/utils/timeline-util';
|
import { fromLocalDateTimeToObject } from '$lib/utils/timeline-util';
|
||||||
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
||||||
import { AssetStore, type TimelineAsset } from './assets-store.svelte';
|
|
||||||
|
|
||||||
async function getAssets(store: AssetStore) {
|
async function getAssets(store: AssetStore) {
|
||||||
const assets = [];
|
const assets = [];
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
|
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||||
import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import type { StackResponse } from '$lib/utils/asset-utils';
|
import type { StackResponse } from '$lib/utils/asset-utils';
|
||||||
import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets } from '@immich/sdk';
|
import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets } from '@immich/sdk';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
@ -5,13 +5,11 @@ import { notificationController, NotificationType } from '$lib/components/shared
|
|||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||||
|
import type { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import {
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
assetsSnapshot,
|
|
||||||
isSelectingAllAssets,
|
|
||||||
type AssetStore,
|
|
||||||
type TimelineAsset,
|
|
||||||
} from '$lib/stores/assets-store.svelte';
|
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { downloadRequest, withError } from '$lib/utils';
|
import { downloadRequest, withError } from '$lib/utils';
|
||||||
import { createAlbum } from '$lib/utils/album-utils';
|
import { createAlbum } from '$lib/utils/album-utils';
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
|
// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
|
||||||
// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
|
// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
|
||||||
|
|
||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||||
import { isTimelineAsset } from '$lib/utils/timeline-util';
|
import { isTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { init, register, waitLocale } from 'svelte-i18n';
|
import { init, register, waitLocale } from 'svelte-i18n';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { fromTimelinePlainDateTime } from '$lib/utils/timeline-util';
|
import { fromTimelinePlainDateTime } from '$lib/utils/timeline-util';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
@ -29,6 +29,6 @@ export const TUNABLES = {
|
|||||||
NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(storage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false),
|
NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(storage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false),
|
||||||
},
|
},
|
||||||
IMAGE_THUMBNAIL: {
|
IMAGE_THUMBNAIL: {
|
||||||
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 150),
|
THUMBHASH_FADE_DURATION: getNumber(storage.getItem('THUMBHASH_FADE_DURATION'), 100),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
@ -87,7 +87,7 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
|
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
|
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
|
||||||
import { Button } from '@immich/ui';
|
import { Button } from '@immich/ui';
|
||||||
import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
|
import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
import { mdiArrowLeft, mdiPlus } from '@mdi/js';
|
import { mdiArrowLeft, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
|
@ -36,7 +36,8 @@
|
|||||||
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import {
|
import {
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore, type TimelineAsset, type Viewport } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
|
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { lang, locale } from '$lib/stores/preferences.store';
|
import { lang, locale } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
|
import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||||
import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
|
import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
|
||||||
import { Button, HStack, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
|
import { Button, HStack, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/managers/timeline-manager/asset-store.svelte';
|
||||||
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { fromLocalDateTimeToObject, fromTimelinePlainDateTime } from '$lib/utils/timeline-util';
|
import { fromLocalDateTimeToObject, fromTimelinePlainDateTime } from '$lib/utils/timeline-util';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { AssetTypeEnum, AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
import { AssetTypeEnum, AssetVisibility, type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user