@muhgholy/next-drive 4.23.23 → 4.23.25

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.
Files changed (44) hide show
  1. package/dist/{chunk-IVSBLEMY.cjs → chunk-45FZNILV.cjs} +170 -1031
  2. package/dist/chunk-45FZNILV.cjs.map +1 -0
  3. package/dist/{chunk-CPMMHIQM.js → chunk-VBFXHZBY.js} +159 -1026
  4. package/dist/chunk-VBFXHZBY.js.map +1 -0
  5. package/dist/chunk-VTCBYFAK.cjs +765 -0
  6. package/dist/chunk-VTCBYFAK.cjs.map +1 -0
  7. package/dist/chunk-ZNWS7VJP.js +755 -0
  8. package/dist/chunk-ZNWS7VJP.js.map +1 -0
  9. package/dist/client/components/drive/dnd/context.d.ts.map +1 -1
  10. package/dist/client/components/drive/explorer.d.ts.map +1 -1
  11. package/dist/client/components/drive/file-grid.d.ts.map +1 -1
  12. package/dist/client/hooks/use-marquee.d.ts +14 -0
  13. package/dist/client/hooks/use-marquee.d.ts.map +1 -0
  14. package/dist/client/index.cjs +170 -150
  15. package/dist/client/index.cjs.map +1 -1
  16. package/dist/client/index.js +169 -150
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/upload.d.ts +9 -4
  19. package/dist/client/upload.d.ts.map +1 -1
  20. package/dist/server/actions/drive.d.ts.map +1 -1
  21. package/dist/server/controllers/drive.d.ts.map +1 -1
  22. package/dist/server/express.cjs +15 -14
  23. package/dist/server/express.cjs.map +1 -1
  24. package/dist/server/express.js +4 -2
  25. package/dist/server/express.js.map +1 -1
  26. package/dist/server/hono.cjs +15 -14
  27. package/dist/server/hono.cjs.map +1 -1
  28. package/dist/server/hono.js +4 -2
  29. package/dist/server/hono.js.map +1 -1
  30. package/dist/server/index.cjs +28 -27
  31. package/dist/server/index.js +2 -1
  32. package/dist/server/tus.d.ts +17 -0
  33. package/dist/server/tus.d.ts.map +1 -0
  34. package/dist/server/zod/schemas.d.ts +0 -53
  35. package/dist/server/zod/schemas.d.ts.map +1 -1
  36. package/dist/tus-6OAOA454.js +3 -0
  37. package/dist/tus-6OAOA454.js.map +1 -0
  38. package/dist/tus-P67YULWB.cjs +16 -0
  39. package/dist/tus-P67YULWB.cjs.map +1 -0
  40. package/dist/types/server/api.d.ts +1 -1
  41. package/dist/types/server/api.d.ts.map +1 -1
  42. package/package.json +4 -1
  43. package/dist/chunk-CPMMHIQM.js.map +0 -1
  44. package/dist/chunk-IVSBLEMY.cjs.map +0 -1
@@ -3,6 +3,7 @@ export { driveFileSchemaZod } from '../chunk-FJRGKB3U.js';
3
3
  import * as React3 from 'react';
4
4
  import React3__default, { createContext, useContext, useState, useMemo, useCallback, useRef, useEffect } from 'react';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+ import * as tus from 'tus-js-client';
6
7
  import { Slot } from '@radix-ui/react-slot';
7
8
  import { cva } from 'class-variance-authority';
8
9
  import * as ProgressPrimitive from '@radix-ui/react-progress';
@@ -306,8 +307,6 @@ var useDrive = () => {
306
307
  if (!context) throw new Error("useDrive must be used within a DriveProvider");
307
308
  return context;
308
309
  };
309
-
310
- // src/client/upload.ts
311
310
  var MAX_CONCURRENT_UPLOADS = 2;
