backend: generate high resolution preview

This commit is contained in:
2025-10-13 02:39:02 +02:00
parent 86c75cea8e
commit 159577dff9
5 changed files with 552 additions and 224 deletions
+257 -114
View File
@@ -517,6 +517,7 @@ const DocumentsTable = ({
onDocumentOpen,
selectedDocumentIds,
focusedDocumentId,
focusedRowKey,
draggingDocumentIds = [],
onDocumentDragStart,
onDocumentDragEnd,
@@ -525,6 +526,7 @@ const DocumentsTable = ({
tagLookupById,
onDocumentListFocus,
onDocumentListKeyDown,
onFocusedRowChange,
}) => {
const showingSearchResults = searchResults !== null;
const rows = showingSearchResults ? searchResults : documents;
@@ -538,10 +540,19 @@ const DocumentsTable = ({
);
const scrollRef = useRef(null);
const ensureFocusedRowVisible = useCallback(() => {
if (!focusedDocumentId) return;
if (!focusedRowKey) return;
const container = scrollRef.current;
if (!container) return;
const row = container.querySelector(`#document-row-${focusedDocumentId}`);
let selector = null;
if (focusedRowKey.startsWith('document:')) {
selector = `#document-row-${focusedRowKey.slice('document:'.length)}`;
} else if (focusedRowKey.startsWith('folder:')) {
selector = `#folder-row-${focusedRowKey.slice('folder:'.length)}`;
}
if (!selector) {
return;
}
const row = container.querySelector(selector);
if (!row || !container.contains(row)) {
return;
}
@@ -562,12 +573,23 @@ const DocumentsTable = ({
const nextScrollTop = rowBottom - container.clientHeight;
container.scrollTop = Math.max(nextScrollTop, 0);
}
}, [focusedDocumentId]);
}, [focusedRowKey]);
useEffect(() => {
ensureFocusedRowVisible();
}, [ensureFocusedRowVisible]);
const activeDescendantId = useMemo(() => {
if (!focusedRowKey) return undefined;
if (focusedRowKey.startsWith('document:')) {
return `document-row-${focusedRowKey.slice('document:'.length)}`;
}
if (focusedRowKey.startsWith('folder:')) {
return `folder-row-${focusedRowKey.slice('folder:'.length)}`;
}
return undefined;
}, [focusedRowKey]);
return (
<section className="documents-panel column">
<div className="column-header">
@@ -624,9 +646,7 @@ const DocumentsTable = ({
onDocumentListKeyDown(event);
}
}}
aria-activedescendant={
focusedDocumentId ? `document-row-${focusedDocumentId}` : undefined
}
aria-activedescendant={activeDescendantId}
>
{!showingSearchResults && !subfolders.length && rows.length === 0 ? (
<div className="empty-state">
@@ -651,9 +671,13 @@ const DocumentsTable = ({
return (
<tr
key={folder.id}
className={`folder${isDraggingFolder ? ' is-dragging' : ''}`}
className={`folder${isDraggingFolder ? ' is-dragging' : ''}${
focusedRowKey === `folder:${folder.id}` ? ' focused' : ''
}`}
id={`folder-row-${folder.id}`}
onClick={() => {
scrollRef.current?.focus({ preventScroll: true });
onFocusedRowChange?.(`folder:${folder.id}`);
onFolderSelect(folder.id);
}}
onDragOver={(event) => onFolderDragOver(event, folder.id)}
@@ -698,7 +722,11 @@ const DocumentsTable = ({
const isSelected = selectedSet.has(doc.id);
const rowClasses = ['document'];
if (isSelected) rowClasses.push('selected');
if (focusedDocumentId === doc.id) rowClasses.push('focused');
if (
focusedDocumentId === doc.id || focusedRowKey === `document:${doc.id}`
) {
rowClasses.push('focused');
}
if (draggingSet.has(doc.id)) {
rowClasses.push('dragging');
}
@@ -711,6 +739,7 @@ const DocumentsTable = ({
aria-selected={isSelected}
onClick={(event) => {
scrollRef.current?.focus({ preventScroll: true });
onFocusedRowChange?.(`document:${doc.id}`);
onDocumentRowClick(doc.id, event);
}}
onDoubleClick={(event) => {
@@ -1813,6 +1842,9 @@ const AppLayout = () => {
const [selectedDocumentIds, setSelectedDocumentIds] = useState(initialSelection);
const [selectionOrder, setSelectionOrder] = useState(initialSelection);
const [focusedDocumentId, setFocusedDocumentId] = useState(routeDocumentId);
const [focusedRowKey, setFocusedRowKey] = useState(() =>
routeDocumentId ? `document:${routeDocumentId}` : null,
);
const [documentDetails, setDocumentDetails] = useState(() => new Map());
const documentDetailsRef = useRef(documentDetails);
const tokenRef = useRef(token);
@@ -2091,9 +2123,11 @@ const AppLayout = () => {
[focusedDocumentId, setSelectionOrder],
);
const showingSearchResults = searchResults !== null;
const visibleDocuments = useMemo(
() => (searchResults !== null ? searchResults : documents),
[searchResults, documents],
() => (showingSearchResults ? searchResults : documents),
[showingSearchResults, searchResults, documents],
);
const visibleDocumentIds = useMemo(
@@ -2277,116 +2311,57 @@ const AppLayout = () => {
],
);
const handleDocumentListFocus = useCallback(() => {
if (focusedDocumentId && visibleDocumentIds.includes(focusedDocumentId)) {
const navigableRows = useMemo(() => {
const entries = [];
if (!showingSearchResults) {
currentSubfolders.forEach((folder) => {
entries.push({ key: `folder:${folder.id}`, type: 'folder', id: folder.id });
});
}
visibleDocuments.forEach((doc) => {
entries.push({ key: `document:${doc.id}`, type: 'document', id: doc.id });
});
return entries;
}, [showingSearchResults, currentSubfolders, visibleDocuments]);
const navigableRowKeys = useMemo(
() => navigableRows.map((entry) => entry.key),
[navigableRows],
);
const prevFocusedDocIdRef = useRef(focusedDocumentId);
useEffect(() => {
const previous = prevFocusedDocIdRef.current;
if (previous === focusedDocumentId) {
return;
}
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
const candidate = selectedDocumentIds[index];
if (visibleDocumentIds.includes(candidate)) {
setFocusedDocumentId(candidate);
return;
}
prevFocusedDocIdRef.current = focusedDocumentId;
if (focusedDocumentId) {
setFocusedRowKey(`document:${focusedDocumentId}`);
} else {
setFocusedRowKey((current) => (current?.startsWith('folder:') ? current : null));
}
}, [focusedDocumentId]);
if (visibleDocumentIds.length) {
const firstId = visibleDocumentIds[0];
applySelection([firstId], { anchor: firstId, interactedIds: [firstId] });
useEffect(() => {
if (!focusedRowKey) {
return;
}
}, [
focusedDocumentId,
visibleDocumentIds,
selectedDocumentIds,
setFocusedDocumentId,
applySelection,
]);
if (navigableRowKeys.includes(focusedRowKey)) {
return;
}
const docKey = focusedDocumentId ? `document:${focusedDocumentId}` : null;
if (docKey && navigableRowKeys.includes(docKey)) {
setFocusedRowKey(docKey);
return;
}
if (navigableRowKeys.length) {
setFocusedRowKey(navigableRowKeys[0]);
} else {
setFocusedRowKey(null);
}
}, [focusedRowKey, navigableRowKeys, focusedDocumentId]);
const handleDocumentListKeyDown = useCallback(
(event) => {
const { key, shiftKey } = event;
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key)) {
return;
}
if (!visibleDocumentIds.length) {
return;
}
event.preventDefault();
const activeId = (() => {
if (focusedDocumentId && visibleDocumentIds.includes(focusedDocumentId)) {
return focusedDocumentId;
}
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
const candidate = selectedDocumentIds[index];
if (visibleDocumentIds.includes(candidate)) {
return candidate;
}
}
return null;
})();
let currentIndex = activeId ? visibleDocumentIds.indexOf(activeId) : -1;
let nextIndex = currentIndex;
if (key === 'ArrowDown') {
nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, visibleDocumentIds.length - 1);
} else if (key === 'ArrowUp') {
if (currentIndex === -1) {
nextIndex = visibleDocumentIds.length - 1;
} else {
nextIndex = Math.max(currentIndex - 1, 0);
}
} else if (key === 'Home') {
nextIndex = 0;
} else if (key === 'End') {
nextIndex = visibleDocumentIds.length - 1;
}
if (nextIndex === -1 || nextIndex >= visibleDocumentIds.length) {
return;
}
if (nextIndex === currentIndex && key !== 'Home' && key !== 'End') {
return;
}
const targetId = visibleDocumentIds[nextIndex];
if (!targetId) {
return;
}
if (shiftKey) {
let anchorId = selectionAnchorRef.current;
if (!anchorId || !visibleDocumentIds.includes(anchorId)) {
anchorId = activeId || targetId;
}
const anchorIndex = visibleDocumentIds.indexOf(anchorId);
const boundedAnchorIndex = anchorIndex === -1 ? nextIndex : anchorIndex;
const start = Math.min(boundedAnchorIndex, nextIndex);
const end = Math.max(boundedAnchorIndex, nextIndex);
const range = visibleDocumentIds.slice(start, end + 1);
applySelection(range, { anchor: anchorId, interactedIds: [targetId] });
} else {
applySelection([targetId], { anchor: targetId, interactedIds: [targetId] });
}
if (focusedDocumentId !== targetId) {
setFocusedDocumentId(targetId);
}
},
[
visibleDocumentIds,
focusedDocumentId,
selectedDocumentIds,
applySelection,
selectionAnchorRef,
setFocusedDocumentId,
],
);
const handleDocumentDragStart = useCallback(
(event, documentId) => {
@@ -3523,6 +3498,172 @@ const AppLayout = () => {
[ensureDocumentDetail, ensurePreviewUrl, navigate],
);
const handleDocumentListFocus = useCallback(() => {
if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) {
return;
}
let resolvedKey = null;
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
const candidate = selectedDocumentIds[index];
const rowKey = `document:${candidate}`;
if (navigableRowKeys.includes(rowKey)) {
resolvedKey = rowKey;
break;
}
}
if (!resolvedKey && navigableRows.length) {
resolvedKey = navigableRows[0].key;
}
if (!resolvedKey) {
return;
}
setFocusedRowKey(resolvedKey);
if (resolvedKey.startsWith('document:')) {
const docId = resolvedKey.slice('document:'.length);
if (!selectedDocumentIds.includes(docId)) {
applySelection([docId], { anchor: docId, interactedIds: [docId] });
}
}
}, [
focusedRowKey,
navigableRowKeys,
selectedDocumentIds,
navigableRows,
applySelection,
]);
const handleDocumentListKeyDown = useCallback(
(event) => {
const { key, shiftKey } = event;
const triggers = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar'];
if (!triggers.includes(key)) {
return;
}
if (!navigableRows.length) {
return;
}
event.preventDefault();
let activeKey =
focusedRowKey && navigableRowKeys.includes(focusedRowKey)
? focusedRowKey
: null;
if (!activeKey) {
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
const candidate = selectedDocumentIds[index];
const rowKey = `document:${candidate}`;
if (navigableRowKeys.includes(rowKey)) {
activeKey = rowKey;
break;
}
}
}
if (!activeKey) {
activeKey = navigableRows[0].key;
setFocusedRowKey(activeKey);
}
let currentIndex = navigableRowKeys.indexOf(activeKey);
if (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') {
const row = currentIndex === -1 ? navigableRows[0] : navigableRows[currentIndex];
if (!row) {
return;
}
if (row.type === 'folder') {
setFocusedRowKey(`folder:${row.id}`);
selectFolder(row.id);
} else {
setFocusedRowKey(`document:${row.id}`);
applySelection([row.id], { anchor: row.id, interactedIds: [row.id] });
openDocumentPreview(row.id);
}
return;
}
let nextIndex = currentIndex;
if (key === 'ArrowDown') {
nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, navigableRows.length - 1);
} else if (key === 'ArrowUp') {
nextIndex = currentIndex === -1 ? navigableRows.length - 1 : Math.max(currentIndex - 1, 0);
} else if (key === 'Home') {
nextIndex = 0;
} else if (key === 'End') {
nextIndex = navigableRows.length - 1;
}
if (nextIndex === -1 || nextIndex >= navigableRows.length) {
return;
}
if (nextIndex === currentIndex && key !== 'Home' && key !== 'End') {
return;
}
const targetRow = navigableRows[nextIndex];
if (!targetRow) {
return;
}
setFocusedRowKey(targetRow.key);
if (targetRow.type === 'folder') {
return;
}
const targetId = targetRow.id;
if (!targetId) {
return;
}
if (shiftKey) {
let anchorId = selectionAnchorRef.current;
if (!anchorId || !visibleDocumentIds.includes(anchorId)) {
if (focusedDocumentId && visibleDocumentIds.includes(focusedDocumentId)) {
anchorId = focusedDocumentId;
} else {
anchorId = targetId;
}
}
const anchorIndex = visibleDocumentIds.indexOf(anchorId);
const targetIndex = visibleDocumentIds.indexOf(targetId);
if (anchorIndex !== -1 && targetIndex !== -1) {
const start = Math.min(anchorIndex, targetIndex);
const end = Math.max(anchorIndex, targetIndex);
const range = visibleDocumentIds.slice(start, end + 1);
applySelection(range, { anchor: anchorId, interactedIds: [targetId] });
} else {
applySelection([targetId], { anchor: targetId, interactedIds: [targetId] });
}
} else {
applySelection([targetId], { anchor: targetId, interactedIds: [targetId] });
}
},
[
navigableRows,
navigableRowKeys,
focusedRowKey,
selectedDocumentIds,
selectFolder,
openDocumentPreview,
visibleDocumentIds,
focusedDocumentId,
applySelection,
selectionAnchorRef,
],
);
const closeDocumentPreview = useCallback(
(folderId = null) => {
setPreviewDocumentId(null);
@@ -4501,6 +4642,7 @@ const AppLayout = () => {
onDocumentDelete: handleDocumentDelete,
selectedDocumentIds,
focusedDocumentId,
focusedRowKey,
draggingDocumentIds: draggedDocumentIds,
onDocumentDragStart: handleDocumentDragStart,
onDocumentDragEnd: handleDocumentDragEnd,
@@ -4508,6 +4650,7 @@ const AppLayout = () => {
tagLookupById,
onDocumentListFocus: handleDocumentListFocus,
onDocumentListKeyDown: handleDocumentListKeyDown,
onFocusedRowChange: setFocusedRowKey,
};
const detailPanelProps = {
+5
View File
@@ -738,6 +738,11 @@ button.icon-button.ghost:hover:not([disabled]) {
font-weight: 600;
}
.documents-panel tbody tr.folder.focused {
box-shadow: inset 2px 0 0 rgba(43, 92, 255, 0.45);
background: rgba(43, 92, 255, 0.08);
}
.documents-panel tbody tr.document {
cursor: pointer;
}