@noya-app/noya-file-explorer 0.0.14 → 0.0.16
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/.eslintrc.js +1 -5
- package/.turbo/turbo-build.log +13 -13
- package/CHANGELOG.md +25 -0
- package/dist/index.css +166 -34
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +10577 -4
- package/dist/index.d.ts +10577 -4
- package/dist/index.js +251 -113
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +251 -113
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/MediaCollection.tsx +114 -66
- package/src/utils/files.ts +3 -0
- package/src/utils/handleFileDrop.ts +142 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-file-explorer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"dev": "npm run build:main -- --watch"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@noya-app/noya-designsystem": "0.1.
|
|
24
|
-
"@noya-app/noya-icons": "0.1.
|
|
25
|
-
"@noya-app/noya-multiplayer-react": "0.1.
|
|
26
|
-
"@noya-app/noya-keymap": "0.1.
|
|
27
|
-
"@noya-app/react-utils": "0.1.
|
|
28
|
-
"@noya-app/noya-utils": "0.1.
|
|
29
|
-
"@noya-app/noya-schemas": "0.1.
|
|
23
|
+
"@noya-app/noya-designsystem": "0.1.64",
|
|
24
|
+
"@noya-app/noya-icons": "0.1.12",
|
|
25
|
+
"@noya-app/noya-multiplayer-react": "0.1.63",
|
|
26
|
+
"@noya-app/noya-keymap": "0.1.4",
|
|
27
|
+
"@noya-app/react-utils": "0.1.24",
|
|
28
|
+
"@noya-app/noya-utils": "0.1.6",
|
|
29
|
+
"@noya-app/noya-schemas": "0.1.6",
|
|
30
30
|
"imfs": "^0.1.0",
|
|
31
31
|
"browser-fs-access": "0.35.0"
|
|
32
32
|
},
|
package/src/MediaCollection.tsx
CHANGED
|
@@ -13,8 +13,11 @@ import {
|
|
|
13
13
|
FileExplorerEmptyState,
|
|
14
14
|
FileExplorerLayout,
|
|
15
15
|
FileExplorerUploadButton,
|
|
16
|
+
formatByteSize,
|
|
16
17
|
ListView,
|
|
17
18
|
MediaThumbnail,
|
|
19
|
+
MediaThumbnailProps,
|
|
20
|
+
RelativeDropPosition,
|
|
18
21
|
} from "@noya-app/noya-designsystem";
|
|
19
22
|
import {
|
|
20
23
|
DownloadIcon,
|
|
@@ -56,6 +59,7 @@ import {
|
|
|
56
59
|
createMediaFile,
|
|
57
60
|
createMediaFolder,
|
|
58
61
|
createMediaItemTree,
|
|
62
|
+
MediaItemTree,
|
|
59
63
|
PLACEHOLDER_ITEM_NAME,
|
|
60
64
|
rootMediaItem,
|
|
61
65
|
rootMediaItemName,
|
|
@@ -75,6 +79,7 @@ import {
|
|
|
75
79
|
updateExpandedMap,
|
|
76
80
|
validateMediaItemRename,
|
|
77
81
|
} from "./utils/files";
|
|
82
|
+
import { handleDataTransfer } from "./utils/handleFileDrop";
|
|
78
83
|
|
|
79
84
|
const extensionToContentType = {
|
|
80
85
|
svg: "image/svg+xml",
|
|
@@ -118,8 +123,10 @@ const MediaThumbnailInternal = memoGeneric(
|
|
|
118
123
|
selected,
|
|
119
124
|
size,
|
|
120
125
|
path: pathProp,
|
|
126
|
+
renderThumbnailIcon,
|
|
121
127
|
}: CollectionThumbnailProps<MediaItem> & {
|
|
122
128
|
path?: string;
|
|
129
|
+
renderThumbnailIcon?: MediaThumbnailProps["renderThumbnailIcon"];
|
|
123
130
|
}) => {
|
|
124
131
|
const asset = useAsset(item.kind === "asset" ? item.assetId : undefined);
|
|
125
132
|
const isRoot = item.id === rootMediaItem.id;
|
|
@@ -151,6 +158,7 @@ const MediaThumbnailInternal = memoGeneric(
|
|
|
151
158
|
selected={selected}
|
|
152
159
|
size={size}
|
|
153
160
|
fileName={fileName}
|
|
161
|
+
renderThumbnailIcon={renderThumbnailIcon}
|
|
154
162
|
/>
|
|
155
163
|
);
|
|
156
164
|
}
|
|
@@ -176,6 +184,7 @@ export type MediaCollectionRef = {
|
|
|
176
184
|
moveUpAFolder: (selectedIds: string[]) => void;
|
|
177
185
|
replace: (selectedItem: MediaItem) => void;
|
|
178
186
|
preview: (selectedItems: MediaItem[]) => void;
|
|
187
|
+
getItemAtIndex: (index: number) => MediaItem | undefined;
|
|
179
188
|
};
|
|
180
189
|
|
|
181
190
|
type MediaCollectionProps = {
|
|
@@ -230,6 +239,8 @@ type MediaCollectionProps = {
|
|
|
230
239
|
>;
|
|
231
240
|
/** @default false */
|
|
232
241
|
sortable?: boolean;
|
|
242
|
+
renderThumbnailIcon?: MediaThumbnailProps["renderThumbnailIcon"];
|
|
243
|
+
onDidDeleteItems?: (items: [string, MediaItem][]) => void;
|
|
233
244
|
} & Pick<
|
|
234
245
|
CollectionProps<MediaItem, MenuAction>,
|
|
235
246
|
| "sortableId"
|
|
@@ -239,6 +250,7 @@ type MediaCollectionProps = {
|
|
|
239
250
|
| "scrollable"
|
|
240
251
|
| "renderEmptyState"
|
|
241
252
|
| "sharedDragProps"
|
|
253
|
+
| "onClickItem"
|
|
242
254
|
>;
|
|
243
255
|
|
|
244
256
|
export const MediaCollection = memo(
|
|
@@ -269,6 +281,9 @@ export const MediaCollection = memo(
|
|
|
269
281
|
sortable = false,
|
|
270
282
|
renderEmptyState,
|
|
271
283
|
sharedDragProps,
|
|
284
|
+
onClickItem,
|
|
285
|
+
renderThumbnailIcon,
|
|
286
|
+
onDidDeleteItems,
|
|
272
287
|
},
|
|
273
288
|
ref
|
|
274
289
|
) {
|
|
@@ -371,8 +386,7 @@ export const MediaCollection = memo(
|
|
|
371
386
|
}
|
|
372
387
|
}, [initialExpanded]);
|
|
373
388
|
|
|
374
|
-
|
|
375
|
-
const handleExpanded = useCallback(
|
|
389
|
+
const getExpanded = useCallback(
|
|
376
390
|
(item: MediaItem) => {
|
|
377
391
|
if (!expandable) return undefined;
|
|
378
392
|
if (item.kind !== "folder") return undefined;
|
|
@@ -384,15 +398,27 @@ export const MediaCollection = memo(
|
|
|
384
398
|
|
|
385
399
|
const handleDelete = useCallback(
|
|
386
400
|
(selectedIds: string[]) => {
|
|
401
|
+
const deletedItems = Object.entries(media).flatMap(
|
|
402
|
+
([path, item]): [string, MediaItem][] => {
|
|
403
|
+
if (selectedIds.includes(item.id)) {
|
|
404
|
+
return [[path, item]];
|
|
405
|
+
}
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
|
|
387
410
|
const newMedia = deleteMediaItems({
|
|
388
411
|
selectedIds,
|
|
389
412
|
media,
|
|
390
413
|
tree,
|
|
391
414
|
});
|
|
415
|
+
|
|
392
416
|
setSelectedIds([rootMediaItem.id]);
|
|
393
417
|
setMedia({ name: "Delete items", timestamp: Date.now() }, newMedia);
|
|
418
|
+
|
|
419
|
+
onDidDeleteItems?.(deletedItems);
|
|
394
420
|
},
|
|
395
|
-
[media, setMedia, setSelectedIds, tree]
|
|
421
|
+
[media, setMedia, setSelectedIds, tree, onDidDeleteItems]
|
|
396
422
|
);
|
|
397
423
|
|
|
398
424
|
const onRename = useCallback(
|
|
@@ -477,10 +503,13 @@ export const MediaCollection = memo(
|
|
|
477
503
|
[media, tree, setMedia]
|
|
478
504
|
);
|
|
479
505
|
|
|
506
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
507
|
+
|
|
480
508
|
const handleUpload = useCallback(
|
|
481
509
|
async (selectedId: string) => {
|
|
482
510
|
try {
|
|
483
511
|
const files = await fileOpen({ multiple: true });
|
|
512
|
+
|
|
484
513
|
if (!files || !Array.isArray(files) || files.length === 0) return;
|
|
485
514
|
|
|
486
515
|
const parentPath = tree.idToPathMap.get(selectedId);
|
|
@@ -496,6 +525,8 @@ export const MediaCollection = memo(
|
|
|
496
525
|
};
|
|
497
526
|
});
|
|
498
527
|
|
|
528
|
+
setIsUploading(true);
|
|
529
|
+
|
|
499
530
|
const newMediaMap = await Promise.all(uploadPromises);
|
|
500
531
|
|
|
501
532
|
setMedia(
|
|
@@ -509,6 +540,8 @@ export const MediaCollection = memo(
|
|
|
509
540
|
);
|
|
510
541
|
} catch (error) {
|
|
511
542
|
console.error("Failed to upload files:", error);
|
|
543
|
+
} finally {
|
|
544
|
+
setIsUploading(false);
|
|
512
545
|
}
|
|
513
546
|
},
|
|
514
547
|
[tree.idToPathMap, setMedia, media, assetManager]
|
|
@@ -580,15 +613,13 @@ export const MediaCollection = memo(
|
|
|
580
613
|
|
|
581
614
|
const handleMoveMediaInsideFolder = useCallback(
|
|
582
615
|
(sourceItem: MediaItem, targetItem: MediaItem) => {
|
|
583
|
-
const sourceItemPath = tree.idToPathMap.get(sourceItem.id);
|
|
584
|
-
const targetItemPath = tree.idToPathMap.get(targetItem.id);
|
|
585
|
-
if (!sourceItemPath || !targetItemPath) return;
|
|
586
616
|
const newMedia = moveMediaInsideFolder({
|
|
587
617
|
sourceItemIds: [sourceItem.id],
|
|
588
618
|
targetItemId: targetItem.id,
|
|
589
619
|
media,
|
|
590
620
|
tree,
|
|
591
621
|
});
|
|
622
|
+
|
|
592
623
|
setMedia(
|
|
593
624
|
{
|
|
594
625
|
name: "Move media file inside folder",
|
|
@@ -622,12 +653,12 @@ export const MediaCollection = memo(
|
|
|
622
653
|
],
|
|
623
654
|
[
|
|
624
655
|
onlySingleFolderSelected && {
|
|
625
|
-
title: "
|
|
656
|
+
title: "Upload Files",
|
|
626
657
|
value: "upload",
|
|
627
658
|
icon: <UploadIcon />,
|
|
628
659
|
},
|
|
629
660
|
onlySingleFolderSelected && {
|
|
630
|
-
title: "Add
|
|
661
|
+
title: "Add Folder",
|
|
631
662
|
value: "addFolder",
|
|
632
663
|
icon: <FolderIcon />,
|
|
633
664
|
},
|
|
@@ -760,6 +791,7 @@ export const MediaCollection = memo(
|
|
|
760
791
|
replace: handleReplace,
|
|
761
792
|
preview: handlePreview,
|
|
762
793
|
moveMediaInsideFolder: handleMoveMediaInsideFolder,
|
|
794
|
+
getItemAtIndex: (index) => visibleItems[index],
|
|
763
795
|
}));
|
|
764
796
|
|
|
765
797
|
return (
|
|
@@ -771,6 +803,7 @@ export const MediaCollection = memo(
|
|
|
771
803
|
<FileExplorerUploadButton
|
|
772
804
|
showUploadButton={showUploadButton}
|
|
773
805
|
onUpload={() => handleUpload(rootMediaItem.id)}
|
|
806
|
+
isUploading={isUploading}
|
|
774
807
|
>
|
|
775
808
|
{right}
|
|
776
809
|
</FileExplorerUploadButton>
|
|
@@ -801,10 +834,11 @@ export const MediaCollection = memo(
|
|
|
801
834
|
return "Enter folder name";
|
|
802
835
|
case "asset":
|
|
803
836
|
case "file":
|
|
837
|
+
case "noyaFile":
|
|
804
838
|
return "Enter file name";
|
|
805
839
|
}
|
|
806
840
|
}}
|
|
807
|
-
getExpanded={
|
|
841
|
+
getExpanded={getExpanded}
|
|
808
842
|
setExpanded={handleSetExpanded}
|
|
809
843
|
getRenamable={(item) => {
|
|
810
844
|
if (item.id === rootMediaItem.id) return false;
|
|
@@ -814,6 +848,7 @@ export const MediaCollection = memo(
|
|
|
814
848
|
menuItems={assetContextMenuItems}
|
|
815
849
|
onSelectMenuItem={handleMenuAction}
|
|
816
850
|
onSelectionChange={setSelectedIds}
|
|
851
|
+
onClickItem={onClickItem}
|
|
817
852
|
onDoubleClickItem={onDoubleClickItem}
|
|
818
853
|
onRename={onRename}
|
|
819
854
|
renamable={renamable}
|
|
@@ -822,6 +857,7 @@ export const MediaCollection = memo(
|
|
|
822
857
|
<MediaThumbnailInternal
|
|
823
858
|
{...props}
|
|
824
859
|
path={tree.idToPathMap.get(props.item.id)}
|
|
860
|
+
renderThumbnailIcon={renderThumbnailIcon}
|
|
825
861
|
/>
|
|
826
862
|
)}
|
|
827
863
|
renderAction={renderAction}
|
|
@@ -831,7 +867,7 @@ export const MediaCollection = memo(
|
|
|
831
867
|
if (!asset) return null;
|
|
832
868
|
return (
|
|
833
869
|
<FileExplorerDetail selected={selected} size={size}>
|
|
834
|
-
{(asset.size
|
|
870
|
+
{formatByteSize(asset.size)}
|
|
835
871
|
</FileExplorerDetail>
|
|
836
872
|
);
|
|
837
873
|
}}
|
|
@@ -839,7 +875,7 @@ export const MediaCollection = memo(
|
|
|
839
875
|
renderEmptyState?.() ?? <FileExplorerEmptyState />
|
|
840
876
|
}
|
|
841
877
|
itemRoleDescription="clickable file item"
|
|
842
|
-
getDropTargetParentIndex={(overIndex
|
|
878
|
+
getDropTargetParentIndex={(overIndex) => {
|
|
843
879
|
const item = visibleItems[overIndex];
|
|
844
880
|
const parentIndex = visibleItems.findIndex(
|
|
845
881
|
(i) => i.id === tree.getParentIdForId(item.id)
|
|
@@ -857,32 +893,31 @@ export const MediaCollection = memo(
|
|
|
857
893
|
return false;
|
|
858
894
|
}
|
|
859
895
|
|
|
860
|
-
|
|
861
|
-
const targetItem = visibleItems[targetIndex];
|
|
862
|
-
|
|
863
|
-
if (position !== "inside" || targetItem.kind === "asset") {
|
|
896
|
+
if (sourceListId !== sortableId || targetListId !== sortableId) {
|
|
864
897
|
return false;
|
|
865
898
|
}
|
|
866
|
-
const sourcePath = tree.findPath(
|
|
867
|
-
rootMediaItem,
|
|
868
|
-
(item) => item.id === sourceItem.id
|
|
869
|
-
);
|
|
870
|
-
const targetPath = tree.findPath(
|
|
871
|
-
rootMediaItem,
|
|
872
|
-
(item) => item.id === targetItem.id
|
|
873
|
-
);
|
|
874
899
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (
|
|
878
|
-
isDeepEqual(sourcePath, targetPath.slice(0, sourcePath.length))
|
|
879
|
-
) {
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
900
|
+
const sourceItem = visibleItems[sourceIndex];
|
|
901
|
+
const targetItem = visibleItems[targetIndex];
|
|
882
902
|
|
|
883
|
-
return
|
|
903
|
+
return acceptsMediaItemDrop({
|
|
904
|
+
position,
|
|
905
|
+
sourceItem,
|
|
906
|
+
targetItem,
|
|
907
|
+
tree,
|
|
908
|
+
});
|
|
884
909
|
}}
|
|
885
|
-
onMoveItem={({
|
|
910
|
+
onMoveItem={({
|
|
911
|
+
sourceListId,
|
|
912
|
+
sourceIndex,
|
|
913
|
+
targetListId,
|
|
914
|
+
targetIndex,
|
|
915
|
+
position,
|
|
916
|
+
}) => {
|
|
917
|
+
if (sourceListId !== sortableId || targetListId !== sortableId) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
886
921
|
const sourceItem = visibleItems[sourceIndex];
|
|
887
922
|
const targetItem = visibleItems[targetIndex];
|
|
888
923
|
if (position === "inside") {
|
|
@@ -892,40 +927,23 @@ export const MediaCollection = memo(
|
|
|
892
927
|
onFilesDrop={async (event: React.DragEvent<Element>) => {
|
|
893
928
|
event.preventDefault();
|
|
894
929
|
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
if (!rootItemPath) return;
|
|
913
|
-
// Add all the media file references to our state
|
|
914
|
-
setMedia(
|
|
915
|
-
{ name: "Add media files", timestamp: Date.now() },
|
|
916
|
-
{
|
|
917
|
-
...media,
|
|
918
|
-
...Object.fromEntries(
|
|
919
|
-
newMediaItems.map((item) => [
|
|
920
|
-
path.join(rootItemPath, item.name),
|
|
921
|
-
item.asset,
|
|
922
|
-
])
|
|
923
|
-
),
|
|
924
|
-
}
|
|
925
|
-
);
|
|
926
|
-
} catch (error) {
|
|
927
|
-
console.error("Failed to upload dropped files:", error);
|
|
928
|
-
}
|
|
930
|
+
const rootItemPath = tree.idToPathMap.get(rootItemId);
|
|
931
|
+
|
|
932
|
+
if (!rootItemPath) return;
|
|
933
|
+
|
|
934
|
+
const newMedia = await handleDataTransfer({
|
|
935
|
+
dataTransfer: event.dataTransfer,
|
|
936
|
+
rootItemPath,
|
|
937
|
+
media,
|
|
938
|
+
uploadAsset: (file) => assetManager.create(file),
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
if (!newMedia) return;
|
|
942
|
+
|
|
943
|
+
setMedia(
|
|
944
|
+
{ name: "Add media files", timestamp: Date.now() },
|
|
945
|
+
newMedia
|
|
946
|
+
);
|
|
929
947
|
}}
|
|
930
948
|
/>
|
|
931
949
|
</FileExplorerLayout>
|
|
@@ -933,3 +951,33 @@ export const MediaCollection = memo(
|
|
|
933
951
|
);
|
|
934
952
|
})
|
|
935
953
|
);
|
|
954
|
+
|
|
955
|
+
export function acceptsMediaItemDrop(parameters: {
|
|
956
|
+
position: RelativeDropPosition;
|
|
957
|
+
sourceItem: MediaItem;
|
|
958
|
+
targetItem: MediaItem;
|
|
959
|
+
tree: MediaItemTree;
|
|
960
|
+
}) {
|
|
961
|
+
const { position, sourceItem, targetItem, tree } = parameters;
|
|
962
|
+
|
|
963
|
+
if (position !== "inside" || targetItem.kind === "asset") {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
const sourcePath = tree.findPath(
|
|
967
|
+
rootMediaItem,
|
|
968
|
+
(item) => item.id === sourceItem.id
|
|
969
|
+
);
|
|
970
|
+
const targetPath = tree.findPath(
|
|
971
|
+
rootMediaItem,
|
|
972
|
+
(item) => item.id === targetItem.id
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
// Don't allow dragging into a descendant
|
|
976
|
+
if (!sourcePath || !targetPath) return false;
|
|
977
|
+
|
|
978
|
+
if (isDeepEqual(sourcePath, targetPath.slice(0, sourcePath.length))) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return true;
|
|
983
|
+
}
|
package/src/utils/files.ts
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Asset, MediaItem, MediaMap } from "@noya-app/noya-schemas";
|
|
2
|
+
import { path } from "imfs";
|
|
3
|
+
import { createMediaAsset, createMediaFolder } from "./mediaItemTree";
|
|
4
|
+
|
|
5
|
+
function isDirectoryEntry(
|
|
6
|
+
entry: FileSystemEntry
|
|
7
|
+
): entry is FileSystemDirectoryEntry {
|
|
8
|
+
return entry.isDirectory;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isFileEntry(entry: FileSystemEntry): entry is FileSystemFileEntry {
|
|
12
|
+
return entry.isFile;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleDataTransfer({
|
|
16
|
+
dataTransfer,
|
|
17
|
+
rootItemPath,
|
|
18
|
+
media,
|
|
19
|
+
uploadAsset,
|
|
20
|
+
}: {
|
|
21
|
+
dataTransfer: DataTransfer;
|
|
22
|
+
rootItemPath: string;
|
|
23
|
+
media: MediaMap;
|
|
24
|
+
uploadAsset: (file: File) => Promise<Asset>;
|
|
25
|
+
}): Promise<MediaMap | undefined> {
|
|
26
|
+
try {
|
|
27
|
+
const dataTransferItems = Array.from(dataTransfer.items ?? []);
|
|
28
|
+
|
|
29
|
+
const supportsEntries = dataTransferItems.some(
|
|
30
|
+
(item) => typeof item?.webkitGetAsEntry === "function"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
type DroppedFile = { file: File; relativePath: string };
|
|
34
|
+
|
|
35
|
+
const collectFromEntry = async (
|
|
36
|
+
entry: FileSystemEntry,
|
|
37
|
+
basePath: string
|
|
38
|
+
): Promise<DroppedFile[]> => {
|
|
39
|
+
if (!entry) return [];
|
|
40
|
+
if (isFileEntry(entry)) {
|
|
41
|
+
const file: File = await new Promise((resolve) =>
|
|
42
|
+
(entry as FileSystemFileEntry).file((f: File) => resolve(f))
|
|
43
|
+
);
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
file,
|
|
47
|
+
relativePath: path.join(basePath, file.name),
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
if (isDirectoryEntry(entry)) {
|
|
52
|
+
const reader = entry.createReader();
|
|
53
|
+
const readAll = async (): Promise<FileSystemEntry[]> => {
|
|
54
|
+
const all: FileSystemEntry[] = [];
|
|
55
|
+
// readEntries may return partial results; loop until empty
|
|
56
|
+
// eslint-disable-next-line no-constant-condition
|
|
57
|
+
while (true) {
|
|
58
|
+
const entries: FileSystemEntry[] = await new Promise((resolve) =>
|
|
59
|
+
reader.readEntries((ents: FileSystemEntry[]) => resolve(ents))
|
|
60
|
+
);
|
|
61
|
+
if (!entries.length) break;
|
|
62
|
+
all.push(...entries);
|
|
63
|
+
}
|
|
64
|
+
return all;
|
|
65
|
+
};
|
|
66
|
+
const children = await readAll();
|
|
67
|
+
const results = await Promise.all(
|
|
68
|
+
children.map((child) =>
|
|
69
|
+
collectFromEntry(child, path.join(basePath, entry.name))
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
return results.flat();
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let dropped: DroppedFile[] = [];
|
|
78
|
+
|
|
79
|
+
if (supportsEntries) {
|
|
80
|
+
const topLevelEntries = dataTransferItems.flatMap((item) => {
|
|
81
|
+
const entry = item.webkitGetAsEntry?.();
|
|
82
|
+
if (!entry) return [];
|
|
83
|
+
return [entry];
|
|
84
|
+
});
|
|
85
|
+
const nested = await Promise.all(
|
|
86
|
+
topLevelEntries.map((entry) => collectFromEntry(entry, ""))
|
|
87
|
+
);
|
|
88
|
+
dropped = nested.flat();
|
|
89
|
+
} else {
|
|
90
|
+
const files = Array.from(dataTransfer.files);
|
|
91
|
+
if (files.length === 0) return;
|
|
92
|
+
dropped = files.map((file) => ({
|
|
93
|
+
file,
|
|
94
|
+
// Best effort: try webkitRelativePath; fall back to name
|
|
95
|
+
relativePath:
|
|
96
|
+
file.webkitRelativePath && file.webkitRelativePath.length > 0
|
|
97
|
+
? file.webkitRelativePath
|
|
98
|
+
: file.name,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (dropped.length === 0) return;
|
|
103
|
+
|
|
104
|
+
const folderRelativePaths = new Set<string>();
|
|
105
|
+
for (const { relativePath } of dropped) {
|
|
106
|
+
const dir = path.dirname(relativePath);
|
|
107
|
+
if (dir && dir !== ".") {
|
|
108
|
+
const parts = dir.split("/").filter(Boolean);
|
|
109
|
+
let acc = "";
|
|
110
|
+
for (const part of parts) {
|
|
111
|
+
acc = acc ? path.join(acc, part) : part;
|
|
112
|
+
folderRelativePaths.add(acc);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const folderEntries: [string, MediaItem][] = Array.from(folderRelativePaths)
|
|
118
|
+
.map((rel) => path.join(rootItemPath, rel))
|
|
119
|
+
.filter((full) => !media[full])
|
|
120
|
+
.map((full) => [full, createMediaFolder() as MediaItem]);
|
|
121
|
+
|
|
122
|
+
const uploadResults = await Promise.all(
|
|
123
|
+
dropped.map(
|
|
124
|
+
async ({ file, relativePath }): Promise<[string, MediaItem]> => {
|
|
125
|
+
const asset = await uploadAsset(file);
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
path.join(rootItemPath, relativePath),
|
|
129
|
+
createMediaAsset({ assetId: asset.id }) as MediaItem,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...media,
|
|
137
|
+
...Object.fromEntries([...folderEntries, ...uploadResults]),
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("Failed to upload dropped files:", error);
|
|
141
|
+
}
|
|
142
|
+
}
|