@vryzel/file-next-ui 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +595 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.js +597 -154
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createContext, createElement, useContext, useRef, useLayoutEffect, useEffect,
|
|
1
|
+
import { createContext, createElement, useContext, useRef, useState, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
|
|
2
2
|
import { useFileExplorer } from '@vryzel/file-next-headless';
|
|
3
|
-
import { Music, FileCode, FileVideo, FileAudio, FileImage, Archive, FileText, ChevronRight, File, Folder, Link2, MoreHorizontal, ChevronUp, ChevronDown, List, LayoutGrid, Search, Upload, FolderPlus, Trash, Copy, RotateCcw, X, Eye, Download, Pencil, ListChecks, Inbox, AlertCircleIcon } from 'lucide-react';
|
|
3
|
+
import { Music, FileCode, FileVideo, FileAudio, FileImage, Archive, FileText, ChevronRight, File, Folder, Link2, MoreHorizontal, ChevronUp, ChevronDown, List, LayoutGrid, Search, Upload, FolderPlus, Trash, Copy, RotateCcw, X, Eye, Download, Pencil, ListChecks, Inbox, AlertCircleIcon, Minus, Plus } from 'lucide-react';
|
|
4
4
|
import { clsx } from 'clsx';
|
|
5
5
|
import { twMerge } from 'tailwind-merge';
|
|
6
6
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
7
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
7
8
|
|
|
8
9
|
// src/file-explorer.tsx
|
|
9
10
|
function cn(...inputs) {
|
|
@@ -127,11 +128,17 @@ var defaultLabels = {
|
|
|
127
128
|
clearClipboard: "Clear clipboard",
|
|
128
129
|
uploadStatus: "uploading",
|
|
129
130
|
dismissUploads: "Dismiss uploads",
|
|
131
|
+
cancelQueue: "Cancel queue",
|
|
132
|
+
removeUpload: "Remove from queue",
|
|
130
133
|
uploadQueued: "Queued",
|
|
131
134
|
uploadDone: "Done",
|
|
132
135
|
uploadFailed: "Failed",
|
|
133
136
|
unknownType: "Unknown type",
|
|
134
|
-
previewUnavailable: "Preview is not available for this file."
|
|
137
|
+
previewUnavailable: "Preview is not available for this file.",
|
|
138
|
+
closePreview: "Close preview",
|
|
139
|
+
zoomIn: "Zoom in",
|
|
140
|
+
zoomOut: "Zoom out",
|
|
141
|
+
zoomFit: "Fit"
|
|
135
142
|
};
|
|
136
143
|
var LabelsContext = createContext(defaultLabels);
|
|
137
144
|
function ExplorerLabelsProvider({
|
|
@@ -280,6 +287,39 @@ function useExplorerItemPointer(opts) {
|
|
|
280
287
|
}, [coarse, file, onActivate]);
|
|
281
288
|
return { onPointerDown, onClick, onDoubleClick };
|
|
282
289
|
}
|
|
290
|
+
function shouldLoadMore(input) {
|
|
291
|
+
const threshold = input.threshold ?? 8;
|
|
292
|
+
if (!input.hasMore || input.loading || input.loaded === 0) return false;
|
|
293
|
+
if (input.lastIndex === void 0) return false;
|
|
294
|
+
return input.lastIndex >= input.loaded - 1 - threshold;
|
|
295
|
+
}
|
|
296
|
+
function gridColumnCount(width) {
|
|
297
|
+
if (width < 640) return 3;
|
|
298
|
+
if (width < 768) return 5;
|
|
299
|
+
if (width < 1024) return 6;
|
|
300
|
+
return 8;
|
|
301
|
+
}
|
|
302
|
+
function selectionEdgeIndex(files, selectedIds, previous) {
|
|
303
|
+
let minPrev = Number.POSITIVE_INFINITY;
|
|
304
|
+
const added = [];
|
|
305
|
+
for (let i = 0; i < files.length; i++) {
|
|
306
|
+
const id = files[i]?.id;
|
|
307
|
+
if (!id) continue;
|
|
308
|
+
if (previous.has(id) && i < minPrev) minPrev = i;
|
|
309
|
+
if (selectedIds.has(id) && !previous.has(id)) added.push(i);
|
|
310
|
+
}
|
|
311
|
+
const minAdded = added[0];
|
|
312
|
+
const maxAdded = added[added.length - 1];
|
|
313
|
+
if (minAdded === void 0 || maxAdded === void 0) return -1;
|
|
314
|
+
return minAdded < minPrev ? minAdded : maxAdded;
|
|
315
|
+
}
|
|
316
|
+
function useLoadMoreOnEnd(input) {
|
|
317
|
+
const { lastIndex, loaded, hasMore, loading, onLoadMore } = input;
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (!onLoadMore) return;
|
|
320
|
+
if (shouldLoadMore({ lastIndex, loaded, hasMore, loading })) onLoadMore();
|
|
321
|
+
}, [hasMore, lastIndex, loaded, loading, onLoadMore]);
|
|
322
|
+
}
|
|
283
323
|
var ROW_GRID = "grid-cols-[2.25rem_1.5rem_minmax(12rem,1fr)_5rem_2.75rem] sm:grid-cols-[2.25rem_1.25rem_minmax(0,1fr)_5rem_7.5rem]";
|
|
284
324
|
var DRAG_MIME = "application/x-file-next-id";
|
|
285
325
|
function ExplorerListView(props) {
|
|
@@ -303,58 +343,123 @@ function ExplorerListView(props) {
|
|
|
303
343
|
renamingId,
|
|
304
344
|
onRenameCommit,
|
|
305
345
|
onRenameCancel,
|
|
306
|
-
className
|
|
346
|
+
className,
|
|
347
|
+
scrollRef: scrollRefProp,
|
|
348
|
+
hasMore = false,
|
|
349
|
+
loadingMore = false,
|
|
350
|
+
onLoadMore
|
|
307
351
|
} = props;
|
|
308
352
|
const labels = useExplorerLabels();
|
|
309
353
|
const coarse = useCoarsePointer();
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
354
|
+
const ownScrollRef = useRef(null);
|
|
355
|
+
const scrollRef = scrollRefProp ?? ownScrollRef;
|
|
356
|
+
const headerRef = useRef(null);
|
|
357
|
+
const [scrollMargin, setScrollMargin] = useState(0);
|
|
358
|
+
useLayoutEffect(() => {
|
|
359
|
+
const header = headerRef.current;
|
|
360
|
+
if (!header) return;
|
|
361
|
+
const measure = () => setScrollMargin(header.offsetHeight);
|
|
362
|
+
measure();
|
|
363
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
364
|
+
const observer = new ResizeObserver(measure);
|
|
365
|
+
observer.observe(header);
|
|
366
|
+
return () => observer.disconnect();
|
|
367
|
+
}, []);
|
|
368
|
+
const virtualizer = useVirtualizer({
|
|
369
|
+
count: files.length,
|
|
370
|
+
getScrollElement: () => scrollRef.current,
|
|
371
|
+
estimateSize: () => coarse ? 64 : 44,
|
|
372
|
+
overscan: 8,
|
|
373
|
+
scrollMargin,
|
|
374
|
+
scrollPaddingStart: scrollMargin,
|
|
375
|
+
getItemKey: (index) => files[index]?.id ?? index,
|
|
376
|
+
initialRect: { width: 800, height: 600 }
|
|
377
|
+
});
|
|
378
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
379
|
+
useLoadMoreOnEnd({
|
|
380
|
+
lastIndex: virtualItems.at(-1)?.index,
|
|
381
|
+
loaded: files.length,
|
|
382
|
+
hasMore,
|
|
383
|
+
loading: loadingMore,
|
|
384
|
+
onLoadMore
|
|
385
|
+
});
|
|
386
|
+
const prevSelected = useRef(selectedIds);
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const index = renamingId ? files.findIndex((file) => file.id === renamingId) : selectionEdgeIndex(files, selectedIds, prevSelected.current);
|
|
389
|
+
prevSelected.current = selectedIds;
|
|
390
|
+
if (index < 0) return;
|
|
391
|
+
virtualizer.scrollToIndex(index, { align: "auto" });
|
|
392
|
+
}, [renamingId, selectedIds]);
|
|
393
|
+
return /* @__PURE__ */ jsx(
|
|
394
|
+
"div",
|
|
395
|
+
{
|
|
396
|
+
ref: scrollRefProp ? void 0 : ownScrollRef,
|
|
397
|
+
className: cn(
|
|
398
|
+
!scrollRefProp && "min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-x-contain",
|
|
399
|
+
className
|
|
400
|
+
),
|
|
401
|
+
children: /* @__PURE__ */ jsxs("div", { className: "min-w-[32rem] sm:min-w-0", children: [
|
|
402
|
+
/* @__PURE__ */ jsxs(
|
|
403
|
+
"div",
|
|
404
|
+
{
|
|
405
|
+
ref: headerRef,
|
|
406
|
+
role: "row",
|
|
407
|
+
className: cn(
|
|
408
|
+
"sticky top-0 z-10 grid items-center gap-3 border-b border-border bg-background px-3 py-2 font-mono text-[10px] font-medium uppercase tracking-[0.12em] text-muted-foreground",
|
|
409
|
+
ROW_GRID
|
|
410
|
+
),
|
|
411
|
+
children: [
|
|
412
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true" }),
|
|
413
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true" }),
|
|
414
|
+
/* @__PURE__ */ jsx(SortHeader, { col: "name", label: labels.sortName, sortKey, sortDirection, onSort: onSortChange }),
|
|
415
|
+
/* @__PURE__ */ jsx(SortHeader, { col: "size", label: labels.sortSize, sortKey, sortDirection, onSort: onSortChange, className: "justify-end text-right" }),
|
|
416
|
+
coarse ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(SortHeader, { col: "modified", label: labels.sortModified, sortKey, sortDirection, onSort: onSortChange, className: "hidden sm:flex" })
|
|
417
|
+
]
|
|
418
|
+
}
|
|
318
419
|
),
|
|
319
|
-
children:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
/* @__PURE__ */ jsx(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
420
|
+
/* @__PURE__ */ jsx("ul", { role: "list", className: "relative z-0 w-full", style: { height: virtualizer.getTotalSize() }, children: virtualItems.map((virtualRow) => {
|
|
421
|
+
const file = files[virtualRow.index];
|
|
422
|
+
if (!file) return null;
|
|
423
|
+
return /* @__PURE__ */ jsx(
|
|
424
|
+
ExplorerListRow,
|
|
425
|
+
{
|
|
426
|
+
file,
|
|
427
|
+
idx: virtualRow.index,
|
|
428
|
+
dataIndex: virtualRow.index,
|
|
429
|
+
measureRef: virtualizer.measureElement,
|
|
430
|
+
translateY: virtualRow.start - virtualizer.options.scrollMargin,
|
|
431
|
+
selected: selectedIds.has(file.id),
|
|
432
|
+
dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
|
|
433
|
+
dropTarget: file.id === dropTargetId && file.kind === "folder",
|
|
434
|
+
isProtected: protectedIds?.has(file.id) ?? false,
|
|
435
|
+
draggingId,
|
|
436
|
+
dropTargetId,
|
|
437
|
+
coarse,
|
|
438
|
+
onSelect,
|
|
439
|
+
onActivate,
|
|
440
|
+
onContextMenu,
|
|
441
|
+
onDragStart,
|
|
442
|
+
onDragEnd,
|
|
443
|
+
onDrop,
|
|
444
|
+
onDragOverRow,
|
|
445
|
+
renaming: file.id === renamingId,
|
|
446
|
+
onRenameCommit,
|
|
447
|
+
onRenameCancel
|
|
448
|
+
},
|
|
449
|
+
virtualRow.key
|
|
450
|
+
);
|
|
451
|
+
}) }),
|
|
452
|
+
loadingMore ? /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : null
|
|
453
|
+
] })
|
|
454
|
+
}
|
|
455
|
+
);
|
|
354
456
|
}
|
|
355
457
|
function ExplorerListRow({
|
|
356
458
|
file,
|
|
357
459
|
idx,
|
|
460
|
+
dataIndex,
|
|
461
|
+
measureRef,
|
|
462
|
+
translateY,
|
|
358
463
|
selected,
|
|
359
464
|
dragging,
|
|
360
465
|
dropTarget,
|
|
@@ -381,6 +486,8 @@ function ExplorerListRow({
|
|
|
381
486
|
{
|
|
382
487
|
role: "row",
|
|
383
488
|
"data-file-id": file.id,
|
|
489
|
+
"data-index": dataIndex,
|
|
490
|
+
ref: measureRef,
|
|
384
491
|
"aria-selected": selected,
|
|
385
492
|
draggable: !coarse && !renaming,
|
|
386
493
|
onDragStart: (e) => {
|
|
@@ -422,8 +529,9 @@ function ExplorerListRow({
|
|
|
422
529
|
else onDragEnd();
|
|
423
530
|
},
|
|
424
531
|
tabIndex: 0,
|
|
532
|
+
style: { transform: `translateY(${translateY}px)` },
|
|
425
533
|
className: cn(
|
|
426
|
-
"
|
|
534
|
+
"absolute left-0 top-0 grid w-full items-center gap-3 border-b border-border/60 px-3 py-3.5 text-sm outline-none sm:py-2",
|
|
427
535
|
"touch-manipulation select-none [-webkit-touch-callout:none]",
|
|
428
536
|
ROW_GRID,
|
|
429
537
|
"hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
@@ -506,6 +614,7 @@ var formatSize = (bytes) => {
|
|
|
506
614
|
};
|
|
507
615
|
var formatDate = (d) => d.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
508
616
|
var EXPLORER_DRAG_MIME = DRAG_MIME;
|
|
617
|
+
var GRID_PAD = 12;
|
|
509
618
|
function ExplorerGridView(props) {
|
|
510
619
|
const {
|
|
511
620
|
files,
|
|
@@ -524,38 +633,116 @@ function ExplorerGridView(props) {
|
|
|
524
633
|
renamingId,
|
|
525
634
|
onRenameCommit,
|
|
526
635
|
onRenameCancel,
|
|
527
|
-
className
|
|
636
|
+
className,
|
|
637
|
+
scrollRef: scrollRefProp,
|
|
638
|
+
hasMore = false,
|
|
639
|
+
loadingMore = false,
|
|
640
|
+
onLoadMore
|
|
528
641
|
} = props;
|
|
529
642
|
const labels = useExplorerLabels();
|
|
530
643
|
const coarse = useCoarsePointer();
|
|
531
|
-
|
|
532
|
-
|
|
644
|
+
const ownScrollRef = useRef(null);
|
|
645
|
+
const scrollRef = scrollRefProp ?? ownScrollRef;
|
|
646
|
+
const [width, setWidth] = useState(null);
|
|
647
|
+
useLayoutEffect(() => {
|
|
648
|
+
const el = scrollRef.current;
|
|
649
|
+
if (!el) return;
|
|
650
|
+
const measure = () => setWidth(el.clientWidth);
|
|
651
|
+
measure();
|
|
652
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
653
|
+
const observer = new ResizeObserver(measure);
|
|
654
|
+
observer.observe(el);
|
|
655
|
+
return () => observer.disconnect();
|
|
656
|
+
}, [scrollRef]);
|
|
657
|
+
const columns = width != null && width > 0 ? gridColumnCount(width) : 6;
|
|
658
|
+
const rowCount = Math.ceil(files.length / columns);
|
|
659
|
+
const virtualizer = useVirtualizer({
|
|
660
|
+
count: rowCount,
|
|
661
|
+
getScrollElement: () => scrollRef.current,
|
|
662
|
+
estimateSize: () => 156,
|
|
663
|
+
overscan: 4,
|
|
664
|
+
gap: 6,
|
|
665
|
+
// ponytail: gap-1.5 at 16px root
|
|
666
|
+
scrollMargin: GRID_PAD,
|
|
667
|
+
getItemKey: (index) => files[index * columns]?.id ?? index,
|
|
668
|
+
initialRect: { width: 800, height: 600 }
|
|
669
|
+
});
|
|
670
|
+
const virtualRows = virtualizer.getVirtualItems();
|
|
671
|
+
const viewportEnd = (virtualizer.scrollOffset ?? 0) + (virtualizer.scrollRect?.height ?? 0);
|
|
672
|
+
const lastVisibleRow = virtualRows.filter((row) => row.start < viewportEnd).at(-1);
|
|
673
|
+
useLoadMoreOnEnd({
|
|
674
|
+
lastIndex: lastVisibleRow ? Math.min(files.length - 1, (lastVisibleRow.index + 1) * columns - 1) : void 0,
|
|
675
|
+
loaded: files.length,
|
|
676
|
+
hasMore,
|
|
677
|
+
loading: loadingMore,
|
|
678
|
+
onLoadMore
|
|
679
|
+
});
|
|
680
|
+
const prevSelected = useRef(selectedIds);
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
const itemIndex = renamingId ? files.findIndex((file) => file.id === renamingId) : selectionEdgeIndex(files, selectedIds, prevSelected.current);
|
|
683
|
+
prevSelected.current = selectedIds;
|
|
684
|
+
if (itemIndex < 0) return;
|
|
685
|
+
virtualizer.scrollToIndex(Math.floor(itemIndex / columns), { align: "auto" });
|
|
686
|
+
}, [renamingId, selectedIds]);
|
|
687
|
+
return /* @__PURE__ */ jsxs(
|
|
688
|
+
"div",
|
|
533
689
|
{
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
690
|
+
ref: scrollRefProp ? void 0 : ownScrollRef,
|
|
691
|
+
className: cn(!scrollRefProp && "min-h-0 flex-1 overflow-y-auto", className),
|
|
692
|
+
children: [
|
|
693
|
+
/* @__PURE__ */ jsxs("div", { role: "list", className: "pt-3", children: [
|
|
694
|
+
/* @__PURE__ */ jsx("div", { role: "presentation", className: "relative z-0 w-full", style: { height: virtualizer.getTotalSize() }, children: virtualRows.map((virtualRow) => {
|
|
695
|
+
const start = virtualRow.index * columns;
|
|
696
|
+
const rowFiles = files.slice(start, start + columns);
|
|
697
|
+
return /* @__PURE__ */ jsx(
|
|
698
|
+
"div",
|
|
699
|
+
{
|
|
700
|
+
role: "presentation",
|
|
701
|
+
"data-index": virtualRow.index,
|
|
702
|
+
ref: virtualizer.measureElement,
|
|
703
|
+
className: "absolute left-0 top-0 grid w-full gap-1.5 px-3",
|
|
704
|
+
style: {
|
|
705
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
706
|
+
transform: `translateY(${virtualRow.start - virtualizer.options.scrollMargin}px)`
|
|
707
|
+
},
|
|
708
|
+
children: rowFiles.map((file, offset) => /* @__PURE__ */ jsx(
|
|
709
|
+
ExplorerGridItem,
|
|
710
|
+
{
|
|
711
|
+
file,
|
|
712
|
+
idx: start + offset,
|
|
713
|
+
selected: selectedIds.has(file.id),
|
|
714
|
+
dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
|
|
715
|
+
dropTarget: file.id === dropTargetId && file.kind === "folder",
|
|
716
|
+
isProtected: protectedIds?.has(file.id) ?? false,
|
|
717
|
+
draggingId,
|
|
718
|
+
dropTargetId,
|
|
719
|
+
coarse,
|
|
720
|
+
folderLabel: labels.folder,
|
|
721
|
+
inUseLabel: labels.inUse,
|
|
722
|
+
actionsLabel: labels.itemActions,
|
|
723
|
+
onSelect,
|
|
724
|
+
onActivate,
|
|
725
|
+
onContextMenu,
|
|
726
|
+
onDragStart,
|
|
727
|
+
onDragEnd,
|
|
728
|
+
onDrop,
|
|
729
|
+
onDragOverRow,
|
|
730
|
+
renaming: file.id === renamingId,
|
|
731
|
+
onRenameCommit,
|
|
732
|
+
onRenameCancel
|
|
733
|
+
},
|
|
734
|
+
file.id
|
|
735
|
+
))
|
|
736
|
+
},
|
|
737
|
+
virtualRow.key
|
|
738
|
+
);
|
|
739
|
+
}) }),
|
|
740
|
+
/* @__PURE__ */ jsx("div", { className: "h-3" })
|
|
741
|
+
] }),
|
|
742
|
+
loadingMore ? /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : null
|
|
743
|
+
]
|
|
744
|
+
}
|
|
745
|
+
);
|
|
559
746
|
}
|
|
560
747
|
function ExplorerGridItem({
|
|
561
748
|
file,
|
|
@@ -612,7 +799,7 @@ function ExplorerGridItem({
|
|
|
612
799
|
) : /* @__PURE__ */ jsx("span", { className: cn("truncate text-[11px]", file.kind === "folder" && "text-primary"), children: file.name }),
|
|
613
800
|
/* @__PURE__ */ jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground", children: file.kind === "folder" ? folderLabel : formatSizeShort(file.size) })
|
|
614
801
|
] });
|
|
615
|
-
return /* @__PURE__ */ jsxs("
|
|
802
|
+
return /* @__PURE__ */ jsxs("div", { role: "listitem", className: "relative", children: [
|
|
616
803
|
coarse && onContextMenu ? /* @__PURE__ */ jsx(
|
|
617
804
|
"button",
|
|
618
805
|
{
|
|
@@ -1314,19 +1501,47 @@ function ExplorerClipboardToast({
|
|
|
1314
1501
|
)
|
|
1315
1502
|
] });
|
|
1316
1503
|
}
|
|
1504
|
+
function uploadQueuePercent(items) {
|
|
1505
|
+
const total = items.reduce((sum, item) => sum + item.size, 0);
|
|
1506
|
+
if (total <= 0) return items.length === 0 ? 0 : 100;
|
|
1507
|
+
const loaded = items.reduce((sum, item) => {
|
|
1508
|
+
if (item.status === "success") return sum + item.size;
|
|
1509
|
+
if (item.status === "uploading" || item.status === "error") {
|
|
1510
|
+
return sum + item.size * item.progress / 100;
|
|
1511
|
+
}
|
|
1512
|
+
return sum;
|
|
1513
|
+
}, 0);
|
|
1514
|
+
return Math.min(100, Math.round(loaded / total * 100));
|
|
1515
|
+
}
|
|
1317
1516
|
function ExplorerUploadStatus({
|
|
1318
1517
|
items,
|
|
1319
1518
|
onDismiss,
|
|
1519
|
+
onCancel,
|
|
1520
|
+
onRemove,
|
|
1320
1521
|
className
|
|
1321
1522
|
}) {
|
|
1322
1523
|
const labels = useExplorerLabels();
|
|
1524
|
+
const listRef = useRef(null);
|
|
1525
|
+
const activeId = items.find((item) => item.status === "uploading")?.id;
|
|
1526
|
+
useLayoutEffect(() => {
|
|
1527
|
+
const list = listRef.current;
|
|
1528
|
+
if (!list || !activeId) return;
|
|
1529
|
+
const row = list.querySelector(`[data-upload-id="${activeId}"]`);
|
|
1530
|
+
if (!row) return;
|
|
1531
|
+
const top = row.offsetTop;
|
|
1532
|
+
const bottom = top + row.offsetHeight;
|
|
1533
|
+
if (top < list.scrollTop) list.scrollTop = top;
|
|
1534
|
+
else if (bottom > list.scrollTop + list.clientHeight) {
|
|
1535
|
+
list.scrollTop = bottom - list.clientHeight;
|
|
1536
|
+
}
|
|
1537
|
+
}, [activeId]);
|
|
1323
1538
|
if (items.length === 0) return null;
|
|
1324
1539
|
const done = items.filter((item) => item.status === "success").length;
|
|
1325
1540
|
const failed = items.filter((item) => item.status === "error").length;
|
|
1326
1541
|
const active = items.some(
|
|
1327
1542
|
(item) => item.status === "queued" || item.status === "uploading"
|
|
1328
1543
|
);
|
|
1329
|
-
const
|
|
1544
|
+
const totalPercent = uploadQueuePercent(items);
|
|
1330
1545
|
return /* @__PURE__ */ jsxs(
|
|
1331
1546
|
"div",
|
|
1332
1547
|
{
|
|
@@ -1350,20 +1565,44 @@ function ExplorerUploadStatus({
|
|
|
1350
1565
|
failed
|
|
1351
1566
|
] }) : null
|
|
1352
1567
|
] }),
|
|
1568
|
+
/* @__PURE__ */ jsxs("span", { className: "ml-auto font-mono text-[10px] tabular-nums text-primary", children: [
|
|
1569
|
+
totalPercent,
|
|
1570
|
+
"%"
|
|
1571
|
+
] }),
|
|
1572
|
+
active && onCancel ? /* @__PURE__ */ jsx(
|
|
1573
|
+
"button",
|
|
1574
|
+
{
|
|
1575
|
+
type: "button",
|
|
1576
|
+
className: "inline-flex h-7 items-center rounded-[10px] border border-border px-2 text-[10px] font-medium hover:bg-muted",
|
|
1577
|
+
onClick: onCancel,
|
|
1578
|
+
children: labels.cancelQueue
|
|
1579
|
+
}
|
|
1580
|
+
) : null,
|
|
1353
1581
|
!active ? /* @__PURE__ */ jsx(
|
|
1354
1582
|
"button",
|
|
1355
1583
|
{
|
|
1356
1584
|
type: "button",
|
|
1357
1585
|
"aria-label": labels.dismissUploads,
|
|
1358
|
-
className: "
|
|
1586
|
+
className: "inline-flex size-7 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
|
|
1359
1587
|
onClick: onDismiss,
|
|
1360
1588
|
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
|
|
1361
1589
|
}
|
|
1362
|
-
) :
|
|
1590
|
+
) : null
|
|
1363
1591
|
] }),
|
|
1364
|
-
/* @__PURE__ */ jsx("
|
|
1592
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 h-1 overflow-hidden rounded-[10px] bg-muted", children: /* @__PURE__ */ jsx("div", { className: "h-full bg-primary", style: { width: `${totalPercent}%` } }) }),
|
|
1593
|
+
/* @__PURE__ */ jsx("ul", { ref: listRef, className: "flex max-h-48 min-h-0 flex-col gap-1.5 overflow-y-auto overscroll-contain", children: items.map((item) => /* @__PURE__ */ jsxs("li", { "data-upload-id": item.id, className: "min-w-0 shrink-0", children: [
|
|
1365
1594
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1366
1595
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs", children: item.name }),
|
|
1596
|
+
onRemove ? /* @__PURE__ */ jsx(
|
|
1597
|
+
"button",
|
|
1598
|
+
{
|
|
1599
|
+
type: "button",
|
|
1600
|
+
"aria-label": `${labels.removeUpload} ${item.name}`,
|
|
1601
|
+
className: "inline-flex size-6 shrink-0 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
|
|
1602
|
+
onClick: () => onRemove(item.id),
|
|
1603
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" })
|
|
1604
|
+
}
|
|
1605
|
+
) : null,
|
|
1367
1606
|
/* @__PURE__ */ jsx(
|
|
1368
1607
|
"span",
|
|
1369
1608
|
{
|
|
@@ -1390,6 +1629,11 @@ function useUploadQueue(options) {
|
|
|
1390
1629
|
confirmUploadRef.current = options.confirmUpload;
|
|
1391
1630
|
const queueRef = useRef([]);
|
|
1392
1631
|
const running = useRef(false);
|
|
1632
|
+
const dropped = useRef(/* @__PURE__ */ new Set());
|
|
1633
|
+
const activeId = useRef(null);
|
|
1634
|
+
const stepAbort = useRef(null);
|
|
1635
|
+
const xhrRef = useRef(null);
|
|
1636
|
+
const xhrOwner = useRef(null);
|
|
1393
1637
|
const [items, setItems] = useState([]);
|
|
1394
1638
|
const publish = useCallback(() => {
|
|
1395
1639
|
setItems(queueRef.current.map(({ content: _content, ...item }) => item));
|
|
@@ -1399,6 +1643,13 @@ function useUploadQueue(options) {
|
|
|
1399
1643
|
const next = queueRef.current.find((item) => item.status === "queued");
|
|
1400
1644
|
if (!next) return;
|
|
1401
1645
|
running.current = true;
|
|
1646
|
+
activeId.current = next.id;
|
|
1647
|
+
const controller = new AbortController();
|
|
1648
|
+
stepAbort.current = controller;
|
|
1649
|
+
let confirming = false;
|
|
1650
|
+
const forget = () => {
|
|
1651
|
+
queueRef.current = queueRef.current.filter((item) => item.id !== next.id);
|
|
1652
|
+
};
|
|
1402
1653
|
next.status = "uploading";
|
|
1403
1654
|
next.progress = 0;
|
|
1404
1655
|
publish();
|
|
@@ -1409,31 +1660,62 @@ function useUploadQueue(options) {
|
|
|
1409
1660
|
type: next.type,
|
|
1410
1661
|
content: next.content,
|
|
1411
1662
|
parentId: next.parentId
|
|
1412
|
-
});
|
|
1663
|
+
}, controller.signal);
|
|
1664
|
+
if (dropped.current.has(next.id)) {
|
|
1665
|
+
forget();
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1413
1668
|
let lastPublish = 0;
|
|
1414
|
-
await putFile(
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1669
|
+
await putFile(
|
|
1670
|
+
target,
|
|
1671
|
+
next,
|
|
1672
|
+
(progress) => {
|
|
1673
|
+
next.progress = progress;
|
|
1674
|
+
const now = Date.now();
|
|
1675
|
+
if (progress === 100 || now - lastPublish > 200) {
|
|
1676
|
+
lastPublish = now;
|
|
1677
|
+
publish();
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
(xhr) => {
|
|
1681
|
+
xhrRef.current = xhr;
|
|
1682
|
+
xhrOwner.current = next.id;
|
|
1420
1683
|
}
|
|
1421
|
-
|
|
1684
|
+
);
|
|
1685
|
+
if (dropped.current.has(next.id)) {
|
|
1686
|
+
forget();
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
confirming = true;
|
|
1422
1690
|
await confirmUploadRef.current?.({
|
|
1423
1691
|
name: next.name,
|
|
1424
1692
|
size: next.size,
|
|
1425
1693
|
type: next.type,
|
|
1426
1694
|
content: next.content
|
|
1427
|
-
});
|
|
1695
|
+
}, controller.signal);
|
|
1696
|
+
dropped.current.delete(next.id);
|
|
1428
1697
|
next.status = "success";
|
|
1429
1698
|
next.progress = 100;
|
|
1430
1699
|
if (typeof window !== "undefined") {
|
|
1431
1700
|
window.dispatchEvent(new Event("file-next-uploaded"));
|
|
1432
1701
|
}
|
|
1433
1702
|
} catch (error) {
|
|
1703
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
1704
|
+
if (!confirming && dropped.current.has(next.id) || confirming && aborted) {
|
|
1705
|
+
forget();
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1434
1708
|
next.status = "error";
|
|
1435
1709
|
next.error = error instanceof Error ? error.message : "Upload failed";
|
|
1436
1710
|
} finally {
|
|
1711
|
+
if (activeId.current === next.id) {
|
|
1712
|
+
activeId.current = null;
|
|
1713
|
+
stepAbort.current = null;
|
|
1714
|
+
}
|
|
1715
|
+
if (xhrOwner.current === next.id) {
|
|
1716
|
+
xhrRef.current = null;
|
|
1717
|
+
xhrOwner.current = null;
|
|
1718
|
+
}
|
|
1437
1719
|
publish();
|
|
1438
1720
|
running.current = false;
|
|
1439
1721
|
void pump();
|
|
@@ -1458,6 +1740,26 @@ function useUploadQueue(options) {
|
|
|
1458
1740
|
},
|
|
1459
1741
|
[publish, pump]
|
|
1460
1742
|
);
|
|
1743
|
+
const dropIds = useCallback(
|
|
1744
|
+
(ids) => {
|
|
1745
|
+
if (ids.length === 0) return;
|
|
1746
|
+
for (const id of ids) dropped.current.add(id);
|
|
1747
|
+
if (xhrOwner.current && ids.includes(xhrOwner.current)) xhrRef.current?.abort();
|
|
1748
|
+
if (activeId.current && ids.includes(activeId.current)) stepAbort.current?.abort();
|
|
1749
|
+
const idSet = new Set(ids);
|
|
1750
|
+
queueRef.current = queueRef.current.filter(
|
|
1751
|
+
(item) => !idSet.has(item.id) || item.id === activeId.current
|
|
1752
|
+
);
|
|
1753
|
+
publish();
|
|
1754
|
+
},
|
|
1755
|
+
[publish]
|
|
1756
|
+
);
|
|
1757
|
+
const remove = useCallback((id) => dropIds([id]), [dropIds]);
|
|
1758
|
+
const cancel = useCallback(() => {
|
|
1759
|
+
dropIds(
|
|
1760
|
+
queueRef.current.filter((item) => item.status === "queued" || item.status === "uploading").map((item) => item.id)
|
|
1761
|
+
);
|
|
1762
|
+
}, [dropIds]);
|
|
1461
1763
|
const dismiss = useCallback(() => {
|
|
1462
1764
|
queueRef.current = queueRef.current.filter(
|
|
1463
1765
|
(item) => item.status === "queued" || item.status === "uploading"
|
|
@@ -1477,11 +1779,19 @@ function useUploadQueue(options) {
|
|
|
1477
1779
|
window.clearTimeout(timeout);
|
|
1478
1780
|
};
|
|
1479
1781
|
}, [active, dismiss, itemCount]);
|
|
1480
|
-
return { items, enqueue, dismiss, active };
|
|
1782
|
+
return { items, enqueue, dismiss, cancel, remove, active };
|
|
1481
1783
|
}
|
|
1482
|
-
function putFile(target, file, onProgress) {
|
|
1784
|
+
function putFile(target, file, onProgress, onStart) {
|
|
1483
1785
|
return new Promise((resolve, reject) => {
|
|
1786
|
+
let settled = false;
|
|
1787
|
+
const finish = (fn) => {
|
|
1788
|
+
if (settled) return;
|
|
1789
|
+
settled = true;
|
|
1790
|
+
fn();
|
|
1791
|
+
};
|
|
1484
1792
|
const xhr = new XMLHttpRequest();
|
|
1793
|
+
onStart(xhr);
|
|
1794
|
+
xhr.addEventListener("abort", () => finish(() => reject(new Error("aborted"))));
|
|
1485
1795
|
xhr.upload.addEventListener("progress", (event) => {
|
|
1486
1796
|
if (event.lengthComputable) {
|
|
1487
1797
|
onProgress(Math.min(100, Math.round(event.loaded / event.total * 100)));
|
|
@@ -1495,13 +1805,13 @@ function putFile(target, file, onProgress) {
|
|
|
1495
1805
|
if (parsed.error?.message) message = parsed.error.message;
|
|
1496
1806
|
} catch {
|
|
1497
1807
|
}
|
|
1498
|
-
reject(new Error(message));
|
|
1808
|
+
finish(() => reject(new Error(message)));
|
|
1499
1809
|
return;
|
|
1500
1810
|
}
|
|
1501
|
-
resolve
|
|
1811
|
+
finish(resolve);
|
|
1502
1812
|
});
|
|
1503
1813
|
xhr.addEventListener("error", () => {
|
|
1504
|
-
reject(new Error("Upload failed"));
|
|
1814
|
+
finish(() => reject(new Error("Upload failed")));
|
|
1505
1815
|
});
|
|
1506
1816
|
xhr.open(target.method ?? "PUT", target.url);
|
|
1507
1817
|
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
@@ -1522,8 +1832,16 @@ function UploadQueueProvider({
|
|
|
1522
1832
|
}) {
|
|
1523
1833
|
const uploads = useUploadQueue({ requestUpload, confirmUpload });
|
|
1524
1834
|
const panel = useMemo(
|
|
1525
|
-
() => /* @__PURE__ */ jsx("div", { className: "fixed right-4 bottom-4 z-50 flex w-[min(22rem,calc(100vw-2rem))] flex-col gap-2", children: /* @__PURE__ */ jsx(
|
|
1526
|
-
|
|
1835
|
+
() => /* @__PURE__ */ jsx("div", { className: "fixed right-4 bottom-4 z-50 flex w-[min(22rem,calc(100vw-2rem))] flex-col gap-2", children: /* @__PURE__ */ jsx(
|
|
1836
|
+
ExplorerUploadStatus,
|
|
1837
|
+
{
|
|
1838
|
+
items: uploads.items,
|
|
1839
|
+
onDismiss: uploads.dismiss,
|
|
1840
|
+
onCancel: uploads.cancel,
|
|
1841
|
+
onRemove: uploads.remove
|
|
1842
|
+
}
|
|
1843
|
+
) }),
|
|
1844
|
+
[uploads.cancel, uploads.dismiss, uploads.items, uploads.remove]
|
|
1527
1845
|
);
|
|
1528
1846
|
return /* @__PURE__ */ jsxs(EnqueueContext.Provider, { value: uploads.enqueue, children: [
|
|
1529
1847
|
children,
|
|
@@ -1659,6 +1977,7 @@ function FileExplorerInner(props) {
|
|
|
1659
1977
|
onMove
|
|
1660
1978
|
});
|
|
1661
1979
|
const uploadInputRef = useRef(null);
|
|
1980
|
+
const scrollRef = useRef(null);
|
|
1662
1981
|
const [query, setQuery] = useState("");
|
|
1663
1982
|
const [trashOpen, setTrashOpen] = useState(false);
|
|
1664
1983
|
const [pageItems, setPageItems] = useState([]);
|
|
@@ -1817,6 +2136,10 @@ function FileExplorerInner(props) {
|
|
|
1817
2136
|
},
|
|
1818
2137
|
[listTrash, query, searchFiles, trashOpen]
|
|
1819
2138
|
);
|
|
2139
|
+
const loadNext = useCallback(() => {
|
|
2140
|
+
if (overlayFiles) void loadOverlay("more");
|
|
2141
|
+
else void loadBrowse("more");
|
|
2142
|
+
}, [loadBrowse, loadOverlay, overlayFiles]);
|
|
1820
2143
|
const refreshTrashFlag = useCallback(async () => {
|
|
1821
2144
|
if (!listTrash) {
|
|
1822
2145
|
setTrashHasItems(false);
|
|
@@ -2120,7 +2443,7 @@ function FileExplorerInner(props) {
|
|
|
2120
2443
|
}
|
|
2121
2444
|
)
|
|
2122
2445
|
] }),
|
|
2123
|
-
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: (overlayLoading || pageStatus === "loading") && displayFiles.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-12 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : overlayError || pageStatus === "error" && pageError ? /* @__PURE__ */ jsx("div", { className: "border-b border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive", children: (overlayError ?? pageError)?.message ?? labels.folderCreateFailed }) : displayFiles.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
|
|
2446
|
+
/* @__PURE__ */ jsx("div", { ref: scrollRef, className: "min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-x-contain", children: (overlayLoading || pageStatus === "loading") && displayFiles.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-12 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : overlayError || pageStatus === "error" && pageError ? /* @__PURE__ */ jsx("div", { className: "border-b border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive", children: (overlayError ?? pageError)?.message ?? labels.folderCreateFailed }) : displayFiles.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
|
|
2124
2447
|
/* @__PURE__ */ jsx("p", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: trashOpen ? labels.emptyTrash : query.trim() ? labels.noMatches : labels.emptyFolder }),
|
|
2125
2448
|
trashOpen ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.trashRetention }) : !query.trim() ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.dropHint }) : null
|
|
2126
2449
|
] }) : explorer.view === "list" ? /* @__PURE__ */ jsx(
|
|
@@ -2151,7 +2474,11 @@ function FileExplorerInner(props) {
|
|
|
2151
2474
|
onSortChange: handleSortChange,
|
|
2152
2475
|
renamingId,
|
|
2153
2476
|
onRenameCommit: commitRename,
|
|
2154
|
-
onRenameCancel: cancelRename
|
|
2477
|
+
onRenameCancel: cancelRename,
|
|
2478
|
+
scrollRef,
|
|
2479
|
+
hasMore,
|
|
2480
|
+
loadingMore: loadingMore || overlayMore,
|
|
2481
|
+
onLoadMore: loadNext
|
|
2155
2482
|
}
|
|
2156
2483
|
)
|
|
2157
2484
|
}
|
|
@@ -2180,24 +2507,15 @@ function FileExplorerInner(props) {
|
|
|
2180
2507
|
onDragOverRow: explorer.setDropTarget,
|
|
2181
2508
|
renamingId,
|
|
2182
2509
|
onRenameCommit: commitRename,
|
|
2183
|
-
onRenameCancel: cancelRename
|
|
2510
|
+
onRenameCancel: cancelRename,
|
|
2511
|
+
scrollRef,
|
|
2512
|
+
hasMore,
|
|
2513
|
+
loadingMore: loadingMore || overlayMore,
|
|
2514
|
+
onLoadMore: loadNext
|
|
2184
2515
|
}
|
|
2185
2516
|
)
|
|
2186
2517
|
}
|
|
2187
2518
|
) }),
|
|
2188
|
-
hasMore && sortedFiles.length > 0 ? /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border p-3", children: /* @__PURE__ */ jsx(
|
|
2189
|
-
"button",
|
|
2190
|
-
{
|
|
2191
|
-
type: "button",
|
|
2192
|
-
disabled: loadingMore || overlayMore,
|
|
2193
|
-
onClick: () => {
|
|
2194
|
-
if (overlayFiles) void loadOverlay("more");
|
|
2195
|
-
else void loadBrowse("more");
|
|
2196
|
-
},
|
|
2197
|
-
className: "inline-flex h-8 w-full items-center justify-center rounded-[10px] border border-border text-xs font-medium hover:bg-muted disabled:opacity-50",
|
|
2198
|
-
children: loadingMore || overlayMore ? labels.loading : labels.loadMore
|
|
2199
|
-
}
|
|
2200
|
-
) }) : null,
|
|
2201
2519
|
/* @__PURE__ */ jsxs("footer", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border px-4 py-2", children: [
|
|
2202
2520
|
/* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
|
|
2203
2521
|
/* @__PURE__ */ jsxs("span", { className: "text-primary tabular-nums", children: [
|
|
@@ -2378,18 +2696,27 @@ function ErrorState(props) {
|
|
|
2378
2696
|
}
|
|
2379
2697
|
);
|
|
2380
2698
|
}
|
|
2699
|
+
var MIN_ZOOM = 1;
|
|
2700
|
+
var MAX_ZOOM = 4;
|
|
2701
|
+
function clampPreviewZoom(scale) {
|
|
2702
|
+
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number.isFinite(scale) ? scale : 1));
|
|
2703
|
+
}
|
|
2381
2704
|
function canPreviewFile(file) {
|
|
2382
2705
|
return previewKind(file.mimeType, file.name) !== "none";
|
|
2383
2706
|
}
|
|
2384
2707
|
function previewKind(mimeType, name) {
|
|
2385
2708
|
const mime = mimeType.toLowerCase();
|
|
2386
2709
|
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
2387
|
-
if (mime
|
|
2710
|
+
if (mime === "text/html" || mime === "application/xhtml+xml" || ext === "html" || ext === "htm") {
|
|
2711
|
+
return "none";
|
|
2712
|
+
}
|
|
2713
|
+
if (mime.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp", "avif"].includes(ext)) {
|
|
2388
2714
|
return "image";
|
|
2389
2715
|
}
|
|
2390
2716
|
if (mime === "application/pdf" || ext === "pdf") return "pdf";
|
|
2391
|
-
if (mime.startsWith("video/") || ["mp4", "webm"
|
|
2392
|
-
if (mime.startsWith("
|
|
2717
|
+
if (mime.startsWith("video/") || ["mp4", "webm"].includes(ext)) return "video";
|
|
2718
|
+
if (mime.startsWith("audio/") || ["mp3", "wav", "ogg"].includes(ext)) return "audio";
|
|
2719
|
+
if (mime.startsWith("text/") || ["txt", "csv", "md"].includes(ext)) return "text";
|
|
2393
2720
|
return "none";
|
|
2394
2721
|
}
|
|
2395
2722
|
function FilePreviewDialog({
|
|
@@ -2399,46 +2726,162 @@ function FilePreviewDialog({
|
|
|
2399
2726
|
onDownload
|
|
2400
2727
|
}) {
|
|
2401
2728
|
const labels = useExplorerLabels();
|
|
2729
|
+
const [scale, setScale] = useState(1);
|
|
2730
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
2731
|
+
const drag = useRef(null);
|
|
2732
|
+
const closeRef = useRef(null);
|
|
2733
|
+
useEffect(() => {
|
|
2734
|
+
setScale(1);
|
|
2735
|
+
setPan({ x: 0, y: 0 });
|
|
2736
|
+
closeRef.current?.focus();
|
|
2737
|
+
}, [file?.id, src]);
|
|
2738
|
+
useEffect(() => {
|
|
2739
|
+
const onKey = (event) => {
|
|
2740
|
+
if (event.key === "Escape") onClose();
|
|
2741
|
+
};
|
|
2742
|
+
window.addEventListener("keydown", onKey);
|
|
2743
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2744
|
+
}, [onClose]);
|
|
2402
2745
|
if (!file) return null;
|
|
2403
2746
|
const kind = previewKind(file.mimeType, file.name);
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
"
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
className: "
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2747
|
+
const zoom = (next) => {
|
|
2748
|
+
const clamped = clampPreviewZoom(next);
|
|
2749
|
+
setScale(clamped);
|
|
2750
|
+
if (clamped === 1) setPan({ x: 0, y: 0 });
|
|
2751
|
+
};
|
|
2752
|
+
const stop = (event) => event.stopPropagation();
|
|
2753
|
+
return /* @__PURE__ */ jsxs(
|
|
2754
|
+
"div",
|
|
2755
|
+
{
|
|
2756
|
+
role: "dialog",
|
|
2757
|
+
"aria-modal": "true",
|
|
2758
|
+
"aria-label": file.name,
|
|
2759
|
+
className: "fixed inset-0 z-50 bg-black/90 text-white",
|
|
2760
|
+
onClick: onClose,
|
|
2761
|
+
children: [
|
|
2762
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute inset-x-0 top-0 z-10 flex items-center gap-2 px-3 py-2", onClick: stop, children: [
|
|
2763
|
+
/* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 truncate text-sm", children: file.name }),
|
|
2764
|
+
/* @__PURE__ */ jsx(
|
|
2765
|
+
"button",
|
|
2766
|
+
{
|
|
2767
|
+
type: "button",
|
|
2768
|
+
className: "inline-flex h-8 items-center rounded-full px-3 text-sm hover:bg-white/10",
|
|
2769
|
+
onClick: () => onDownload(file),
|
|
2770
|
+
children: labels.download
|
|
2771
|
+
}
|
|
2772
|
+
),
|
|
2773
|
+
/* @__PURE__ */ jsx(
|
|
2774
|
+
"button",
|
|
2775
|
+
{
|
|
2776
|
+
ref: closeRef,
|
|
2777
|
+
type: "button",
|
|
2778
|
+
"aria-label": labels.closePreview,
|
|
2779
|
+
className: "inline-flex size-8 items-center justify-center rounded-full hover:bg-white/10",
|
|
2780
|
+
onClick: onClose,
|
|
2781
|
+
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
|
|
2782
|
+
}
|
|
2783
|
+
)
|
|
2784
|
+
] }),
|
|
2785
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center px-4 pt-12 pb-16", children: kind === "image" ? /* @__PURE__ */ jsx(
|
|
2786
|
+
"img",
|
|
2787
|
+
{
|
|
2788
|
+
src,
|
|
2789
|
+
alt: file.name,
|
|
2790
|
+
draggable: false,
|
|
2791
|
+
className: "max-h-full max-w-full cursor-zoom-in select-none object-contain",
|
|
2792
|
+
style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` },
|
|
2793
|
+
onClick: stop,
|
|
2794
|
+
onDoubleClick: (event) => {
|
|
2795
|
+
stop(event);
|
|
2796
|
+
zoom(scale === 1 ? 2 : 1);
|
|
2797
|
+
},
|
|
2798
|
+
onWheel: (event) => {
|
|
2799
|
+
event.preventDefault();
|
|
2800
|
+
stop(event);
|
|
2801
|
+
zoom(scale + (event.deltaY < 0 ? 0.25 : -0.25));
|
|
2802
|
+
},
|
|
2803
|
+
onPointerDown: (event) => {
|
|
2804
|
+
if (scale === 1) return;
|
|
2805
|
+
stop(event);
|
|
2806
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
2807
|
+
drag.current = { x: event.clientX, y: event.clientY, px: pan.x, py: pan.y };
|
|
2808
|
+
},
|
|
2809
|
+
onPointerMove: (event) => {
|
|
2810
|
+
if (!drag.current) return;
|
|
2811
|
+
setPan({
|
|
2812
|
+
x: drag.current.px + event.clientX - drag.current.x,
|
|
2813
|
+
y: drag.current.py + event.clientY - drag.current.y
|
|
2814
|
+
});
|
|
2815
|
+
},
|
|
2816
|
+
onPointerUp: () => {
|
|
2817
|
+
drag.current = null;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
) : kind === "pdf" || kind === "text" ? /* @__PURE__ */ jsx(
|
|
2821
|
+
"iframe",
|
|
2822
|
+
{
|
|
2823
|
+
src,
|
|
2824
|
+
title: file.name,
|
|
2825
|
+
className: "h-full w-full max-w-5xl bg-white",
|
|
2826
|
+
onClick: stop
|
|
2827
|
+
}
|
|
2828
|
+
) : kind === "video" ? /* @__PURE__ */ jsx(
|
|
2829
|
+
"video",
|
|
2830
|
+
{
|
|
2831
|
+
controls: true,
|
|
2832
|
+
playsInline: true,
|
|
2833
|
+
preload: "metadata",
|
|
2834
|
+
src,
|
|
2835
|
+
className: "max-h-full max-w-full",
|
|
2836
|
+
onClick: stop,
|
|
2837
|
+
children: labels.previewUnavailable
|
|
2838
|
+
}
|
|
2839
|
+
) : kind === "audio" ? /* @__PURE__ */ jsx("audio", { controls: true, preload: "metadata", src, className: "w-full max-w-md", onClick: stop }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable }) }),
|
|
2840
|
+
kind === "image" ? /* @__PURE__ */ jsxs(
|
|
2841
|
+
"div",
|
|
2842
|
+
{
|
|
2843
|
+
className: "absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center gap-1 rounded-full bg-black/70 px-1 py-1",
|
|
2844
|
+
onClick: stop,
|
|
2845
|
+
children: [
|
|
2846
|
+
/* @__PURE__ */ jsx(
|
|
2847
|
+
"button",
|
|
2848
|
+
{
|
|
2849
|
+
type: "button",
|
|
2850
|
+
"aria-label": labels.zoomOut,
|
|
2851
|
+
className: "inline-flex size-8 items-center justify-center rounded-full hover:bg-white/10",
|
|
2852
|
+
onClick: () => zoom(scale - 0.25),
|
|
2853
|
+
children: /* @__PURE__ */ jsx(Minus, { "aria-hidden": "true", className: "size-4" })
|
|
2854
|
+
}
|
|
2855
|
+
),
|
|
2856
|
+
/* @__PURE__ */ jsxs(
|
|
2857
|
+
"button",
|
|
2858
|
+
{
|
|
2859
|
+
type: "button",
|
|
2860
|
+
"aria-label": labels.zoomFit,
|
|
2861
|
+
className: "min-w-14 px-2 text-xs tabular-nums",
|
|
2862
|
+
onClick: () => zoom(1),
|
|
2863
|
+
children: [
|
|
2864
|
+
Math.round(scale * 100),
|
|
2865
|
+
"%"
|
|
2866
|
+
]
|
|
2867
|
+
}
|
|
2868
|
+
),
|
|
2869
|
+
/* @__PURE__ */ jsx(
|
|
2870
|
+
"button",
|
|
2871
|
+
{
|
|
2872
|
+
type: "button",
|
|
2873
|
+
"aria-label": labels.zoomIn,
|
|
2874
|
+
className: "inline-flex size-8 items-center justify-center rounded-full hover:bg-white/10",
|
|
2875
|
+
onClick: () => zoom(scale + 0.25),
|
|
2876
|
+
children: /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true", className: "size-4" })
|
|
2877
|
+
}
|
|
2878
|
+
)
|
|
2879
|
+
]
|
|
2880
|
+
}
|
|
2881
|
+
) : null
|
|
2882
|
+
]
|
|
2883
|
+
}
|
|
2884
|
+
);
|
|
2442
2885
|
}
|
|
2443
2886
|
function CreateFolderDialog({
|
|
2444
2887
|
open,
|