@lvce-editor/explorer-view 6.1.0 → 6.3.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/explorerViewWorkerMain.js +946 -844
- package/package.json +1 -1
|
@@ -941,6 +941,7 @@ const OpenFolder$1 = 'Open folder';
|
|
|
941
941
|
const OpenInIntegratedTerminal = 'Open in integrated Terminal';
|
|
942
942
|
const Paste = 'Paste';
|
|
943
943
|
const RefreshExplorer = 'Refresh Explorer';
|
|
944
|
+
const RemoveFolderFromWorkspace = 'Remove folder from workspace';
|
|
944
945
|
const Rename = 'Rename';
|
|
945
946
|
const TheNameIsNotValid = 'The name **{0}** is not valid as a file or folder name. Please choose a different name.';
|
|
946
947
|
const TypeAFileName = 'Type file name. Press Enter to confirm or Escape to cancel.'; // TODO use keybinding
|
|
@@ -982,6 +983,9 @@ const deleteItem = () => {
|
|
|
982
983
|
const refresh$1 = () => {
|
|
983
984
|
return i18nString(RefreshExplorer);
|
|
984
985
|
};
|
|
986
|
+
const removeFolderFromWorkspace = () => {
|
|
987
|
+
return i18nString(RemoveFolderFromWorkspace);
|
|
988
|
+
};
|
|
985
989
|
const collapseAll$1 = () => {
|
|
986
990
|
return i18nString(CollapseAllFoldersInExplorer);
|
|
987
991
|
};
|
|
@@ -1325,14 +1329,9 @@ const getFocusedDirent$1 = state => {
|
|
|
1325
1329
|
|
|
1326
1330
|
const copyPath = async state => {
|
|
1327
1331
|
const dirent = getFocusedDirent$1(state);
|
|
1328
|
-
if (!dirent) {
|
|
1329
|
-
return state;
|
|
1330
|
-
}
|
|
1331
1332
|
// TODO windows paths
|
|
1332
1333
|
// TODO handle error
|
|
1333
|
-
const
|
|
1334
|
-
path
|
|
1335
|
-
} = dirent;
|
|
1334
|
+
const path = dirent ? dirent.path : state.root;
|
|
1336
1335
|
await writeClipBoardText(path);
|
|
1337
1336
|
return state;
|
|
1338
1337
|
};
|
|
@@ -2556,6 +2555,12 @@ const menuEntryDelete = {
|
|
|
2556
2555
|
id: 'delete',
|
|
2557
2556
|
label: deleteItem()
|
|
2558
2557
|
};
|
|
2558
|
+
const menuEntryRemoveFolderFromWorkspace = {
|
|
2559
|
+
command: 'Workspace.close',
|
|
2560
|
+
flags: None$2,
|
|
2561
|
+
id: 'removeFolderFromWorkspace',
|
|
2562
|
+
label: removeFolderFromWorkspace()
|
|
2563
|
+
};
|
|
2559
2564
|
const ALL_ENTRIES = [menuEntryNewFile, menuEntryNewFolder, menuEntryOpenContainingFolder, menuEntryOpenInIntegratedTerminal, menuEntrySeparator, menuEntryCut, menuEntryCopy, menuEntryPaste, menuEntrySeparator, menuEntryCopyPath, menuEntryCopyRelativePath, menuEntrySeparator, menuEntryRename, menuEntryDelete];
|
|
2560
2565
|
|
|
2561
2566
|
// TODO there are two possible ways of getting the focused dirent of explorer
|
|
@@ -2576,13 +2581,17 @@ const getMenuEntriesFile = () => {
|
|
|
2576
2581
|
const getMenuEntriesDefault = () => {
|
|
2577
2582
|
return ALL_ENTRIES;
|
|
2578
2583
|
};
|
|
2579
|
-
const getMenuEntriesRoot =
|
|
2580
|
-
|
|
2584
|
+
const getMenuEntriesRoot = root => {
|
|
2585
|
+
const entries = [menuEntryNewFile, menuEntryNewFolder, menuEntryOpenContainingFolder, menuEntryOpenInIntegratedTerminal, menuEntrySeparator, menuEntryPaste, menuEntrySeparator, menuEntryCopyPath, menuEntryCopyRelativePath];
|
|
2586
|
+
if (root) {
|
|
2587
|
+
entries.push(menuEntrySeparator, menuEntryRemoveFolderFromWorkspace);
|
|
2588
|
+
}
|
|
2589
|
+
return entries;
|
|
2581
2590
|
};
|
|
2582
2591
|
const getMenuEntries = state => {
|
|
2583
2592
|
const focusedDirent = getFocusedDirent(state);
|
|
2584
2593
|
if (!focusedDirent) {
|
|
2585
|
-
return getMenuEntriesRoot();
|
|
2594
|
+
return getMenuEntriesRoot(state.root);
|
|
2586
2595
|
}
|
|
2587
2596
|
switch (focusedDirent.type) {
|
|
2588
2597
|
case Directory:
|
|
@@ -3500,1023 +3509,1109 @@ const handleDragStart = state => {
|
|
|
3500
3509
|
return state;
|
|
3501
3510
|
};
|
|
3502
3511
|
|
|
3503
|
-
const getChildHandles = async fileHandle => {
|
|
3504
|
-
// @ts-ignore
|
|
3505
|
-
const values = fileHandle.values();
|
|
3506
|
-
const children = await fromAsync(values);
|
|
3507
|
-
return children;
|
|
3508
|
-
};
|
|
3509
|
-
|
|
3510
|
-
const getFileHandleText = async fileHandle => {
|
|
3511
|
-
const file = await fileHandle.getFile();
|
|
3512
|
-
const text = await file.text();
|
|
3513
|
-
return text;
|
|
3514
|
-
};
|
|
3515
|
-
|
|
3516
3512
|
const isDirectoryHandle = fileHandle => {
|
|
3517
3513
|
return fileHandle.kind === 'directory';
|
|
3518
3514
|
};
|
|
3519
3515
|
|
|
3520
|
-
const
|
|
3521
|
-
return
|
|
3516
|
+
const isValid = decoration => {
|
|
3517
|
+
return decoration && typeof decoration.decoration === 'string' && typeof decoration.uri === 'string';
|
|
3522
3518
|
};
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
const normalized = fileHandles.filter(Boolean);
|
|
3527
|
-
for (const fileHandle of normalized) {
|
|
3528
|
-
const {
|
|
3529
|
-
name
|
|
3530
|
-
} = fileHandle;
|
|
3531
|
-
if (isDirectoryHandle(fileHandle)) {
|
|
3532
|
-
const children = await getChildHandles(fileHandle);
|
|
3533
|
-
const childTree = await createUploadTree(name, children);
|
|
3534
|
-
uploadTree[name] = childTree;
|
|
3535
|
-
} else if (isFileHandle(fileHandle)) {
|
|
3536
|
-
// TODO maybe save blob and use filesystem.writeblob
|
|
3537
|
-
const text = await getFileHandleText(fileHandle);
|
|
3538
|
-
uploadTree[name] = text;
|
|
3539
|
-
}
|
|
3519
|
+
const normalizeDecorations = decorations => {
|
|
3520
|
+
if (!decorations || !Array.isArray(decorations)) {
|
|
3521
|
+
return [];
|
|
3540
3522
|
}
|
|
3541
|
-
return
|
|
3523
|
+
return decorations.filter(isValid);
|
|
3542
3524
|
};
|
|
3543
3525
|
|
|
3544
|
-
const
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
const fullPath = currentPath ? join2(currentPath, path) : path;
|
|
3549
|
-
if (typeof value === 'object') {
|
|
3550
|
-
operations.push({
|
|
3551
|
-
path: join2(root, fullPath),
|
|
3552
|
-
type: CreateFolder$1
|
|
3553
|
-
});
|
|
3554
|
-
processTree(value, fullPath);
|
|
3555
|
-
} else if (typeof value === 'string') {
|
|
3556
|
-
operations.push({
|
|
3557
|
-
path: join2(root, fullPath),
|
|
3558
|
-
text: value,
|
|
3559
|
-
type: CreateFile$1
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3526
|
+
const getFileDecorations = async (scheme, root, maybeUris, decorationsEnabled, assetDir, platform) => {
|
|
3527
|
+
try {
|
|
3528
|
+
if (!decorationsEnabled) {
|
|
3529
|
+
return [];
|
|
3562
3530
|
}
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3531
|
+
const providerIds = await invoke$1('SourceControl.getEnabledProviderIds', scheme, root, assetDir, platform);
|
|
3532
|
+
if (providerIds.length === 0) {
|
|
3533
|
+
return [];
|
|
3534
|
+
}
|
|
3535
|
+
// TODO how to handle multiple providers?
|
|
3536
|
+
const providerId = providerIds.at(-1);
|
|
3537
|
+
const uris = ensureUris(maybeUris);
|
|
3538
|
+
const decorations = await invoke$1('SourceControl.getFileDecorations', providerId, uris, assetDir, platform);
|
|
3539
|
+
const normalized = normalizeDecorations(decorations);
|
|
3540
|
+
return normalized;
|
|
3541
|
+
} catch (error) {
|
|
3542
|
+
console.error(error);
|
|
3543
|
+
return [];
|
|
3544
|
+
}
|
|
3566
3545
|
};
|
|
3567
3546
|
|
|
3568
|
-
const
|
|
3569
|
-
|
|
3570
|
-
|
|
3547
|
+
const RE_PROTOCOL = /^[a-z+]:\/\//;
|
|
3548
|
+
const getScheme = uri => {
|
|
3549
|
+
const match = uri.match(RE_PROTOCOL);
|
|
3550
|
+
if (!match) {
|
|
3551
|
+
return '';
|
|
3571
3552
|
}
|
|
3572
|
-
|
|
3573
|
-
const fileOperations = getFileOperations(root, uploadTree);
|
|
3574
|
-
await applyFileOperations(fileOperations);
|
|
3575
|
-
|
|
3576
|
-
// TODO
|
|
3577
|
-
// 1. in electron, use webutils.getPathForFile to see if a path is available
|
|
3578
|
-
// 2. else, walk all files and folders recursively and upload all of them (if there are many, show a progress bar)
|
|
3579
|
-
|
|
3580
|
-
// TODO send file system operations to renderer worker
|
|
3581
|
-
return true;
|
|
3553
|
+
return match[0];
|
|
3582
3554
|
};
|
|
3583
3555
|
|
|
3584
|
-
const
|
|
3585
|
-
return
|
|
3586
|
-
|
|
3587
|
-
const
|
|
3588
|
-
const
|
|
3589
|
-
const
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
const
|
|
3593
|
-
const
|
|
3594
|
-
|
|
3595
|
-
pathSeparator,
|
|
3596
|
-
root
|
|
3597
|
-
} = state;
|
|
3598
|
-
const handled = await uploadFileSystemHandles(root, pathSeparator, fileHandles);
|
|
3599
|
-
if (handled) {
|
|
3600
|
-
const updated = await refresh(state);
|
|
3601
|
-
return {
|
|
3602
|
-
...updated,
|
|
3603
|
-
dropTargets: []
|
|
3604
|
-
};
|
|
3605
|
-
}
|
|
3606
|
-
const mergedDirents = await getMergedDirents$2(root, pathSeparator, items);
|
|
3556
|
+
const getSettings = async () => {
|
|
3557
|
+
// TODO don't return false always
|
|
3558
|
+
// TODO get all settings at once
|
|
3559
|
+
const useChevronsRaw = await invoke$2('Preferences.get', 'explorer.useChevrons');
|
|
3560
|
+
const useChevrons = useChevronsRaw === false ? false : true;
|
|
3561
|
+
const confirmDeleteRaw = await invoke$2('Preferences.get', 'explorer.confirmdelete');
|
|
3562
|
+
const confirmDelete = confirmDeleteRaw === false ? false : false;
|
|
3563
|
+
const confirmPasteRaw = await invoke$2('Preferences.get', 'explorer.confirmpaste');
|
|
3564
|
+
const confirmPaste = confirmPasteRaw === false ? false : false;
|
|
3565
|
+
const sourceControlDecorationsRaw = await invoke$2('Preferences.get', 'explorer.sourceControlDecorations');
|
|
3566
|
+
const sourceControlDecorations = sourceControlDecorationsRaw === false ? false : true;
|
|
3607
3567
|
return {
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3568
|
+
confirmDelete,
|
|
3569
|
+
confirmPaste,
|
|
3570
|
+
sourceControlDecorations,
|
|
3571
|
+
useChevrons
|
|
3611
3572
|
};
|
|
3612
3573
|
};
|
|
3613
3574
|
|
|
3614
|
-
const
|
|
3615
|
-
|
|
3616
|
-
for (let i = 0; i < paths.length; i++) {
|
|
3617
|
-
const fileHandle = fileHandles[i];
|
|
3618
|
-
const {
|
|
3619
|
-
name
|
|
3620
|
-
} = fileHandle;
|
|
3621
|
-
const path = paths[i];
|
|
3622
|
-
operations.push({
|
|
3623
|
-
from: path,
|
|
3624
|
-
path: join(pathSeparator, root, name),
|
|
3625
|
-
type: Copy$1
|
|
3626
|
-
});
|
|
3627
|
-
}
|
|
3628
|
-
return operations;
|
|
3629
|
-
};
|
|
3630
|
-
|
|
3631
|
-
// TODO copy files in parallel
|
|
3632
|
-
const copyFilesElectron = async (root, fileHandles, files, paths) => {
|
|
3633
|
-
const pathSeparator = await getPathSeparator$1(root);
|
|
3634
|
-
const operations = await getFileOperationsElectron(root, paths, fileHandles, pathSeparator);
|
|
3635
|
-
await applyFileOperations(operations);
|
|
3575
|
+
const getWorkspacePath = () => {
|
|
3576
|
+
return invoke$2('Workspace.getPath');
|
|
3636
3577
|
};
|
|
3637
3578
|
|
|
3638
|
-
const
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
const
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
const
|
|
3647
|
-
const {
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
const
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3579
|
+
const getSavedChildDirents = (map, path, depth, excluded, pathSeparator) => {
|
|
3580
|
+
let children = map[path];
|
|
3581
|
+
if (!children) {
|
|
3582
|
+
return [];
|
|
3583
|
+
}
|
|
3584
|
+
const dirents = [];
|
|
3585
|
+
children = sortExplorerItems(children);
|
|
3586
|
+
const visible = [];
|
|
3587
|
+
const displayRoot = path.endsWith(pathSeparator) ? path : path + pathSeparator;
|
|
3588
|
+
for (const child of children) {
|
|
3589
|
+
if (excluded.includes(child.name)) {
|
|
3590
|
+
continue;
|
|
3591
|
+
}
|
|
3592
|
+
visible.push(child);
|
|
3593
|
+
}
|
|
3594
|
+
const visibleLength = visible.length;
|
|
3595
|
+
for (let i = 0; i < visibleLength; i++) {
|
|
3596
|
+
const child = visible[i];
|
|
3597
|
+
const {
|
|
3598
|
+
name,
|
|
3599
|
+
type
|
|
3600
|
+
} = child;
|
|
3601
|
+
const childPath = displayRoot + name;
|
|
3602
|
+
if ((child.type === Directory || child.type === SymLinkFolder) && childPath in map) {
|
|
3603
|
+
dirents.push({
|
|
3604
|
+
depth,
|
|
3605
|
+
icon: '',
|
|
3606
|
+
name,
|
|
3607
|
+
path: childPath,
|
|
3608
|
+
posInSet: i + 1,
|
|
3609
|
+
setSize: visibleLength,
|
|
3610
|
+
type: DirectoryExpanded
|
|
3611
|
+
});
|
|
3612
|
+
dirents.push(...getSavedChildDirents(map, childPath, depth + 1, excluded, pathSeparator));
|
|
3613
|
+
} else {
|
|
3614
|
+
dirents.push({
|
|
3615
|
+
depth,
|
|
3616
|
+
icon: '',
|
|
3617
|
+
name,
|
|
3618
|
+
path: childPath,
|
|
3619
|
+
posInSet: i + 1,
|
|
3620
|
+
setSize: visibleLength,
|
|
3621
|
+
type
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return dirents;
|
|
3659
3626
|
};
|
|
3660
3627
|
|
|
3661
|
-
const
|
|
3628
|
+
const Fulfilled = 'fulfilled';
|
|
3629
|
+
const Rejected = 'rejected';
|
|
3662
3630
|
|
|
3663
|
-
const
|
|
3664
|
-
|
|
3665
|
-
|
|
3631
|
+
const createDirents = (root, expandedDirentPaths, expandedDirentChildren, excluded, pathSeparator) => {
|
|
3632
|
+
const dirents = [];
|
|
3633
|
+
const map = Object.create(null);
|
|
3634
|
+
for (let i = 0; i < expandedDirentPaths.length; i++) {
|
|
3635
|
+
const path = expandedDirentPaths[i];
|
|
3636
|
+
const children = expandedDirentChildren[i];
|
|
3637
|
+
if (children.status === Fulfilled) {
|
|
3638
|
+
map[path] = children.value;
|
|
3639
|
+
}
|
|
3666
3640
|
}
|
|
3667
|
-
|
|
3641
|
+
dirents.push(...getSavedChildDirents(map, root, 1, excluded, pathSeparator));
|
|
3642
|
+
return dirents;
|
|
3668
3643
|
};
|
|
3669
|
-
const
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3644
|
+
const getSavedExpandedPaths = (savedState, root) => {
|
|
3645
|
+
if (savedState && savedState.root !== root) {
|
|
3646
|
+
return [];
|
|
3647
|
+
}
|
|
3648
|
+
if (savedState && savedState.expandedPaths && Array.isArray(savedState.expandedPaths)) {
|
|
3649
|
+
return savedState.expandedPaths;
|
|
3650
|
+
}
|
|
3651
|
+
return [];
|
|
3652
|
+
};
|
|
3653
|
+
const restoreExpandedState = async (savedState, root, pathSeparator, excluded) => {
|
|
3654
|
+
// TODO read all opened folders in parallel
|
|
3655
|
+
// ignore ENOENT errors
|
|
3656
|
+
// ignore ENOTDIR errors
|
|
3657
|
+
// merge all dirents
|
|
3658
|
+
// restore scroll location
|
|
3659
|
+
const expandedPaths = getSavedExpandedPaths(savedState, root);
|
|
3660
|
+
if (root === EmptyString) {
|
|
3661
|
+
return [];
|
|
3662
|
+
}
|
|
3663
|
+
const expandedDirentPaths = [root, ...expandedPaths];
|
|
3664
|
+
const expandedDirentChildren = await Promise.allSettled(expandedDirentPaths.map(getChildDirentsRaw));
|
|
3665
|
+
if (expandedDirentChildren[0].status === Rejected) {
|
|
3666
|
+
throw expandedDirentChildren[0].reason;
|
|
3667
|
+
}
|
|
3668
|
+
const dirents = createDirents(root, expandedDirentPaths, expandedDirentChildren, excluded, pathSeparator);
|
|
3669
|
+
return dirents;
|
|
3673
3670
|
};
|
|
3674
3671
|
|
|
3675
|
-
const
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3672
|
+
const getPathSeparator = root => {
|
|
3673
|
+
return getPathSeparator$1(root);
|
|
3674
|
+
};
|
|
3675
|
+
const getExcluded = () => {
|
|
3676
|
+
const excludedObject = {};
|
|
3677
|
+
const excluded = [];
|
|
3678
|
+
for (const [key, value] of Object.entries(excludedObject)) {
|
|
3679
|
+
if (value) {
|
|
3680
|
+
excluded.push(key);
|
|
3679
3681
|
}
|
|
3680
3682
|
}
|
|
3681
|
-
return
|
|
3683
|
+
return excluded;
|
|
3682
3684
|
};
|
|
3683
|
-
const
|
|
3684
|
-
|
|
3685
|
-
const endIndex = getEndIndex(items, index, dirent);
|
|
3686
|
-
const mergedDirents = [...items.slice(0, startIndex), {
|
|
3687
|
-
...dirent,
|
|
3688
|
-
type: DirectoryExpanded
|
|
3689
|
-
}, ...childDirents, ...items.slice(endIndex)];
|
|
3690
|
-
return mergedDirents;
|
|
3685
|
+
const getSavedRoot = (savedState, workspacePath) => {
|
|
3686
|
+
return workspacePath;
|
|
3691
3687
|
};
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
await uploadFileSystemHandles(dirent.path, '/', fileHandles);
|
|
3698
|
-
const childDirents = await getChildDirents(pathSeparator, dirent.path, dirent.depth);
|
|
3699
|
-
const mergedDirents = getMergedDirents(items, index, dirent, childDirents);
|
|
3700
|
-
// TODO update maxlineY
|
|
3701
|
-
return {
|
|
3702
|
-
...state,
|
|
3703
|
-
dropTargets: [],
|
|
3704
|
-
items: mergedDirents
|
|
3705
|
-
};
|
|
3688
|
+
const getErrorCode = error => {
|
|
3689
|
+
if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string') {
|
|
3690
|
+
return error.code;
|
|
3691
|
+
}
|
|
3692
|
+
return '';
|
|
3706
3693
|
};
|
|
3707
|
-
const
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
} = state;
|
|
3711
|
-
const parentIndex = getParentStartIndex(items, index);
|
|
3712
|
-
if (parentIndex === -1) {
|
|
3713
|
-
return handleDropRoot(state, fileHandles, files, paths);
|
|
3694
|
+
const getErrorMessage = error => {
|
|
3695
|
+
if (error instanceof Error) {
|
|
3696
|
+
return error.message;
|
|
3714
3697
|
}
|
|
3715
|
-
|
|
3698
|
+
if (typeof error === 'string') {
|
|
3699
|
+
return error;
|
|
3700
|
+
}
|
|
3701
|
+
return 'Unknown error';
|
|
3716
3702
|
};
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
case
|
|
3727
|
-
|
|
3728
|
-
return handleDropIntoFolder(state, dirent, index, fileHandles);
|
|
3729
|
-
case File:
|
|
3730
|
-
return handleDropIntoFile(state, dirent, index, fileHandles, files, paths);
|
|
3703
|
+
const getFriendlyErrorMessage = (errorMessage, errorCode) => {
|
|
3704
|
+
switch (errorCode) {
|
|
3705
|
+
case 'EACCES':
|
|
3706
|
+
case 'EPERM':
|
|
3707
|
+
return 'permission was denied';
|
|
3708
|
+
case 'EBUSY':
|
|
3709
|
+
return 'the folder is currently in use';
|
|
3710
|
+
case 'ENOENT':
|
|
3711
|
+
return 'the folder does not exist';
|
|
3712
|
+
case 'ENOTDIR':
|
|
3713
|
+
return 'the path is not a folder';
|
|
3731
3714
|
default:
|
|
3732
|
-
return
|
|
3715
|
+
return errorMessage || 'an unexpected error occurred';
|
|
3733
3716
|
}
|
|
3734
3717
|
};
|
|
3735
|
-
|
|
3736
|
-
const
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3718
|
+
const loadContent = async (state, savedState) => {
|
|
3719
|
+
const {
|
|
3720
|
+
assetDir,
|
|
3721
|
+
platform
|
|
3722
|
+
} = state;
|
|
3723
|
+
const {
|
|
3724
|
+
confirmDelete,
|
|
3725
|
+
sourceControlDecorations,
|
|
3726
|
+
useChevrons
|
|
3727
|
+
} = await getSettings();
|
|
3728
|
+
const workspacePath = await getWorkspacePath();
|
|
3729
|
+
const root = getSavedRoot(savedState, workspacePath);
|
|
3730
|
+
try {
|
|
3731
|
+
// TODO path separator could be restored from saved state
|
|
3732
|
+
const pathSeparator = await getPathSeparator(root); // TODO only load path separator once
|
|
3733
|
+
const excluded = getExcluded();
|
|
3734
|
+
const restoredDirents = await restoreExpandedState(savedState, root, pathSeparator, excluded);
|
|
3735
|
+
let minLineY = 0;
|
|
3736
|
+
if (savedState && typeof savedState.minLineY === 'number') {
|
|
3737
|
+
minLineY = savedState.minLineY;
|
|
3738
|
+
}
|
|
3739
|
+
let deltaY = 0;
|
|
3740
|
+
if (savedState && typeof savedState.deltaY === 'number') {
|
|
3741
|
+
deltaY = savedState.deltaY;
|
|
3742
|
+
}
|
|
3743
|
+
const scheme = getScheme(root);
|
|
3744
|
+
const decorations = await getFileDecorations(scheme, root, restoredDirents.filter(item => item.depth === 1).map(item => item.path), sourceControlDecorations, assetDir, platform);
|
|
3745
|
+
return {
|
|
3746
|
+
...state,
|
|
3747
|
+
confirmDelete,
|
|
3748
|
+
decorations,
|
|
3749
|
+
deltaY,
|
|
3750
|
+
errorCode: '',
|
|
3751
|
+
errorMessage: '',
|
|
3752
|
+
excluded,
|
|
3753
|
+
hasError: false,
|
|
3754
|
+
initial: false,
|
|
3755
|
+
items: restoredDirents,
|
|
3756
|
+
maxIndent: 10,
|
|
3757
|
+
minLineY,
|
|
3758
|
+
pathSeparator,
|
|
3759
|
+
root,
|
|
3760
|
+
useChevrons
|
|
3761
|
+
};
|
|
3762
|
+
} catch (error) {
|
|
3763
|
+
const errorCode = getErrorCode(error);
|
|
3764
|
+
const errorMessage = getFriendlyErrorMessage(getErrorMessage(error), errorCode);
|
|
3765
|
+
return {
|
|
3766
|
+
...state,
|
|
3767
|
+
confirmDelete,
|
|
3768
|
+
errorCode,
|
|
3769
|
+
errorMessage,
|
|
3770
|
+
hasError: true,
|
|
3771
|
+
initial: false,
|
|
3772
|
+
items: [],
|
|
3773
|
+
root,
|
|
3774
|
+
useChevrons
|
|
3775
|
+
};
|
|
3742
3776
|
}
|
|
3743
3777
|
};
|
|
3744
3778
|
|
|
3745
|
-
const
|
|
3779
|
+
const getChildHandles = async fileHandle => {
|
|
3746
3780
|
// @ts-ignore
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
const getFileHandles = async fileIds => {
|
|
3752
|
-
if (fileIds.length === 0) {
|
|
3753
|
-
return [];
|
|
3754
|
-
}
|
|
3755
|
-
const files = await getFileHandles$1(fileIds);
|
|
3756
|
-
return files;
|
|
3781
|
+
const values = fileHandle.values();
|
|
3782
|
+
const children = await fromAsync(values);
|
|
3783
|
+
return children;
|
|
3757
3784
|
};
|
|
3758
3785
|
|
|
3759
|
-
const
|
|
3760
|
-
|
|
3786
|
+
const getFileHandleText = async fileHandle => {
|
|
3787
|
+
const file = await fileHandle.getFile();
|
|
3788
|
+
const text = await file.text();
|
|
3789
|
+
return text;
|
|
3761
3790
|
};
|
|
3762
3791
|
|
|
3763
|
-
const
|
|
3764
|
-
return
|
|
3765
|
-
};
|
|
3766
|
-
const getFilePaths = async (files, platform) => {
|
|
3767
|
-
if (platform !== Electron) {
|
|
3768
|
-
return files.map(file => '');
|
|
3769
|
-
}
|
|
3770
|
-
const promises = files.map(getFilepath);
|
|
3771
|
-
const paths = await Promise.all(promises);
|
|
3772
|
-
return paths;
|
|
3792
|
+
const isFileHandle = fileHandle => {
|
|
3793
|
+
return fileHandle.kind === 'file';
|
|
3773
3794
|
};
|
|
3774
3795
|
|
|
3775
|
-
const
|
|
3776
|
-
|
|
3796
|
+
const createUploadTree = async (root, fileHandles) => {
|
|
3797
|
+
const uploadTree = Object.create(null);
|
|
3798
|
+
const normalized = fileHandles.filter(Boolean);
|
|
3799
|
+
for (const fileHandle of normalized) {
|
|
3777
3800
|
const {
|
|
3778
|
-
|
|
3779
|
-
} =
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3801
|
+
name
|
|
3802
|
+
} = fileHandle;
|
|
3803
|
+
if (isDirectoryHandle(fileHandle)) {
|
|
3804
|
+
const children = await getChildHandles(fileHandle);
|
|
3805
|
+
const childTree = await createUploadTree(name, children);
|
|
3806
|
+
uploadTree[name] = childTree;
|
|
3807
|
+
} else if (isFileHandle(fileHandle)) {
|
|
3808
|
+
// TODO maybe save blob and use filesystem.writeblob
|
|
3809
|
+
const text = await getFileHandleText(fileHandle);
|
|
3810
|
+
uploadTree[name] = text;
|
|
3811
|
+
}
|
|
3789
3812
|
}
|
|
3813
|
+
return uploadTree;
|
|
3790
3814
|
};
|
|
3791
3815
|
|
|
3792
|
-
const
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
} = state;
|
|
3812
|
-
const visible = items.slice(minLineY, maxLineY);
|
|
3813
|
-
const {
|
|
3814
|
-
icons,
|
|
3815
|
-
newFileIconCache
|
|
3816
|
-
} = await getFileIcons(visible, Object.create(null));
|
|
3817
|
-
return {
|
|
3818
|
-
...state,
|
|
3819
|
-
fileIconCache: newFileIconCache,
|
|
3820
|
-
icons
|
|
3816
|
+
const getFileOperations = (root, uploadTree) => {
|
|
3817
|
+
const operations = [];
|
|
3818
|
+
const processTree = (tree, currentPath) => {
|
|
3819
|
+
for (const [path, value] of Object.entries(tree)) {
|
|
3820
|
+
const fullPath = currentPath ? join2(currentPath, path) : path;
|
|
3821
|
+
if (typeof value === 'object') {
|
|
3822
|
+
operations.push({
|
|
3823
|
+
path: join2(root, fullPath),
|
|
3824
|
+
type: CreateFolder$1
|
|
3825
|
+
});
|
|
3826
|
+
processTree(value, fullPath);
|
|
3827
|
+
} else if (typeof value === 'string') {
|
|
3828
|
+
operations.push({
|
|
3829
|
+
path: join2(root, fullPath),
|
|
3830
|
+
text: value,
|
|
3831
|
+
type: CreateFile$1
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3821
3835
|
};
|
|
3836
|
+
processTree(uploadTree, '');
|
|
3837
|
+
return operations;
|
|
3822
3838
|
};
|
|
3823
3839
|
|
|
3824
|
-
const
|
|
3825
|
-
return
|
|
3840
|
+
const isDroppedFile = item => {
|
|
3841
|
+
return item.kind === 'file' && 'value' in item;
|
|
3826
3842
|
};
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
editingErrorMessage,
|
|
3831
|
-
editingValue
|
|
3832
|
-
} = state;
|
|
3833
|
-
if (editingErrorMessage || !editingValue) {
|
|
3834
|
-
return cancelEditInternal(state, false);
|
|
3843
|
+
const getFileSystemHandle = item => {
|
|
3844
|
+
if (isDroppedFile(item)) {
|
|
3845
|
+
return item.value;
|
|
3835
3846
|
}
|
|
3836
|
-
return
|
|
3847
|
+
return item;
|
|
3837
3848
|
};
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3849
|
+
const getDroppedName = item => {
|
|
3850
|
+
if (isDroppedFile(item)) {
|
|
3851
|
+
return item.value.name;
|
|
3852
|
+
}
|
|
3853
|
+
return item.name;
|
|
3841
3854
|
};
|
|
3855
|
+
const getFileSystemHandlesNormalized = fileSystemHandles => {
|
|
3856
|
+
const normalized = [];
|
|
3857
|
+
for (const item of fileSystemHandles) {
|
|
3858
|
+
normalized.push(getFileSystemHandle(item));
|
|
3859
|
+
}
|
|
3860
|
+
return normalized;
|
|
3861
|
+
};
|
|
3862
|
+
const uploadFileSystemHandles = async (root, pathSeparator, fileSystemHandles) => {
|
|
3863
|
+
if (fileSystemHandles.length === 0) {
|
|
3864
|
+
return true;
|
|
3865
|
+
}
|
|
3866
|
+
const fileSystemHandlesNormalized = getFileSystemHandlesNormalized(fileSystemHandles);
|
|
3867
|
+
const uploadTree = await createUploadTree(root, fileSystemHandlesNormalized);
|
|
3868
|
+
const fileOperations = getFileOperations(root, uploadTree);
|
|
3869
|
+
await applyFileOperations(fileOperations);
|
|
3842
3870
|
|
|
3843
|
-
|
|
3844
|
-
|
|
3871
|
+
// TODO
|
|
3872
|
+
// 1. in electron, use webutils.getPathForFile to see if a path is available
|
|
3873
|
+
// 2. else, walk all files and folders recursively and upload all of them (if there are many, show a progress bar)
|
|
3874
|
+
|
|
3875
|
+
// TODO send file system operations to renderer worker
|
|
3876
|
+
return true;
|
|
3845
3877
|
};
|
|
3846
3878
|
|
|
3847
|
-
const
|
|
3848
|
-
|
|
3849
|
-
|
|
3879
|
+
const mergeDirents$1 = (oldDirents, newDirents) => {
|
|
3880
|
+
return newDirents;
|
|
3881
|
+
};
|
|
3882
|
+
const getMergedDirents$2 = async (root, pathSeparator, dirents) => {
|
|
3883
|
+
const childDirents = await getChildDirents(pathSeparator, root, 0);
|
|
3884
|
+
const mergedDirents = mergeDirents$1(dirents, childDirents);
|
|
3885
|
+
return mergedDirents;
|
|
3886
|
+
};
|
|
3887
|
+
const getDroppedDirectoryWorkspacePath = fileHandle => {
|
|
3888
|
+
return `html://${fileHandle.name}`;
|
|
3889
|
+
};
|
|
3890
|
+
const openDroppedDirectoryAsWorkspace$1 = async (state, fileHandle) => {
|
|
3891
|
+
const path = getDroppedDirectoryWorkspacePath(fileHandle);
|
|
3892
|
+
await invoke$2('PersistentFileHandle.addHandle', fileHandle.name, fileHandle);
|
|
3893
|
+
await invoke$2('Workspace.setPath', path);
|
|
3894
|
+
const updated = await loadContent(state, undefined);
|
|
3895
|
+
return {
|
|
3896
|
+
...updated,
|
|
3897
|
+
dropTargets: []
|
|
3898
|
+
};
|
|
3899
|
+
};
|
|
3900
|
+
const getFirstDroppedDirectory = (state, fileHandles) => {
|
|
3901
|
+
if (state.root !== '') {
|
|
3902
|
+
return undefined;
|
|
3850
3903
|
}
|
|
3851
|
-
const
|
|
3852
|
-
|
|
3853
|
-
if (
|
|
3854
|
-
|
|
3904
|
+
for (const item of fileHandles) {
|
|
3905
|
+
const fileHandle = getFileSystemHandle(item);
|
|
3906
|
+
if (isDirectoryHandle(fileHandle)) {
|
|
3907
|
+
return fileHandle;
|
|
3855
3908
|
}
|
|
3856
3909
|
}
|
|
3857
|
-
|
|
3858
|
-
return -1;
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
// Find the next match after the current focus
|
|
3862
|
-
let nextIndex = matches.findIndex(index => index > focusedIndex);
|
|
3863
|
-
if (nextIndex === -1) {
|
|
3864
|
-
// If no match found after current focus, wrap around to the first match
|
|
3865
|
-
nextIndex = 0;
|
|
3866
|
-
}
|
|
3867
|
-
return matches[nextIndex];
|
|
3868
|
-
};
|
|
3869
|
-
|
|
3870
|
-
const RE_ASCII = /^[a-z]$/;
|
|
3871
|
-
const isAscii = key => {
|
|
3872
|
-
return RE_ASCII.test(key);
|
|
3910
|
+
return undefined;
|
|
3873
3911
|
};
|
|
3874
|
-
|
|
3875
|
-
let timeout;
|
|
3876
|
-
const handleKeyDown = (state, key) => {
|
|
3912
|
+
const handleDrop$2 = async (state, fileHandles, files, paths) => {
|
|
3877
3913
|
const {
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
items
|
|
3914
|
+
items,
|
|
3915
|
+
pathSeparator,
|
|
3916
|
+
root
|
|
3882
3917
|
} = state;
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
if (!isAscii(key)) {
|
|
3887
|
-
return state;
|
|
3888
|
-
}
|
|
3889
|
-
const newFocusWord = focusWord + key.toLowerCase();
|
|
3890
|
-
const itemNames = items.map(item => item.name);
|
|
3891
|
-
const matchingIndex = filterByFocusWord(itemNames, focusedIndex, newFocusWord);
|
|
3892
|
-
if (timeout) {
|
|
3893
|
-
clearTimeout(timeout);
|
|
3918
|
+
const droppedDirectory = getFirstDroppedDirectory(state, fileHandles);
|
|
3919
|
+
if (droppedDirectory) {
|
|
3920
|
+
return openDroppedDirectoryAsWorkspace$1(state, droppedDirectory);
|
|
3894
3921
|
}
|
|
3895
|
-
|
|
3896
|
-
await invoke$2('Explorer.cancelTypeAhead');
|
|
3897
|
-
}, focusWordTimeout);
|
|
3898
|
-
if (matchingIndex === -1) {
|
|
3922
|
+
if (root === '') {
|
|
3899
3923
|
return {
|
|
3900
3924
|
...state,
|
|
3901
|
-
|
|
3925
|
+
dropTargets: []
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
const handled = await uploadFileSystemHandles(root, pathSeparator, fileHandles);
|
|
3929
|
+
if (handled) {
|
|
3930
|
+
const updated = await refresh(state);
|
|
3931
|
+
return {
|
|
3932
|
+
...updated,
|
|
3933
|
+
dropTargets: []
|
|
3902
3934
|
};
|
|
3903
3935
|
}
|
|
3936
|
+
const mergedDirents = await getMergedDirents$2(root, pathSeparator, items);
|
|
3904
3937
|
return {
|
|
3905
3938
|
...state,
|
|
3906
|
-
|
|
3907
|
-
|
|
3939
|
+
dropTargets: [],
|
|
3940
|
+
items: mergedDirents
|
|
3908
3941
|
};
|
|
3909
3942
|
};
|
|
3910
3943
|
|
|
3911
|
-
const
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
return {
|
|
3923
|
-
newMaxLineY: index + largerHalf,
|
|
3924
|
-
newMinLineY: index - smallerHalf
|
|
3925
|
-
};
|
|
3944
|
+
const getFileOperationsElectron = async (root, paths, fileHandles, pathSeparator) => {
|
|
3945
|
+
const operations = [];
|
|
3946
|
+
for (let i = 0; i < paths.length; i++) {
|
|
3947
|
+
const fileHandle = fileHandles[i];
|
|
3948
|
+
const name = getDroppedName(fileHandle);
|
|
3949
|
+
const path = paths[i];
|
|
3950
|
+
operations.push({
|
|
3951
|
+
from: path,
|
|
3952
|
+
path: join(pathSeparator, root, name),
|
|
3953
|
+
type: Copy$1
|
|
3954
|
+
});
|
|
3926
3955
|
}
|
|
3956
|
+
return operations;
|
|
3957
|
+
};
|
|
3958
|
+
|
|
3959
|
+
// TODO copy files in parallel
|
|
3960
|
+
const copyFilesElectron = async (root, fileHandles, files, paths) => {
|
|
3961
|
+
const pathSeparator = await getPathSeparator$1(root);
|
|
3962
|
+
const operations = await getFileOperationsElectron(root, paths, fileHandles, pathSeparator);
|
|
3963
|
+
await applyFileOperations(operations);
|
|
3964
|
+
};
|
|
3965
|
+
|
|
3966
|
+
const mergeDirents = (oldDirents, newDirents) => {
|
|
3967
|
+
return newDirents;
|
|
3968
|
+
};
|
|
3969
|
+
const getMergedDirents$1 = async (root, pathSeparator, dirents) => {
|
|
3970
|
+
const childDirents = await getChildDirents(pathSeparator, root, 0);
|
|
3971
|
+
const mergedDirents = mergeDirents(dirents, childDirents);
|
|
3972
|
+
return mergedDirents;
|
|
3973
|
+
};
|
|
3974
|
+
const openDroppedDirectoryAsWorkspace = async (state, path) => {
|
|
3975
|
+
await invoke$2('Workspace.setPath', path);
|
|
3976
|
+
const updated = await loadContent(state, undefined);
|
|
3927
3977
|
return {
|
|
3928
|
-
|
|
3929
|
-
|
|
3978
|
+
...updated,
|
|
3979
|
+
dropTargets: []
|
|
3930
3980
|
};
|
|
3931
3981
|
};
|
|
3932
|
-
|
|
3933
|
-
|
|
3982
|
+
const getFirstDroppedDirectoryPath = (state, fileHandles, paths) => {
|
|
3983
|
+
if (state.root !== '') {
|
|
3984
|
+
return undefined;
|
|
3985
|
+
}
|
|
3986
|
+
for (let i = 0; i < fileHandles.length; i++) {
|
|
3987
|
+
const fileHandle = getFileSystemHandle(fileHandles[i]);
|
|
3988
|
+
if (isDirectoryHandle(fileHandle)) {
|
|
3989
|
+
return paths[i];
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
return undefined;
|
|
3993
|
+
};
|
|
3994
|
+
const handleDrop$1 = async (state, fileHandles, files, paths) => {
|
|
3934
3995
|
const {
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3996
|
+
items,
|
|
3997
|
+
pathSeparator,
|
|
3998
|
+
root
|
|
3938
3999
|
} = state;
|
|
3939
|
-
const
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
}
|
|
3943
|
-
|
|
4000
|
+
const droppedDirectoryPath = getFirstDroppedDirectoryPath(state, fileHandles, paths);
|
|
4001
|
+
if (droppedDirectoryPath) {
|
|
4002
|
+
return openDroppedDirectoryAsWorkspace(state, droppedDirectoryPath);
|
|
4003
|
+
}
|
|
4004
|
+
if (root === '') {
|
|
4005
|
+
return {
|
|
4006
|
+
...state,
|
|
4007
|
+
dropTargets: []
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
await copyFilesElectron(root, fileHandles, files, paths);
|
|
4011
|
+
const mergedDirents = await getMergedDirents$1(root, pathSeparator, items);
|
|
3944
4012
|
return {
|
|
3945
4013
|
...state,
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
focusedIndex,
|
|
3949
|
-
maxLineY: newMaxLineY,
|
|
3950
|
-
minLineY: newMinLineY
|
|
4014
|
+
dropTargets: [],
|
|
4015
|
+
items: mergedDirents
|
|
3951
4016
|
};
|
|
3952
4017
|
};
|
|
3953
4018
|
|
|
3954
|
-
const
|
|
3955
|
-
// Handle files with extensions
|
|
3956
|
-
const lastDotIndex = baseName.lastIndexOf('.');
|
|
3957
|
-
const hasExtension = lastDotIndex !== -1 && lastDotIndex !== 0 && lastDotIndex !== baseName.length - 1;
|
|
3958
|
-
let nameWithoutExtension;
|
|
3959
|
-
let extension;
|
|
3960
|
-
if (hasExtension) {
|
|
3961
|
-
nameWithoutExtension = baseName.slice(0, lastDotIndex);
|
|
3962
|
-
extension = baseName.slice(lastDotIndex);
|
|
3963
|
-
} else {
|
|
3964
|
-
nameWithoutExtension = baseName;
|
|
3965
|
-
extension = '';
|
|
3966
|
-
}
|
|
4019
|
+
const Electron = 2;
|
|
3967
4020
|
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
return baseName;
|
|
3972
|
-
}
|
|
3973
|
-
|
|
3974
|
-
// Try "original copy"
|
|
3975
|
-
const copyName = `${nameWithoutExtension} copy${extension}`;
|
|
3976
|
-
const copyPath = join2(root, copyName);
|
|
3977
|
-
if (!existingPaths.includes(copyPath)) {
|
|
3978
|
-
return copyName;
|
|
3979
|
-
}
|
|
3980
|
-
|
|
3981
|
-
// Try "original copy 1", "original copy 2", etc.
|
|
3982
|
-
let counter = 1;
|
|
3983
|
-
while (true) {
|
|
3984
|
-
const numberedCopyName = `${nameWithoutExtension} copy ${counter}${extension}`;
|
|
3985
|
-
const numberedCopyPath = join2(root, numberedCopyName);
|
|
3986
|
-
if (!existingPaths.includes(numberedCopyPath)) {
|
|
3987
|
-
return numberedCopyName;
|
|
3988
|
-
}
|
|
3989
|
-
counter++;
|
|
4021
|
+
const getModule = isElectron => {
|
|
4022
|
+
if (isElectron) {
|
|
4023
|
+
return handleDrop$1;
|
|
3990
4024
|
}
|
|
4025
|
+
return handleDrop$2;
|
|
4026
|
+
};
|
|
4027
|
+
const handleDropRoot = async (state, fileHandles, files, paths) => {
|
|
4028
|
+
const isElectron = state.platform === Electron;
|
|
4029
|
+
const fn = getModule(isElectron);
|
|
4030
|
+
return fn(state, fileHandles, files, paths);
|
|
3991
4031
|
};
|
|
3992
4032
|
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
if (existingUris.includes(file)) {
|
|
3998
|
-
operations.push({
|
|
3999
|
-
from: file,
|
|
4000
|
-
path: join2(focusedUri, baseName),
|
|
4001
|
-
type: Rename$2
|
|
4002
|
-
});
|
|
4003
|
-
} else {
|
|
4004
|
-
const uniqueName = generateUniqueName(baseName, existingUris, root);
|
|
4005
|
-
const newUri = join2(root, uniqueName);
|
|
4006
|
-
operations.push({
|
|
4007
|
-
from: file,
|
|
4008
|
-
// TODO ensure file is uri
|
|
4009
|
-
path: newUri,
|
|
4010
|
-
type: Copy$1
|
|
4011
|
-
});
|
|
4033
|
+
const getEndIndex = (items, index, dirent) => {
|
|
4034
|
+
for (let i = index + 1; i < items.length; i++) {
|
|
4035
|
+
if (items[i].depth === dirent.depth) {
|
|
4036
|
+
return i;
|
|
4012
4037
|
}
|
|
4013
4038
|
}
|
|
4014
|
-
return
|
|
4039
|
+
return items.length;
|
|
4015
4040
|
};
|
|
4016
|
-
|
|
4017
|
-
const
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4041
|
+
const getMergedDirents = (items, index, dirent, childDirents) => {
|
|
4042
|
+
const startIndex = index;
|
|
4043
|
+
const endIndex = getEndIndex(items, index, dirent);
|
|
4044
|
+
const mergedDirents = [...items.slice(0, startIndex), {
|
|
4045
|
+
...dirent,
|
|
4046
|
+
type: DirectoryExpanded
|
|
4047
|
+
}, ...childDirents, ...items.slice(endIndex)];
|
|
4048
|
+
return mergedDirents;
|
|
4049
|
+
};
|
|
4050
|
+
const handleDropIntoFolder = async (state, dirent, index, fileHandles, files, paths) => {
|
|
4025
4051
|
const {
|
|
4026
|
-
focusedIndex,
|
|
4027
4052
|
items,
|
|
4028
|
-
|
|
4053
|
+
pathSeparator
|
|
4029
4054
|
} = state;
|
|
4030
|
-
|
|
4031
|
-
const
|
|
4032
|
-
const
|
|
4033
|
-
// TODO
|
|
4034
|
-
await applyFileOperations(operations);
|
|
4035
|
-
|
|
4036
|
-
// TODO use refreshExplorer with the paths that have been affected by file operations
|
|
4037
|
-
// TODO only update folder at which level it changed
|
|
4038
|
-
const latestState = await refresh(state);
|
|
4039
|
-
|
|
4040
|
-
// Focus on the first newly created file and adjust scroll position
|
|
4041
|
-
const newFilePaths = operations.map(operation => operation.path);
|
|
4042
|
-
if (newFilePaths.length > 0) {
|
|
4043
|
-
const firstNewFilePath = newFilePaths[0];
|
|
4044
|
-
const newFileIndex = getIndex(latestState.items, firstNewFilePath);
|
|
4045
|
-
if (newFileIndex !== -1) {
|
|
4046
|
-
const adjustedState = adjustScrollAfterPaste(latestState, newFileIndex);
|
|
4047
|
-
return {
|
|
4048
|
-
...adjustedState,
|
|
4049
|
-
pasteShouldMove: false
|
|
4050
|
-
};
|
|
4051
|
-
}
|
|
4052
|
-
}
|
|
4053
|
-
// If there are no items, ensure focusedIndex is 0
|
|
4054
|
-
if (latestState.items.length === 0) {
|
|
4055
|
-
return {
|
|
4056
|
-
...latestState,
|
|
4057
|
-
focusedIndex: 0,
|
|
4058
|
-
pasteShouldMove: false
|
|
4059
|
-
};
|
|
4060
|
-
}
|
|
4055
|
+
await uploadFileSystemHandles(dirent.path, '/', fileHandles);
|
|
4056
|
+
const childDirents = await getChildDirents(pathSeparator, dirent.path, dirent.depth);
|
|
4057
|
+
const mergedDirents = getMergedDirents(items, index, dirent, childDirents);
|
|
4058
|
+
// TODO update maxlineY
|
|
4061
4059
|
return {
|
|
4062
|
-
...
|
|
4063
|
-
|
|
4060
|
+
...state,
|
|
4061
|
+
dropTargets: [],
|
|
4062
|
+
items: mergedDirents
|
|
4064
4063
|
};
|
|
4065
4064
|
};
|
|
4066
|
-
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
from: file,
|
|
4074
|
-
path: newUri,
|
|
4075
|
-
type: Rename$2
|
|
4076
|
-
});
|
|
4077
|
-
}
|
|
4078
|
-
return operations;
|
|
4079
|
-
};
|
|
4080
|
-
const getTargetUri = (root, items, index) => {
|
|
4081
|
-
if (index === -1) {
|
|
4082
|
-
return root;
|
|
4065
|
+
const handleDropIntoFile = (state, dirent, index, fileHandles, files, paths) => {
|
|
4066
|
+
const {
|
|
4067
|
+
items
|
|
4068
|
+
} = state;
|
|
4069
|
+
const parentIndex = getParentStartIndex(items, index);
|
|
4070
|
+
if (parentIndex === -1) {
|
|
4071
|
+
return handleDropRoot(state, fileHandles, files, paths);
|
|
4083
4072
|
}
|
|
4084
|
-
return
|
|
4073
|
+
return handleDropIndex(state, fileHandles, files, paths, parentIndex);
|
|
4085
4074
|
};
|
|
4086
|
-
const
|
|
4075
|
+
const handleDropIndex = async (state, fileHandles, files, paths, index) => {
|
|
4087
4076
|
const {
|
|
4088
|
-
|
|
4089
|
-
items,
|
|
4090
|
-
pathSeparator,
|
|
4091
|
-
root
|
|
4077
|
+
items
|
|
4092
4078
|
} = state;
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
const pastedFileIndex = getIndex(latestState.items, targetPath);
|
|
4106
|
-
if (pastedFileIndex !== -1) {
|
|
4107
|
-
const adjustedState = adjustScrollAfterPaste(latestState, pastedFileIndex);
|
|
4108
|
-
return {
|
|
4109
|
-
...adjustedState,
|
|
4110
|
-
cutItems: [],
|
|
4111
|
-
pasteShouldMove: false
|
|
4112
|
-
};
|
|
4113
|
-
}
|
|
4079
|
+
const dirent = items[index];
|
|
4080
|
+
// TODO if it is a file, drop into the folder of the file
|
|
4081
|
+
// TODO if it is a folder, drop into the folder
|
|
4082
|
+
// TODO if it is a symlink, read symlink and determine if file can be dropped
|
|
4083
|
+
switch (dirent.type) {
|
|
4084
|
+
case Directory:
|
|
4085
|
+
case DirectoryExpanded:
|
|
4086
|
+
return handleDropIntoFolder(state, dirent, index, fileHandles);
|
|
4087
|
+
case File:
|
|
4088
|
+
return handleDropIntoFile(state, dirent, index, fileHandles, files, paths);
|
|
4089
|
+
default:
|
|
4090
|
+
return state;
|
|
4114
4091
|
}
|
|
4115
|
-
return {
|
|
4116
|
-
...latestState,
|
|
4117
|
-
cutItems: [],
|
|
4118
|
-
pasteShouldMove: false
|
|
4119
|
-
};
|
|
4120
4092
|
};
|
|
4121
4093
|
|
|
4122
|
-
const
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4094
|
+
const getDropHandler = index => {
|
|
4095
|
+
switch (index) {
|
|
4096
|
+
case -1:
|
|
4097
|
+
return handleDropRoot;
|
|
4098
|
+
default:
|
|
4099
|
+
return handleDropIndex;
|
|
4128
4100
|
}
|
|
4129
|
-
|
|
4130
|
-
// TODO check that pasted folder is not a parent folder of opened folder
|
|
4131
|
-
// TODO support pasting multiple paths
|
|
4132
|
-
// TODO what happens when pasting multiple paths, but some of them error?
|
|
4133
|
-
// how many error messages should be shown? Should the operation be undone?
|
|
4134
|
-
// TODO what if it is a large folder and takes a long time to copy? Should show progress
|
|
4135
|
-
// TODO what if there is a permission error? Probably should show a modal to ask for permission
|
|
4136
|
-
// TODO if error is EEXISTS, just rename the copy (e.g. file-copy-1.txt, file-copy-2.txt)
|
|
4137
|
-
// TODO actual target should be selected folder
|
|
4138
|
-
// TODO but what if a file is currently selected? Then maybe the parent folder
|
|
4139
|
-
// TODO but will it work if the folder is a symlink?
|
|
4140
|
-
// TODO handle error gracefully when copy fails
|
|
4101
|
+
};
|
|
4141
4102
|
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4103
|
+
const getFileArray = fileList => {
|
|
4104
|
+
// @ts-ignore
|
|
4105
|
+
const files = [...fileList];
|
|
4106
|
+
return files;
|
|
4107
|
+
};
|
|
4146
4108
|
|
|
4147
|
-
|
|
4148
|
-
if (
|
|
4149
|
-
return
|
|
4109
|
+
const getFileHandles = async fileIds => {
|
|
4110
|
+
if (fileIds.length === 0) {
|
|
4111
|
+
return [];
|
|
4150
4112
|
}
|
|
4151
|
-
|
|
4113
|
+
const files = await getFileHandles$1(fileIds);
|
|
4114
|
+
return files;
|
|
4152
4115
|
};
|
|
4153
4116
|
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
if (button === LeftClick && index === -1) {
|
|
4157
|
-
return {
|
|
4158
|
-
...state,
|
|
4159
|
-
focus: List,
|
|
4160
|
-
focused: true,
|
|
4161
|
-
focusedIndex: -1
|
|
4162
|
-
};
|
|
4163
|
-
}
|
|
4164
|
-
return state;
|
|
4117
|
+
const getFilePathElectron = async file => {
|
|
4118
|
+
return invoke$2('FileSystemHandle.getFilePathElectron', file);
|
|
4165
4119
|
};
|
|
4166
4120
|
|
|
4167
|
-
const
|
|
4168
|
-
|
|
4169
|
-
|
|
4121
|
+
const getFilepath = async file => {
|
|
4122
|
+
return getFilePathElectron(file);
|
|
4123
|
+
};
|
|
4124
|
+
const getFilePaths = async (files, platform) => {
|
|
4125
|
+
if (platform !== Electron) {
|
|
4126
|
+
return files.map(file => '');
|
|
4170
4127
|
}
|
|
4171
|
-
|
|
4128
|
+
const promises = files.map(getFilepath);
|
|
4129
|
+
const paths = await Promise.all(promises);
|
|
4130
|
+
return paths;
|
|
4172
4131
|
};
|
|
4173
4132
|
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
} = state;
|
|
4189
|
-
const contentHeight = items.length * itemHeight;
|
|
4190
|
-
const maxDeltaY = Math.max(contentHeight - height, 0);
|
|
4191
|
-
const newDeltaY = Math.min(Math.max(deltaY, 0), maxDeltaY);
|
|
4192
|
-
const minLineY = Math.round(newDeltaY / itemHeight);
|
|
4193
|
-
const maxLineY = getExplorerMaxLineY(minLineY, height, itemHeight, items.length);
|
|
4194
|
-
const scrollBarHeight = getScrollBarSize(height, contentHeight, 20);
|
|
4195
|
-
if (state.height === height && state.width === width && state.deltaY === newDeltaY && state.minLineY === minLineY && state.maxLineY === maxLineY && state.scrollBarHeight === scrollBarHeight) {
|
|
4196
|
-
return state;
|
|
4133
|
+
const handleDrop = async (state, x, y, fileIds, fileList) => {
|
|
4134
|
+
try {
|
|
4135
|
+
const {
|
|
4136
|
+
platform
|
|
4137
|
+
} = state;
|
|
4138
|
+
const files = getFileArray(fileList);
|
|
4139
|
+
const fileHandles = await getFileHandles(fileIds);
|
|
4140
|
+
const paths = await getFilePaths(files, platform);
|
|
4141
|
+
const index = getIndexFromPosition(state, x, y);
|
|
4142
|
+
const fn = getDropHandler(index);
|
|
4143
|
+
const result = await fn(state, fileHandles, files, paths, index);
|
|
4144
|
+
return result;
|
|
4145
|
+
} catch (error) {
|
|
4146
|
+
throw new VError(error, 'Failed to drop files');
|
|
4197
4147
|
}
|
|
4148
|
+
};
|
|
4149
|
+
|
|
4150
|
+
const handleEscape = async state => {
|
|
4198
4151
|
return {
|
|
4199
4152
|
...state,
|
|
4200
|
-
|
|
4201
|
-
|
|
4153
|
+
cutItems: []
|
|
4154
|
+
};
|
|
4155
|
+
};
|
|
4156
|
+
|
|
4157
|
+
const handleFocus = async state => {
|
|
4158
|
+
return {
|
|
4159
|
+
...state,
|
|
4160
|
+
focus: List
|
|
4161
|
+
};
|
|
4162
|
+
};
|
|
4163
|
+
|
|
4164
|
+
const updateIcons = async state => {
|
|
4165
|
+
const {
|
|
4166
|
+
items,
|
|
4202
4167
|
maxLineY,
|
|
4203
|
-
minLineY
|
|
4204
|
-
|
|
4205
|
-
|
|
4168
|
+
minLineY
|
|
4169
|
+
} = state;
|
|
4170
|
+
const visible = items.slice(minLineY, maxLineY);
|
|
4171
|
+
const {
|
|
4172
|
+
icons,
|
|
4173
|
+
newFileIconCache
|
|
4174
|
+
} = await getFileIcons(visible, Object.create(null));
|
|
4175
|
+
return {
|
|
4176
|
+
...state,
|
|
4177
|
+
fileIconCache: newFileIconCache,
|
|
4178
|
+
icons
|
|
4206
4179
|
};
|
|
4207
4180
|
};
|
|
4208
4181
|
|
|
4209
|
-
const
|
|
4182
|
+
const handleIconThemeChange = state => {
|
|
4183
|
+
return updateIcons(state);
|
|
4184
|
+
};
|
|
4185
|
+
|
|
4186
|
+
const handleInputBlur = async state => {
|
|
4210
4187
|
const {
|
|
4211
|
-
|
|
4212
|
-
|
|
4188
|
+
editingErrorMessage,
|
|
4189
|
+
editingValue
|
|
4213
4190
|
} = state;
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
// TODO symlink might not be possible to be copied
|
|
4217
|
-
// TODO create folder if type is 2
|
|
4218
|
-
if (dirent.type === /* File */1) {
|
|
4219
|
-
// TODO reading text might be inefficient for binary files
|
|
4220
|
-
// but not sure how else to send them via jsonrpc
|
|
4221
|
-
const content = await dirent.file.text();
|
|
4222
|
-
const absolutePath = [root, dirent.file.name].join(pathSeparator);
|
|
4223
|
-
await writeFile(absolutePath, content);
|
|
4224
|
-
}
|
|
4191
|
+
if (editingErrorMessage || !editingValue) {
|
|
4192
|
+
return cancelEditInternal(state, false);
|
|
4225
4193
|
}
|
|
4194
|
+
return acceptEdit(state);
|
|
4226
4195
|
};
|
|
4227
4196
|
|
|
4228
|
-
const
|
|
4229
|
-
|
|
4230
|
-
|
|
4197
|
+
const handleInputClick = state => {
|
|
4198
|
+
return state;
|
|
4199
|
+
};
|
|
4200
|
+
|
|
4201
|
+
const handleInputKeyDown = (state, key) => {
|
|
4202
|
+
return state;
|
|
4203
|
+
};
|
|
4204
|
+
|
|
4205
|
+
const filterByFocusWord = (items, focusedIndex, focusWord) => {
|
|
4206
|
+
if (items.length === 0) {
|
|
4207
|
+
return -1;
|
|
4208
|
+
}
|
|
4209
|
+
const matches = [];
|
|
4210
|
+
for (let i = 0; i < items.length; i++) {
|
|
4211
|
+
if (items[i].toLowerCase().includes(focusWord)) {
|
|
4212
|
+
matches.push(i);
|
|
4213
|
+
}
|
|
4231
4214
|
}
|
|
4215
|
+
if (matches.length === 0) {
|
|
4216
|
+
return -1;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
// Find the next match after the current focus
|
|
4220
|
+
let nextIndex = matches.findIndex(index => index > focusedIndex);
|
|
4221
|
+
if (nextIndex === -1) {
|
|
4222
|
+
// If no match found after current focus, wrap around to the first match
|
|
4223
|
+
nextIndex = 0;
|
|
4224
|
+
}
|
|
4225
|
+
return matches[nextIndex];
|
|
4226
|
+
};
|
|
4227
|
+
|
|
4228
|
+
const RE_ASCII = /^[a-z]$/;
|
|
4229
|
+
const isAscii = key => {
|
|
4230
|
+
return RE_ASCII.test(key);
|
|
4231
|
+
};
|
|
4232
|
+
|
|
4233
|
+
let timeout;
|
|
4234
|
+
const handleKeyDown = (state, key) => {
|
|
4232
4235
|
const {
|
|
4233
|
-
|
|
4234
|
-
|
|
4236
|
+
focusedIndex,
|
|
4237
|
+
focusWord,
|
|
4238
|
+
focusWordTimeout,
|
|
4235
4239
|
items
|
|
4236
4240
|
} = state;
|
|
4237
|
-
if (
|
|
4238
|
-
|
|
4239
|
-
} else if (deltaY > items.length * itemHeight - height) {
|
|
4240
|
-
deltaY = Math.max(items.length * itemHeight - height, 0);
|
|
4241
|
+
if (focusWord && key === '') {
|
|
4242
|
+
return cancelTypeAhead(state);
|
|
4241
4243
|
}
|
|
4242
|
-
if (
|
|
4244
|
+
if (!isAscii(key)) {
|
|
4243
4245
|
return state;
|
|
4244
4246
|
}
|
|
4245
|
-
const
|
|
4246
|
-
const
|
|
4247
|
+
const newFocusWord = focusWord + key.toLowerCase();
|
|
4248
|
+
const itemNames = items.map(item => item.name);
|
|
4249
|
+
const matchingIndex = filterByFocusWord(itemNames, focusedIndex, newFocusWord);
|
|
4250
|
+
if (timeout) {
|
|
4251
|
+
clearTimeout(timeout);
|
|
4252
|
+
}
|
|
4253
|
+
timeout = setTimeout(async () => {
|
|
4254
|
+
await invoke$2('Explorer.cancelTypeAhead');
|
|
4255
|
+
}, focusWordTimeout);
|
|
4256
|
+
if (matchingIndex === -1) {
|
|
4257
|
+
return {
|
|
4258
|
+
...state,
|
|
4259
|
+
focusWord: newFocusWord
|
|
4260
|
+
};
|
|
4261
|
+
}
|
|
4247
4262
|
return {
|
|
4248
4263
|
...state,
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
minLineY
|
|
4264
|
+
focusedIndex: matchingIndex,
|
|
4265
|
+
focusWord: newFocusWord
|
|
4252
4266
|
};
|
|
4253
4267
|
};
|
|
4254
4268
|
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4269
|
+
const scrollInto = (index, minLineY, maxLineY) => {
|
|
4270
|
+
const diff = maxLineY - minLineY;
|
|
4271
|
+
const smallerHalf = Math.floor(diff / 2);
|
|
4272
|
+
const largerHalf = diff - smallerHalf;
|
|
4273
|
+
if (index < minLineY) {
|
|
4274
|
+
return {
|
|
4275
|
+
newMaxLineY: index + largerHalf,
|
|
4276
|
+
newMinLineY: index - smallerHalf
|
|
4277
|
+
};
|
|
4278
|
+
}
|
|
4279
|
+
if (index >= maxLineY) {
|
|
4280
|
+
return {
|
|
4281
|
+
newMaxLineY: index + largerHalf,
|
|
4282
|
+
newMinLineY: index - smallerHalf
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
return {
|
|
4286
|
+
newMaxLineY: maxLineY,
|
|
4287
|
+
newMinLineY: minLineY
|
|
4288
|
+
};
|
|
4257
4289
|
};
|
|
4258
4290
|
|
|
4259
|
-
const
|
|
4260
|
-
|
|
4291
|
+
const adjustScrollAfterPaste = (state, focusedIndex) => {
|
|
4292
|
+
const {
|
|
4293
|
+
itemHeight,
|
|
4294
|
+
maxLineY,
|
|
4295
|
+
minLineY
|
|
4296
|
+
} = state;
|
|
4297
|
+
const {
|
|
4298
|
+
newMaxLineY,
|
|
4299
|
+
newMinLineY
|
|
4300
|
+
} = scrollInto(focusedIndex, minLineY, maxLineY);
|
|
4301
|
+
const newDeltaY = newMinLineY * itemHeight;
|
|
4302
|
+
return {
|
|
4303
|
+
...state,
|
|
4304
|
+
deltaY: newDeltaY,
|
|
4305
|
+
focused: true,
|
|
4306
|
+
focusedIndex,
|
|
4307
|
+
maxLineY: newMaxLineY,
|
|
4308
|
+
minLineY: newMinLineY
|
|
4309
|
+
};
|
|
4261
4310
|
};
|
|
4262
4311
|
|
|
4263
|
-
const
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
|
|
4312
|
+
const generateUniqueName = (baseName, existingPaths, root) => {
|
|
4313
|
+
// Handle files with extensions
|
|
4314
|
+
const lastDotIndex = baseName.lastIndexOf('.');
|
|
4315
|
+
const hasExtension = lastDotIndex !== -1 && lastDotIndex !== 0 && lastDotIndex !== baseName.length - 1;
|
|
4316
|
+
let nameWithoutExtension;
|
|
4317
|
+
let extension;
|
|
4318
|
+
if (hasExtension) {
|
|
4319
|
+
nameWithoutExtension = baseName.slice(0, lastDotIndex);
|
|
4320
|
+
extension = baseName.slice(lastDotIndex);
|
|
4321
|
+
} else {
|
|
4322
|
+
nameWithoutExtension = baseName;
|
|
4323
|
+
extension = '';
|
|
4269
4324
|
}
|
|
4270
|
-
return decorations.filter(isValid);
|
|
4271
|
-
};
|
|
4272
4325
|
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4326
|
+
// Check if original name exists
|
|
4327
|
+
const originalPath = join2(root, baseName);
|
|
4328
|
+
if (!existingPaths.includes(originalPath)) {
|
|
4329
|
+
return baseName;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
// Try "original copy"
|
|
4333
|
+
const copyName = `${nameWithoutExtension} copy${extension}`;
|
|
4334
|
+
const copyPath = join2(root, copyName);
|
|
4335
|
+
if (!existingPaths.includes(copyPath)) {
|
|
4336
|
+
return copyName;
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
// Try "original copy 1", "original copy 2", etc.
|
|
4340
|
+
let counter = 1;
|
|
4341
|
+
while (true) {
|
|
4342
|
+
const numberedCopyName = `${nameWithoutExtension} copy ${counter}${extension}`;
|
|
4343
|
+
const numberedCopyPath = join2(root, numberedCopyName);
|
|
4344
|
+
if (!existingPaths.includes(numberedCopyPath)) {
|
|
4345
|
+
return numberedCopyName;
|
|
4281
4346
|
}
|
|
4282
|
-
|
|
4283
|
-
const providerId = providerIds.at(-1);
|
|
4284
|
-
const uris = ensureUris(maybeUris);
|
|
4285
|
-
const decorations = await invoke$1('SourceControl.getFileDecorations', providerId, uris, assetDir, platform);
|
|
4286
|
-
const normalized = normalizeDecorations(decorations);
|
|
4287
|
-
return normalized;
|
|
4288
|
-
} catch (error) {
|
|
4289
|
-
console.error(error);
|
|
4290
|
-
return [];
|
|
4347
|
+
counter++;
|
|
4291
4348
|
}
|
|
4292
4349
|
};
|
|
4293
4350
|
|
|
4294
|
-
const
|
|
4295
|
-
const
|
|
4296
|
-
const
|
|
4297
|
-
|
|
4298
|
-
|
|
4351
|
+
const getFileOperationsCopy = (root, existingUris, files, focusedUri) => {
|
|
4352
|
+
const operations = [];
|
|
4353
|
+
for (const file of files) {
|
|
4354
|
+
const baseName = getBaseName('/', file);
|
|
4355
|
+
if (existingUris.includes(file)) {
|
|
4356
|
+
operations.push({
|
|
4357
|
+
from: file,
|
|
4358
|
+
path: join2(focusedUri, baseName),
|
|
4359
|
+
type: Rename$2
|
|
4360
|
+
});
|
|
4361
|
+
} else {
|
|
4362
|
+
const uniqueName = generateUniqueName(baseName, existingUris, root);
|
|
4363
|
+
const newUri = join2(root, uniqueName);
|
|
4364
|
+
operations.push({
|
|
4365
|
+
from: file,
|
|
4366
|
+
// TODO ensure file is uri
|
|
4367
|
+
path: newUri,
|
|
4368
|
+
type: Copy$1
|
|
4369
|
+
});
|
|
4370
|
+
}
|
|
4299
4371
|
}
|
|
4300
|
-
return
|
|
4372
|
+
return operations;
|
|
4301
4373
|
};
|
|
4302
4374
|
|
|
4303
|
-
const
|
|
4304
|
-
// TODO
|
|
4305
|
-
// TODO
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
const
|
|
4312
|
-
|
|
4313
|
-
|
|
4375
|
+
const handlePasteCopy = async (state, nativeFiles) => {
|
|
4376
|
+
// TODO handle pasting files into nested folder
|
|
4377
|
+
// TODO handle pasting files into symlink
|
|
4378
|
+
// TODO handle pasting files into broken symlink
|
|
4379
|
+
// TODO handle pasting files into hardlink
|
|
4380
|
+
// TODO what if folder is big and it takes a long time
|
|
4381
|
+
|
|
4382
|
+
// TODO use file operations and bulk edit
|
|
4383
|
+
const {
|
|
4384
|
+
focusedIndex,
|
|
4385
|
+
items,
|
|
4386
|
+
root
|
|
4387
|
+
} = state;
|
|
4388
|
+
const focusedUri = items[focusedIndex]?.path || root;
|
|
4389
|
+
const existingUris = items.map(item => item.path);
|
|
4390
|
+
const operations = getFileOperationsCopy(root, existingUris, nativeFiles.files, focusedUri);
|
|
4391
|
+
// TODO handle error?
|
|
4392
|
+
await applyFileOperations(operations);
|
|
4393
|
+
|
|
4394
|
+
// TODO use refreshExplorer with the paths that have been affected by file operations
|
|
4395
|
+
// TODO only update folder at which level it changed
|
|
4396
|
+
const latestState = await refresh(state);
|
|
4397
|
+
|
|
4398
|
+
// Focus on the first newly created file and adjust scroll position
|
|
4399
|
+
const newFilePaths = operations.map(operation => operation.path);
|
|
4400
|
+
if (newFilePaths.length > 0) {
|
|
4401
|
+
const firstNewFilePath = newFilePaths[0];
|
|
4402
|
+
const newFileIndex = getIndex(latestState.items, firstNewFilePath);
|
|
4403
|
+
if (newFileIndex !== -1) {
|
|
4404
|
+
const adjustedState = adjustScrollAfterPaste(latestState, newFileIndex);
|
|
4405
|
+
return {
|
|
4406
|
+
...adjustedState,
|
|
4407
|
+
pasteShouldMove: false
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
// If there are no items, ensure focusedIndex is 0
|
|
4412
|
+
if (latestState.items.length === 0) {
|
|
4413
|
+
return {
|
|
4414
|
+
...latestState,
|
|
4415
|
+
focusedIndex: 0,
|
|
4416
|
+
pasteShouldMove: false
|
|
4417
|
+
};
|
|
4418
|
+
}
|
|
4314
4419
|
return {
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
sourceControlDecorations,
|
|
4318
|
-
useChevrons
|
|
4420
|
+
...latestState,
|
|
4421
|
+
pasteShouldMove: false
|
|
4319
4422
|
};
|
|
4320
4423
|
};
|
|
4321
4424
|
|
|
4322
|
-
const
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
if (excluded.includes(child.name)) {
|
|
4333
|
-
continue;
|
|
4334
|
-
}
|
|
4335
|
-
visible.push(child);
|
|
4425
|
+
const getOperations = (toUri, files) => {
|
|
4426
|
+
const operations = [];
|
|
4427
|
+
for (const file of files) {
|
|
4428
|
+
const baseName = getBaseName('/', file);
|
|
4429
|
+
const newUri = join2(toUri, baseName);
|
|
4430
|
+
operations.push({
|
|
4431
|
+
from: file,
|
|
4432
|
+
path: newUri,
|
|
4433
|
+
type: Rename$2
|
|
4434
|
+
});
|
|
4336
4435
|
}
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
type
|
|
4343
|
-
} = child;
|
|
4344
|
-
const childPath = displayRoot + name;
|
|
4345
|
-
if ((child.type === Directory || child.type === SymLinkFolder) && childPath in map) {
|
|
4346
|
-
dirents.push({
|
|
4347
|
-
depth,
|
|
4348
|
-
icon: '',
|
|
4349
|
-
name,
|
|
4350
|
-
path: childPath,
|
|
4351
|
-
posInSet: i + 1,
|
|
4352
|
-
setSize: visibleLength,
|
|
4353
|
-
type: DirectoryExpanded
|
|
4354
|
-
});
|
|
4355
|
-
dirents.push(...getSavedChildDirents(map, childPath, depth + 1, excluded, pathSeparator));
|
|
4356
|
-
} else {
|
|
4357
|
-
dirents.push({
|
|
4358
|
-
depth,
|
|
4359
|
-
icon: '',
|
|
4360
|
-
name,
|
|
4361
|
-
path: childPath,
|
|
4362
|
-
posInSet: i + 1,
|
|
4363
|
-
setSize: visibleLength,
|
|
4364
|
-
type
|
|
4365
|
-
});
|
|
4366
|
-
}
|
|
4436
|
+
return operations;
|
|
4437
|
+
};
|
|
4438
|
+
const getTargetUri = (root, items, index) => {
|
|
4439
|
+
if (index === -1) {
|
|
4440
|
+
return root;
|
|
4367
4441
|
}
|
|
4368
|
-
return
|
|
4442
|
+
return items[index].path;
|
|
4369
4443
|
};
|
|
4444
|
+
const handlePasteCut = async (state, nativeFiles) => {
|
|
4445
|
+
const {
|
|
4446
|
+
focusedIndex,
|
|
4447
|
+
items,
|
|
4448
|
+
pathSeparator,
|
|
4449
|
+
root
|
|
4450
|
+
} = state;
|
|
4451
|
+
// TODO root is not necessrily target uri
|
|
4452
|
+
const targetUri = getTargetUri(root, items, focusedIndex);
|
|
4453
|
+
const operations = getOperations(targetUri, nativeFiles.files);
|
|
4454
|
+
await applyFileOperations(operations);
|
|
4370
4455
|
|
|
4371
|
-
|
|
4372
|
-
const
|
|
4456
|
+
// Refresh the state after cut operations
|
|
4457
|
+
const latestState = await refresh(state);
|
|
4373
4458
|
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
const
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4459
|
+
// Focus on the first pasted file and adjust scroll position
|
|
4460
|
+
if (nativeFiles.files.length > 0) {
|
|
4461
|
+
const firstPastedFile = nativeFiles.files[0];
|
|
4462
|
+
const targetPath = `${root}${pathSeparator}${getBaseName(pathSeparator, firstPastedFile)}`;
|
|
4463
|
+
const pastedFileIndex = getIndex(latestState.items, targetPath);
|
|
4464
|
+
if (pastedFileIndex !== -1) {
|
|
4465
|
+
const adjustedState = adjustScrollAfterPaste(latestState, pastedFileIndex);
|
|
4466
|
+
return {
|
|
4467
|
+
...adjustedState,
|
|
4468
|
+
cutItems: [],
|
|
4469
|
+
pasteShouldMove: false
|
|
4470
|
+
};
|
|
4382
4471
|
}
|
|
4383
4472
|
}
|
|
4384
|
-
|
|
4385
|
-
|
|
4473
|
+
return {
|
|
4474
|
+
...latestState,
|
|
4475
|
+
cutItems: [],
|
|
4476
|
+
pasteShouldMove: false
|
|
4477
|
+
};
|
|
4386
4478
|
};
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4479
|
+
|
|
4480
|
+
const None$1 = 'none';
|
|
4481
|
+
|
|
4482
|
+
const handlePaste = async state => {
|
|
4483
|
+
const nativeFiles = await readNativeFiles();
|
|
4484
|
+
if (!nativeFiles) {
|
|
4485
|
+
return state;
|
|
4393
4486
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
// TODO
|
|
4398
|
-
//
|
|
4399
|
-
//
|
|
4400
|
-
//
|
|
4401
|
-
//
|
|
4402
|
-
|
|
4403
|
-
if
|
|
4404
|
-
|
|
4487
|
+
// TODO detect cut/paste event, not sure if that is possible
|
|
4488
|
+
// TODO check that pasted folder is not a parent folder of opened folder
|
|
4489
|
+
// TODO support pasting multiple paths
|
|
4490
|
+
// TODO what happens when pasting multiple paths, but some of them error?
|
|
4491
|
+
// how many error messages should be shown? Should the operation be undone?
|
|
4492
|
+
// TODO what if it is a large folder and takes a long time to copy? Should show progress
|
|
4493
|
+
// TODO what if there is a permission error? Probably should show a modal to ask for permission
|
|
4494
|
+
// TODO if error is EEXISTS, just rename the copy (e.g. file-copy-1.txt, file-copy-2.txt)
|
|
4495
|
+
// TODO actual target should be selected folder
|
|
4496
|
+
// TODO but what if a file is currently selected? Then maybe the parent folder
|
|
4497
|
+
// TODO but will it work if the folder is a symlink?
|
|
4498
|
+
// TODO handle error gracefully when copy fails
|
|
4499
|
+
|
|
4500
|
+
// If no files to paste, return original state unchanged
|
|
4501
|
+
if (nativeFiles.type === None$1) {
|
|
4502
|
+
return state;
|
|
4405
4503
|
}
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
if (
|
|
4409
|
-
|
|
4504
|
+
|
|
4505
|
+
// Use the pasteShouldMove flag to determine whether to cut or copy
|
|
4506
|
+
if (state.pasteShouldMove) {
|
|
4507
|
+
return handlePasteCut(state, nativeFiles);
|
|
4410
4508
|
}
|
|
4411
|
-
|
|
4412
|
-
return dirents;
|
|
4509
|
+
return handlePasteCopy(state, nativeFiles);
|
|
4413
4510
|
};
|
|
4414
4511
|
|
|
4415
|
-
const
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
}
|
|
4512
|
+
const handlePointerDown = (state, button, x, y) => {
|
|
4513
|
+
const index = getIndexFromPosition(state, x, y);
|
|
4514
|
+
if (button === LeftClick && index === -1) {
|
|
4515
|
+
return {
|
|
4516
|
+
...state,
|
|
4517
|
+
focus: List,
|
|
4518
|
+
focused: true,
|
|
4519
|
+
focusedIndex: -1
|
|
4520
|
+
};
|
|
4425
4521
|
}
|
|
4426
|
-
return
|
|
4427
|
-
};
|
|
4428
|
-
const getSavedRoot = (savedState, workspacePath) => {
|
|
4429
|
-
return workspacePath;
|
|
4522
|
+
return state;
|
|
4430
4523
|
};
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4524
|
+
|
|
4525
|
+
const getScrollBarSize = (size, contentSize, minimumSliderSize) => {
|
|
4526
|
+
if (size >= contentSize) {
|
|
4527
|
+
return 0;
|
|
4434
4528
|
}
|
|
4435
|
-
return
|
|
4529
|
+
return Math.max(Math.round(size ** 2 / contentSize), minimumSliderSize);
|
|
4436
4530
|
};
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4531
|
+
|
|
4532
|
+
const handleResize = (state, dimensions) => {
|
|
4533
|
+
const {
|
|
4534
|
+
height: rawHeight,
|
|
4535
|
+
width: rawWidth
|
|
4536
|
+
} = dimensions;
|
|
4537
|
+
if (!Number.isFinite(rawHeight) || !Number.isFinite(rawWidth)) {
|
|
4538
|
+
return state;
|
|
4440
4539
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4540
|
+
const height = Math.max(0, rawHeight);
|
|
4541
|
+
const width = Math.max(0, rawWidth);
|
|
4542
|
+
const {
|
|
4543
|
+
deltaY,
|
|
4544
|
+
itemHeight,
|
|
4545
|
+
items
|
|
4546
|
+
} = state;
|
|
4547
|
+
const contentHeight = items.length * itemHeight;
|
|
4548
|
+
const maxDeltaY = Math.max(contentHeight - height, 0);
|
|
4549
|
+
const newDeltaY = Math.min(Math.max(deltaY, 0), maxDeltaY);
|
|
4550
|
+
const minLineY = Math.round(newDeltaY / itemHeight);
|
|
4551
|
+
const maxLineY = getExplorerMaxLineY(minLineY, height, itemHeight, items.length);
|
|
4552
|
+
const scrollBarHeight = getScrollBarSize(height, contentHeight, 20);
|
|
4553
|
+
if (state.height === height && state.width === width && state.deltaY === newDeltaY && state.minLineY === minLineY && state.maxLineY === maxLineY && state.scrollBarHeight === scrollBarHeight) {
|
|
4554
|
+
return state;
|
|
4443
4555
|
}
|
|
4444
|
-
return
|
|
4556
|
+
return {
|
|
4557
|
+
...state,
|
|
4558
|
+
deltaY: newDeltaY,
|
|
4559
|
+
height,
|
|
4560
|
+
maxLineY,
|
|
4561
|
+
minLineY,
|
|
4562
|
+
scrollBarHeight,
|
|
4563
|
+
width
|
|
4564
|
+
};
|
|
4445
4565
|
};
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4566
|
+
|
|
4567
|
+
const handleUpload = async (state, dirents) => {
|
|
4568
|
+
const {
|
|
4569
|
+
pathSeparator,
|
|
4570
|
+
root
|
|
4571
|
+
} = state;
|
|
4572
|
+
for (const dirent of dirents) {
|
|
4573
|
+
// TODO switch
|
|
4574
|
+
// TODO symlink might not be possible to be copied
|
|
4575
|
+
// TODO create folder if type is 2
|
|
4576
|
+
if (dirent.type === /* File */1) {
|
|
4577
|
+
// TODO reading text might be inefficient for binary files
|
|
4578
|
+
// but not sure how else to send them via jsonrpc
|
|
4579
|
+
const content = await dirent.file.text();
|
|
4580
|
+
const absolutePath = [root, dirent.file.name].join(pathSeparator);
|
|
4581
|
+
await writeFile(absolutePath, content);
|
|
4582
|
+
}
|
|
4459
4583
|
}
|
|
4460
4584
|
};
|
|
4461
|
-
|
|
4585
|
+
|
|
4586
|
+
const setDeltaY = async (state, deltaY) => {
|
|
4587
|
+
if (!Number.isFinite(deltaY)) {
|
|
4588
|
+
return state;
|
|
4589
|
+
}
|
|
4462
4590
|
const {
|
|
4463
|
-
|
|
4464
|
-
|
|
4591
|
+
height,
|
|
4592
|
+
itemHeight,
|
|
4593
|
+
items
|
|
4465
4594
|
} = state;
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
} = await getSettings();
|
|
4471
|
-
const workspacePath = await getWorkspacePath();
|
|
4472
|
-
const root = getSavedRoot(savedState, workspacePath);
|
|
4473
|
-
try {
|
|
4474
|
-
// TODO path separator could be restored from saved state
|
|
4475
|
-
const pathSeparator = await getPathSeparator(root); // TODO only load path separator once
|
|
4476
|
-
const excluded = getExcluded();
|
|
4477
|
-
const restoredDirents = await restoreExpandedState(savedState, root, pathSeparator, excluded);
|
|
4478
|
-
let minLineY = 0;
|
|
4479
|
-
if (savedState && typeof savedState.minLineY === 'number') {
|
|
4480
|
-
minLineY = savedState.minLineY;
|
|
4481
|
-
}
|
|
4482
|
-
let deltaY = 0;
|
|
4483
|
-
if (savedState && typeof savedState.deltaY === 'number') {
|
|
4484
|
-
deltaY = savedState.deltaY;
|
|
4485
|
-
}
|
|
4486
|
-
const scheme = getScheme(root);
|
|
4487
|
-
const decorations = await getFileDecorations(scheme, root, restoredDirents.filter(item => item.depth === 1).map(item => item.path), sourceControlDecorations, assetDir, platform);
|
|
4488
|
-
return {
|
|
4489
|
-
...state,
|
|
4490
|
-
confirmDelete,
|
|
4491
|
-
decorations,
|
|
4492
|
-
deltaY,
|
|
4493
|
-
errorCode: '',
|
|
4494
|
-
errorMessage: '',
|
|
4495
|
-
excluded,
|
|
4496
|
-
hasError: false,
|
|
4497
|
-
initial: false,
|
|
4498
|
-
items: restoredDirents,
|
|
4499
|
-
maxIndent: 10,
|
|
4500
|
-
minLineY,
|
|
4501
|
-
pathSeparator,
|
|
4502
|
-
root,
|
|
4503
|
-
useChevrons
|
|
4504
|
-
};
|
|
4505
|
-
} catch (error) {
|
|
4506
|
-
const errorCode = getErrorCode(error);
|
|
4507
|
-
const errorMessage = getFriendlyErrorMessage(getErrorMessage(error), errorCode);
|
|
4508
|
-
return {
|
|
4509
|
-
...state,
|
|
4510
|
-
confirmDelete,
|
|
4511
|
-
errorCode,
|
|
4512
|
-
errorMessage,
|
|
4513
|
-
hasError: true,
|
|
4514
|
-
initial: false,
|
|
4515
|
-
items: [],
|
|
4516
|
-
root,
|
|
4517
|
-
useChevrons
|
|
4518
|
-
};
|
|
4595
|
+
if (deltaY < 0) {
|
|
4596
|
+
deltaY = 0;
|
|
4597
|
+
} else if (deltaY > items.length * itemHeight - height) {
|
|
4598
|
+
deltaY = Math.max(items.length * itemHeight - height, 0);
|
|
4519
4599
|
}
|
|
4600
|
+
if (state.deltaY === deltaY) {
|
|
4601
|
+
return state;
|
|
4602
|
+
}
|
|
4603
|
+
const minLineY = Math.round(deltaY / itemHeight);
|
|
4604
|
+
const maxLineY = minLineY + Math.round(height / itemHeight);
|
|
4605
|
+
return {
|
|
4606
|
+
...state,
|
|
4607
|
+
deltaY,
|
|
4608
|
+
maxLineY,
|
|
4609
|
+
minLineY
|
|
4610
|
+
};
|
|
4611
|
+
};
|
|
4612
|
+
|
|
4613
|
+
const handleWheel = (state, deltaMode, deltaY) => {
|
|
4614
|
+
return setDeltaY(state, state.deltaY + deltaY);
|
|
4520
4615
|
};
|
|
4521
4616
|
|
|
4522
4617
|
const handleWorkspaceChange = async state => {
|
|
@@ -4884,10 +4979,17 @@ const HandlePointerDown = 14;
|
|
|
4884
4979
|
const HandleWheel = 15;
|
|
4885
4980
|
const HandleDoubleClick = 16;
|
|
4886
4981
|
|
|
4887
|
-
const
|
|
4982
|
+
const getClassName$1 = dropTargets => {
|
|
4983
|
+
const extraClassName = dropTargets === dropTargetFull ? ExplorerDropTarget : Empty;
|
|
4984
|
+
return mergeClassNames(Viewlet, Explorer$1, extraClassName);
|
|
4985
|
+
};
|
|
4986
|
+
const getExplorerWelcomeVirtualDom = (isWide, dropTargets) => {
|
|
4888
4987
|
return [{
|
|
4889
4988
|
childCount: 1,
|
|
4890
|
-
className:
|
|
4989
|
+
className: getClassName$1(dropTargets),
|
|
4990
|
+
onDragLeave: HandleDragLeave,
|
|
4991
|
+
onDragOver: HandleDragOver,
|
|
4992
|
+
onDrop: HandleDrop,
|
|
4891
4993
|
tabIndex: 0,
|
|
4892
4994
|
type: Div
|
|
4893
4995
|
}, {
|
|
@@ -5110,7 +5212,7 @@ const getChildCount = (scrollBarDomLength, errorDomLength) => {
|
|
|
5110
5212
|
};
|
|
5111
5213
|
const getExplorerVirtualDom = (visibleItems, focusedIndex, root, isWide, focused, dropTargets, height, contentHeight, editingErrorMessage, loadErrorMessage) => {
|
|
5112
5214
|
if (!root) {
|
|
5113
|
-
return getExplorerWelcomeVirtualDom(isWide);
|
|
5215
|
+
return getExplorerWelcomeVirtualDom(isWide, dropTargets);
|
|
5114
5216
|
}
|
|
5115
5217
|
if (loadErrorMessage) {
|
|
5116
5218
|
return getLoadErrorVirtualDom(loadErrorMessage);
|