mirror of
https://github.com/immich-app/immich.git
synced 2026-05-24 16:42:30 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b1e7795e2 | |||
| 5b10ff0eff | |||
| 6eab14f6a4 |
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte';
|
|
||||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||||
import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
@@ -13,6 +12,8 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
viewerAssets: ViewerAsset[];
|
viewerAssets: ViewerAsset[];
|
||||||
|
firstInOrNearIndex: number;
|
||||||
|
lastInOrNearIndex: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
manager: VirtualScrollManager;
|
manager: VirtualScrollManager;
|
||||||
@@ -27,15 +28,27 @@
|
|||||||
customThumbnailLayout?: Snippet<[asset: TimelineAsset]>;
|
customThumbnailLayout?: Snippet<[asset: TimelineAsset]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { viewerAssets, width, height, manager, thumbnail, customThumbnailLayout }: Props = $props();
|
const {
|
||||||
|
viewerAssets,
|
||||||
|
firstInOrNearIndex,
|
||||||
|
lastInOrNearIndex,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
manager,
|
||||||
|
thumbnail,
|
||||||
|
customThumbnailLayout,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
|
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
|
||||||
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||||
|
const visibleViewerAssets = $derived(
|
||||||
|
firstInOrNearIndex === -1 ? [] : viewerAssets.slice(firstInOrNearIndex, lastInOrNearIndex + 1),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
||||||
{#each filterIsInOrNearViewport(viewerAssets) as viewerAsset (viewerAsset.id)}
|
{#each visibleViewerAssets as viewerAsset (viewerAsset.id)}
|
||||||
{@const position = viewerAsset.position!}
|
{@const position = viewerAsset.position!}
|
||||||
{@const asset = viewerAsset.asset!}
|
{@const asset = viewerAsset.asset!}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,8 @@
|
|||||||
<AssetLayout
|
<AssetLayout
|
||||||
{manager}
|
{manager}
|
||||||
viewerAssets={timelineDay.viewerAssets}
|
viewerAssets={timelineDay.viewerAssets}
|
||||||
|
firstInOrNearIndex={timelineDay.firstInOrNearIndex}
|
||||||
|
lastInOrNearIndex={timelineDay.lastInOrNearIndex}
|
||||||
height={timelineDay.height}
|
height={timelineDay.height}
|
||||||
width={timelineDay.width}
|
width={timelineDay.width}
|
||||||
{customThumbnailLayout}
|
{customThumbnailLayout}
|
||||||
|
|||||||
@@ -54,16 +54,3 @@ export function updateTimelineMonthViewportProximity(timelineManager: TimelineMa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateViewerAssetViewportProximity(
|
|
||||||
timelineManager: TimelineManager,
|
|
||||||
positionTop: number,
|
|
||||||
positionHeight: number,
|
|
||||||
) {
|
|
||||||
const headerHeight = timelineManager.headerHeight;
|
|
||||||
return calculateViewportProximity(
|
|
||||||
positionTop,
|
|
||||||
positionTop + positionHeight,
|
|
||||||
timelineManager.visibleWindow.top - headerHeight,
|
|
||||||
timelineManager.visibleWindow.bottom + headerHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ import { SvelteSet } from 'svelte/reactivity';
|
|||||||
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
|
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
|
||||||
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
||||||
import { getOrderingDate, plainDateTimeCompare } from '$lib/utils/timeline-util';
|
import { getOrderingDate, plainDateTimeCompare } from '$lib/utils/timeline-util';
|
||||||
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import type { TimelineMonth } from './timeline-month.svelte';
|
import type { TimelineMonth } from './timeline-month.svelte';
|
||||||
import type { Direction, MoveAsset, TimelineAsset } from './types';
|
import type { Direction, MoveAsset, TimelineAsset } from './types';
|
||||||
import { ViewerAsset } from './viewer-asset.svelte';
|
import { ViewerAsset } from './viewer-asset.svelte';
|
||||||
|
|
||||||
|
const {
|
||||||
|
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||||
|
} = TUNABLES;
|
||||||
|
|
||||||
export class TimelineDay {
|
export class TimelineDay {
|
||||||
readonly timelineMonth: TimelineMonth;
|
readonly timelineMonth: TimelineMonth;
|
||||||
readonly index: number;
|
readonly index: number;
|
||||||
@@ -17,7 +22,12 @@ export class TimelineDay {
|
|||||||
|
|
||||||
height = $state(0);
|
height = $state(0);
|
||||||
width = $state(0);
|
width = $state(0);
|
||||||
isInOrNearViewport = $derived.by(() => this.viewerAssets.some((viewAsset) => viewAsset.isInOrNearViewport));
|
|
||||||
|
// Indices into viewerAssets bounding the in-or-near range. -1/-1 means no assets are in-or-near.
|
||||||
|
// Updated imperatively by updateAssetBoundaries() from updateViewportProximities() and layout().
|
||||||
|
firstInOrNearIndex = $state(-1);
|
||||||
|
lastInOrNearIndex = $state(-1);
|
||||||
|
isInOrNearViewport = $derived(this.firstInOrNearIndex !== -1);
|
||||||
|
|
||||||
#top: number = $state(0);
|
#top: number = $state(0);
|
||||||
#start: number = $state(0);
|
#start: number = $state(0);
|
||||||
@@ -149,6 +159,73 @@ export class TimelineDay {
|
|||||||
for (let i = 0; i < this.viewerAssets.length; i++) {
|
for (let i = 0; i < this.viewerAssets.length; i++) {
|
||||||
this.viewerAssets[i].position = geometry.getPosition(i);
|
this.viewerAssets[i].position = geometry.getPosition(i);
|
||||||
}
|
}
|
||||||
|
this.updateAssetBoundaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imperatively (re)computes firstInOrNearIndex / lastInOrNearIndex via binary search on
|
||||||
|
// asset positions. Called from layout() (positions changed) and from
|
||||||
|
// updateViewportProximities() (viewport changed). Cost: O(log N) per day instead of the
|
||||||
|
// O(N) per-asset $derived recompute that the reactive equivalent would do.
|
||||||
|
updateAssetBoundaries() {
|
||||||
|
const manager = this.timelineMonth.timelineManager;
|
||||||
|
const visibleWindow = manager.visibleWindow;
|
||||||
|
if (this.viewerAssets.length === 0 || !this.viewerAssets[0].position) {
|
||||||
|
this.firstInOrNearIndex = -1;
|
||||||
|
this.lastInOrNearIndex = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Match the asset-level proximity zone from calculateViewerAssetViewportProximity:
|
||||||
|
// window is expanded by headerHeight on both sides, then by INTERSECTION_EXPAND_*
|
||||||
|
// for the "near" band. Combined: the in-or-near zone reaches headerHeight + EXPAND
|
||||||
|
// beyond the visible window on each side.
|
||||||
|
const headerHeight = manager.headerHeight;
|
||||||
|
const dayOffset = this.absoluteTimelineDayTop;
|
||||||
|
const localExpandedTop = visibleWindow.top - headerHeight - INTERSECTION_EXPAND_TOP - dayOffset;
|
||||||
|
const localExpandedBottom = visibleWindow.bottom + headerHeight + INTERSECTION_EXPAND_BOTTOM - dayOffset;
|
||||||
|
|
||||||
|
// Lower bound: smallest i where asset[i].bottom >= localExpandedTop
|
||||||
|
// (asset's bottom edge is at or below the in-or-near top boundary).
|
||||||
|
let lo = 0;
|
||||||
|
let hi = this.viewerAssets.length;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = (lo + hi) >>> 1;
|
||||||
|
const position = this.viewerAssets[mid].position!;
|
||||||
|
if (position.top + position.height < localExpandedTop) {
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
hi = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const firstIdx = lo;
|
||||||
|
|
||||||
|
if (firstIdx >= this.viewerAssets.length) {
|
||||||
|
// Entire day is above the in-or-near zone.
|
||||||
|
this.firstInOrNearIndex = -1;
|
||||||
|
this.lastInOrNearIndex = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upper bound: smallest i where asset[i].top >= localExpandedBottom
|
||||||
|
// (asset's top edge crosses past the in-or-near bottom boundary).
|
||||||
|
lo = firstIdx;
|
||||||
|
hi = this.viewerAssets.length;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = (lo + hi) >>> 1;
|
||||||
|
if (this.viewerAssets[mid].position!.top >= localExpandedBottom) {
|
||||||
|
hi = mid;
|
||||||
|
} else {
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lastIdx = lo - 1;
|
||||||
|
|
||||||
|
if (lastIdx < firstIdx) {
|
||||||
|
this.firstInOrNearIndex = -1;
|
||||||
|
this.lastInOrNearIndex = -1;
|
||||||
|
} else {
|
||||||
|
this.firstInOrNearIndex = firstIdx;
|
||||||
|
this.lastInOrNearIndex = lastIdx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get absoluteTimelineDayTop() {
|
get absoluteTimelineDayTop() {
|
||||||
|
|||||||
@@ -214,6 +214,11 @@ export class TimelineManager extends VirtualScrollManager {
|
|||||||
|
|
||||||
for (const month of this.months) {
|
for (const month of this.months) {
|
||||||
updateTimelineMonthViewportProximity(this, month);
|
updateTimelineMonthViewportProximity(this, month);
|
||||||
|
if (month.isInOrNearViewport && month.isLoaded) {
|
||||||
|
for (const day of month.timelineDays) {
|
||||||
|
day.updateAssetBoundaries();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const month = this.months.find((month) => month.isInViewport);
|
const month = this.months.find((month) => month.isInViewport);
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export class TimelineMonth {
|
|||||||
addContext.newTimelineDays.add(timelineDay);
|
addContext.newTimelineDays.add(timelineDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewerAsset = new ViewerAsset(timelineDay, timelineAsset);
|
const viewerAsset = new ViewerAsset(timelineAsset);
|
||||||
timelineDay.viewerAssets.push(viewerAsset);
|
timelineDay.viewerAssets.push(viewerAsset);
|
||||||
addContext.changedTimelineDays.add(timelineDay);
|
addContext.changedTimelineDays.add(timelineDay);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,12 @@
|
|||||||
import type { CommonPosition } from '$lib/utils/layout-utils';
|
import type { CommonPosition } from '$lib/utils/layout-utils';
|
||||||
import {
|
|
||||||
ViewportProximity,
|
|
||||||
calculateViewerAssetViewportProximity,
|
|
||||||
isInOrNearViewport,
|
|
||||||
} from './internal/intersection-support.svelte';
|
|
||||||
import type { TimelineDay } from './timeline-day.svelte';
|
|
||||||
import type { TimelineAsset } from './types';
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
export class ViewerAsset {
|
export class ViewerAsset {
|
||||||
readonly #group: TimelineDay;
|
|
||||||
|
|
||||||
#viewportProximity = $derived.by(() => {
|
|
||||||
if (!this.position) {
|
|
||||||
return ViewportProximity.FarFromViewport;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = this.#group.timelineMonth.timelineManager;
|
|
||||||
const positionTop = this.#group.absoluteTimelineDayTop + this.position.top;
|
|
||||||
|
|
||||||
return calculateViewerAssetViewportProximity(store, positionTop, this.position.height);
|
|
||||||
});
|
|
||||||
|
|
||||||
get isInOrNearViewport() {
|
|
||||||
return isInOrNearViewport(this.#viewportProximity);
|
|
||||||
}
|
|
||||||
|
|
||||||
position: CommonPosition | undefined = $state.raw();
|
position: CommonPosition | undefined = $state.raw();
|
||||||
asset: TimelineAsset = $state() as TimelineAsset;
|
asset: TimelineAsset = $state() as TimelineAsset;
|
||||||
id: string = $derived(this.asset.id);
|
id: string = $derived(this.asset.id);
|
||||||
|
|
||||||
constructor(group: TimelineDay, asset: TimelineAsset) {
|
constructor(asset: TimelineAsset) {
|
||||||
this.#group = group;
|
|
||||||
this.asset = asset;
|
this.asset = asset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user