mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-31 01:24:37 +02:00
implement clipboard
This commit is contained in:
parent
de67f5a2c6
commit
339aad2d6e
@ -15,7 +15,7 @@ import { ref, reactive, provide, onBeforeMount } from "vue";
|
||||
import SettingsMenu from "./components/SettingsMenu.vue";
|
||||
import Notifications from './components/Notifications.vue';
|
||||
import { FIFO } from '@45drives/cockpit-helpers';
|
||||
import { settingsInjectionKey, notificationsInjectionKey, pathHistoryInjectionKey } from "./keys";
|
||||
import { settingsInjectionKey, notificationsInjectionKey, pathHistoryInjectionKey, clipboardInjectionKey } from "./keys";
|
||||
|
||||
const props = defineProps({ notificationFIFO: FIFO });
|
||||
|
||||
@ -48,6 +48,11 @@ const pathHistory = reactive({
|
||||
forwardAllowed: () => pathHistory.index < pathHistory.stack.length - 1,
|
||||
});
|
||||
provide(pathHistoryInjectionKey, pathHistory);
|
||||
|
||||
const clipboard = reactive({
|
||||
content: [],
|
||||
});
|
||||
provide(clipboardInjectionKey, clipboard);
|
||||
providesValid.value = true;
|
||||
|
||||
const routerViewFooterText = ref("");
|
||||
|
@ -3,11 +3,11 @@
|
||||
<tr
|
||||
v-show="show || showEntries"
|
||||
@dblclick.stop="doubleClickCallback"
|
||||
@click.stop.prevent="$emit('toggleSelected', { ctrlKey: $event.ctrlKey, shiftKey: $event.shiftKey })"
|
||||
@click.prevent.stop="$emit('toggleSelected', { ctrlKey: $event.ctrlKey, shiftKey: $event.shiftKey })"
|
||||
:class="['hover:!bg-red-600/10 select-none']"
|
||||
>
|
||||
<td :class="['!pl-1', ...selectedClasses]">
|
||||
<div :class="['flex items-center gap-1']">
|
||||
<div :class="[entry.cut ? 'line-through' : '', 'flex items-center gap-1']">
|
||||
<div :style="{ width: `${24 * level}px` }"></div>
|
||||
<div class="relative w-6">
|
||||
<component :is="icon" class="size-icon icon-default" />
|
||||
@ -79,7 +79,7 @@
|
||||
v-else
|
||||
v-show="show"
|
||||
@dblclick.stop="doubleClickCallback"
|
||||
@click.stop.prevent="$emit('toggleSelected', { ctrlKey: $event.ctrlKey, shiftKey: $event.shiftKey })"
|
||||
@click.prevent.stop="$emit('toggleSelected', { ctrlKey: $event.ctrlKey, shiftKey: $event.shiftKey })"
|
||||
>
|
||||
<div :class="[...selectedClasses, 'flex flex-col items-center w-20 overflow-hidden select-none']">
|
||||
<div class="relative w-20">
|
||||
@ -150,7 +150,7 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
const getSelected = () => directoryViewRef.value?.getSelected?.() ?? [];
|
||||
const getSelected = () => directoryViewRef.value.getSelected() ?? [];
|
||||
|
||||
const selectAll = () => {
|
||||
directoryViewRef.value?.selection.selectAll();
|
||||
|
@ -21,7 +21,7 @@
|
||||
<script>
|
||||
import { ref, reactive, computed, inject, watch, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { useSpawn, errorString, errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers';
|
||||
import { notificationsInjectionKey, settingsInjectionKey } from '../keys';
|
||||
import { notificationsInjectionKey, settingsInjectionKey, clipboardInjectionKey } from '../keys';
|
||||
import DirectoryEntry from './DirectoryEntry.vue';
|
||||
import getDirListing from '../functions/getDirListing';
|
||||
import getDirEntryObjects from '../functions/getDirEntryObjects';
|
||||
@ -47,6 +47,7 @@ export default {
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const settings = inject(settingsInjectionKey);
|
||||
const clipboard = inject(clipboardInjectionKey);
|
||||
/**
|
||||
* @type {Ref<DirectoryEntryObj[]>}
|
||||
*/
|
||||
@ -162,7 +163,7 @@ export default {
|
||||
);
|
||||
if (props.path !== cwd)
|
||||
return; // changed directory before could finish
|
||||
entries.value = [...tmpEntries.sort(sortCallbackComputed.value)].map(entry => reactive(entry));
|
||||
entries.value = [...tmpEntries.sort(sortCallbackComputed.value)].map(entry => reactive({...entry, cut: clipboard.content.find(a => a.path === entry.path && a.host === entry.host) ?? false}));
|
||||
} catch (error) {
|
||||
entries.value = [];
|
||||
notifications.value.constructNotification("Error getting directory entries", errorStringHTML(error), 'error');
|
||||
|
@ -1,13 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full" @keydown.prevent.stop="keyHandler($event)" tabindex="-1">
|
||||
<Table
|
||||
v-if="settings.directoryView?.view === 'list'"
|
||||
emptyText="No entries."
|
||||
noHeader
|
||||
stickyHeaders
|
||||
noShrink
|
||||
noShrinkHeight="h-full"
|
||||
>
|
||||
<Table v-if="settings.directoryView?.view === 'list'" emptyText="No entries." noHeader stickyHeaders noShrink
|
||||
noShrinkHeight="h-full">
|
||||
<template #thead>
|
||||
<tr>
|
||||
<th class="!pl-1 border-l-2 border-l-transparent last:border-r-2 last:border-r-transparent">
|
||||
@ -16,66 +10,43 @@
|
||||
<LoadingSpinner v-if="processing" class="size-icon" />
|
||||
</div>
|
||||
<div class="grow">Name</div>
|
||||
<SortCallbackButton
|
||||
initialFuncIsMine
|
||||
v-model="sortCallback"
|
||||
:compareFunc="sortCallbacks.name"
|
||||
/>
|
||||
<SortCallbackButton initialFuncIsMine v-model="sortCallback"
|
||||
:compareFunc="sortCallbacks.name" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="last:border-r-2 last:border-r-transparent"
|
||||
v-if="settings?.directoryView?.cols?.mode"
|
||||
>Mode</th>
|
||||
<th
|
||||
class="last:border-r-2 last:border-r-transparent"
|
||||
v-if="settings?.directoryView?.cols?.owner"
|
||||
>
|
||||
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.mode">
|
||||
Mode</th>
|
||||
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.owner">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow">Owner</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.owner" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="last:border-r-2 last:border-r-transparent"
|
||||
v-if="settings?.directoryView?.cols?.group"
|
||||
>
|
||||
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.group">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow">Group</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.group" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="last:border-r-2 last:border-r-transparent"
|
||||
v-if="settings?.directoryView?.cols?.size"
|
||||
>
|
||||
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.size">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow text-right">Size</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.size" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="last:border-r-2 last:border-r-transparent"
|
||||
v-if="settings?.directoryView?.cols?.ctime"
|
||||
>
|
||||
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.ctime">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow">Created</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.ctime" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="last:border-r-2 last:border-r-transparent"
|
||||
v-if="settings?.directoryView?.cols?.mtime"
|
||||
>
|
||||
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.mtime">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow">Modified</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.mtime" />
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="last:border-r-2 last:border-r-transparent"
|
||||
v-if="settings?.directoryView?.cols?.atime"
|
||||
>
|
||||
<th class="last:border-r-2 last:border-r-transparent" v-if="settings?.directoryView?.cols?.atime">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow">Accessed</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.atime" />
|
||||
@ -84,35 +55,19 @@
|
||||
</tr>
|
||||
</template>
|
||||
<template #tbody>
|
||||
<DirectoryEntryList
|
||||
:host="host"
|
||||
:path="path"
|
||||
:sortCallback="sortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp"
|
||||
@cd="(...args) => $emit('cd', ...args)"
|
||||
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
|
||||
@edit="(...args) => $emit('edit', ...args)"
|
||||
@updateStats="(...args) => $emit('updateStats', ...args)"
|
||||
@startProcessing="processing++"
|
||||
@stopProcessing="processing--"
|
||||
ref="directoryEntryListRef"
|
||||
:level="0"
|
||||
/>
|
||||
@updateStats="(...args) => $emit('updateStats', ...args)" @startProcessing="processing++"
|
||||
@stopProcessing="processing--" ref="directoryEntryListRef" :level="0" />
|
||||
</template>
|
||||
</Table>
|
||||
<div v-else class="flex flex-wrap p-2 gap-2 bg-well h-full overflow-y-auto content-start">
|
||||
<DirectoryEntryList
|
||||
:host="host"
|
||||
:path="path"
|
||||
:sortCallback="sortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp"
|
||||
@cd="(...args) => $emit('cd', ...args)"
|
||||
@edit="(...args) => $emit('edit', ...args)"
|
||||
@updateStats="(...args) => $emit('updateStats', ...args)"
|
||||
@startProcessing="processing++"
|
||||
@stopProcessing="processing--"
|
||||
ref="directoryEntryListRef"
|
||||
:level="0"
|
||||
/>
|
||||
<div v-else class="flex flex-wrap p-2 gap-2 bg-well h-full overflow-y-auto content-start"
|
||||
@click.prevent.stop="directoryEntryListRef.selection.deselectAllForward()">
|
||||
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
|
||||
@edit="(...args) => $emit('edit', ...args)" @updateStats="(...args) => $emit('updateStats', ...args)"
|
||||
@startProcessing="processing++" @stopProcessing="processing--" ref="directoryEntryListRef" :level="0" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -120,7 +75,7 @@
|
||||
<script>
|
||||
import { ref, inject } from 'vue';
|
||||
import Table from './Table.vue';
|
||||
import { notificationsInjectionKey, settingsInjectionKey } from '../keys';
|
||||
import { clipboardInjectionKey, notificationsInjectionKey, settingsInjectionKey } from '../keys';
|
||||
import LoadingSpinner from './LoadingSpinner.vue';
|
||||
import SortCallbackButton from './SortCallbackButton.vue';
|
||||
import DirectoryEntryList from './DirectoryEntryList.vue';
|
||||
@ -131,11 +86,13 @@ export default {
|
||||
path: String,
|
||||
searchFilterRegExp: RegExp,
|
||||
},
|
||||
setup() {
|
||||
setup(props) {
|
||||
/**
|
||||
* @type {NavigatorSettings}
|
||||
*/
|
||||
const settings = inject(settingsInjectionKey);
|
||||
const clipboard = inject(clipboardInjectionKey);
|
||||
const notifications = inject(notificationsInjectionKey);
|
||||
const processing = ref(0);
|
||||
const directoryEntryListRef = ref();
|
||||
|
||||
@ -154,7 +111,7 @@ export default {
|
||||
return directoryEntryListRef.value?.refresh?.();
|
||||
}
|
||||
|
||||
const getSelected = () => directoryEntryListRef.value?.selection.getSelected?.() ?? [];
|
||||
const getSelected = () => directoryEntryListRef.value.selection.getSelected() ?? [];
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
@ -163,10 +120,44 @@ export default {
|
||||
console.log("DirectoryView::keyHandler:", event);
|
||||
if (event.key === 'Escape')
|
||||
directoryEntryListRef.value?.selection.deselectAllForward();
|
||||
else if (event.ctrlKey && event.key.toLowerCase() === 'a')
|
||||
directoryEntryListRef.value?.selection.selectAll();
|
||||
else if (event.ctrlKey && event.key.toLowerCase() === 'h')
|
||||
settings.directoryView.showHidden = !settings.directoryView.showHidden;
|
||||
if (event.ctrlKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'a':
|
||||
directoryEntryListRef.value?.selection.selectAll();
|
||||
break;
|
||||
case 'h':
|
||||
settings.directoryView.showHidden = !settings.directoryView.showHidden;
|
||||
break;
|
||||
case 'c':
|
||||
clipboard.content = getSelected();
|
||||
break;
|
||||
case 'x':
|
||||
clipboard.content = getSelected().map(entry => {
|
||||
entry.cut = true;
|
||||
return entry;
|
||||
});
|
||||
break;
|
||||
case 'v':
|
||||
const selected = getSelected();
|
||||
let destination;
|
||||
if (selected.length === 1) {
|
||||
destination = selected[0];
|
||||
if (destination.type !== 'directory' && !(destination.type === 'symbolic link' && destination.target.type === 'directory')) {
|
||||
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
|
||||
return;
|
||||
}
|
||||
} else if (selected.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.map(entry => ({ host: entry.host, path: entry.path })), destination);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -105,6 +105,7 @@ function parseRawEntryStats(raw, cwd, host, failCallback, byteFormatter = cockpi
|
||||
target: {},
|
||||
selected: false,
|
||||
host,
|
||||
cut: false,
|
||||
};
|
||||
if (type === 'symbolic link') {
|
||||
entry.target.rawPath = [
|
||||
@ -159,4 +160,6 @@ export default getDirEntryObjects;
|
||||
* @property {String} target.rawPath - Symlink target path directly grabbed from stat
|
||||
* @property {String} target.path - Resolved symlink target path
|
||||
* @property {Boolean} selected - Whether or not the user has selected this entry in the browser
|
||||
* @property {String} host - host that owns entry
|
||||
* @property {Boolean} cut - whether or not the file is going to be cut
|
||||
*/
|
||||
|
@ -10,6 +10,10 @@ export const settingsInjectionKey = Symbol();
|
||||
* Path history object
|
||||
*/
|
||||
export const pathHistoryInjectionKey = Symbol();
|
||||
/**
|
||||
* Clipboard array ref
|
||||
*/
|
||||
export const clipboardInjectionKey = Symbol();
|
||||
/**
|
||||
* localStorage lookup key for last path
|
||||
*/
|
||||
|
@ -37,7 +37,7 @@ const errorHandler = (error, title = "System Error") => {
|
||||
let lastValidRoutePath = null;
|
||||
router.beforeEach(async (to, from) => {
|
||||
if (to.name === 'root')
|
||||
return `/browse${localStorage.getItem(lastPathStorageKey) ?? '/'}`;
|
||||
return localStorage.getItem(lastPathStorageKey) ?? '/browse/';
|
||||
if (to.fullPath === lastValidRoutePath) {
|
||||
return true; // ignore from updating window.location.hash
|
||||
}
|
||||
@ -61,6 +61,7 @@ router.beforeEach(async (to, from) => {
|
||||
}
|
||||
lastValidRoutePath = to.fullPath; // protect double-update from next line
|
||||
window.location.hash = '#' + to.fullPath; // needed to update URL in address bar
|
||||
localStorage.setItem(lastPathStorageKey, to.fullPath);
|
||||
return true;
|
||||
})
|
||||
|
||||
|
@ -160,8 +160,6 @@ export default {
|
||||
if (route.name !== 'browse')
|
||||
return;
|
||||
const host = route.params.host?.replace(/^\/|:$/g, '') || cockpit.transport.host;
|
||||
console.log(route.params.host);
|
||||
localStorage.setItem(lastPathStorageKey, route.params.path);
|
||||
if (pathHistory.current()?.path !== route.params.path || pathHistory.current()?.host !== host) {
|
||||
pathHistory.push({ path: route.params.path, host }); // updates actual view
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user