312
311
  var getChunkSize = (fileSize) => {
313
312
  if (fileSize < 50 * 1024 * 1024) return 2 * 1024 * 1024;
@@ -341,161 +340,92 @@ var uploadFile = (options) => {
341
340
  onComplete
342
341
  } = options;
343
342
  const id = options.id ?? generateId();
344
- const endpoint = apiEndpoint.replace(/\/$/, "");
345
- const uploadUrl = `${endpoint}?action=upload`;
346
- const controller = new AbortController();
347
- let activeXhr = null;
348
- let driveId;
349
- let completedItem;
350
- let cancelled = false;
351
- const totalBytes = file.size;
343
+ const endpoint = `${apiEndpoint.replace(/\/$/, "")}?action=upload`;
352
344
  const chunkSize = getChunkSize(file.size);
353
- const totalChunks = Math.ceil(file.size / chunkSize);
345
+ const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
354
346
  const log = (type, message) => onLog?.({ id, type, message, timestamp: Date.now() });
355
- const emitProgress = (currentChunk, uploadedBytes) => {
356
- const percent = totalBytes === 0 ? 100 : Math.min(100, Math.round(uploadedBytes / totalBytes * 100));
357
- onProgress?.({ id, percent, uploadedBytes, totalBytes, currentChunk, totalChunks });
347
+ let settle = () => {
358
348
  };
359
- const sendChunk = (chunkIndex, formData, chunkStart) => new Promise((resolve) => {
360
- const xhr = new XMLHttpRequest();
361
- activeXhr = xhr;
362
- log("info", `Sending chunk to ${uploadUrl}`);
363
- xhr.upload.onprogress = (e) => {
364
- if (e.lengthComputable) emitProgress(chunkIndex, chunkStart + e.loaded);
365
- };
366
- xhr.onload = () => {
367
- activeXhr = null;
368
- const status = xhr.status;
369
- const text = xhr.responseText;
370
- if (status < 200 || status >= 300) {
371
- log("error", `HTTP ${status}: ${xhr.statusText}`);
372
- try {
373
- const json = JSON.parse(text);
374
- const msg = json.message || json.error?.message || `Status ${status}`;
375
- log("error", `Server response: ${msg}`);
376
- const canRetry = [408, 429, 502, 503, 504].includes(status);
377
- if (canRetry) log("warning", `Error is retryable (status ${status})`);
378
- resolve([false, msg, canRetry]);
379
- } catch {
380
- log("error", `Failed to parse error response: ${text.slice(0, 100)}`);
381
- const isRetryable = status >= 500 || status === 429;
382
- resolve([false, `Server error ${status}: ${text.slice(0, 100)}`, isRetryable]);
383
- }
384
- return;
349
+ const promise = new Promise((resolve) => {
350
+ settle = resolve;
351
+ });
352
+ let completedItem;
353
+ let cancelled = false;
354
+ const metadata = {
355
+ filename: file.name,
356
+ filetype: file.type || "application/octet-stream",
357
+ folderId: folderId || "root"
358
+ };
359
+ if (conflictAction) metadata.conflictAction = conflictAction;
360
+ const upload = new tus.Upload(file, {
361
+ endpoint,
362
+ chunkSize,
363
+ // ** Retry with backoff. On a dropped connection tus first re-checks the server offset (HEAD)
364
+ // ** and resumes from there this is what prevents the "session expired" failures at ~100%.
365
+ retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
366
+ removeFingerprintOnSuccess: true,
367
+ metadata,
368
+ headers: accountId ? { "x-drive-account": accountId } : {},
369
+ onBeforeRequest(req) {
370
+ if (withCredentials) {
371
+ const xhr = req.getUnderlyingObject();
372
+ if (xhr && "withCredentials" in xhr) xhr.withCredentials = true;
385
373
  }
386
- try {
387
- const data = JSON.parse(text);
388
- if (data.status !== 200) {
389
- log("error", `Upload API error: ${data.message || "Unknown error"}`);
390
- resolve([false, data.message || "Upload failed", false]);
391
- return;
392
- }
393
- log("success", `Chunk uploaded successfully`);
394
- resolve([true, data.data, false]);
395
- } catch {
396
- log("error", `Failed to parse response: ${text.slice(0, 100)}`);
397
- resolve([false, "Invalid server response", true]);
374
+ },
375
+ onShouldRetry(err) {
376
+ const status = err?.originalResponse?.getStatus?.() ?? 0;
377
+ if ([400, 401, 403, 413].includes(status)) {
378
+ log("error", `Non-retryable error (status ${status})`);
379
+ return false;
398
380
  }
399
- };
400
- xhr.onerror = () => {
401
- activeXhr = null;
402
- log("error", `Network/Fetch error: request failed`);
403
- resolve([false, `Upload connection failed`, true]);
404
- };
405
- xhr.onabort = () => {
406
- activeXhr = null;
407
- resolve([false, "Cancelled", false]);
408
- };
409
- xhr.open("POST", uploadUrl);
410
- if (accountId) xhr.setRequestHeader("x-drive-account", accountId);
411
- xhr.withCredentials = withCredentials;
412
- xhr.send(formData);
413
- });
414
- const run = async () => {
415
- log("info", `Starting upload: ${file.name} (${file.size} bytes)`);
416
- log("info", `File split into ${totalChunks} chunks of ${chunkSize} bytes each`);
417
- emitProgress(0, 0);
418
- try {
419
- for (let i = 0; i < totalChunks; i++) {
420
- if (controller.signal.aborted) throw new Error("Cancelled");
421
- log("info", `Processing chunk ${i + 1}/${totalChunks}`);
422
- const start = i * chunkSize;
423
- const end = Math.min(start + chunkSize, file.size);
424
- const chunk = file.slice(start, end);
425
- const formData = new FormData();
426
- formData.append("chunk", chunk);
427
- formData.append("chunkIndex", String(i));
428
- formData.append("totalChunks", String(totalChunks));
429
- formData.append("fileName", file.name);
430
- formData.append("fileSize", String(file.size));
431
- formData.append("fileType", file.type);
432
- formData.append("folderId", folderId || "root");
433
- if (conflictAction) formData.append("conflictAction", conflictAction);
434
- if (driveId) formData.append("driveId", driveId);
435
- let attempts = 0;
436
- let success = false;
437
- while (!success && attempts < 3 && !controller.signal.aborted) {
438
- const [ok, result, canRetry] = await sendChunk(i, formData, start);
439
- if (ok) {
440
- success = true;
441
- emitProgress(i + 1, end);
442
- if (result.type === "UPLOAD_STARTED" && result.driveId) {
443
- driveId = result.driveId;
444
- log("success", `Upload session started with ID: ${driveId}`);
445
- } else if (result.type === "UPLOAD_COMPLETE") {
446
- if (result.driveId) driveId = result.driveId;
447
- log("success", `Upload completed successfully`);
448
- completedItem = result.item;
449
- }
450
- } else {
451
- if (result === "Cancelled") throw new Error("Cancelled");
452
- if (!canRetry) {
453
- log("error", `Non-retryable error: ${result}`);
454
- throw new Error(result);
455
- }
456
- attempts++;
457
- log("warning", `Retry attempt ${attempts}/3 after error`);
458
- if (attempts === 3) {
459
- log("error", `Max retry attempts reached: ${result}`);
460
- throw new Error(result);
461
- }
462
- await new Promise((r) => setTimeout(r, 1e3 * attempts));
463
- }
381
+ log("warning", `Upload interrupted (status ${status}), retrying`);
382
+ return true;
383
+ },
384
+ onProgress(bytesUploaded, bytesTotal) {
385
+ const percent = bytesTotal === 0 ? 100 : Math.min(100, Math.round(bytesUploaded / bytesTotal * 100));
386
+ onProgress?.({
387
+ id,
388
+ percent,
389
+ uploadedBytes: bytesUploaded,
390
+ totalBytes: bytesTotal,
391
+ currentChunk: Math.min(totalChunks, Math.floor(bytesUploaded / chunkSize)),
392
+ totalChunks
393
+ });
394
+ },
395
+ onAfterResponse(_req, res) {
396
+ const header = res.getHeader("X-Drive-Item");
397
+ if (header) {
398
+ try {
399
+ completedItem = JSON.parse(decodeURIComponent(header));
400
+ } catch {
464
401
  }
465
- if (!success && controller.signal.aborted) throw new Error("Cancelled");
466
402
  }
467
- log("success", `All ${totalChunks} chunks uploaded successfully`);
468
- emitProgress(totalChunks, totalBytes);
403
+ },
404
+ onSuccess() {
405
+ log("success", "Upload completed successfully");
469
406
  if (completedItem !== void 0) onComplete?.(completedItem);
470
- return { id, status: "complete", driveId: driveId ?? "", file: toDriveFile(completedItem), item: completedItem };
471
- } catch (error) {
472
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
473
- if (errorMessage === "Cancelled") {
474
- log("warning", "Upload cancelled by user");
475
- return { id, status: "cancelled", driveId: driveId ?? null };
476
- }
477
- log("error", `Upload failed: ${errorMessage}`);
478
- return { id, status: "error", driveId: driveId ?? null, error: errorMessage };
407
+ settle({ id, status: "complete", driveId: completedItem?.id ?? "", file: toDriveFile(completedItem), item: completedItem });
408
+ },
409
+ onError(error) {
410
+ const message = error instanceof Error ? error.message : "Upload failed";
411
+ log("error", `Upload failed: ${message}`);
412
+ settle({ id, status: "error", driveId: null, error: message });
479
413
  }
480
- };
414
+ });
481
415
  const cancel = () => {
482
416
  if (cancelled) return;
483
417
  cancelled = true;
484
- controller.abort();
485
- activeXhr?.abort();
486
- if (driveId) {
487
- fetch(`${endpoint}?action=cancel&id=${driveId}`, {
488
- method: "POST",
489
- credentials: withCredentials ? "include" : "same-origin"
490
- }).catch(() => {
491
- });
492
- }
418
+ upload.abort(true).catch(() => {
419
+ });
420
+ settle({ id, status: "cancelled", driveId: null });
493
421
  };
494
422
  if (signal) {
495
423
  if (signal.aborted) cancel();
496
424
  else signal.addEventListener("abort", cancel, { once: true });
497
425
  }
498
- return { id, promise: run(), cancel };
426
+ log("info", `Starting upload: ${file.name} (${file.size} bytes)`);
427
+ upload.start();
428
+ return { id, promise, cancel };
499
429
  };
500
430
  var uploadFiles = (files, options) => {
501
431
  const { concurrency = MAX_CONCURRENT_UPLOADS, onFileComplete, ...base } = options;
@@ -1530,9 +1460,16 @@ var DriveDndContext = createContext({
1530
1460
  var useDriveDnd = () => useContext(DriveDndContext);
1531
1461
  var DriveDndProvider = (props) => {
1532
1462
  const { children } = props;
1533
- const { items, setItems, sortBy, setSortBy, moveItem, callAPI, currentView } = useDrive();
1463
+ const { items, setItems, sortBy, setSortBy, moveItem, callAPI, currentView, selectedFileIds, setSelectedFiles } = useDrive();
1534
1464
  const [dragOverFolderId, setDragOverFolderId] = useState(null);
1535
1465
  const [draggingItemId, setDraggingItemId] = useState(null);
1466
+ const moveSelection = useCallback(async (activeId, targetFolderId) => {
1467
+ const ids = selectedFileIds.includes(activeId) && selectedFileIds.length > 1 ? selectedFileIds : [activeId];
1468
+ if (ids.length === 1) return moveItem(ids[0], targetFolderId);
1469
+ setItems((prev) => prev.filter((i) => !ids.includes(i.id)));
1470
+ setSelectedFiles([]);
1471
+ await callAPI("move", { method: "POST", body: JSON.stringify({ ids, targetFolderId }) });
1472
+ }, [selectedFileIds, moveItem, setItems, setSelectedFiles, callAPI]);
1536
1473
  const sensors = useSensors(
1537
1474
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
1538
1475
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
@@ -1567,12 +1504,12 @@ var DriveDndProvider = (props) => {
1567
1504
  if (overId.startsWith("path-")) {
1568
1505
  const pathId = overId.slice(5);
1569
1506
  const targetFolderId = pathId === "root" || pathId === "null" ? "root" : pathId;
1570
- await moveItem(active.id, targetFolderId);
1507
+ await moveSelection(active.id, targetFolderId);
1571
1508
  return;
1572
1509
  }
1573
1510
  const overItem = items.find((i) => i.id === over.id);
1574
1511
  if (overItem?.information.type === "FOLDER") {
1575
- await moveItem(active.id, over.id);
1512
+ await moveSelection(active.id, over.id);
1576
1513
  return;
1577
1514
  }
1578
1515
  const oldIndex = items.findIndex((i) => i.id === active.id);
@@ -1582,7 +1519,7 @@ var DriveDndProvider = (props) => {
1582
1519
  setItems(reordered);
1583
1520
  if (sortBy.field !== "order") setSortBy({ field: "order", order: 1 });
1584
1521
  await callAPI("reorder", { method: "POST", body: JSON.stringify({ ids: reordered.map((i) => i.id) }) });
1585
- }, [items, setItems, sortBy, setSortBy, moveItem, callAPI]);
1522
+ }, [items, setItems, sortBy, setSortBy, moveSelection, callAPI]);
1586
1523
  const enableDnd = currentView === "BROWSE";
1587
1524
  return /* @__PURE__ */ jsx(DriveDndContext.Provider, { value: { dragOverFolderId, draggingItemId }, children: enableDnd ? /* @__PURE__ */ jsx(
1588
1525
  DndContext,
@@ -1948,6 +1885,49 @@ var DriveItemRename = (props) => {
1948
1885
  }
1949
1886
  ) });
1950
1887
  };
1888
+ var useMarquee = (onSelectIds) => {
1889
+ const [box, setBox] = useState(null);
1890
+ const onMouseDown = (e) => {
1891
+ if (e.button !== 0 || e.target.closest("[data-marquee-id]")) return;
1892
+ const container = e.currentTarget;
1893
+ const start = { x: e.clientX, y: e.clientY };
1894
+ let moved = false;
1895
+ const handleMove = (ev) => {
1896
+ if (!moved && Math.abs(ev.clientX - start.x) < 4 && Math.abs(ev.clientY - start.y) < 4) return;
1897
+ moved = true;
1898
+ const left = Math.min(start.x, ev.clientX);
1899
+ const top = Math.min(start.y, ev.clientY);
1900
+ const right = Math.max(start.x, ev.clientX);
1901
+ const bottom = Math.max(start.y, ev.clientY);
1902
+ const cRect = container.getBoundingClientRect();
1903
+ setBox({
1904
+ left: left - cRect.left + container.scrollLeft,
1905
+ top: top - cRect.top + container.scrollTop,
1906
+ width: right - left,
1907
+ height: bottom - top
1908
+ });
1909
+ const ids = [];
1910
+ container.querySelectorAll("[data-marquee-id]").forEach((el) => {
1911
+ const r = el.getBoundingClientRect();
1912
+ if (r.left < right && r.right > left && r.top < bottom && r.bottom > top) ids.push(el.dataset.marqueeId);
1913
+ });
1914
+ onSelectIds(ids);
1915
+ };
1916
+ const handleUp = () => {
1917
+ window.removeEventListener("mousemove", handleMove);
1918
+ window.removeEventListener("mouseup", handleUp);
1919
+ if (!moved) onSelectIds([]);
1920
+ setBox(null);
1921
+ };
1922
+ window.addEventListener("mousemove", handleMove);
1923
+ window.addEventListener("mouseup", handleUp);
1924
+ };
1925
+ const overlay = box ? React3__default.createElement("div", {
1926
+ className: "nd:absolute nd:z-50 nd:rounded-sm nd:border nd:border-primary nd:bg-primary/10 nd:pointer-events-none",
1927
+ style: { left: box.left, top: box.top, width: box.width, height: box.height }
1928
+ }) : null;
1929
+ return { onMouseDown, overlay };
1930
+ };
1951
1931
  var SortableItem = (props) => {
1952
1932
  const { id, children, disabled, isDragOverTarget } = props;
1953
1933
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled });
@@ -1958,7 +1938,7 @@ var SortableItem = (props) => {
1958
1938
  zIndex: isDragging ? 50 : "auto",
1959
1939
  position: "relative"
1960
1940
  };
1961
- return /* @__PURE__ */ jsx("div", { ref: setNodeRef, style, ...attributes, ...listeners, children });
1941
+ return /* @__PURE__ */ jsx("div", { ref: setNodeRef, "data-marquee-id": id, style, ...attributes, ...listeners, children });
1962
1942
  };
1963
1943
  var FileItem = (props) => {
1964
1944
  const { item, isSelected, isDragOver, onSelect, onDoubleClick, onRename, onDelete, onRestore } = props;
@@ -2045,6 +2025,8 @@ var DriveFileGrid = (props) => {
2045
2025
  sortBy,
2046
2026
  selectedFileIds,
2047
2027
  toggleSelected,
2028
+ setSelectedFiles,
2029
+ selectionMode,
2048
2030
  navigateToFolder,
2049
2031
  hasMore,
2050
2032
  loadMore,
@@ -2056,6 +2038,19 @@ var DriveFileGrid = (props) => {
2056
2038
  restoreItem
2057
2039
  } = useDrive();
2058
2040
  const { dragOverFolderId } = useDriveDnd();
2041
+ const { onMouseDown: marqueeMouseDown, overlay: marqueeOverlay } = useMarquee((ids) => {
2042
+ let files = items.filter((i) => ids.includes(i.id)).map((i) => ({
2043
+ id: i.id,
2044
+ file: {
2045
+ name: i.name,
2046
+ mime: i.information.type === "FILE" ? i.information.mime : "",
2047
+ size: i.information.type === "FILE" ? i.information.sizeInBytes : 0
2048
+ }
2049
+ }));
2050
+ if (selectionMode.type === "SINGLE") files = files.slice(0, 1);
2051
+ else if (selectionMode.maxFile) files = files.slice(0, selectionMode.maxFile);
2052
+ setSelectedFiles(files);
2053
+ });
2059
2054
  const [dialogs, setDialogs] = React3__default.useState({ newFolder: false, rename: false, delete: false });
2060
2055
  const [itemToDelete, setItemToDelete] = React3__default.useState(null);
2061
2056
  const [itemToRename, setItemToRename] = React3__default.useState(null);
@@ -2178,7 +2173,8 @@ var DriveFileGrid = (props) => {
2178
2173
  indicatorClassName: "nd:bg-primary"
2179
2174
  }
2180
2175
  ) }),
2181
- /* @__PURE__ */ jsxs("div", { className: cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:p-2 nd:sm:p-3 nd:md:p-4", className), children: [
2176
+ /* @__PURE__ */ jsxs("div", { className: cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:relative nd:p-2 nd:sm:p-3 nd:md:p-4", className), onMouseDown: marqueeMouseDown, children: [
2177
+ marqueeOverlay,
2182
2178
  /* @__PURE__ */ jsx("div", { className: "nd:space-y-4 nd:sm:space-y-6", children: Object.entries(groupedItems).map(([groupName, groupItems]) => /* @__PURE__ */ jsxs("div", { children: [
2183
2179
  groupBy !== "NONE" && /* @__PURE__ */ jsxs("h3", { className: "nd:text-xs nd:font-semibold nd:text-muted-foreground nd:uppercase nd:tracking-wide nd:mb-2 nd:sm:mb-3 nd:px-1", children: [
2184
2180
  groupName,
@@ -3829,7 +3825,7 @@ var SortableItem2 = (props) => {
3829
3825
  zIndex: isDragging ? 50 : "auto",
3830
3826
  position: "relative"
3831
3827
  };
3832
- return /* @__PURE__ */ jsx("div", { ref: setNodeRef, style, ...attributes, ...listeners, children });
3828
+ return /* @__PURE__ */ jsx("div", { ref: setNodeRef, "data-marquee-id": id, style, ...attributes, ...listeners, children });
3833
3829
  };
3834
3830
  var DriveExplorer = (props) => {
3835
3831
  const { onItemClick, onItemDoubleClick, mimeFilter, className, selectableFolders = false } = props;
@@ -3846,6 +3842,8 @@ var DriveExplorer = (props) => {
3846
3842
  setSortBy,
3847
3843
  selectedFileIds,
3848
3844
  toggleSelected,
3845
+ setSelectedFiles,
3846
+ selectionMode,
3849
3847
  navigateToFolder,
3850
3848
  hasMore,
3851
3849
  loadMore,
@@ -3875,6 +3873,13 @@ var DriveExplorer = (props) => {
3875
3873
  const handleDragStart = (event) => {
3876
3874
  setDraggingItemId(event.active.id);
3877
3875
  };
3876
+ const moveSelection = async (activeId, targetFolderId) => {
3877
+ const ids = selectedFileIds.includes(activeId) && selectedFileIds.length > 1 ? selectedFileIds : [activeId];
3878
+ if (ids.length === 1) return moveItem(ids[0], targetFolderId);
3879
+ setItems((prev) => prev.filter((i) => !ids.includes(i.id)));
3880
+ setSelectedFiles([]);
3881
+ await callAPI("move", { method: "POST", body: JSON.stringify({ ids, targetFolderId }) });
3882
+ };
3878
3883
  const handleDragOver = (event) => {
3879
3884
  const { over } = event;
3880
3885
  if (!over) {
@@ -3898,13 +3903,13 @@ var DriveExplorer = (props) => {
3898
3903
  const overId = over.id;
3899
3904
  if (overId.startsWith("path-")) {
3900
3905
  const targetFolderId = overId.replace("path-", "");
3901
- await moveItem(active.id, targetFolderId === "root" ? "root" : targetFolderId);
3906
+ await moveSelection(active.id, targetFolderId === "root" ? "root" : targetFolderId);
3902
3907
  return;
3903
3908
  }
3904
3909
  const overItem = items.find((i) => i.id === over.id);
3905
3910
  const activeItem = items.find((i) => i.id === active.id);
3906
3911
  if (overItem?.information.type === "FOLDER" && activeItem) {
3907
- await moveItem(active.id, over.id);
3912
+ await moveSelection(active.id, over.id);
3908
3913
  return;
3909
3914
  }
3910
3915
  setItems((prevItems) => {
@@ -4008,6 +4013,19 @@ var DriveExplorer = (props) => {
4008
4013
  } else onItemDoubleClick?.(item);
4009
4014
  };
4010
4015
  const enableDrag = currentView === "BROWSE";
4016
+ const { onMouseDown: marqueeMouseDown, overlay: marqueeOverlay } = useMarquee((ids) => {
4017
+ let files = items.filter((i) => ids.includes(i.id)).map((i) => ({
4018
+ id: i.id,
4019
+ file: {
4020
+ name: i.name,
4021
+ mime: i.information.type === "FILE" ? i.information.mime : "",
4022
+ size: i.information.type === "FILE" ? i.information.sizeInBytes : 0
4023
+ }
4024
+ }));
4025
+ if (selectionMode.type === "SINGLE") files = files.slice(0, 1);
4026
+ else if (selectionMode.maxFile) files = files.slice(0, selectionMode.maxFile);
4027
+ setSelectedFiles(files);
4028
+ });
4011
4029
  const stateContent = (() => {
4012
4030
  if (isLoading && items.length === 0) {
4013
4031
  return /* @__PURE__ */ jsx("div", { className: "nd:flex nd:items-center nd:justify-center nd:py-12 nd:flex-1", children: /* @__PURE__ */ jsx(Loader2, { className: "nd:size-6 nd:animate-spin nd:text-muted-foreground" }) });
@@ -4038,7 +4056,8 @@ var DriveExplorer = (props) => {
4038
4056
  ] }),
4039
4057
  /* @__PURE__ */ jsx("div", { className: "nd:lg:hidden nd:px-3 nd:py-2 nd:border-b nd:bg-background/95 nd:dark:bg-background/80 nd:backdrop-blur-sm", children: /* @__PURE__ */ jsx(DrivePathBar, {}) }),
4040
4058
  stateContent || /* @__PURE__ */ jsxs(ContextMenu, { children: [
4041
- /* @__PURE__ */ jsx(ContextMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs("div", { className: cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:container nd:mx-auto nd:p-2 nd:sm:p-3 nd:md:p-4", className), children: [
4059
+ /* @__PURE__ */ jsx(ContextMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs("div", { className: cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:relative nd:container nd:mx-auto nd:p-2 nd:sm:p-3 nd:md:p-4", className), onMouseDown: marqueeMouseDown, children: [
4060
+ marqueeOverlay,
4042
4061
  /* @__PURE__ */ jsx("div", { className: "nd:space-y-4 nd:sm:space-y-6 nd:pb-8 nd:sm:pb-12", children: Object.entries(groupedItems).map(([groupName, groupItems]) => /* @__PURE__ */ jsxs("div", { className: "nd:space-y-3", children: [
4043
4062
  groupBy !== "NONE" && /* @__PURE__ */ jsxs("h3", { className: "nd:text-sm nd:font-medium nd:text-muted-foreground nd:flex nd:items-center nd:gap-2", children: [
4044
4063
  groupName,