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 |
@@ -10,16 +10,9 @@ class ImmichFormController extends ChangeNotifier {
|
||||
FutureOr<void> Function()? onSubmit;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
bool _isDisposed = false;
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> submit() async {
|
||||
if (_isLoading) {
|
||||
return;
|
||||
@@ -34,9 +27,7 @@ class ImmichFormController extends ChangeNotifier {
|
||||
await onSubmit?.call();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +38,13 @@ class ImmichForm extends StatefulWidget {
|
||||
final String? submitText;
|
||||
final IconData? submitIcon;
|
||||
|
||||
const ImmichForm({super.key, this.onSubmit, this.submitText, this.submitIcon, required this.builder});
|
||||
const ImmichForm({
|
||||
super.key,
|
||||
this.onSubmit,
|
||||
this.submitText,
|
||||
this.submitIcon,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichForm> createState() => _ImmichFormState();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
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 { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
@@ -13,6 +12,8 @@
|
||||
|
||||
type Props = {
|
||||
viewerAssets: ViewerAsset[];
|
||||
firstInOrNearIndex: number;
|
||||
lastInOrNearIndex: number;
|
||||
width: number;
|
||||
height: number;
|
||||
manager: VirtualScrollManager;
|
||||
@@ -27,15 +28,27 @@
|
||||
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 scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||
const visibleViewerAssets = $derived(
|
||||
firstInOrNearIndex === -1 ? [] : viewerAssets.slice(firstInOrNearIndex, lastInOrNearIndex + 1),
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- Image grid -->
|
||||
<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 asset = viewerAsset.asset!}
|
||||
|
||||
|
||||
@@ -101,6 +101,8 @@
|
||||
<AssetLayout
|
||||
{manager}
|
||||
viewerAssets={timelineDay.viewerAssets}
|
||||
firstInOrNearIndex={timelineDay.firstInOrNearIndex}
|
||||
lastInOrNearIndex={timelineDay.lastInOrNearIndex}
|
||||
height={timelineDay.height}
|
||||
width={timelineDay.width}
|
||||
{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 { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
||||
import { getOrderingDate, plainDateTimeCompare } from '$lib/utils/timeline-util';
|
||||
import { TUNABLES } from '$lib/utils/tunables';
|
||||
import type { TimelineMonth } from './timeline-month.svelte';
|
||||
import type { Direction, MoveAsset, TimelineAsset } from './types';
|
||||
import { ViewerAsset } from './viewer-asset.svelte';
|
||||
|
||||
const {
|
||||
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||
} = TUNABLES;
|
||||
|
||||
export class TimelineDay {
|
||||
readonly timelineMonth: TimelineMonth;
|
||||
readonly index: number;
|
||||
@@ -17,7 +22,12 @@ export class TimelineDay {
|
||||
|
||||
height = $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);
|
||||
#start: number = $state(0);
|
||||
@@ -149,6 +159,73 @@ export class TimelineDay {
|
||||
for (let i = 0; i < this.viewerAssets.length; 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() {
|
||||
|
||||
@@ -214,6 +214,11 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
|
||||
for (const month of this.months) {
|
||||
updateTimelineMonthViewportProximity(this, month);
|
||||
if (month.isInOrNearViewport && month.isLoaded) {
|
||||
for (const day of month.timelineDays) {
|
||||
day.updateAssetBoundaries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const month = this.months.find((month) => month.isInViewport);
|
||||
|
||||
@@ -254,7 +254,7 @@ export class TimelineMonth {
|
||||
addContext.newTimelineDays.add(timelineDay);
|
||||
}
|
||||
|
||||
const viewerAsset = new ViewerAsset(timelineDay, timelineAsset);
|
||||
const viewerAsset = new ViewerAsset(timelineAsset);
|
||||
timelineDay.viewerAssets.push(viewerAsset);
|
||||
addContext.changedTimelineDays.add(timelineDay);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,12 @@
|
||||
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';
|
||||
|
||||
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();
|
||||
asset: TimelineAsset = $state() as TimelineAsset;
|
||||
id: string = $derived(this.asset.id);
|
||||
|
||||
constructor(group: TimelineDay, asset: TimelineAsset) {
|
||||
this.#group = group;
|
||||
constructor(asset: TimelineAsset) {
|
||||
this.asset = asset;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user