mirror of
https://github.com/immich-app/immich.git
synced 2025-12-11 15:45:18 -05:00
- Extract common timeline functionality into Photostream.svelte base component - Create PhotostreamWithScrubber.svelte to handle scrubber integration - Simplify Timeline.svelte by removing ~300 lines of scrolling/scrubber logic - Add findMonthAtScrollPosition utility with binary search for better performance - Maintain all existing functionality while improving code organization
171 lines
6.2 KiB
Svelte
171 lines
6.2 KiB
Svelte
<script lang="ts">
|
|
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
|
import MonthSegment from '$lib/components/timeline/MonthSegment.svelte';
|
|
import PhotostreamWithScrubber from '$lib/components/timeline/PhotostreamWithScrubber.svelte';
|
|
import SelectableDay from '$lib/components/timeline/SelectableDay.svelte';
|
|
import SelectableSegment from '$lib/components/timeline/SelectableSegment.svelte';
|
|
import TimelineAssetViewer from '$lib/components/timeline/TimelineAssetViewer.svelte';
|
|
import TimelineKeyboardActions from '$lib/components/timeline/actions/TimelineKeyboardActions.svelte';
|
|
import { AssetAction } from '$lib/constants';
|
|
import Portal from '$lib/elements/Portal.svelte';
|
|
import Skeleton from '$lib/elements/Skeleton.svelte';
|
|
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
import { getSegmentIdentifier, getTimes } from '$lib/utils/timeline-util';
|
|
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
|
import { DateTime } from 'luxon';
|
|
import { type Snippet } from 'svelte';
|
|
|
|
interface Props {
|
|
isSelectionMode?: boolean;
|
|
singleSelect?: boolean;
|
|
/** `true` if this asset grid is responds to navigation events; if `true`, then look at the
|
|
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
|
|
additionally, update the page location/url with the asset as the timeline is scrolled */
|
|
enableRouting: boolean;
|
|
timelineManager: TimelineManager;
|
|
assetInteraction: AssetInteraction;
|
|
removeAction?:
|
|
| AssetAction.UNARCHIVE
|
|
| AssetAction.ARCHIVE
|
|
| AssetAction.FAVORITE
|
|
| AssetAction.UNFAVORITE
|
|
| AssetAction.SET_VISIBILITY_TIMELINE
|
|
| null;
|
|
withStacked?: boolean;
|
|
showArchiveIcon?: boolean;
|
|
isShared?: boolean;
|
|
album?: AlbumResponseDto | null;
|
|
person?: PersonResponseDto | null;
|
|
isShowDeleteConfirmation?: boolean;
|
|
onAssetOpen?: (asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
|
onAssetSelect?: (asset: TimelineAsset) => void;
|
|
onEscape?: () => void;
|
|
children?: Snippet;
|
|
empty?: Snippet;
|
|
customThumbnailLayout?: Snippet<[TimelineAsset]>;
|
|
}
|
|
|
|
let {
|
|
isSelectionMode = false,
|
|
singleSelect = false,
|
|
enableRouting,
|
|
timelineManager = $bindable(),
|
|
assetInteraction,
|
|
removeAction = null,
|
|
withStacked = false,
|
|
showArchiveIcon = false,
|
|
isShared = false,
|
|
album = null,
|
|
person = null,
|
|
isShowDeleteConfirmation = $bindable(false),
|
|
|
|
onAssetSelect,
|
|
onAssetOpen,
|
|
onEscape = () => {},
|
|
children,
|
|
empty,
|
|
customThumbnailLayout,
|
|
}: Props = $props();
|
|
|
|
let { isViewing: showAssetViewer, asset: viewingAsset } = assetViewingStore;
|
|
|
|
let viewer: PhotostreamWithScrubber | undefined = $state();
|
|
let showSkeleton: boolean = $state(true);
|
|
|
|
$effect(() => {
|
|
if ($showAssetViewer) {
|
|
const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60);
|
|
void timelineManager.loadSegment(getSegmentIdentifier({ year: localDateTime.year, month: localDateTime.month }));
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<TimelineKeyboardActions
|
|
scrollToAsset={(asset) => viewer?.scrollToAsset(asset) ?? false}
|
|
{timelineManager}
|
|
{assetInteraction}
|
|
bind:isShowDeleteConfirmation
|
|
{onEscape}
|
|
/>
|
|
|
|
<PhotostreamWithScrubber
|
|
bind:this={viewer}
|
|
{enableRouting}
|
|
{timelineManager}
|
|
{isShowDeleteConfirmation}
|
|
{showSkeleton}
|
|
{children}
|
|
{empty}
|
|
>
|
|
{#snippet skeleton({ segment })}
|
|
<Skeleton
|
|
height={segment.height - segment.timelineManager.headerHeight}
|
|
title={(segment as MonthGroup).monthGroupTitle}
|
|
/>
|
|
{/snippet}
|
|
{#snippet segment({ segment, onScrollCompensationMonthInDOM })}
|
|
<SelectableSegment
|
|
{segment}
|
|
{onScrollCompensationMonthInDOM}
|
|
{timelineManager}
|
|
{assetInteraction}
|
|
{isSelectionMode}
|
|
{singleSelect}
|
|
{onAssetOpen}
|
|
{onAssetSelect}
|
|
>
|
|
{#snippet content({ onAssetOpen, onAssetSelect, onAssetHover })}
|
|
<SelectableDay {assetInteraction} {onAssetSelect}>
|
|
{#snippet content({ onDayGroupSelect, onDayGroupAssetSelect })}
|
|
<MonthSegment
|
|
{assetInteraction}
|
|
{customThumbnailLayout}
|
|
{singleSelect}
|
|
monthGroup={segment as MonthGroup}
|
|
{timelineManager}
|
|
{onDayGroupSelect}
|
|
>
|
|
{#snippet thumbnail({ asset, position, dayGroup, groupIndex })}
|
|
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
|
{@const isAssetSelected =
|
|
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
|
|
{@const isAssetDisabled = timelineManager.albumAssets.has(asset.id)}
|
|
<Thumbnail
|
|
showStackedIcon={withStacked}
|
|
{showArchiveIcon}
|
|
{asset}
|
|
{groupIndex}
|
|
onClick={() => onAssetOpen(asset)}
|
|
onSelect={() => onDayGroupAssetSelect(dayGroup, asset)}
|
|
onMouseEvent={(isMouseOver) => {
|
|
if (isMouseOver) {
|
|
onAssetHover(asset);
|
|
} else {
|
|
onAssetHover(null);
|
|
}
|
|
}}
|
|
selected={isAssetSelected}
|
|
selectionCandidate={isAssetSelectionCandidate}
|
|
disabled={isAssetDisabled}
|
|
thumbnailWidth={position.width}
|
|
thumbnailHeight={position.height}
|
|
/>
|
|
{/snippet}
|
|
</MonthSegment>
|
|
{/snippet}
|
|
</SelectableDay>
|
|
{/snippet}
|
|
</SelectableSegment>
|
|
{/snippet}
|
|
</PhotostreamWithScrubber>
|
|
|
|
<Portal target="body">
|
|
{#if $showAssetViewer}
|
|
<TimelineAssetViewer bind:showSkeleton {timelineManager} {removeAction} {withStacked} {isShared} {album} {person} />
|
|
{/if}
|
|
</Portal>
|