mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-08-17 07:38:31 +02:00
overhaul selection and entry gathering
This commit is contained in:
parent
c4ef085c1f
commit
2508e23541
@ -1,20 +1,15 @@
|
||||
<template>
|
||||
<template v-if="settings.directoryView?.view === 'list'">
|
||||
<tr
|
||||
v-show="show || showEntries"
|
||||
@dblclick.stop="doubleClickCallback"
|
||||
@click.prevent.stop="$emit('toggleSelected', { ctrlKey: $event.ctrlKey, shiftKey: $event.shiftKey })"
|
||||
:class="['hover:!bg-red-600/10 select-none']"
|
||||
>
|
||||
<td :class="['!pl-1', ...selectedClasses]">
|
||||
<tr v-show="show || showEntries" @dblclick="doubleClickCallback"
|
||||
@click.prevent="$emit('toggleSelected', entry, $event)"
|
||||
:class="['hover:!bg-red-600/10 select-none dir-entry', entry.selected ? 'dir-entry-selected' : '']"
|
||||
ref="selectIntersectElement">
|
||||
<td class="!pl-1" :class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">
|
||||
<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" />
|
||||
<LinkIcon
|
||||
v-if="entry.type === 'l'"
|
||||
class="w-2 h-2 absolute right-0 bottom-0 text-default"
|
||||
/>
|
||||
<component :is="icon" class="size-icon icon-default" :class="{ 'text-gray-500/50': entry.cut }" />
|
||||
<LinkIcon v-if="entry.type === 'l'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
|
||||
</div>
|
||||
<button v-if="directoryLike" @click.stop="toggleShowEntries">
|
||||
<ChevronDownIcon v-if="!showEntries" class="size-icon icon-default" />
|
||||
@ -24,83 +19,68 @@
|
||||
<div v-if="entry.type === 'l'" class="inline-flex gap-1 items-center">
|
||||
<div class="inline relative">
|
||||
<ArrowNarrowRightIcon class="text-default size-icon-sm inline" />
|
||||
<XIcon
|
||||
v-if="entry.target?.broken"
|
||||
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0"
|
||||
/>
|
||||
<XIcon v-if="entry.target?.broken"
|
||||
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0" />
|
||||
</div>
|
||||
<div v-html="escapeStringHTML(entry.target?.rawPath ?? '')" :title="entry.target.rawPath"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="settings?.directoryView?.cols?.mode"
|
||||
:class="['font-mono', ...(selectedClasses)]"
|
||||
>{{ entry.modeStr }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.owner" :class="selectedClasses">{{ entry.owner }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.group" :class="selectedClasses">{{ entry.group }}</td>
|
||||
<td
|
||||
v-if="settings?.directoryView?.cols?.size"
|
||||
:class="['font-mono text-right', ...(selectedClasses)]"
|
||||
>{{ entry.sizeHuman }}</td>
|
||||
<td
|
||||
v-if="settings?.directoryView?.cols?.ctime"
|
||||
:class="selectedClasses"
|
||||
>{{ entry.ctime?.toLocaleString() ?? '-' }}</td>
|
||||
<td
|
||||
v-if="settings?.directoryView?.cols?.mtime"
|
||||
:class="selectedClasses"
|
||||
>{{ entry.mtime?.toLocaleString() ?? '-' }}</td>
|
||||
<td
|
||||
v-if="settings?.directoryView?.cols?.atime"
|
||||
:class="selectedClasses"
|
||||
>{{ entry.atime?.toLocaleString() ?? '-' }}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.mode" class="font-mono"
|
||||
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{ entry.modeStr
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.owner"
|
||||
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{ entry.owner }}
|
||||
</td>
|
||||
<td v-if="settings?.directoryView?.cols?.group"
|
||||
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{ entry.group }}
|
||||
</td>
|
||||
<td v-if="settings?.directoryView?.cols?.size" class="font-mono text-right"
|
||||
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
|
||||
entry.sizeHuman
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.ctime"
|
||||
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
|
||||
entry.ctime?.toLocaleString() ?? '-'
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.mtime"
|
||||
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
|
||||
entry.mtime?.toLocaleString() ?? '-'
|
||||
}}</td>
|
||||
<td v-if="settings?.directoryView?.cols?.atime"
|
||||
:class="{ '!border-t-0': suppressBorders.top, '!border-b-0': suppressBorders.bottom }">{{
|
||||
entry.atime?.toLocaleString() ?? '-'
|
||||
}}</td>
|
||||
</tr>
|
||||
<component
|
||||
:show="show || showEntries"
|
||||
:is="DirectoryEntryList"
|
||||
v-if="directoryLike && showEntries"
|
||||
:host="host"
|
||||
:path="entry.path"
|
||||
:isChild="true"
|
||||
:sortCallback="inheritedSortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp"
|
||||
@cd="(...args) => $emit('cd', ...args)"
|
||||
<component :show="show || showEntries" :is="DirectoryEntryList" v-if="directoryLike && showEntries" :host="host"
|
||||
:path="entry.path" :isChild="true" :sortCallback="inheritedSortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
|
||||
@edit="(...args) => $emit('edit', ...args)"
|
||||
@startProcessing="(...args) => $emit('startProcessing', ...args)"
|
||||
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
|
||||
@cancelShowEntries="showEntries = false"
|
||||
@deselectAll="$emit('deselectAll')"
|
||||
ref="directoryViewRef"
|
||||
:level="level + 1"
|
||||
/>
|
||||
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false"
|
||||
ref="directoryEntryListRef" :level="level + 1"
|
||||
@toggleSelected="(...args) => $emit('toggleSelected', ...args)" />
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
v-show="show"
|
||||
@dblclick.stop="doubleClickCallback"
|
||||
@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']">
|
||||
<template v-else>
|
||||
<div v-show="show" @dblclick="doubleClickCallback"
|
||||
@click.prevent="$emit('toggleSelected', entry, $event)"
|
||||
ref="selectIntersectElement"
|
||||
:class="['hover:!bg-red-600/10 select-none dir-entry flex flex-col items-center w-20 overflow-hidden', entry.selected ? 'dir-entry-selected' : '']">
|
||||
<div class="relative w-20">
|
||||
<component :is="icon" class="icon-default w-20 h-auto" />
|
||||
<div
|
||||
:class="[directoryLike ? 'right-3 bottom-5' : 'right-5 bottom-3', 'inline absolute']"
|
||||
:title="`-> ${entry.target?.rawPath ?? '?'}`"
|
||||
>
|
||||
<LinkIcon
|
||||
v-if="entry.type === 'l'"
|
||||
:class="[entry.target?.broken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900', 'w-4 h-auto']"
|
||||
/>
|
||||
<component :is="icon" class="icon-default w-20 h-auto" :class="{ 'text-gray-500/50': entry.cut }" />
|
||||
<div :class="[directoryLike ? 'right-3 bottom-5' : 'right-5 bottom-3', 'inline absolute']"
|
||||
:title="`-> ${entry.target?.rawPath ?? '?'}`">
|
||||
<LinkIcon v-if="entry.type === 'l'"
|
||||
:class="[entry.target?.broken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900', 'w-4 h-auto']" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center w-full" style="overflow-wrap: break-word;">{{ entry.name }}</div>
|
||||
<div class="text-center w-full" :class="{ truncate: !entry.selected, 'line-through': entry.cut }" style="overflow-wrap: break-word;" v-html="escapeStringHTML(entry.name)" :title="entry.name"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, inject, watch, nextTick } from 'vue';
|
||||
import { ref, inject, watch, nextTick, onBeforeUnmount, onMounted, onActivated, onDeactivated } from 'vue';
|
||||
import { DocumentIcon, FolderIcon, LinkIcon, DocumentRemoveIcon, ArrowNarrowRightIcon, XIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/solid';
|
||||
import { settingsInjectionKey } from '../keys';
|
||||
import DirectoryEntryList from './DirectoryEntryList.vue';
|
||||
@ -119,16 +99,34 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
level: Number,
|
||||
neighboursSelected: Object,
|
||||
suppressBorders: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {
|
||||
top: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const settings = inject(settingsInjectionKey);
|
||||
const icon = ref(FolderIcon);
|
||||
const directoryLike = ref(false);
|
||||
const showEntries = ref(false);
|
||||
const directoryViewRef = ref();
|
||||
const directoryEntryListRef = ref();
|
||||
const selectIntersectElement = ref();
|
||||
|
||||
const selectedClasses = ref([]);
|
||||
// const selectedClasses = ref([]);
|
||||
|
||||
if (props.entry.type === 'd' || (props.entry.type === 'l' && props.entry.target?.type === 'd')) {
|
||||
icon.value = FolderIcon;
|
||||
directoryLike.value = true;
|
||||
} else {
|
||||
icon.value = DocumentIcon;
|
||||
directoryLike.value = false;
|
||||
}
|
||||
|
||||
const doubleClickCallback = () => {
|
||||
if (directoryLike.value) {
|
||||
@ -139,60 +137,50 @@ export default {
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
return directoryViewRef.value?.refresh?.();
|
||||
return directoryEntryListRef.value?.refresh?.();
|
||||
}
|
||||
|
||||
const toggleShowEntries = () => {
|
||||
emit('startProcessing');
|
||||
nextTick(() => {
|
||||
showEntries.value = !showEntries.value;
|
||||
nextTick(() => emit('stopProcessing'));
|
||||
})
|
||||
showEntries.value = !showEntries.value;
|
||||
emit('setEntryProp', 'dirOpen', showEntries.value);
|
||||
}
|
||||
|
||||
const getSelected = () => directoryViewRef.value.getSelected() ?? [];
|
||||
|
||||
const selectAll = () => {
|
||||
directoryViewRef.value?.selection.selectAll();
|
||||
/**
|
||||
* Recursive get all entries for browser
|
||||
*
|
||||
* @param {DirectoryEntryObj[]} - Holds all entries
|
||||
*
|
||||
* @returns {DirectoryEntryObj[]} - the accumulator
|
||||
*/
|
||||
const gatherEntries = (accumulator = [], onlyVisible = true) => {
|
||||
return directoryEntryListRef.value?.gatherEntries(accumulator, onlyVisible) ?? accumulator;
|
||||
}
|
||||
|
||||
const deselectAllForward = () => {
|
||||
directoryViewRef.value?.selection.deselectAllForward();
|
||||
}
|
||||
onMounted(() => {
|
||||
watch(selectIntersectElement, () =>
|
||||
emit('setEntryProp', 'DOMElement', selectIntersectElement.value), { immediate: true }
|
||||
);
|
||||
});
|
||||
|
||||
watch(props.entry, () => {
|
||||
if (props.entry.type === 'd' || (props.entry.type === 'l' && props.entry.target?.type === 'd')) {
|
||||
icon.value = FolderIcon;
|
||||
directoryLike.value = true;
|
||||
} else {
|
||||
icon.value = DocumentIcon;
|
||||
directoryLike.value = false;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
watch([() => props.neighboursSelected, () => props.entry.selected, () => settings.directoryView?.view], () => selectedClasses.value = [
|
||||
'border-dashed border-red-600/50 first:border-l-2 last:border-r-2',
|
||||
props.entry.selected ? 'bg-red-600/5 first:border-l-red-600/50 last:border-r-red-600/50' : 'first:border-l-transparent last:border-r-transparent',
|
||||
props.entry.selected && (!props.neighboursSelected.above || settings.directoryView?.view !== 'list') ? 'border-t-2' : 'border-t-0',
|
||||
props.entry.selected && (!props.neighboursSelected.below || settings.directoryView?.view !== 'list') ? 'border-b-2' : 'border-b-0',
|
||||
], { immediate: true, deep: true });
|
||||
onBeforeUnmount(() => {
|
||||
emit('setEntryProp', 'dirOpen', false)
|
||||
emit('setEntryProp', 'DOMElement', undefined), { immediate: true }
|
||||
});
|
||||
|
||||
return {
|
||||
settings,
|
||||
icon,
|
||||
directoryLike,
|
||||
selectedClasses,
|
||||
showEntries,
|
||||
directoryViewRef,
|
||||
directoryEntryListRef,
|
||||
doubleClickCallback,
|
||||
refresh,
|
||||
toggleShowEntries,
|
||||
getSelected,
|
||||
selectAll,
|
||||
deselectAllForward,
|
||||
gatherEntries,
|
||||
escapeStringHTML,
|
||||
DirectoryEntryList,
|
||||
nextTick,
|
||||
selectIntersectElement,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@ -212,7 +200,25 @@ export default {
|
||||
'toggleSelected',
|
||||
'startProcessing',
|
||||
'stopProcessing',
|
||||
'deselectAll',
|
||||
'setEntryProp',
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
tr.dir-entry>td {
|
||||
@apply border-solid border-y-red-600/50 border-y-0 first:border-l last:border-r first:border-l-transparent last:border-r-transparent;
|
||||
}
|
||||
|
||||
tr.dir-entry-selected>td {
|
||||
@apply border-y first:border-l-red-600/50 last:border-red-600/50 bg-red-600/10;
|
||||
}
|
||||
/*
|
||||
div.dir-entry {
|
||||
@apply border-solid border border-transparent;
|
||||
} */
|
||||
|
||||
div.dir-entry-selected {
|
||||
@apply bg-red-600/10;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,11 +3,12 @@
|
||||
<DirectoryEntry :show="true" :host="host" :entry="entry" :inheritedSortCallback="sortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
|
||||
@edit="(...args) => $emit('edit', ...args)"
|
||||
@toggleSelected="modifiers => selection.toggle(entry, index, modifiers)"
|
||||
@deselectAll="selection.deselectAllBackward()" @sortEntries="sortEntries" @updateStats="emitStats"
|
||||
@toggleSelected="(...args) => $emit('toggleSelected', ...args)"
|
||||
@sortEntries="sortEntries" @updateStats="emitStats"
|
||||
@startProcessing="(...args) => $emit('startProcessing', ...args)"
|
||||
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level"
|
||||
:neighboursSelected="{ above: visibleEntries[index - 1]?.selected ?? false, below: visibleEntries[index + 1]?.selected ?? false }" />
|
||||
@setEntryProp="(prop, value) => entry[prop] = value"
|
||||
:suppressBorders="{ top: visibleEntries[index - 1]?.selected && !(visibleEntries[index - 1]?.dirOpen), bottom: visibleEntries[index + 1]?.selected && !(entry.dirOpen), left: false, right: false }" />
|
||||
</template>
|
||||
<tr v-if="show && visibleEntries.length === 0">
|
||||
<td :colspan="Object.values(settings?.directoryView?.cols ?? {}).reduce((sum, current) => current ? sum + 1 : sum, 1) ?? 100"
|
||||
@ -72,59 +73,6 @@ export default {
|
||||
return props.sortCallback(a, b);
|
||||
}
|
||||
});
|
||||
const selection = reactive({
|
||||
lastSelectedInd: null,
|
||||
toggle: (entry, index, modifiers) => {
|
||||
const entrySelectedValue = entry.selected;
|
||||
if (!modifiers.ctrlKey) {
|
||||
const tmpLastSelectedInd = selection.lastSelectedInd;
|
||||
selection.deselectAllBackward();
|
||||
selection.lastSelectedInd = tmpLastSelectedInd;
|
||||
}
|
||||
if (modifiers.shiftKey && selection.lastSelectedInd !== null) {
|
||||
let [startInd, endInd] = [selection.lastSelectedInd, index];
|
||||
if (endInd < startInd)
|
||||
[startInd, endInd] = [endInd, startInd];
|
||||
visibleEntries.value
|
||||
.slice(startInd, endInd + 1)
|
||||
.map(entry => entry.selected = true);
|
||||
} else {
|
||||
entry.selected = modifiers.ctrlKey ? !entrySelectedValue : true;
|
||||
if (entry.selected)
|
||||
selection.lastSelectedInd = index;
|
||||
else
|
||||
selection.lastSelectedInd = null;
|
||||
}
|
||||
},
|
||||
getSelected: () => [
|
||||
...visibleEntries.value.filter(entry => entry.selected),
|
||||
...entryRefs.value
|
||||
.filter(entryRef => entryRef.showEntries)
|
||||
.map(entryRef => entryRef.getSelected())
|
||||
.flat(1),
|
||||
],
|
||||
clear: () => {
|
||||
entries.value.map(entry => entry.selected = false);
|
||||
},
|
||||
selectAll: () => {
|
||||
visibleEntries.value
|
||||
.map(entry => entry.selected = true);
|
||||
entryRefs.value
|
||||
.map(entryRef => entryRef.selectAll());
|
||||
},
|
||||
deselectAllBackward: () => {
|
||||
if (props.level > 0)
|
||||
emit('deselectAll');
|
||||
else
|
||||
selection.deselectAllForward();
|
||||
},
|
||||
deselectAllForward: () => {
|
||||
selection.clear();
|
||||
selection.lastSelectedInd = null;
|
||||
entryRefs.value
|
||||
.map(entryRef => entryRef.deselectAllForward());
|
||||
},
|
||||
});
|
||||
const processingHandler = {
|
||||
count: 0,
|
||||
start: () => {
|
||||
@ -147,7 +95,6 @@ export default {
|
||||
if (!props.path) {
|
||||
return;
|
||||
}
|
||||
selection.lastSelectedInd = null;
|
||||
processingHandler.start();
|
||||
try {
|
||||
const cwd = props.path;
|
||||
@ -162,7 +109,7 @@ export default {
|
||||
);
|
||||
if (props.path !== cwd)
|
||||
return; // changed directory before could finish
|
||||
entries.value = [...tmpEntries.sort(sortCallbackComputed.value)].map(entry => reactive({...entry, cut: clipboard.content.find(a => a.path === entry.path && a.host === entry.host) ?? false}));
|
||||
entries.value = [...tmpEntries.sort(sortCallbackComputed.value)].map(entry => reactive({ ...entry, cut: clipboard.content.find(a => a.path === entry.path && a.host === entry.host) ?? false }));
|
||||
console.timeEnd('getEntries');
|
||||
} catch (error) {
|
||||
entries.value = [];
|
||||
@ -203,6 +150,23 @@ export default {
|
||||
(!/^\./.test(entry.name) || settings?.directoryView?.showHidden)
|
||||
&& (props.searchFilterRegExp?.test(entry.name) ?? true);
|
||||
|
||||
/**
|
||||
* Recursive get all entries for browser
|
||||
*
|
||||
* @param {DirectoryEntryObj[]} - Holds all entries
|
||||
*
|
||||
* @returns {DirectoryEntryObj[]} - the accumulator
|
||||
*/
|
||||
const gatherEntries = (accumulator = [], onlyVisible = true) => {
|
||||
const subset = onlyVisible ? visibleEntries.value : entries.value;
|
||||
return accumulator.concat(
|
||||
subset,
|
||||
entryRefs.value
|
||||
.filter(entryRef => entryRef.showEntries)
|
||||
.map(entryRef => entryRef.gatherEntries(accumulator, onlyVisible)).flat(1)
|
||||
);
|
||||
}
|
||||
|
||||
const fileSystemWatcher = FileSystemWatcher(props.path, { superuser: 'try', host: props.host, ignoreSelf: true });
|
||||
|
||||
fileSystemWatcher.onCreated = async (eventObj) => {
|
||||
@ -278,12 +242,12 @@ export default {
|
||||
entries,
|
||||
visibleEntries,
|
||||
entryRefs,
|
||||
selection,
|
||||
getEntries,
|
||||
refresh,
|
||||
emitStats,
|
||||
sortEntries,
|
||||
entryFilterCallback,
|
||||
gatherEntries,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -1,74 +1,83 @@
|
||||
<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">
|
||||
<template #thead>
|
||||
<tr>
|
||||
<th class="!pl-1 border-l-2 border-l-transparent last:border-r-2 last:border-r-transparent">
|
||||
<div class="flex flex-row flex-nowrap gap-1 items-center">
|
||||
<div class="flex items-center justify-center w-6">
|
||||
<LoadingSpinner v-if="processing" class="size-icon" />
|
||||
<div class="h-full" @keydown="keyHandler($event)" tabindex="-1">
|
||||
<DragSelectArea class="h-full" @selectRectangle="selectRectangle" @mouseup.exact="deselectAll()">
|
||||
<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">
|
||||
<div class="flex flex-row flex-nowrap gap-1 items-center">
|
||||
<div class="flex items-center justify-center w-6">
|
||||
<LoadingSpinner v-if="processing" class="size-icon" />
|
||||
</div>
|
||||
<div class="grow">Name</div>
|
||||
<SortCallbackButton initialFuncIsMine v-model="sortCallback"
|
||||
:compareFunc="sortCallbacks.name" />
|
||||
</div>
|
||||
<div class="grow">Name</div>
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow">Accessed</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.atime" />
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template #tbody>
|
||||
</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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<div class="flex flex-row flex-nowrap gap-2 items-center">
|
||||
<div class="grow">Accessed</div>
|
||||
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.atime" />
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template #tbody>
|
||||
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
|
||||
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)"
|
||||
@edit="(...args) => $emit('edit', ...args)" @toggleSelected="toggleSelected"
|
||||
@updateStats="(...args) => $emit('updateStats', ...args)" @startProcessing="processing++"
|
||||
@stopProcessing="processing--" ref="directoryEntryListRef" :level="0" />
|
||||
</template>
|
||||
</Table>
|
||||
<div v-else class="flex flex-wrap p-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)"
|
||||
@edit="(...args) => $emit('edit', ...args)" @toggleSelected="toggleSelected"
|
||||
@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"
|
||||
@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>
|
||||
</DragSelectArea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -79,6 +88,7 @@ import { clipboardInjectionKey, notificationsInjectionKey, settingsInjectionKey
|
||||
import LoadingSpinner from './LoadingSpinner.vue';
|
||||
import SortCallbackButton from './SortCallbackButton.vue';
|
||||
import DirectoryEntryList from './DirectoryEntryList.vue';
|
||||
import DragSelectArea from './DragSelectArea.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -111,29 +121,80 @@ export default {
|
||||
return directoryEntryListRef.value?.refresh?.();
|
||||
}
|
||||
|
||||
const getSelected = () => directoryEntryListRef.value.selection.getSelected() ?? [];
|
||||
const getSelected = () => directoryEntryListRef.value?.gatherEntries().filter(entry => entry.selected) ?? [];
|
||||
|
||||
let lastSelectedEntry = null;
|
||||
const toggleSelected = (entry, { ctrlKey, shiftKey }) => {
|
||||
const entrySelectedValue = entry.selected;
|
||||
if (!ctrlKey)
|
||||
deselectAll();
|
||||
if (shiftKey && lastSelectedEntry !== null) {
|
||||
const entries = directoryEntryListRef.value?.gatherEntries();
|
||||
let [startInd, endInd] = [entries.indexOf(lastSelectedEntry), entries.indexOf(entry)];
|
||||
if (startInd != -1 && endInd != -1) {
|
||||
if (endInd < startInd)
|
||||
[startInd, endInd] = [endInd, startInd];
|
||||
entries
|
||||
.slice(startInd, endInd + 1)
|
||||
.map(entry => entry.selected = true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
entry.selected = ctrlKey ? !entrySelectedValue : true;
|
||||
if (entry.selected)
|
||||
lastSelectedEntry = entry;
|
||||
else
|
||||
lastSelectedEntry = null;
|
||||
}
|
||||
|
||||
const selectAll = () => directoryEntryListRef.value?.gatherEntries().map(entry => entry.selected = true);
|
||||
|
||||
const deselectAll = () => directoryEntryListRef.value?.gatherEntries([], false).map(entry => entry.selected = false);
|
||||
|
||||
const selectRectangle = (rect, { ctrlKey, shiftKey }) => {
|
||||
if (!(ctrlKey || shiftKey))
|
||||
deselectAll();
|
||||
|
||||
directoryEntryListRef.value?.gatherEntries().map(entry => {
|
||||
const entryRect = entry.DOMElement?.getBoundingClientRect();
|
||||
if (
|
||||
!entryRect
|
||||
|| rect.x1 > entryRect.right || rect.x2 < entryRect.left
|
||||
|| rect.y1 > entryRect.bottom || rect.y2 < entryRect.top
|
||||
)
|
||||
return;
|
||||
entry.selected = ctrlKey ? !entry.selected : true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const keyHandler = (event) => {
|
||||
console.log("DirectoryView::keyHandler:", event);
|
||||
if (event.key === 'Escape')
|
||||
directoryEntryListRef.value?.selection.deselectAllForward();
|
||||
if (event.key === 'Escape') {
|
||||
if (getSelected().length === 0) {
|
||||
clipboard.content.map(entry => entry.cut = false);
|
||||
clipboard.content = [];
|
||||
} else {
|
||||
deselectAll();
|
||||
}
|
||||
}
|
||||
if (event.ctrlKey) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
const keypress = event.key.toLowerCase();
|
||||
switch (keypress) {
|
||||
case 'a':
|
||||
directoryEntryListRef.value?.selection.selectAll();
|
||||
selectAll();
|
||||
break;
|
||||
case 'h':
|
||||
settings.directoryView.showHidden = !settings.directoryView.showHidden;
|
||||
break;
|
||||
case 'c':
|
||||
clipboard.content = getSelected();
|
||||
break;
|
||||
case 'x':
|
||||
const isCut = keypress === 'x';
|
||||
clipboard.content.map(entry => entry.cut = false);
|
||||
clipboard.content = getSelected().map(entry => {
|
||||
entry.cut = true;
|
||||
entry.cut = isCut;
|
||||
return entry;
|
||||
});
|
||||
break;
|
||||
@ -144,20 +205,23 @@ export default {
|
||||
destination = selected[0];
|
||||
if (destination.type !== 'd' && !(destination.type === 'l' && destination.target.type === 'd')) {
|
||||
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
|
||||
return;
|
||||
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');
|
||||
return;
|
||||
break;
|
||||
}
|
||||
console.log("paste", clipboard.content.map(entry => ({ host: entry.host, path: entry.path })), destination);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return {
|
||||
@ -169,6 +233,11 @@ export default {
|
||||
refresh,
|
||||
getSelected,
|
||||
keyHandler,
|
||||
getSelected,
|
||||
toggleSelected,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
selectRectangle,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@ -176,6 +245,7 @@ export default {
|
||||
Table,
|
||||
LoadingSpinner,
|
||||
SortCallbackButton,
|
||||
DragSelectArea,
|
||||
},
|
||||
emits: [
|
||||
'cd',
|
||||
|
Loading…
x
Reference in New Issue
Block a user