6
This commit is contained in:
+151
-16
@@ -557,7 +557,7 @@ const computeStackAngle = (docId, index) => {
|
||||
for (let i = 0; i < source.length; i += 1) {
|
||||
hash = (hash * 31 + source.charCodeAt(i)) % 997;
|
||||
}
|
||||
const magnitude = (hash % 15) + 1; // 1..15
|
||||
const magnitude = Math.max(3, (hash % 13) + 3); // 3..15
|
||||
const sign = index % 2 === 0 ? 1 : -1;
|
||||
return magnitude * sign;
|
||||
};
|
||||
@@ -568,7 +568,13 @@ const PreviewStack = ({
|
||||
items = [],
|
||||
maxItems = MAX_PREVIEW_STACK_ITEMS,
|
||||
emptyMessage = 'Preview unavailable',
|
||||
onItemActivate,
|
||||
onOpenPreview,
|
||||
activeItemId = null,
|
||||
}) => {
|
||||
const angleMapRef = useRef(new Map());
|
||||
const previousOrderRef = useRef([]);
|
||||
|
||||
if (!items.length) {
|
||||
return <span className="meta">{emptyMessage}</span>;
|
||||
}
|
||||
@@ -576,25 +582,106 @@ const PreviewStack = ({
|
||||
const limited = items.slice(0, maxItems);
|
||||
const hasMultiple = limited.length > 1;
|
||||
|
||||
const preparedItems = useMemo(() => {
|
||||
const angleMap = angleMapRef.current;
|
||||
const previousOrder = previousOrderRef.current;
|
||||
const prevFrontId = previousOrder.length ? previousOrder[0] : null;
|
||||
const currentFrontId = limited.length ? limited[0].id : null;
|
||||
const currentIds = new Set();
|
||||
|
||||
limited.forEach((entry, index) => {
|
||||
const id = entry.id;
|
||||
currentIds.add(id);
|
||||
if (!angleMap.has(id)) {
|
||||
const initialAngle = index === 0 ? 0 : computeStackAngle(id, index);
|
||||
angleMap.set(id, initialAngle);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
onItemActivate &&
|
||||
activeItemId &&
|
||||
currentFrontId === activeItemId &&
|
||||
prevFrontId &&
|
||||
prevFrontId !== activeItemId
|
||||
) {
|
||||
const frontAngle = angleMap.get(prevFrontId);
|
||||
const activeAngle = angleMap.get(activeItemId);
|
||||
angleMap.set(
|
||||
prevFrontId,
|
||||
typeof activeAngle === 'number' ? activeAngle : computeStackAngle(prevFrontId, 1),
|
||||
);
|
||||
angleMap.set(activeItemId, typeof frontAngle === 'number' ? frontAngle : 0);
|
||||
} else if (currentFrontId && (!angleMap.has(currentFrontId) || angleMap.get(currentFrontId) !== 0)) {
|
||||
angleMap.set(currentFrontId, 0);
|
||||
}
|
||||
|
||||
angleMap.forEach((_, id) => {
|
||||
if (!currentIds.has(id)) {
|
||||
angleMap.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
const result = limited.map((entry, index) => {
|
||||
const angle = angleMap.get(entry.id);
|
||||
return {
|
||||
entry,
|
||||
angle: typeof angle === 'number' ? angle : computeStackAngle(entry.id, index),
|
||||
offset: hasMultiple ? index * 8 : 0,
|
||||
};
|
||||
});
|
||||
|
||||
previousOrderRef.current = limited.map((entry) => entry.id);
|
||||
|
||||
return result;
|
||||
}, [limited, hasMultiple, activeItemId, onItemActivate]);
|
||||
|
||||
return (
|
||||
<div className="preview-stack preview-stack--stacked">
|
||||
{limited.map((item, index) => {
|
||||
const angle = hasMultiple ? computeStackAngle(item.id, index) : 0;
|
||||
const offset = hasMultiple ? index * 8 : 0;
|
||||
{preparedItems.map(({ entry, angle, offset }, index) => {
|
||||
const transform = hasMultiple
|
||||
? `translate(-50%, -50%) rotate(${angle}deg) translateY(${offset}px)`
|
||||
: 'translate(-50%, -50%)';
|
||||
const isFront = index === 0;
|
||||
return (
|
||||
<div
|
||||
key={item.id || index}
|
||||
className={`preview-stack__item orientation-${item.orientation || 'landscape'}`}
|
||||
key={entry.id || index}
|
||||
className={`preview-stack__item orientation-${entry.orientation || 'landscape'}`}
|
||||
style={{
|
||||
zIndex: limited.length - index,
|
||||
zIndex: preparedItems.length - index,
|
||||
transform,
|
||||
}}
|
||||
aria-hidden={hasMultiple ? 'true' : undefined}
|
||||
aria-hidden={
|
||||
hasMultiple && !onItemActivate && !onOpenPreview ? 'true' : undefined
|
||||
}
|
||||
>
|
||||
<img src={item.url} alt={item.alt || ''} className="preview-stack__image" />
|
||||
<img
|
||||
src={entry.url}
|
||||
alt={entry.alt || ''}
|
||||
className="preview-stack__image"
|
||||
role={onItemActivate || onOpenPreview ? 'button' : undefined}
|
||||
tabIndex={onItemActivate || onOpenPreview ? 0 : -1}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (isFront && onOpenPreview) {
|
||||
onOpenPreview(entry.id);
|
||||
} else if (onItemActivate) {
|
||||
onItemActivate(entry.id);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!onItemActivate && !onOpenPreview) return;
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (isFront && onOpenPreview) {
|
||||
onOpenPreview(entry.id);
|
||||
} else {
|
||||
onItemActivate?.(entry.id);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -619,12 +706,22 @@ const DetailPanel = ({
|
||||
onBulkReanalyze,
|
||||
folderOptions = [],
|
||||
defaultMoveTarget = 'root',
|
||||
onPromoteSelection,
|
||||
activePreviewId = null,
|
||||
}) => {
|
||||
const lookup = detailMap && typeof detailMap.get === 'function' ? detailMap : new Map();
|
||||
const selectedCount = selectedDocuments.length;
|
||||
const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null;
|
||||
const detail = singleDoc ? lookup.get(singleDoc.id) || null : null;
|
||||
|
||||
const handlePreviewActivate = useCallback(
|
||||
(docId) => {
|
||||
if (!docId) return;
|
||||
onPromoteSelection?.(docId);
|
||||
},
|
||||
[onPromoteSelection],
|
||||
);
|
||||
|
||||
const makePreviewItem = useCallback((doc, detailEntry, fallbackUrl = null) => {
|
||||
if (!doc) return null;
|
||||
const url = doc.thumbnail?.url || detailEntry?.document?.thumbnail?.url || fallbackUrl;
|
||||
@@ -709,7 +806,14 @@ const DetailPanel = ({
|
||||
<>
|
||||
<div>
|
||||
<div className="preview-pane preview-pane--stack">
|
||||
<PreviewStack items={singlePreviewItems} maxItems={1} emptyMessage="Preview loading…" />
|
||||
<PreviewStack
|
||||
items={singlePreviewItems}
|
||||
maxItems={1}
|
||||
emptyMessage="Preview loading…"
|
||||
onItemActivate={handlePreviewActivate}
|
||||
onOpenPreview={onOpenPreview}
|
||||
activeItemId={activePreviewId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 style={{ margin: 0 }}>{displayName}</h3>
|
||||
@@ -816,7 +920,13 @@ const DetailPanel = ({
|
||||
<>
|
||||
<div>
|
||||
<div className="preview-pane preview-pane--stack">
|
||||
<PreviewStack items={stackPreviews} emptyMessage="No previews available." />
|
||||
<PreviewStack
|
||||
items={stackPreviews}
|
||||
emptyMessage="No previews available."
|
||||
onItemActivate={handlePreviewActivate}
|
||||
onOpenPreview={onOpenPreview}
|
||||
activeItemId={activePreviewId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 style={{ margin: 0 }}>{countLabel}</h3>
|
||||
@@ -1114,6 +1224,9 @@ function App({ routeFolderId = 'root', routeDocumentId = null, navigate }) {
|
||||
active: false,
|
||||
folderName: DEFAULT_FOLDER_NAME,
|
||||
});
|
||||
const [activePreviewId, setActivePreviewId] = useState(routeDocumentId || null);
|
||||
const [previewDocumentId, setPreviewDocumentId] = useState(null);
|
||||
const [previewDocumentLoading, setPreviewDocumentLoading] = useState(false);
|
||||
|
||||
const initializedRef = useRef(false);
|
||||
const dragCounterRef = useRef(0);
|
||||
@@ -1169,6 +1282,18 @@ function App({ routeFolderId = 'root', routeDocumentId = null, navigate }) {
|
||||
setStatus(message ? { message, variant } : null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDocumentIds.length) {
|
||||
if (activePreviewId !== null) {
|
||||
setActivePreviewId(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!selectedDocumentIds.includes(activePreviewId)) {
|
||||
setActivePreviewId(selectedDocumentIds[selectedDocumentIds.length - 1]);
|
||||
}
|
||||
}, [selectedDocumentIds, activePreviewId]);
|
||||
|
||||
const currentFolderName = useMemo(() => {
|
||||
if (selectedFolder === 'root' || !currentFolder) return DEFAULT_FOLDER_NAME;
|
||||
return currentFolder.name;
|
||||
@@ -1275,6 +1400,18 @@ function App({ routeFolderId = 'root', routeDocumentId = null, navigate }) {
|
||||
[visibleDocumentIds, focusedDocumentId, updateSelectionOrder],
|
||||
);
|
||||
|
||||
const promoteSelectionOrder = useCallback(
|
||||
(docId) => {
|
||||
if (!docId) return;
|
||||
if (!selectedDocumentIds.includes(docId)) return;
|
||||
updateSelectionOrder(selectedDocumentIds, [docId]);
|
||||
selectionAnchorRef.current = docId;
|
||||
setFocusedDocumentId(docId);
|
||||
setActivePreviewId(docId);
|
||||
},
|
||||
[selectedDocumentIds, updateSelectionOrder],
|
||||
);
|
||||
|
||||
const visibleSelectedCount = useMemo(
|
||||
() => selectedDocumentIds.filter((id) => visibleDocumentIds.includes(id)).length,
|
||||
[selectedDocumentIds, visibleDocumentIds],
|
||||
@@ -2057,8 +2194,6 @@ function App({ routeFolderId = 'root', routeDocumentId = null, navigate }) {
|
||||
const folderPathCacheRef = useRef(new Map());
|
||||
const previewCacheRef = useRef(new Map());
|
||||
const [previewCacheTick, setPreviewCacheTick] = useState(0);
|
||||
const [previewDocumentId, setPreviewDocumentId] = useState(null);
|
||||
const [previewDocumentLoading, setPreviewDocumentLoading] = useState(false);
|
||||
|
||||
const ensureFolderPathOnServer = useCallback(
|
||||
async (baseFolderId, segments) => {
|
||||
@@ -2366,9 +2501,7 @@ const handleDownload = useCallback(
|
||||
try {
|
||||
await ensureDocumentDetail(documentId, { force: false });
|
||||
await ensurePreviewUrl(documentId, { force: false });
|
||||
setSelectedDocumentIds([documentId]);
|
||||
setFocusedDocumentId(documentId);
|
||||
selectionAnchorRef.current = documentId;
|
||||
setActivePreviewId(documentId);
|
||||
if (!skipNavigate && navigate) {
|
||||
navigate(`/documents/${documentId}`, { replace });
|
||||
}
|
||||
@@ -3259,6 +3392,8 @@ const handleDownload = useCallback(
|
||||
onBulkReanalyze={handleBulkSelectionReanalyze}
|
||||
folderOptions={folderOptions}
|
||||
defaultMoveTarget={defaultMoveTarget}
|
||||
onPromoteSelection={promoteSelectionOrder}
|
||||
activePreviewId={activePreviewId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user