perf(files-ui): stream media/downloads natively, rAF rubber-band, Map selection
Four related fixes to stop buffering files in page memory and stop hammering layout from the selection paths: - Inline viewer media: video/audio fetched the ENTIRE file into a blob before the first frame (a 2 GB video = 2 GB of tab heap, no progressive playback, no seek). The <video>/<audio> src now points straight at the same-origin API URL — cookies travel automatically and the browser streams with native Range requests, exactly like the music player already did. - Downloads (file, folder ZIP, batch ZIP, viewer button): the fetch → blob → objectURL pattern materialized the whole payload in RAM before the save dialog appeared (a 10 GB batch ZIP risked crashing the tab) with no download progress UI. New shared utils/download.js hands the URL to the browser, which streams to disk with its own progress UI. Batch download uses the existing GET endpoint (same URL contract as the drag-out DownloadURL, now shared via buildBatchDownloadUrl); selections whose id list cannot fit in a URL keep the buffered POST fallback. Trade-off: failed downloads now surface in the browser's download shelf instead of an in-app toast. - Rubber-band lasso: every mousemove (>100/s) walked all cards interleaving getBoundingClientRect() reads with class writes — up to N forced reflows per event, freezing the frame rate on folders with thousands of loaded rows. Card rects (+ item info) are now snapshot once per drag (rebuilt on scroll), and a single rAF pass per frame compares against the cached geometry, touching only cards whose selection state changed. - batchToolbar: getSelection() rebuilt the selection by scanning every .file-item in the document (the TODO admitted it); it now reads the _selected Map that every selection path already keeps in sync. clear() scopes its DOM sweep to #files-list (the only container the toolbar manages) instead of the whole document. https://claude.ai/code/session_01Dp3oWon5GBMVn4j3QXZdgx
This commit is contained in:
@@ -10,6 +10,7 @@ import { showConfirmDialog, ui } from '../../app/ui.js';
|
||||
import { getCsrfHeaders, getCsrfToken } from '../../core/csrf.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { notifications } from '../../core/notifications.js';
|
||||
import { triggerBrowserDownload } from '../../utils/download.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} BatchResult
|
||||
@@ -1369,32 +1370,13 @@ const fileOps = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Download a file
|
||||
* Download a file — handed to the browser so it streams to disk with
|
||||
* its native download UI instead of buffering the file in memory.
|
||||
* @param {string} fileId - File ID
|
||||
* @param {string} fileName - File name
|
||||
*/
|
||||
async downloadFile(fileId, fileName) {
|
||||
try {
|
||||
const response = await fetch(`/api/files/${fileId}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
ui.showNotification('Error', 'Error downloading the file');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
ui.showNotification('Error', 'Error downloading the file');
|
||||
}
|
||||
triggerBrowserDownload(`/api/files/${fileId}`, fileName);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1403,30 +1385,10 @@ const fileOps = {
|
||||
* @param {string} folderName - Folder name
|
||||
*/
|
||||
async downloadFolder(folderId, folderName) {
|
||||
try {
|
||||
// Show notification to user
|
||||
ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
||||
|
||||
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${folderName}.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
ui.showNotification('Error', 'Error downloading the folder');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading folder:', error);
|
||||
ui.showNotification('Error', 'Error downloading the folder');
|
||||
}
|
||||
// Show notification to user (the server still has to assemble the
|
||||
// ZIP before the browser's own download UI takes over).
|
||||
ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
||||
triggerBrowserDownload(`/api/folders/${folderId}/download?format=zip`, `${folderName}.zip`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user