feat(web): add asset navigation and simplify photostream architecture

- Add findNextAsset, findPreviousAsset, and findRandomAsset methods
  to PhotostreamManager
  - Move layout() and findAssetAbsolutePosition() to PhotostreamSegment
   base class
  - Create SimplePhotostreamManager for basic photostream functionalityw
This commit is contained in:
midzelis
2025-09-28 01:45:23 +00:00
parent e95ca39e4f
commit 7806fe7d86
4 changed files with 131 additions and 15 deletions

View File

@@ -305,4 +305,63 @@ export abstract class PhotostreamManager {
// Return the IDs that were not found/removed
return assetIds.filter((id) => !removedIds.includes(id));
}
findNextAsset(currentAssetId: string): { id: string } | undefined {
for (let segmentIndex = 0; segmentIndex < this.months.length; segmentIndex++) {
const segment = this.months[segmentIndex];
const assetIndex = segment.assets.findIndex((asset) => asset.id === currentAssetId);
if (assetIndex !== -1) {
// Found the current asset
if (assetIndex < segment.assets.length - 1) {
// Next asset is in the same segment
return segment.assets[assetIndex + 1];
} else if (segmentIndex < this.months.length - 1) {
// Next asset is in the next segment
const nextSegment = this.months[segmentIndex + 1];
if (nextSegment.assets.length > 0) {
return nextSegment.assets[0];
}
}
break;
}
}
return undefined;
}
findPreviousAsset(currentAssetId: string): { id: string } | undefined {
for (let segmentIndex = 0; segmentIndex < this.months.length; segmentIndex++) {
const segment = this.months[segmentIndex];
const assetIndex = segment.assets.findIndex((asset) => asset.id === currentAssetId);
if (assetIndex !== -1) {
// Found the current asset
if (assetIndex > 0) {
// Previous asset is in the same segment
return segment.assets[assetIndex - 1];
} else if (segmentIndex > 0) {
// Previous asset is in the previous segment
const previousSegment = this.months[segmentIndex - 1];
if (previousSegment.assets.length > 0) {
return previousSegment.assets.at(-1);
}
}
break;
}
}
return undefined;
}
findRandomAsset(): { id: string } | undefined {
// Get all loaded assets across all segments
const allAssets = this.months.flatMap((segment) => segment.assets);
if (allAssets.length === 0) {
return undefined;
}
// Return a random asset
const randomIndex = Math.floor(Math.random() * allAssets.length);
return allAssets[randomIndex];
}
}

View File

@@ -7,6 +7,7 @@ import type { PhotostreamManager } from '$lib/managers/photostream-manager/Photo
import { getTestHook } from '$lib/managers/photostream-manager/TestHooks.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
export type SegmentIdentifier = {
matches(segment: PhotostreamSegment): boolean;
@@ -144,12 +145,31 @@ export abstract class PhotostreamSegment {
this.loader?.cancel();
}
layout(_?: boolean) {}
layout(): void {
const timelineAssets = this.viewerAssets.map((viewerAsset) => viewerAsset.asset);
const layoutOptions = this.timelineManager.layoutOptions;
const geometry = getJustifiedLayoutFromAssets(timelineAssets, layoutOptions);
this.height = timelineAssets.length === 0 ? 0 : geometry.containerHeight + this.timelineManager.headerHeight;
for (let i = 0; i < this.viewerAssets.length; i++) {
const position = getPosition(geometry, i);
this.viewerAssets[i].position = position;
}
}
updateIntersection({ intersecting, actuallyIntersecting }: { intersecting: boolean; actuallyIntersecting: boolean }) {
this.intersecting = intersecting;
this.actuallyIntersecting = actuallyIntersecting;
}
abstract findAssetAbsolutePosition(assetId: string): number;
findAssetAbsolutePosition(assetId: string) {
const viewerAsset = this.viewerAssets.find((viewAsset) => viewAsset.id === assetId);
if (viewerAsset) {
if (!viewerAsset.position) {
console.warn('No position for asset');
return -1;
}
return this.top + viewerAsset.position.top + this.timelineManager.headerHeight;
}
return -1;
}
}

View File

@@ -7,7 +7,6 @@ import type {
SearchTerms,
} from '$lib/managers/searchresults-manager/SearchResultsManager.svelte';
import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables';
import { searchAssets, searchSmart } from '@immich/sdk';
@@ -72,18 +71,6 @@ export class SearchResultsSegment extends PhotostreamSegment {
this.layout();
}
layout(): void {
const timelineAssets = this.#viewerAssets.map((viewerAsset) => viewerAsset.asset);
const layoutOptions = this.timelineManager.layoutOptions;
const geometry = getJustifiedLayoutFromAssets(timelineAssets, layoutOptions);
this.height = timelineAssets.length === 0 ? 0 : geometry.containerHeight + this.timelineManager.headerHeight;
for (let i = 0; i < this.#viewerAssets.length; i++) {
const position = getPosition(geometry, i);
this.#viewerAssets[i].position = position;
}
}
get viewerAssets(): ViewerAsset[] {
return this.#viewerAssets;
}

View File

@@ -0,0 +1,50 @@
import { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
import {
PhotostreamSegment,
type SegmentIdentifier,
} from '$lib/managers/photostream-manager/PhotostreamSegment.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
function createSimpleSegment(manager: SimplePhotostreamManager, assets: TimelineAsset[]) {
class SimpleSegment extends PhotostreamSegment {
#viewerAssets = $derived(assets.map((asset) => new ViewerAsset(this, asset)));
#identifier = {
matches: () => true,
};
get timelineManager(): PhotostreamManager {
return manager;
}
get identifier(): SegmentIdentifier {
return this.#identifier;
}
get id(): string {
return 'one';
}
protected fetch(): Promise<void> {
return Promise.resolve();
}
get viewerAssets(): ViewerAsset[] {
return this.#viewerAssets;
}
}
return new SimpleSegment();
}
export class SimplePhotostreamManager extends PhotostreamManager {
#assets: TimelineAsset[] = $state([]);
#segment: PhotostreamSegment;
constructor() {
super();
this.#segment = createSimpleSegment(this, this.#assets);
}
set assets(assets: TimelineAsset[]) {
this.#assets = assets;
}
get months(): PhotostreamSegment[] {
return [this.#segment];
}
}