mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-29 16:45:13 +02:00
add copy/cut/paste to cxt menu and tweak position
This commit is contained in:
parent
685cdeddc3
commit
1847ecc47b
@ -16,98 +16,138 @@ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 bg-transparent"
|
||||
tabindex="-1"
|
||||
@click="show = false"
|
||||
@contextmenu.prevent="show = false"
|
||||
@keypress="show = false"
|
||||
></div>
|
||||
<transition
|
||||
enter-active-class="origin-top-left transition ease-out duration-100"
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="origin-top-left transition ease-in duration-75"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
@enter="setPosition"
|
||||
@after-leave="reset"
|
||||
>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 bg-transparent"
|
||||
class="fixed z-20 max-w-sm flex flex-col items-stretch bg-default shadow-lg divide-y divide-default"
|
||||
tabindex="-1"
|
||||
@click="show = false"
|
||||
@contextmenu.prevent="show = false"
|
||||
@keypress="show = false"
|
||||
ref="contextMenuRef"
|
||||
>
|
||||
<div
|
||||
class="fixed z-20 max-w-sm flex flex-col items-stretch bg-default shadow-lg divide-y divide-default position-contextmenu">
|
||||
<div class="flex items-stretch">
|
||||
<div class="flex items-stretch">
|
||||
<button
|
||||
:disabled="!pathHistory?.backAllowed()"
|
||||
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.backAllowed() }"
|
||||
@click="$emit('browserAction', 'back')"
|
||||
>
|
||||
<ArrowLeftIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
<button
|
||||
:disabled="!pathHistory?.forwardAllowed()"
|
||||
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.forwardAllowed() }"
|
||||
@click="$emit('browserAction', 'forward')"
|
||||
>
|
||||
<ArrowRightIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
<button
|
||||
class="grow hover:bg-red-600/10 flex items-center justify-center p-2"
|
||||
@click="$emit('browserAction', 'up')"
|
||||
>
|
||||
<ArrowUpIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<!-- Non-selection actions -->
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'createFile', selection[0])"
|
||||
>
|
||||
<DocumentAddIcon class="size-icon icon-default" />
|
||||
<span>New file</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'createDirectory', selection[0])"
|
||||
>
|
||||
<FolderAddIcon class="size-icon icon-default" />
|
||||
<span>New directory</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'createLink', selection[0])"
|
||||
>
|
||||
<LinkIcon class="size-icon icon-default" />
|
||||
<span>New link</span>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="selection.length === 0">
|
||||
<!-- Current directory actions -->
|
||||
<div
|
||||
v-if="clipboard.content.length"
|
||||
class="flex flex-col items-stretch"
|
||||
>
|
||||
<button
|
||||
:disabled="!pathHistory?.backAllowed()"
|
||||
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.backAllowed() }"
|
||||
@click="$emit('browserAction', 'back')"
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'paste', currentDirEntry)"
|
||||
>
|
||||
<ArrowLeftIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
<button
|
||||
:disabled="!pathHistory?.forwardAllowed()"
|
||||
:class="{ 'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.forwardAllowed() }"
|
||||
@click="$emit('browserAction', 'forward')"
|
||||
>
|
||||
<ArrowRightIcon class="size-icon icon-default" />
|
||||
</button>
|
||||
<button
|
||||
class="grow hover:bg-red-600/10 flex items-center justify-center p-2"
|
||||
@click="$emit('browserAction', 'up')"
|
||||
>
|
||||
<ArrowUpIcon class="size-icon icon-default" />
|
||||
<ClipboardIcon class="size-icon icon-default" />
|
||||
<span>
|
||||
Paste {{ clipboard.content.length }}
|
||||
item{{ clipboard.content.length > 1 ? 's' : '' }}
|
||||
here
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<!-- Non-selection actions -->
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'createFile', selection[0])"
|
||||
@click="$emit('browserAction', 'download', currentDirEntry)"
|
||||
>
|
||||
<DocumentAddIcon class="size-icon icon-default" />
|
||||
<span>New file</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'createDirectory', selection[0])"
|
||||
>
|
||||
<FolderAddIcon class="size-icon icon-default" />
|
||||
<span>New directory</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'createLink', selection[0])"
|
||||
>
|
||||
<LinkIcon class="size-icon icon-default" />
|
||||
<span>New link</span>
|
||||
<FolderDownloadIcon class="size-icon icon-default" />
|
||||
<span>Zip and download current directory</span>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="selection.length === 0">
|
||||
<!-- Current directory actions -->
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'download', currentDirEntry)"
|
||||
>
|
||||
<FolderDownloadIcon class="size-icon icon-default" />
|
||||
<span>Zip and download directory</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
v-if="currentDirEntry?.path !== '/'"
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'editPermissions', currentDirEntry)"
|
||||
>
|
||||
<KeyIcon class="size-icon icon-default" />
|
||||
<span>Edit permissions</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="selection.length === 1">
|
||||
<!-- Single entry selection actions -->
|
||||
<div
|
||||
v-if="selection[0]?.resolvedType === 'f'"
|
||||
class="flex flex-col items-stretch"
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
v-if="currentDirEntry?.path !== '/'"
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'editPermissions', currentDirEntry)"
|
||||
>
|
||||
<!-- regular file actions -->
|
||||
<KeyIcon class="size-icon icon-default" />
|
||||
<span>Edit permissions of current directory</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="selection.length === 1">
|
||||
<!-- Single entry selection actions -->
|
||||
<template v-if="selection[0]?.resolvedType === 'f'">
|
||||
<!-- regular file actions -->
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'cut', selection[0])"
|
||||
>
|
||||
<ScissorsIcon class="size-icon icon-default" />
|
||||
<span>Cut file</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'copy', selection[0])"
|
||||
>
|
||||
<ClipboardCopyIcon class="size-icon icon-default" />
|
||||
<span>Copy file</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'edit', selection[0])"
|
||||
@ -130,11 +170,37 @@ If not, see <https://www.gnu.org/licenses/>.
|
||||
<span>Zip and download</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="selection[0]?.resolvedType === 'd'"
|
||||
class="flex flex-col items-stretch"
|
||||
>
|
||||
<!-- directory actions -->
|
||||
</template>
|
||||
<template v-else-if="selection[0]?.resolvedType === 'd'">
|
||||
<!-- directory actions -->
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'cut', selection[0])"
|
||||
>
|
||||
<ScissorsIcon class="size-icon icon-default" />
|
||||
<span>Cut directory</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'copy', selection[0])"
|
||||
>
|
||||
<ClipboardCopyIcon class="size-icon icon-default" />
|
||||
<span>Copy directory</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="clipboard.content.length"
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'paste', selection[0])"
|
||||
>
|
||||
<ClipboardIcon class="size-icon icon-default" />
|
||||
<span>
|
||||
Paste {{ clipboard.content.length }}
|
||||
item{{ clipboard.content.length > 1 ? 's' : '' }} into directory
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'cd', selection[0])"
|
||||
@ -150,72 +216,88 @@ If not, see <https://www.gnu.org/licenses/>.
|
||||
<span>Zip and download directory</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="selection[0]?.type === 'l'"
|
||||
class="flex flex-col items-stretch"
|
||||
>
|
||||
<!-- link actions -->
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'editLink', selection[0])"
|
||||
>
|
||||
<LinkIcon class="size-icon icon-default" />
|
||||
<span>Edit link target</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<!-- general actions -->
|
||||
<button
|
||||
v-if="selection[0]?.path !== '/'"
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'editPermissions', selection[0])"
|
||||
>
|
||||
<KeyIcon class="size-icon icon-default" />
|
||||
<span>Edit permissions</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="selection[0]?.path !== '/'"
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'rename', selection[0])"
|
||||
>
|
||||
<PencilIcon class="size-icon icon-default" />
|
||||
<span>Rename</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'delete', selection[0])"
|
||||
>
|
||||
<TrashIcon class="size-icon icon-default" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<tempalte v-else>
|
||||
<!-- Multi-entry selection actions -->
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'download', [...selection])"
|
||||
>
|
||||
<DownloadIcon class="size-icon-sm icon-default" />
|
||||
<span>Zip and download {{ selection.length }} items</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'delete', [...selection])"
|
||||
>
|
||||
<TrashIcon class="size-icon-sm icon-default" />
|
||||
<span>Delete {{ selection.length }} items</span>
|
||||
</button>
|
||||
</div>
|
||||
</tempalte>
|
||||
</div>
|
||||
<div
|
||||
v-if="selection[0]?.type === 'l'"
|
||||
class="flex flex-col items-stretch"
|
||||
>
|
||||
<!-- link actions -->
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'editLink', selection[0])"
|
||||
>
|
||||
<LinkIcon class="size-icon icon-default" />
|
||||
<span>Edit link target</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<!-- general actions -->
|
||||
<button
|
||||
v-if="selection[0]?.path !== '/'"
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'editPermissions', selection[0])"
|
||||
>
|
||||
<KeyIcon class="size-icon icon-default" />
|
||||
<span>Edit permissions</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="selection[0]?.path !== '/'"
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'rename', selection[0])"
|
||||
>
|
||||
<PencilIcon class="size-icon icon-default" />
|
||||
<span>Rename</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'delete', selection[0])"
|
||||
>
|
||||
<TrashIcon class="size-icon icon-danger" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Multi-entry selection actions -->
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'cut', [...selection])"
|
||||
>
|
||||
<ScissorsIcon class="size-icon icon-default" />
|
||||
<span>Cut {{ selection.length }} items</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('directoryViewAction', 'copy', [...selection])"
|
||||
>
|
||||
<ClipboardCopyIcon class="size-icon icon-default" />
|
||||
<span>Copy {{ selection.length }} items</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-stretch">
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'download', [...selection])"
|
||||
>
|
||||
<DownloadIcon class="size-icon-sm icon-default" />
|
||||
<span>Zip and download {{ selection.length }} items</span>
|
||||
</button>
|
||||
<button
|
||||
class="context-menu-button"
|
||||
@click="$emit('browserAction', 'delete', [...selection])"
|
||||
>
|
||||
<TrashIcon class="size-icon-sm icon-danger" />
|
||||
<span>Delete {{ selection.length }} items</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { inject, ref, computed } from 'vue'
|
||||
import { inject, ref, computed, nextTick } from 'vue'
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
@ -231,8 +313,11 @@ import {
|
||||
PencilIcon,
|
||||
KeyIcon,
|
||||
DownloadIcon,
|
||||
ClipboardIcon,
|
||||
ClipboardCopyIcon,
|
||||
ScissorsIcon,
|
||||
} from '@heroicons/vue/solid';
|
||||
import { pathHistoryInjectionKey } from '../keys';
|
||||
import { pathHistoryInjectionKey, clipboardInjectionKey } from '../keys';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -240,13 +325,18 @@ export default {
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const pathHistory = inject(pathHistoryInjectionKey);
|
||||
const clipboard = inject(clipboardInjectionKey);
|
||||
const show = ref();
|
||||
const event = ref();
|
||||
const selection = ref();
|
||||
const currentDirEntry = computed(() => ({
|
||||
...props.currentPath,
|
||||
name: `Current directory (${props.currentPath.path.split('/').pop()})`
|
||||
name: `Current directory (${props.currentPath.path.split('/').pop()})`,
|
||||
type: 'd',
|
||||
resolvedType: 'd',
|
||||
resolvedPath: props.currentPath.path,
|
||||
}));
|
||||
const contextMenuRef = ref();
|
||||
|
||||
/**
|
||||
* Open the context menu
|
||||
@ -260,6 +350,30 @@ export default {
|
||||
show.value = true;
|
||||
}
|
||||
|
||||
const setPosition = (el, done) => {
|
||||
// const { width: menuWidth, height: menuHeight } = el.getBoundingClientRect();
|
||||
const menuWidth = el.scrollWidth;
|
||||
const menuHeight = el.scrollHeight;
|
||||
let x = event.value.clientX;
|
||||
let y = event.value.clientY;
|
||||
let origin;
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
y -= menuHeight;
|
||||
origin = 'bottom';
|
||||
} else {
|
||||
origin = 'top';
|
||||
}
|
||||
if (x + menuWidth > window.innerWidth) {
|
||||
x -= menuWidth;
|
||||
origin += ' right';
|
||||
} else {
|
||||
origin += ' left';
|
||||
}
|
||||
el.style.left = `${x}px`;
|
||||
el.style.top = `${y}px`;
|
||||
el.style.transformOrigin = origin;
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
event.value = null;
|
||||
selection.value = [];
|
||||
@ -268,12 +382,15 @@ export default {
|
||||
return {
|
||||
// data
|
||||
pathHistory,
|
||||
clipboard,
|
||||
show,
|
||||
event,
|
||||
selection,
|
||||
currentDirEntry,
|
||||
contextMenuRef,
|
||||
// methods
|
||||
open,
|
||||
setPosition,
|
||||
reset,
|
||||
}
|
||||
},
|
||||
@ -292,22 +409,21 @@ export default {
|
||||
PencilIcon,
|
||||
KeyIcon,
|
||||
DownloadIcon,
|
||||
ClipboardIcon,
|
||||
ClipboardCopyIcon,
|
||||
ScissorsIcon,
|
||||
},
|
||||
emits: [
|
||||
'hide',
|
||||
'browserAction',
|
||||
'directoryViewAction',
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div.position-contextmenu {
|
||||
top: v-bind("`${event?.clientY ?? 0}px`");
|
||||
left: v-bind("`${event?.clientX ?? 0}px`");
|
||||
}
|
||||
|
||||
button.context-menu-button {
|
||||
@apply text-default font-normal pl-1 pr-2 text-sm text-left flex items-center gap-1;
|
||||
@apply text-default font-normal pl-1 pr-2 py-1 text-sm text-left flex items-center gap-1;
|
||||
}
|
||||
|
||||
button.context-menu-button:hover {
|
||||
|
@ -26,7 +26,7 @@ If not, see <https://www.gnu.org/licenses/>.
|
||||
class="h-full"
|
||||
@selectRectangle="selectRectangle"
|
||||
@mouseup.exact="deselectAll"
|
||||
@contextmenu.prevent="$emit('browserAction', 'contextMenu', $event)"
|
||||
@contextmenu.prevent="contextMenuCallback"
|
||||
>
|
||||
<Table
|
||||
:key="host + path"
|
||||
@ -36,6 +36,7 @@ If not, see <https://www.gnu.org/licenses/>.
|
||||
stickyHeaders
|
||||
noShrink
|
||||
noShrinkHeight="h-full"
|
||||
class="border-x-0"
|
||||
>
|
||||
<template #thead>
|
||||
<tr>
|
||||
@ -249,6 +250,7 @@ import LoadingSpinner from './LoadingSpinner.vue';
|
||||
import SortCallbackButton from './SortCallbackButton.vue';
|
||||
import DirectoryEntryList from './DirectoryEntryList.vue';
|
||||
import DragSelectArea from './DragSelectArea.vue';
|
||||
import { commonPath } from '../functions/commonPath';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -331,7 +333,7 @@ export default {
|
||||
selectedCount.value = gatherEntries().map(entry => entry.selected = true).length ?? 0;
|
||||
}
|
||||
|
||||
const deselectAll = (event) => {
|
||||
const deselectAll = () => {
|
||||
gatherEntries([], false).map(entry => entry.selected = false);
|
||||
selectedCount.value = 0;
|
||||
}
|
||||
@ -353,11 +355,64 @@ export default {
|
||||
tallySelected();
|
||||
}
|
||||
|
||||
const unCutEntries = () => gatherEntries([], false).map(entry => entry.cut = false);
|
||||
|
||||
const clipboardStore = (items, cut = false, append = false) => {
|
||||
if (!append)
|
||||
unCutEntries();
|
||||
const newContent = items.map(entry => {
|
||||
entry.cut = cut;
|
||||
return {
|
||||
uniqueId: entry.uniqueId,
|
||||
host: entry.host,
|
||||
path: entry.path,
|
||||
name: entry.name,
|
||||
cut,
|
||||
clipboardRelativePath: entry.path.slice(props.path.length).replace(/^\//, '')
|
||||
};
|
||||
});
|
||||
if (append)
|
||||
clipboard.content = [...newContent, ...clipboard.content].filter((a, index, arr) => arr.findIndex(b => b.uniqueId === a.uniqueId) === index);
|
||||
else
|
||||
clipboard.content = newContent;
|
||||
const message = append
|
||||
? `Added ${newContent.length} items to clipboard.\n(${clipboard.content.length} items total)`
|
||||
: `${cut ? 'Cut' : 'Copied'} ${newContent.length} items to clipboard.`;
|
||||
notifications.value.constructNotification('Clipboard', message, 'info', 2000);
|
||||
}
|
||||
|
||||
const paste = (destinations) => {
|
||||
let destination;
|
||||
if (destinations.length === 1) {
|
||||
destination = destinations[0];
|
||||
if (destination.resolvedType !== 'd') {
|
||||
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
|
||||
return;
|
||||
}
|
||||
} else if (destinations.length === 0) {
|
||||
destination = { host: props.host, path: props.path };
|
||||
} else {
|
||||
notifications.value.constructNotification("Paste Failed", 'Cannot paste to multiple directories.', 'error');
|
||||
return;
|
||||
}
|
||||
console.log("paste", clipboard.content, destination);
|
||||
const fullSources = [...clipboard.content];
|
||||
const { common } = commonPath(fullSources.map(item => item.path));
|
||||
const groupedByHost = fullSources.reduce((res, item) => {
|
||||
if (!res[item.host])
|
||||
res[item.host] = [];
|
||||
res[item.host].push(item);
|
||||
return res;
|
||||
}, {});
|
||||
for (const host of Object.keys(groupedByHost)) {
|
||||
console.log(`rsync -avh ${groupedByHost[host].map(a => a.path).join(' ')} ${destination.host}:${destination.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const keyHandler = (event) => {
|
||||
const unCutEntries = () => gatherEntries([], false).map(entry => entry.cut = false);
|
||||
const keypress = event.key.toLowerCase();
|
||||
const handleExact = (keypress) => {
|
||||
switch (keypress) {
|
||||
@ -379,7 +434,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const handleCtrl = (keypress) => {
|
||||
switch (keypress) {
|
||||
@ -392,51 +446,15 @@ export default {
|
||||
break;
|
||||
case 'c':
|
||||
case 'x':
|
||||
const isCut = keypress === 'x';
|
||||
if (!event.shiftKey)
|
||||
unCutEntries();
|
||||
const newContent = getSelected().map(entry => {
|
||||
entry.cut = isCut;
|
||||
return {
|
||||
uniqueId: entry.uniqueId,
|
||||
host: entry.host,
|
||||
path: entry.path,
|
||||
name: entry.name,
|
||||
cut: isCut,
|
||||
clipboardRelativePath: entry.path.slice(props.path.length).replace(/^\//, '')
|
||||
};
|
||||
});
|
||||
if (event.shiftKey)
|
||||
clipboard.content = [...newContent, ...clipboard.content].filter((a, index, arr) => arr.findIndex(b => b.uniqueId === a.uniqueId) === index);
|
||||
else
|
||||
clipboard.content = newContent;
|
||||
const message = event.shiftKey
|
||||
? `Added ${newContent.length} items to clipboard.\n(${clipboard.content.length} items total)`
|
||||
: `Copied ${newContent.length} items to clipboard.`;
|
||||
notifications.value.constructNotification('Clipboard', message, 'info', 2000);
|
||||
clipboardStore(getSelected(), keypress === 'x', event.shiftKey);
|
||||
break;
|
||||
case 'v':
|
||||
const selected = getSelected();
|
||||
let destination;
|
||||
if (selected.length === 1) {
|
||||
destination = selected[0];
|
||||
if (destination.resolvedType !== 'd') {
|
||||
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
|
||||
break;
|
||||
}
|
||||
} else if (selected.length === 0) {
|
||||
destination = { host: props.host, path: props.path };
|
||||
} else {
|
||||
notifications.value.constructNotification("Paste Failed", 'Cannot paste to multiple directories.', 'error');
|
||||
break;
|
||||
}
|
||||
console.log("paste", clipboard.content, destination);
|
||||
paste(getSelected());
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const handleShift = (keypress) => {
|
||||
switch(keypress) {
|
||||
@ -444,7 +462,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const handleCtrlShift = (keypress) => {
|
||||
switch(keypress) {
|
||||
@ -452,7 +469,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const handleAny = (keypress) => {
|
||||
switch(keypress) {
|
||||
@ -460,7 +476,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (event.ctrlKey && event.shiftKey) {
|
||||
handleCtrlShift(keypress);
|
||||
@ -488,11 +503,27 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const contextMenuCallback = (event) => {
|
||||
if (!(event.ctrlKey || event.shiftKey)) {
|
||||
deselectAll();
|
||||
}
|
||||
emit('browserAction', 'contextMenu', event);
|
||||
}
|
||||
|
||||
const handleAction = (action, ...args) => {
|
||||
switch (action) {
|
||||
case 'toggleSelected':
|
||||
toggleSelected(...args);
|
||||
break;
|
||||
case 'copy':
|
||||
clipboardStore(args?.flat(1) ?? getSelected(), false, false);
|
||||
break;
|
||||
case 'cut':
|
||||
clipboardStore(args?.flat(1) ?? getSelected(), true, false);
|
||||
break;
|
||||
case 'paste':
|
||||
paste(args?.flat(1) ?? getSelected());
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown directoryViewAction:', action, args);
|
||||
break;
|
||||
@ -539,6 +570,7 @@ export default {
|
||||
selectAll,
|
||||
deselectAll,
|
||||
selectRectangle,
|
||||
contextMenuCallback,
|
||||
handleAction,
|
||||
tallySelected,
|
||||
}
|
||||
|
@ -192,6 +192,7 @@
|
||||
<ContextMenu
|
||||
:currentPath="pathHistory.current() ?? { path: '/', host: 'localhost' }"
|
||||
@browserAction="handleAction"
|
||||
@directoryViewAction="(...args) => directoryViewRef?.handleAction(...args)"
|
||||
ref="contextMenuRef"
|
||||
/>
|
||||
<ModalPrompt ref="modalPromptRef" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user