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:
@@ -305,4 +305,63 @@ export abstract class PhotostreamManager {
|
|||||||
// Return the IDs that were not found/removed
|
// Return the IDs that were not found/removed
|
||||||
return assetIds.filter((id) => !removedIds.includes(id));
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { PhotostreamManager } from '$lib/managers/photostream-manager/Photo
|
|||||||
import { getTestHook } from '$lib/managers/photostream-manager/TestHooks.svelte';
|
import { getTestHook } from '$lib/managers/photostream-manager/TestHooks.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||||
|
import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
|
||||||
|
|
||||||
export type SegmentIdentifier = {
|
export type SegmentIdentifier = {
|
||||||
matches(segment: PhotostreamSegment): boolean;
|
matches(segment: PhotostreamSegment): boolean;
|
||||||
@@ -144,12 +145,31 @@ export abstract class PhotostreamSegment {
|
|||||||
this.loader?.cancel();
|
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 }) {
|
updateIntersection({ intersecting, actuallyIntersecting }: { intersecting: boolean; actuallyIntersecting: boolean }) {
|
||||||
this.intersecting = intersecting;
|
this.intersecting = intersecting;
|
||||||
this.actuallyIntersecting = actuallyIntersecting;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type {
|
|||||||
SearchTerms,
|
SearchTerms,
|
||||||
} from '$lib/managers/searchresults-manager/SearchResultsManager.svelte';
|
} from '$lib/managers/searchresults-manager/SearchResultsManager.svelte';
|
||||||
import { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.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 { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import { searchAssets, searchSmart } from '@immich/sdk';
|
import { searchAssets, searchSmart } from '@immich/sdk';
|
||||||
@@ -72,18 +71,6 @@ export class SearchResultsSegment extends PhotostreamSegment {
|
|||||||
this.layout();
|
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[] {
|
get viewerAssets(): ViewerAsset[] {
|
||||||
return this.#viewerAssets;
|
return this.#viewerAssets;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user