flatten entry object, make actions specific to component

This commit is contained in:
joshuaboud 2022-06-17 13:39:37 -03:00
parent b528a7f57c
commit 06c6a04d59
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
5 changed files with 135 additions and 101 deletions

View File

@ -5,28 +5,28 @@
<td class="!pl-1 relative"> <td class="!pl-1 relative">
<div :class="[entry.cut ? 'line-through' : '', 'flex items-center gap-1']"> <div :class="[entry.cut ? 'line-through' : '', 'flex items-center gap-1']">
<div class="w-6" v-for="i in Array(level).fill(0)" v-memo="[level]"></div> <div class="w-6" v-for="i in Array(level).fill(0)" v-memo="[level]"></div>
<div class="relative w-6"> <div class="relative w-6" :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']">
<component :is="icon" class="size-icon icon-default" <FolderIcon v-if="entry.resolvedType === 'd'" class="size-icon" />
:class="{ 'text-gray-500/50': entry.cut }" /> <DocumentIcon v-else class="size-icon" />
<LinkIcon v-if="entry.type === 'l'" class="w-2 h-2 absolute right-0 bottom-0 text-default" /> <LinkIcon v-if="entry.type === 'l'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
</div> </div>
<button class="z-10" v-if="directoryLike" @click.stop="toggleShowEntries" @mouseenter="hover = true" <button class="z-10 icon-default" v-if="entry.resolvedType === 'd'" @click.stop="toggleShowEntries" @mouseenter="hover = true"
@mouseleave="hover = false"> @mouseleave="hover = false">
<ChevronDownIcon v-if="!showEntries" class="size-icon icon-default" /> <ChevronDownIcon v-if="!showEntries" class="size-icon" />
<ChevronUpIcon v-else class="size-icon icon-default" /> <ChevronUpIcon v-else class="size-icon" />
</button> </button>
<div v-html="entry.nameHTML" :title="entry.name"></div> <div v-html="entry.nameHTML" :title="entry.name"></div>
<div v-if="entry.type === 'l'" class="inline-flex gap-1 items-center"> <div v-if="entry.type === 'l'" class="inline-flex gap-1 items-center">
<div class="inline relative"> <div class="inline relative">
<ArrowNarrowRightIcon class="text-default size-icon-sm inline" /> <ArrowNarrowRightIcon class="text-default size-icon-sm inline" />
<XIcon v-if="entry.target?.broken" <XIcon v-if="entry.linkBroken"
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0" /> class="icon-danger size-icon-sm absolute inset-x-0 bottom-0" />
</div> </div>
<div v-html="entry.target?.rawPathHTML ?? ''" :title="entry.target.rawPath"></div> <div v-html="entry.linkRawPathHTML ?? ''" :title="entry.linkRawPath"></div>
</div> </div>
</div> </div>
<div class="absolute left-0 top-0 bottom-0 w-full max-w-[50vw]" @mouseup.stop <div class="absolute left-0 top-0 bottom-0 w-full max-w-[50vw]" @mouseup.stop
@click.prevent="$emit('entryAction', 'toggleSelected', entry, $event)" @click.prevent="$emit('directoryViewAction', 'toggleSelected', entry, $event)"
@dblclick="doubleClickCallback" @mouseenter="hover = true" @mouseleave="hover = false" @dblclick="doubleClickCallback" @mouseenter="hover = true" @mouseleave="hover = false"
ref="selectIntersectElement" /> ref="selectIntersectElement" />
</td> </td>
@ -39,8 +39,8 @@
<td v-if="settings?.directoryView?.cols?.size" class="font-mono text-right">{{ <td v-if="settings?.directoryView?.cols?.size" class="font-mono text-right">{{
entry.sizeHuman entry.sizeHuman
}}</td> }}</td>
<td v-if="settings?.directoryView?.cols?.ctime">{{ <td v-if="settings?.directoryView?.cols?.btime">{{
entry.ctimeStr entry.btimeStr
}}</td> }}</td>
<td v-if="settings?.directoryView?.cols?.mtime">{{ <td v-if="settings?.directoryView?.cols?.mtime">{{
entry.mtimeStr entry.mtimeStr
@ -49,23 +49,25 @@
entry.atimeStr entry.atimeStr
}}</td> }}</td>
</tr> </tr>
<component :is="DirectoryEntryList" v-if="directoryLike && showEntries" :host="host" :path="entry.path" <component :is="DirectoryEntryList" v-if="entry.resolvedType === 'd' && showEntries" :host="host" :path="entry.path"
:isChild="true" :sortCallback="inheritedSortCallback" :searchFilterRegExp="searchFilterRegExp" :isChild="true" :sortCallback="inheritedSortCallback" :searchFilterRegExp="searchFilterRegExp"
@startProcessing="(...args) => $emit('startProcessing', ...args)" @startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false" @stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false"
ref="directoryEntryListRef" :level="level + 1" :selectedCount="selectedCount" ref="directoryEntryListRef" :level="level + 1" :selectedCount="selectedCount"
@entryAction="(...args) => $emit('entryAction', ...args)" /> @browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="(...args) => $emit('directoryViewAction', ...args)" />
</template> </template>
<div v-else class="select-none dir-entry flex flex-col items-center overflow-hidden dir-entry-width p-2" <div v-else class="select-none dir-entry flex flex-col items-center overflow-hidden dir-entry-width p-2"
:class="{ '!bg-red-600/10': hover && !entry.selected, '!bg-red-600/20': hover && entry.selected, 'dir-entry-selected': entry.selected, '!border-t-transparent': suppressBorderT, '!border-b-transparent': suppressBorderB, '!border-l-transparent': suppressBorderL, '!border-r-transparent': suppressBorderR }"> :class="{ '!bg-red-600/10': hover && !entry.selected, '!bg-red-600/20': hover && entry.selected, 'dir-entry-selected': entry.selected, '!border-t-transparent': suppressBorderT, '!border-b-transparent': suppressBorderB, '!border-l-transparent': suppressBorderL, '!border-r-transparent': suppressBorderR }">
<div class="w-full" @dblclick="doubleClickCallback" @click.prevent="$emit('entryAction', 'toggleSelected', entry, $event)" <div class="w-full" @dblclick="doubleClickCallback" @click.prevent="$emit('directoryViewAction', 'toggleSelected', entry, $event)"
@mouseup.stop @mouseenter="hover = true" @mouseleave="hover = false" ref="selectIntersectElement"> @mouseup.stop @mouseenter="hover = true" @mouseleave="hover = false" ref="selectIntersectElement">
<div class="relative w-full"> <div class="relative w-full" :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']">
<component :is="icon" class="icon-default w-full h-auto" :class="{ 'text-gray-500/50': entry.cut }" /> <FolderIcon v-if="entry.resolvedType === 'd'" class="w-full h-auto" />
<div :class="[directoryLike ? 'right-[15%] bottom-[25%]' : 'right-[25%] bottom-[15%]', 'inline absolute w-[20%]']" <DocumentIcon v-else class="w-full h-auto" />
:title="`-> ${entry.target?.rawPath ?? '?'}`"> <div :class="[entry.resolvedType === 'd' ? 'right-[15%] bottom-[25%]' : 'right-[25%] bottom-[15%]', 'inline absolute w-[20%]']"
:title="`-> ${entry.linkRawPath ?? '?'}`">
<LinkIcon v-if="entry.type === 'l'" <LinkIcon v-if="entry.type === 'l'"
:class="[entry.target?.broken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900']" /> :class="[entry.linkBroken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900']" />
</div> </div>
</div> </div>
<div class="text-center w-full text-sm break-words" <div class="text-center w-full text-sm break-words"
@ -77,7 +79,8 @@
<div> <div>
<span v-if="level > 0">{{ entry.path.split('/').slice(-1 * (level + 1)).join('/') }}:</span> <span v-if="level > 0">{{ entry.path.split('/').slice(-1 * (level + 1)).join('/') }}:</span>
<span v-else>{{ entry.name }}:</span> <span v-else>{{ entry.name }}:</span>
{{ entry.mode.toString(8) }}, {{ entry.owner }}:{{ entry.group }}, {{ entry.sizeHuman }} {{ entry.mode.toString(8) }}, {{ entry.owner }}:{{ entry.group }}, {{ entry.sizeHuman }},
modified: {{ entry.mtime }}
</div> </div>
</Teleport> </Teleport>
</template> </template>
@ -85,7 +88,7 @@
<script> <script>
import { ref, inject, watch, nextTick, onBeforeUnmount, onMounted, onActivated, onDeactivated, onUpdated } from 'vue'; import { ref, inject, watch, nextTick, onBeforeUnmount, onMounted, onActivated, onDeactivated, onUpdated } from 'vue';
import { DocumentIcon, FolderIcon, LinkIcon, DocumentRemoveIcon, ArrowNarrowRightIcon, XIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/solid'; import { DocumentIcon, FolderIcon, LinkIcon, DocumentRemoveIcon, ArrowNarrowRightIcon, XIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/solid';
import { settingsInjectionKey } from '../keys'; import { notificationsInjectionKey, settingsInjectionKey } from '../keys';
import DirectoryEntryList from './DirectoryEntryList.vue'; import DirectoryEntryList from './DirectoryEntryList.vue';
import { escapeStringHTML } from '../functions/escapeStringHTML'; import { escapeStringHTML } from '../functions/escapeStringHTML';
@ -109,26 +112,19 @@ export default {
}, },
setup(props, { emit }) { setup(props, { emit }) {
const settings = inject(settingsInjectionKey); const settings = inject(settingsInjectionKey);
const icon = ref(FolderIcon); const notifications = inject(notificationsInjectionKey);
const directoryLike = ref(false);
const showEntries = ref(false); const showEntries = ref(false);
const directoryEntryListRef = ref(); const directoryEntryListRef = ref();
const selectIntersectElement = ref(); const selectIntersectElement = ref();
const hover = ref(false); const hover = ref(false);
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 = () => { const doubleClickCallback = () => {
if (directoryLike.value) { if (props.entry.resolvedType === 'd') {
emit('entryAction', 'cd', props.entry); emit('browserAction', 'cd', props.entry);
} else if (props.entry.resolvedType === 'f') {
emit('browserAction', 'openFilePrompt', props.entry);
} else { } else {
emit('entryAction', 'openPrompt', props.entry); notifications.value.constructNotification('Cannot open or download file', `${props.entry.nameHTML} is a ${props.entry.resolvedTypeHuman}`, 'denied');
} }
} }
@ -162,8 +158,6 @@ export default {
return { return {
settings, settings,
icon,
directoryLike,
showEntries, showEntries,
directoryEntryListRef, directoryEntryListRef,
doubleClickCallback, doubleClickCallback,
@ -192,7 +186,8 @@ export default {
'startProcessing', 'startProcessing',
'stopProcessing', 'stopProcessing',
'setEntryProp', 'setEntryProp',
'entryAction', 'browserAction',
'directoryViewAction',
] ]
} }
</script> </script>

View File

@ -5,7 +5,8 @@
@startProcessing="(...args) => $emit('startProcessing', ...args)" @startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level" :selectedCount="selectedCount" @stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level" :selectedCount="selectedCount"
@setEntryProp="(prop, value) => entry[prop] = value" @setEntryProp="(prop, value) => entry[prop] = value"
@entryAction="(...args) => $emit('entryAction', ...args)" @browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="(...args) => $emit('directoryViewAction', ...args)"
:suppressBorderT="visibleEntries[index - cols]?.selected && !(visibleEntries[index - cols]?.dirOpen)" :suppressBorderT="visibleEntries[index - cols]?.selected && !(visibleEntries[index - cols]?.dirOpen)"
:suppressBorderB="visibleEntries[index + cols]?.selected && !(entry.dirOpen)" :suppressBorderB="visibleEntries[index + cols]?.selected && !(entry.dirOpen)"
:suppressBorderL="settings.directoryView.view !== 'list' && (visibleEntries[index - 1]?.selected && (index) % cols !== 0)" :suppressBorderL="settings.directoryView.view !== 'list' && (visibleEntries[index - 1]?.selected && (index) % cols !== 0)"
@ -69,13 +70,9 @@ export default {
const sortCallbackComputed = computed(() => { const sortCallbackComputed = computed(() => {
return (a, b) => { return (a, b) => {
if (settings.directoryView?.separateDirs) { if (settings.directoryView?.separateDirs) {
const checkA = a.type === 'l' ? (a.target?.type ?? null) : a.type; if (a.resolvedType === 'd' && b.resolvedType !== 'd')
const checkB = b.type === 'l' ? (b.target?.type ?? null) : b.type;
if (checkA === null || checkB === null)
return 0;
if (checkA === 'd' && checkB !== 'd')
return -1; return -1;
else if (checkA !== 'd' && checkB === 'd') else if (a.resolvedType !== 'd' && b.resolvedType === 'd')
return 1; return 1;
} }
return props.sortCallback(a, b); return props.sortCallback(a, b);
@ -231,7 +228,7 @@ export default {
visibleEntries.value = entries.value.filter(entryFilterCallback); visibleEntries.value = entries.value.filter(entryFilterCallback);
// nextTick(() => console.timeEnd('updateVisibleEntries-' + props.path)); // nextTick(() => console.timeEnd('updateVisibleEntries-' + props.path));
const _stats = visibleEntries.value.reduce((_stats, entry) => { const _stats = visibleEntries.value.reduce((_stats, entry) => {
if (entry.type === 'd' || (entry.type === 'l' && entry.target?.type === 'd')) if (entry.resolvedType === 'd')
_stats.dirs++; _stats.dirs++;
else else
_stats.files++; _stats.files++;
@ -276,7 +273,8 @@ export default {
'stopProcessing', 'stopProcessing',
'cancelShowEntries', 'cancelShowEntries',
'deselectAll', 'deselectAll',
'entryAction', 'browserAction',
'directoryViewAction',
'tallySelected', 'tallySelected',
] ]
} }

View File

@ -64,10 +64,10 @@
</template> </template>
<template #tbody> <template #tbody>
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback" <DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" @cd="(...args) => $emit('cd', ...args)" :searchFilterRegExp="searchFilterRegExp"
@edit="(...args) => $emit('edit', ...args)"
@startProcessing="processing++" @stopProcessing="processing--" @startProcessing="processing++" @stopProcessing="processing--"
@entryAction="handleEntryAction" ref="directoryEntryListRef" @browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="handleAction" ref="directoryEntryListRef"
@tallySelected="tallySelected" @tallySelected="tallySelected"
:level="0" :cols="1" :selectedCount="selectedCount" /> :level="0" :cols="1" :selectedCount="selectedCount" />
</template> </template>
@ -109,7 +109,8 @@
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback" <DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" :searchFilterRegExp="searchFilterRegExp"
@startProcessing="processing++" @stopProcessing="processing--" @startProcessing="processing++" @stopProcessing="processing--"
@entryAction="handleEntryAction" @browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="handleAction"
@tallySelected="tallySelected" ref="directoryEntryListRef" @tallySelected="tallySelected" ref="directoryEntryListRef"
:level="0" :cols="cols" :selectedCount="selectedCount" /> :level="0" :cols="cols" :selectedCount="selectedCount" />
</div> </div>
@ -291,7 +292,7 @@ export default {
let destination; let destination;
if (selected.length === 1) { if (selected.length === 1) {
destination = selected[0]; destination = selected[0];
if (destination.type !== 'd' && !(destination.type === 'l' && destination.target.type === 'd')) { if (destination.resolvedType !== 'd') {
notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error'); notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error');
break; break;
} }
@ -326,13 +327,13 @@ export default {
} }
} }
const handleEntryAction = (action, entry, event, ...args) => { const handleAction = (action, ...args) => {
switch (action) { switch (action) {
case 'toggleSelected': case 'toggleSelected':
toggleSelected(entry, event); toggleSelected(...args);
break; break;
default: default:
emit('entryAction', action, entry, event, ...args); console.error('Unknown directoryViewAction:', action, args);
break; break;
} }
} }
@ -377,7 +378,7 @@ export default {
selectAll, selectAll,
deselectAll, deselectAll,
selectRectangle, selectRectangle,
handleEntryAction, handleAction,
tallySelected, tallySelected,
} }
}, },
@ -389,7 +390,7 @@ export default {
DragSelectArea, DragSelectArea,
}, },
emits: [ emits: [
'entryAction', 'browserAction',
] ]
} }
</script> </script>

View File

@ -1,12 +1,12 @@
import { useSpawn, errorString } from "@45drives/cockpit-helpers"; import { useSpawn, errorString } from "@45drives/cockpit-helpers";
import { UNIT_SEPARATOR, RECORD_SEPARATOR } from "../constants"; import { UNIT_SEPARATOR, RECORD_SEPARATOR } from "../constants";
import { szudzikPair } from "./szudzikPair"; import { szudzikPair, hashString } from "./szudzikPair";
import { escapeStringHTML } from "./escapeStringHTML"; import { escapeStringHTML } from "./escapeStringHTML";
/** /**
* Get list of directory entry objects from list of directory entry names * Get list of directory entry objects from list of directory entry names
* *
* find -H path -maxdepth 1 -mindepth 1 -printf '%D:%i%f:%m:%M:%s:%u:%g:%B@:%T@:%A@:%y:%Y:%l\n' * find -H path -maxdepth 1 -mindepth 1 -printf '%D:%i%f:%p:%m:%M:%s:%u:%g:%B@:%T@:%A@:%y:%Y:%l\n'
* *
* @param {String} cwd - Working directory to run find in * @param {String} cwd - Working directory to run find in
* @param {String} host - Host to run find on * @param {String} host - Host to run find on
@ -25,7 +25,7 @@ async function getDirEntryObjects(cwd, host, extraFindArgs = [], failCallback =
'%s', // size '%s', // size
'%u', // owner '%u', // owner
'%g', // group '%g', // group
'%B@', // ctime '%B@', // btime
'%T@', // mtime '%T@', // mtime
'%A@', // atime '%A@', // atime
'%y', // type '%y', // type
@ -96,17 +96,31 @@ async function getDirEntryStats(cwd, host, outputFormat, extraFindArguments = []
* @returns {DirectoryEntryObj[]} * @returns {DirectoryEntryObj[]}
*/ */
function parseRawEntryStats(records, cwd, host, failCallback, byteFormatter = cockpit.format_bytes) { function parseRawEntryStats(records, cwd, host, failCallback, byteFormatter = cockpit.format_bytes) {
const typeHumanLUT = {
f: 'regular file',
d: 'directory',
l: 'symbolic link',
c: 'character device',
b: 'block device',
p: 'FIFO (named pipe)',
s: 'socket',
U: 'unknown file type',
L: 'broken link (loop)',
N: 'broken link (non-existent)',
D: 'door (Solaris)', // probably don't need this, but why not right?
}
const hostHash = hashString(host);
return records.map(fields => { return records.map(fields => {
try { try {
let [devId, inode, name, path, mode, modeStr, size, owner, group, ctime, mtime, atime, type, symlinkTargetType, symlinkTargetName] = fields; let [devId, inode, name, path, mode, modeStr, size, owner, group, btime, mtime, atime, type, symlinkTargetType, symlinkTargetName] = fields;
[size, ctime, mtime, atime] = [size, ctime, mtime, atime].map(num => parseInt(num)); [size, btime, mtime, atime] = [size, btime, mtime, atime].map(num => parseInt(num));
[devId, inode] = [devId, inode].map(num => BigInt(num)); [devId, inode] = [devId, inode].map(num => BigInt(num));
[ctime, mtime, atime] = [ctime, mtime, atime].map(ts => (ts && ts > 0) ? new Date(ts * 1000) : null); [btime, mtime, atime] = [btime, mtime, atime].map(ts => (ts && ts > 0) ? new Date(ts * 1000) : null);
let [ctimeStr, mtimeStr, atimeStr] = [ctime, mtime, atime].map(date => date?.toLocaleString() ?? '-'); let [btimeStr, mtimeStr, atimeStr] = [btime, mtime, atime].map(date => date?.toLocaleString() ?? '-');
let [nameHTML, symlinkTargetNameHTML] = [name, symlinkTargetName].map(escapeStringHTML); let [nameHTML, symlinkTargetNameHTML] = [name, symlinkTargetName].map(escapeStringHTML);
mode = parseInt(mode, 8); mode = parseInt(mode, 8);
return { return {
uniqueId: szudzikPair(host, devId, inode), uniqueId: szudzikPair(hostHash, devId, inode),
devId, devId,
inode, inode,
name, name,
@ -118,20 +132,21 @@ function parseRawEntryStats(records, cwd, host, failCallback, byteFormatter = co
sizeHuman: byteFormatter(size, 1000).replace(/(?<!B)$/, ' B'), sizeHuman: byteFormatter(size, 1000).replace(/(?<!B)$/, ' B'),
owner, owner,
group, group,
ctime, btime,
mtime, mtime,
atime, atime,
ctimeStr, btimeStr,
mtimeStr, mtimeStr,
atimeStr, atimeStr,
type, type,
target: { typeHuman: typeHumanLUT[type],
type: symlinkTargetType, linkType: symlinkTargetType || null,
rawPath: symlinkTargetName, linkRawPath: symlinkTargetName || null,
rawPathHTML: symlinkTargetNameHTML, linkRawPathHTML: symlinkTargetNameHTML || null,
path: type === 'l' ? symlinkTargetName.replace(/^(?!\/)/, `${cwd}/`) : '', linkBroken: ['L', 'N', '?'].includes(symlinkTargetType),
broken: ['L', 'N', '?'].includes(symlinkTargetType), // L: loop N: nonexistent ?: error resolvedPath: type === 'l' ? symlinkTargetName.replace(/^(?!\/)/, `${cwd}/`) : path,
}, resolvedType: symlinkTargetType || type,
resolvedTypeHuman: typeHumanLUT[symlinkTargetType || type],
selected: false, selected: false,
host, host,
cut: false, cut: false,
@ -177,17 +192,21 @@ export default getDirEntryObjects;
* @property {String} sizeHuman - Human readable size * @property {String} sizeHuman - Human readable size
* @property {String} owner - File owner * @property {String} owner - File owner
* @property {String} group - File group * @property {String} group - File group
* @property {Date} ctime - Creation time * @property {Date} btime - Creation time
* @property {Date} mtime - Last Modified time * @property {Date} mtime - Last Modified time
* @property {Date} atime - Last Accessed time * @property {Date} atime - Last Accessed time
* @property {String} ctimeStr - Creation time string * @property {String} btimeStr - Creation time string
* @property {String} mtimeStr - Last Modified time string * @property {String} mtimeStr - Last Modified time string
* @property {String} atimeStr - Last Accessed time string * @property {String} atimeStr - Last Accessed time string
* @property {String} type - Type of inode returned by find * @property {String} type - Type of inode returned by find, single character from mode string
* @property {Object} target - Object for symlink target * @property {String} typeHuman - Type of inode returned by find, human readable string
* @property {String} target.rawPath - Symlink target path directly grabbed from find * @property {String|null} linkType - Type of inode link is pointing to or null if not link
* @property {String} target.path - Resolved symlink target path * @property {String|null} linkRawPath - Target of link or null if not link
* @property {Boolean} target.broken - Whether or not the link is broken * @property {String|null} linkRawPathHTML - HTML formatted target of link or null if not link
* @property {Boolean} linkBroken - True if is symlink and broken, otherwise false
* @property {String} resolvedPath - Path to file or path to target if symlink
* @property {String} resolvedType - Type of file or type of target if symlink, single character from mode string
* @property {String} resolvedTypeHuman - Type of file or type of target if symlink, human readable string
* @property {Boolean} selected - Whether or not the user has selected this entry in the browser * @property {Boolean} selected - Whether or not the user has selected this entry in the browser
* @property {String} host - host that owns entry * @property {String} host - host that owns entry
* @property {Boolean} cut - whether or not the file is going to be cut * @property {Boolean} cut - whether or not the file is going to be cut

View File

@ -68,21 +68,21 @@
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<DirectoryView :host="pathHistory.current()?.host" :path="pathHistory.current()?.path" <DirectoryView :host="pathHistory.current()?.host" :path="pathHistory.current()?.path"
:searchFilterRegExp="searchFilterRegExp" @cd="path => cd({ path })" @edit="openEditor" :searchFilterRegExp="searchFilterRegExp" @cd="path => cd({ path })" @edit="openEditor"
@entryAction="handleEntryAction" @browserAction="handleAction"
ref="directoryViewRef" /> ref="directoryViewRef" />
</div> </div>
</div> </div>
</div> </div>
<ModalPopup :showModal="openPrompt.show" :headerText="openPrompt.entry?.name ?? 'NULL'" @close="() => openPrompt.close()"> <ModalPopup :showModal="openFilePromptModal.show" :headerText="openFilePromptModal.entry?.name ?? 'NULL'" @close="() => openFilePromptModal.close()">
What would you like to do with this file? What would you like to do with this file?
<template #footer> <template #footer>
<button type="button" class="btn btn-secondary" @click="() => openPrompt.close()"> <button type="button" class="btn btn-secondary" @click="() => openFilePromptModal.close()">
Cancel Cancel
</button> </button>
<button type="button" class="btn btn-primary" @click="() => openEditor(openPrompt.entry.path)"> <button type="button" class="btn btn-primary" @click="() => openFilePromptModal.action('edit') ">
Open for editing Open for editing
</button> </button>
<button type="button" class="btn btn-primary"> <button type="button" class="btn btn-primary" @click="() => openFilePromptModal.action('download') ">
Download Download
</button> </button>
</template> </template>
@ -112,6 +112,7 @@ import { notificationsInjectionKey, pathHistoryInjectionKey, lastPathStorageKey,
import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, RefreshIcon, ChevronDownIcon, SearchIcon, SunIcon, MoonIcon, EyeIcon, EyeOffIcon, ViewListIcon, ViewGridIcon } from '@heroicons/vue/solid'; import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, RefreshIcon, ChevronDownIcon, SearchIcon, SunIcon, MoonIcon, EyeIcon, EyeOffIcon, ViewListIcon, ViewGridIcon } from '@heroicons/vue/solid';
import IconToggle from '../components/IconToggle.vue'; import IconToggle from '../components/IconToggle.vue';
import ModalPopup from '../components/ModalPopup.vue'; import ModalPopup from '../components/ModalPopup.vue';
import { fileDownload } from '@45drives/cockpit-helpers';
const encodePartial = (string) => const encodePartial = (string) =>
encodeURIComponent(string) encodeURIComponent(string)
@ -160,19 +161,23 @@ export default {
forwardHistoryDropdown.showDropdown = false; forwardHistoryDropdown.showDropdown = false;
} }
}); });
const openPrompt = reactive({ const openFilePromptModal = reactive({
show: false, show: false,
entry: null, entry: null,
resetTimeoutHandle: null, resetTimeoutHandle: null,
open: (entry) => { open: (entry) => {
clearTimeout(openPrompt.resetTimeoutHandle); clearTimeout(openFilePromptModal.resetTimeoutHandle);
openPrompt.entry = entry; openFilePromptModal.entry = entry;
openPrompt.show = true; openFilePromptModal.show = true;
}, },
close: () => { close: () => {
openPrompt.show = false; openFilePromptModal.show = false;
openPrompt.resetTimeoutHandle = setTimeout(() => openPrompt.resetTimeoutHandle = openPrompt.entry = null, 500); openFilePromptModal.resetTimeoutHandle = setTimeout(() => openFilePromptModal.resetTimeoutHandle = openFilePromptModal.entry = null, 500);
}, },
action: (action) => {
handleAction(action, openFilePromptModal.entry);
openFilePromptModal.close();
}
}); });
const cd = ({ path, host }) => { const cd = ({ path, host }) => {
@ -193,25 +198,39 @@ export default {
cd({path: pathHistory.current().path + '/..'}); cd({path: pathHistory.current().path + '/..'});
} }
const openEditor = (path) => { const openEditor = ({ path, host }) => {
router.push(`/edit/${pathHistory.current().host}${encodePartial(path)}`); const newHost = host ?? (pathHistory.current().host);
const newPath = encodePartial(path ?? (pathHistory.current().path));
router.push(`/edit/${newHost}${newPath}`);
}
const download = ({ path, name, host }) => {
fileDownload(path, name, host);
console.log('download', `${host}:${path}`);
}
const openFilePrompt = (entry) => {
openFilePromptModal.open(entry);
} }
const getSelected = () => directoryViewRef.value?.getSelected?.() ?? []; const getSelected = () => directoryViewRef.value?.getSelected?.() ?? [];
const handleEntryAction = (action, entry, event) => { const handleAction = (action, ...args) => {
switch (action) { switch (action) {
case 'cd': case 'cd':
cd({path: entry.path}); cd(...args);
break; break;
case 'edit': case 'edit':
openEditor({path: entry.path}) openEditor(...args);
break; break;
case 'openPrompt': case 'openFilePrompt':
openPrompt.open(entry); openFilePrompt(...args);
break;
case 'download':
download(...args);
break; break;
default: default:
console.error('Unknown entryAction:', action, entry); console.error('Unknown browserAction:', action, args);
break; break;
} }
} }
@ -247,14 +266,16 @@ export default {
searchFilterRegExp, searchFilterRegExp,
backHistoryDropdown, backHistoryDropdown,
forwardHistoryDropdown, forwardHistoryDropdown,
openPrompt, openFilePromptModal,
cd, cd,
back, back,
forward, forward,
up, up,
openEditor, openEditor,
download,
openFilePrompt,
getSelected, getSelected,
handleEntryAction, handleAction,
} }
}, },
components: { components: {