implement clipboard

This commit is contained in:
joshuaboud 2022-05-30 17:22:02 -03:00
parent de67f5a2c6
commit 339aad2d6e
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
8 changed files with 87 additions and 84 deletions

View File

@ -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("");

View File

@ -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();

View File

@ -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');

View File

@ -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 {

View File

@ -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
*/

View File

@ -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
*/

View File

@ -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;
})

View File

@ -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
}