209 lines
5.4 KiB
TypeScript
209 lines
5.4 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { createAssetView } from '../asset_manager';
|
|
|
|
type Identifier = string | number;
|
|
|
|
interface DocumentLike {
|
|
id?: Identifier;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface AssetObject {
|
|
ordinal?: number;
|
|
url?: string | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface AssetLike {
|
|
id?: Identifier;
|
|
cardinality?: number;
|
|
url?: string | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
objects?: AssetObject[];
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
type EnsureAssetUrl = (
|
|
documentId: Identifier,
|
|
asset: AssetLike,
|
|
options?: { start?: number; limit?: number; [key: string]: unknown },
|
|
) => Promise<unknown>;
|
|
|
|
type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null;
|
|
|
|
interface AssetViewLike {
|
|
getCardinality: () => number;
|
|
getObjects: () => AssetObject[];
|
|
getObject: (ordinal?: number) => AssetObject | null;
|
|
getPrimaryUrl: () => string | null;
|
|
getPrimaryMetadata: () => Record<string, unknown> | null;
|
|
}
|
|
|
|
interface UseAssetNavigatorOptions {
|
|
document?: DocumentLike | null;
|
|
assetType: string;
|
|
ensureAssetUrl?: EnsureAssetUrl;
|
|
getAsset?: GetAsset;
|
|
prefetch?: number;
|
|
defaultOrdinal?: number;
|
|
}
|
|
|
|
interface AssetNavigatorReturn {
|
|
document: DocumentLike | null;
|
|
documentId: Identifier | null;
|
|
asset: AssetLike | null;
|
|
assetType: string;
|
|
ordinal: number;
|
|
setOrdinal: (next: number) => void;
|
|
goPrev: () => void;
|
|
goNext: () => void;
|
|
canGoPrev: boolean;
|
|
canGoNext: boolean;
|
|
cardinality: number;
|
|
currentObject: AssetObject | null;
|
|
currentUrl: string | null;
|
|
currentMetadata: AssetObject['metadata'];
|
|
objects: AssetObject[];
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const clampOrdinalValue = (value: number, cardinality: number, defaultOrdinal: number) => {
|
|
const raw = Number.isFinite(value) ? value : defaultOrdinal;
|
|
let next = Math.max(1, Math.floor(raw));
|
|
if (cardinality && cardinality > 0) {
|
|
next = Math.min(next, cardinality);
|
|
}
|
|
return next;
|
|
};
|
|
|
|
export const useAssetNavigator = ({
|
|
document,
|
|
assetType,
|
|
ensureAssetUrl,
|
|
getAsset,
|
|
prefetch = 2,
|
|
defaultOrdinal = 1,
|
|
}: UseAssetNavigatorOptions): AssetNavigatorReturn => {
|
|
const documentId = (document?.id ?? null) as Identifier | null;
|
|
|
|
const asset = useMemo<AssetLike | null>(() => {
|
|
if (!document || !getAsset) {
|
|
return null;
|
|
}
|
|
return getAsset(document, assetType) || null;
|
|
}, [document, assetType, getAsset]);
|
|
|
|
const view = useMemo<AssetViewLike>(
|
|
() => createAssetView(asset) as unknown as AssetViewLike,
|
|
[asset],
|
|
);
|
|
const cardinality = view.getCardinality();
|
|
|
|
const [ordinal, setOrdinalInternal] = useState(defaultOrdinal);
|
|
|
|
useEffect(() => {
|
|
setOrdinalInternal(defaultOrdinal);
|
|
}, [documentId, assetType, defaultOrdinal]);
|
|
|
|
const setOrdinal = useCallback(
|
|
(next: number) => {
|
|
setOrdinalInternal(clampOrdinalValue(next, cardinality, defaultOrdinal));
|
|
},
|
|
[cardinality, defaultOrdinal],
|
|
);
|
|
|
|
const goPrev = useCallback(() => {
|
|
setOrdinalInternal((prev) => clampOrdinalValue(prev - 1, cardinality, defaultOrdinal));
|
|
}, [cardinality, defaultOrdinal]);
|
|
|
|
const goNext = useCallback(() => {
|
|
setOrdinalInternal((prev) => clampOrdinalValue(prev + 1, cardinality, defaultOrdinal));
|
|
}, [cardinality, defaultOrdinal]);
|
|
|
|
const objects = view.getObjects();
|
|
const currentObject = view.getObject(ordinal);
|
|
const currentUrl = currentObject?.url ?? view.getPrimaryUrl() ?? null;
|
|
const currentMetadata = currentObject?.metadata ?? view.getPrimaryMetadata() ?? null;
|
|
|
|
const canGoPrev = ordinal > 1;
|
|
const canGoNext = cardinality ? ordinal < cardinality : true;
|
|
|
|
const ordinalsNeedingLoad = useMemo<number[]>(() => {
|
|
const missing: number[] = [];
|
|
if (!asset) {
|
|
return missing;
|
|
}
|
|
const safePrefetch = Math.max(1, prefetch);
|
|
const maxOrdinal = cardinality && cardinality > 0
|
|
? Math.min(cardinality, ordinal + safePrefetch - 1)
|
|
: ordinal + safePrefetch - 1;
|
|
|
|
for (let ord = ordinal; ord <= maxOrdinal; ord += 1) {
|
|
const object = view.getObject(ord);
|
|
if (!object?.url) {
|
|
missing.push(ord);
|
|
}
|
|
}
|
|
|
|
return missing;
|
|
}, [asset, view, ordinal, prefetch, cardinality]);
|
|
|
|
const fetchStart = ordinalsNeedingLoad.length ? ordinalsNeedingLoad[0] : null;
|
|
const fetchEnd = ordinalsNeedingLoad.length
|
|
? ordinalsNeedingLoad[ordinalsNeedingLoad.length - 1]
|
|
: null;
|
|
const fetchLimit = fetchStart !== null && fetchEnd !== null ? fetchEnd - fetchStart + 1 : null;
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!documentId || !asset || !ensureAssetUrl) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
if (fetchStart === null || fetchLimit === null) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
ensureAssetUrl(documentId, asset, {
|
|
start: fetchStart,
|
|
limit: fetchLimit,
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
if (!cancelled) {
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [documentId, asset, ensureAssetUrl, fetchStart, fetchLimit]);
|
|
|
|
return {
|
|
document,
|
|
documentId,
|
|
asset,
|
|
assetType,
|
|
ordinal,
|
|
setOrdinal,
|
|
goPrev,
|
|
goNext,
|
|
canGoPrev,
|
|
canGoNext,
|
|
cardinality,
|
|
currentObject,
|
|
currentUrl,
|
|
currentMetadata,
|
|
objects,
|
|
isLoading: loading,
|
|
};
|
|
};
|
|
|
|
export default useAssetNavigator;
|