fix HTML formatting and add GPL headers

This commit is contained in:
joshuaboud 2022-06-20 11:20:02 -03:00
parent 116a422320
commit ec57d3f5de
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
29 changed files with 1169 additions and 349 deletions

View File

@ -1,17 +1,43 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<div class="text-default bg-default h-full flex flex-col items-stretch"> <div class="text-default bg-default h-full flex flex-col items-stretch">
<router-view v-if="providesValid" /> <router-view v-if="providesValid" />
<div class="flex flex-row items-center px-4 py-2 gap-2"> <div class="flex flex-row items-center px-4 py-2 gap-2">
<div id="footer-text" class="flex flex-row flex-wrap gap-x-4 gap-y-0 text-xs grow basis-0"></div> <div
id="footer-text"
class="flex flex-row flex-wrap gap-x-4 gap-y-0 text-xs grow basis-0"
></div>
<div class="grow-0"> <div class="grow-0">
45Drives 45Drives
</div> </div>
<div id="footer-buttons" class="flex flex-row-reverse gap-buttons grow basis-0"> <div
id="footer-buttons"
class="flex flex-row-reverse gap-buttons grow basis-0"
>
<SettingsMenu /> <SettingsMenu />
</div> </div>
</div> </div>
</div> </div>
<Notifications :notificationFIFO="notificationFIFO" ref="notifications" /> <Notifications
:notificationFIFO="notificationFIFO"
ref="notifications"
/>
</template> </template>
<script setup> <script setup>

View File

@ -1,42 +1,102 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<transition enter-active-class="origin-top-left transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" <transition
enter-to-class="transform opacity-100 scale-100" leave-active-class="origin-top-left transition ease-in duration-75" enter-active-class="origin-top-left transition ease-out duration-100"
leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95"> enter-from-class="transform opacity-0 scale-95"
<div v-if="show" class="fixed inset-0 bg-transparent" @click="$emit('hide')"> enter-to-class="transform opacity-100 scale-100"
<div class="fixed z-20 max-w-sm flex flex-col items-stretch bg-default shadow-lg divide-y divide-default position-contextmenu"> leave-active-class="origin-top-left transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div
v-if="show"
class="fixed inset-0 bg-transparent"
@click="$emit('hide')"
>
<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()" @click="$emit('browserAction', 'back')" :class="{'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.backAllowed()}"> <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" /> <ArrowLeftIcon class="size-icon icon-default" />
</button> </button>
<button :disabled="!pathHistory?.forwardAllowed()" @click="$emit('browserAction', 'forward')" :class="{'grow flex items-center justify-center p-2': true, 'hover:bg-red-600/10': pathHistory?.forwardAllowed()}"> <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" /> <ArrowRightIcon class="size-icon icon-default" />
</button> </button>
<button @click="$emit('browserAction', 'up')" class="grow hover:bg-red-600/10 flex items-center justify-center p-2"> <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" /> <ArrowUpIcon class="size-icon icon-default" />
</button> </button>
</div> </div>
<div class="flex flex-col items-stretch"> <div class="flex flex-col items-stretch">
<!-- general actions --> <!-- general actions -->
<button v-if="entry?.path !== '/'" @click="$emit('browserAction', 'editPermissions', entry)" class="context-menu-button"> <button
v-if="entry?.path !== '/'"
class="context-menu-button"
@click="$emit('browserAction', 'editPermissions', entry)"
>
Edit permissions Edit permissions
</button> </button>
</div> </div>
<div v-if="entry?.resolvedType === 'f'" class="flex flex-col items-stretch"> <div
v-if="entry?.resolvedType === 'f'"
class="flex flex-col items-stretch"
>
<!-- regular file actions --> <!-- regular file actions -->
<button @click="$emit('browserAction', 'edit', entry)" class="context-menu-button"> <button
class="context-menu-button"
@click="$emit('browserAction', 'edit', entry)"
>
Edit contents Edit contents
</button> </button>
<button @click="$emit('browserAction', 'download', entry)" class="context-menu-button"> <button
class="context-menu-button"
@click="$emit('browserAction', 'download', entry)"
>
Download Download
</button> </button>
</div> </div>
<div v-else-if="entry?.resolvedType === 'd'" class="flex flex-col items-stretch"> <div
v-else-if="entry?.resolvedType === 'd'"
class="flex flex-col items-stretch"
>
<!-- directory actions --> <!-- directory actions -->
<button @click="$emit('browserAction', 'cd', entry)" class="context-menu-button"> <button
class="context-menu-button"
@click="$emit('browserAction', 'cd', entry)"
>
Open Open
</button> </button>
</div> </div>
<div v-if="entry?.type === 'l'" class="flex flex-col items-stretch"> <div
v-if="entry?.type === 'l'"
class="flex flex-col items-stretch"
>
<!-- link actions --> <!-- link actions -->
</div> </div>
</div> </div>
@ -76,11 +136,15 @@ export default {
<style scoped> <style scoped>
div.position-contextmenu { div.position-contextmenu {
top: v-bind(`${event?.clientY ?? 0}px`); top: v-bind("`${event?.clientY ?? 0}px`");
left: v-bind(`${event?.clientX ?? 0}px`); left: v-bind("`${event?.clientX ?? 0}px`");
} }
button.context-menu-button { button.context-menu-button {
@apply text-default hover:bg-red-600/10 font-normal px-4 py-2 text-sm text-left; @apply text-default font-normal px-4 py-2 text-sm text-left;
}
button.context-menu-button:hover {
@apply bg-red-600/10;
} }
</style> </style>

View File

@ -1,73 +1,173 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<template v-if="settings.directoryView?.view === 'list'"> <template v-if="settings.directoryView?.view === 'list'">
<tr <tr
:class="{ 'select-none dir-entry': true, '!bg-red-600/10': hover && !entry.selected, '!bg-red-600/20': hover && entry.selected, 'dir-entry-selected': entry.selected, 'suppress-border-t': suppressBorderT, 'suppress-border-b': suppressBorderB }"> :class="{ 'select-none dir-entry': true, '!bg-red-600/10': hover && !entry.selected, '!bg-red-600/20': hover && entry.selected, 'dir-entry-selected': entry.selected, 'suppress-border-t': suppressBorderT, 'suppress-border-b': suppressBorderB }">
<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).keys()" :key="i" v-memo="[level]"></div> <div class="indent-gap" />
<div class="relative w-6" :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']"> <div
<FolderIcon v-if="entry.resolvedType === 'd'" class="size-icon" /> class="relative w-6"
<DocumentIcon v-else class="size-icon" /> :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']"
<LinkIcon v-if="entry.type === 'l'" class="w-2 h-2 absolute right-0 bottom-0 text-default" /> >
<FolderIcon
v-if="entry.resolvedType === 'd'"
class="size-icon"
/>
<DocumentIcon
v-else
class="size-icon"
/>
<LinkIcon
v-if="entry.type === 'l'"
class="w-2 h-2 absolute right-0 bottom-0 text-default"
/>
</div> </div>
<button class="z-10 icon-default" v-if="entry.resolvedType === 'd'" @click.stop="toggleShowEntries" <button
@mouseenter="hover = true" @mouseleave="hover = false"> class="z-10 icon-default"
<ChevronDownIcon v-if="!showEntries" class="size-icon" /> v-if="entry.resolvedType === 'd'"
<ChevronUpIcon v-else class="size-icon" /> @click.stop="toggleShowEntries"
@mouseenter="hover = true"
@mouseleave="hover = false"
>
<ChevronDownIcon
v-if="!showEntries"
class="size-icon"
/>
<ChevronUpIcon
v-else
class="size-icon"
/>
</button> </button>
<div v-html="entry.nameHTML" :title="entry.name"></div> <div
<div v-if="entry.type === 'l'" class="inline-flex gap-1 items-center"> v-html="entry.nameHTML"
:title="entry.name"
></div>
<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.linkBroken" <XIcon
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0" /> v-if="entry.linkBroken"
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0"
/>
</div> </div>
<div v-html="entry.linkRawPathHTML ?? ''" :title="entry.linkRawPath"></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('directoryViewAction', 'toggleSelected', entry, $event)" @click.prevent="$emit('directoryViewAction', 'toggleSelected', entry, $event)"
@contextmenu.prevent.stop="$emit('browserAction', 'contextMenu', entry, $event)" @contextmenu.prevent.stop="$emit('browserAction', 'contextMenu', entry, $event)"
@dblclick="doubleClickCallback" @mouseenter="hover = true" @mouseleave="hover = false" @dblclick="doubleClickCallback"
ref="selectIntersectElement" /> @mouseenter="hover = true"
@mouseleave="hover = false"
ref="selectIntersectElement"
/>
</td> </td>
<td v-if="settings?.directoryView?.cols?.mode" class="font-mono">{{ entry.modeStr }}</td> <td
v-if="settings?.directoryView?.cols?.mode"
class="font-mono"
>{{ entry.modeStr }}</td>
<td v-if="settings?.directoryView?.cols?.owner">{{ entry.owner }}</td> <td v-if="settings?.directoryView?.cols?.owner">{{ entry.owner }}</td>
<td v-if="settings?.directoryView?.cols?.group">{{ entry.group }} </td> <td v-if="settings?.directoryView?.cols?.group">{{ entry.group }} </td>
<td v-if="settings?.directoryView?.cols?.size" class="font-mono text-right">{{ entry.sizeHuman }}</td> <td
v-if="settings?.directoryView?.cols?.size"
class="font-mono text-right"
>{{ entry.sizeHuman }}</td>
<td v-if="settings?.directoryView?.cols?.btime">{{ entry.btimeStr }}</td> <td v-if="settings?.directoryView?.cols?.btime">{{ entry.btimeStr }}</td>
<td v-if="settings?.directoryView?.cols?.mtime">{{ entry.mtimeStr }}</td> <td v-if="settings?.directoryView?.cols?.mtime">{{ entry.mtimeStr }}</td>
<td v-if="settings?.directoryView?.cols?.atime">{{ entry.atimeStr }}</td> <td v-if="settings?.directoryView?.cols?.atime">{{ entry.atimeStr }}</td>
</tr> </tr>
<component :is="DirectoryEntryList" v-if="entry.resolvedType === 'd' && showEntries" :host="host" <component
:path="entry.path" :isChild="true" :sortCallback="inheritedSortCallback" :is="DirectoryEntryList"
:searchFilterRegExp="searchFilterRegExp" @startProcessing="(...args) => $emit('startProcessing', ...args)" v-if="entry.resolvedType === 'd' && showEntries"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" @cancelShowEntries="showEntries = false" :host="host"
ref="directoryEntryListRef" :level="level + 1" :selectedCount="selectedCount" :path="entry.path"
:isChild="true"
:sortCallback="inheritedSortCallback"
:searchFilterRegExp="searchFilterRegExp"
:level="level + 1"
:selectedCount="selectedCount"
@browserAction="(...args) => $emit('browserAction', ...args)" @browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="(...args) => $emit('directoryViewAction', ...args)" /> @directoryViewAction="(...args) => $emit('directoryViewAction', ...args)"
@startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
@cancelShowEntries="showEntries = false"
ref="directoryEntryListRef"
/>
</template> </template>
<div v-else class="select-none dir-entry flex flex-col items-center overflow-hidden dir-entry-width p-2" <div
: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 }"> v-else
<div class="w-full" @dblclick="doubleClickCallback" class="select-none dir-entry flex flex-col items-center overflow-hidden dir-entry-width p-2"
@click.prevent="$emit('directoryViewAction', 'toggleSelected', entry, $event)" @mouseup.stop :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('directoryViewAction', 'toggleSelected', entry, $event)"
@mouseup.stop
@contextmenu.prevent.stop="$emit('browserAction', 'contextMenu', entry, $event)" @contextmenu.prevent.stop="$emit('browserAction', 'contextMenu', entry, $event)"
@mouseenter="hover = true" @mouseleave="hover = false" ref="selectIntersectElement"> @mouseenter="hover = true"
<div class="relative w-full" :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']"> @mouseleave="hover = false"
<FolderIcon v-if="entry.resolvedType === 'd'" class="w-full h-auto" /> ref="selectIntersectElement"
<DocumentIcon v-else class="w-full h-auto" /> >
<div :class="[entry.resolvedType === 'd' ? 'right-[15%] bottom-[25%]' : 'right-[25%] bottom-[15%]', 'inline absolute w-[20%]']" <div
:title="`-> ${entry.linkRawPath ?? '?'}`"> class="relative w-full"
<LinkIcon v-if="entry.type === 'l'" :class="[entry.cut ? 'text-gray-500/50' : 'icon-default']"
:class="[entry.linkBroken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900']" /> >
<FolderIcon
v-if="entry.resolvedType === 'd'"
class="w-full h-auto"
/>
<DocumentIcon
v-else
class="w-full h-auto"
/>
<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'"
: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"
:class="{ 'multiline-ellipsis': entry.selected && selectedCount > 1, 'truncate': !entry.selected, 'line-through': entry.cut }" :class="{ 'multiline-ellipsis': entry.selected && selectedCount > 1, 'truncate': !entry.selected, 'line-through': entry.cut }"
v-html="entry.nameHTML" :title="entry.name"></div> :title="entry.name"
v-html="entry.nameHTML"
></div>
</div> </div>
</div> </div>
<Teleport to="#footer-text" v-if="entry.selected && selectedCount === 1"> <Teleport
to="#footer-text"
v-if="entry.selected && selectedCount === 1"
>
<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>
@ -78,7 +178,7 @@
</template> </template>
<script> <script>
import { ref, inject, watch, nextTick, onBeforeUnmount, onMounted, onActivated, onDeactivated, onUpdated } from 'vue'; import { ref, inject, nextTick, onMounted, 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 { notificationsInjectionKey, settingsInjectionKey } from '../keys'; import { notificationsInjectionKey, settingsInjectionKey } from '../keys';
import DirectoryEntryList from './DirectoryEntryList.vue'; import DirectoryEntryList from './DirectoryEntryList.vue';
@ -181,3 +281,9 @@ export default {
] ]
} }
</script> </script>
<style>
.indent-gap {
width: v-bind("`${24 * level}px`");
}
</style>

View File

@ -1,24 +1,59 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<DirectoryEntry v-for="entry, index in visibleEntries" :key="entry.path" :host="host" :entry="entry" <DirectoryEntry
:inheritedSortCallback="sortCallback" :searchFilterRegExp="searchFilterRegExp" v-for="entry, index in visibleEntries"
@sortEntries="sortEntries" :key="entry.path"
@startProcessing="(...args) => $emit('startProcessing', ...args)" :host="host"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)" ref="entryRefs" :level="level" :selectedCount="selectedCount" :entry="entry"
@setEntryProp="(prop, value) => entry[prop] = value" :inheritedSortCallback="sortCallback"
@browserAction="(...args) => $emit('browserAction', ...args)" :searchFilterRegExp="searchFilterRegExp"
@directoryViewAction="(...args) => $emit('directoryViewAction', ...args)" :level="level"
:selectedCount="selectedCount"
: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)"
:suppressBorderR="settings.directoryView.view !== 'list' && (visibleEntries[index + 1]?.selected && (index + 1) % cols !== 0)" /> :suppressBorderR="settings.directoryView.view !== 'list' && (visibleEntries[index + 1]?.selected && (index + 1) % cols !== 0)"
@browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="(...args) => $emit('directoryViewAction', ...args)"
@sortEntries="sortEntries"
@startProcessing="(...args) => $emit('startProcessing', ...args)"
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
@setEntryProp="(prop, value) => entry[prop] = value"
ref="entryRefs"
/>
<tr v-if="visibleEntries.length === 0"> <tr v-if="visibleEntries.length === 0">
<td :colspan="Object.values(settings?.directoryView?.cols ?? {}).reduce((sum, current) => current ? sum + 1 : sum, 1) ?? 100" <td
class="!pl-1 text-muted text-sm"> :colspan="Object.values(settings?.directoryView?.cols ?? {}).reduce((sum, current) => current ? sum + 1 : sum, 1) ?? 100"
<div class="w-6" v-for="i in Array(level).fill(0)" v-memo="[level]"></div> class="!pl-1 text-muted text-sm"
>
<div
class="w-6"
v-for="i in Array(level).fill(0)"
v-memo="[level]"
></div>
<div class="inline-block">No entries.</div> <div class="inline-block">No entries.</div>
</td> </td>
</tr> </tr>
<Teleport to="#footer-text" v-if="selectedCount === 0"> <Teleport
to="#footer-text"
v-if="selectedCount === 0"
>
<div> <div>
<span v-if="level > 0">{{ path.split('/').slice(-1 * (level)).join('/') }}:</span> <span v-if="level > 0">{{ path.split('/').slice(-1 * (level)).join('/') }}:</span>
{{ stats }} {{ stats }}
@ -27,7 +62,7 @@
</template> </template>
<script> <script>
import { ref, reactive, computed, inject, watch, onBeforeUnmount, onMounted, nextTick, onUnmounted } from 'vue'; import { ref, reactive, computed, inject, watch, onBeforeUnmount, nextTick } from 'vue';
import { errorStringHTML } from '@45drives/cockpit-helpers'; import { errorStringHTML } from '@45drives/cockpit-helpers';
import { notificationsInjectionKey, settingsInjectionKey, clipboardInjectionKey } from '../keys'; import { notificationsInjectionKey, settingsInjectionKey, clipboardInjectionKey } from '../keys';
import DirectoryEntry from './DirectoryEntry.vue'; import DirectoryEntry from './DirectoryEntry.vue';

View File

@ -1,118 +1,235 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<div class="h-full" @keydown="keyHandler($event)" tabindex="-1" :class="{ '!cursor-wait': processing }"> <div
<DragSelectArea class="h-full" @selectRectangle="selectRectangle" @mouseup.exact="deselectAll" @contextmenu.prevent="$emit('browserAction', 'contextMenu', { host, path, name: `Current directory (${path.split('/').pop() || '/'})` }, $event)"> class="h-full"
<Table :key="host + path" v-if="settings.directoryView?.view === 'list'" emptyText="No entries." noHeader stickyHeaders @keydown="keyHandler($event)"
noShrink noShrinkHeight="h-full"> tabindex="-1"
:class="{ '!cursor-wait': processing }"
>
<DragSelectArea
class="h-full"
@selectRectangle="selectRectangle"
@mouseup.exact="deselectAll"
@contextmenu.prevent="$emit('browserAction', 'contextMenu', { host, path, name: `Current directory (${path.split('/').pop() || '/'})` }, $event)"
>
<Table
:key="host + path"
v-if="settings.directoryView?.view === 'list'"
emptyText="No entries."
noHeader
stickyHeaders
noShrink
noShrinkHeight="h-full"
>
<template #thead> <template #thead>
<tr> <tr>
<th class="!pl-1 border-l-2 border-l-transparent last:border-r-2 last:border-r-transparent"> <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 flex-row flex-nowrap gap-1 items-center">
<div class="flex items-center justify-center w-6"> <div class="flex items-center justify-center w-6">
<LoadingSpinner v-if="processing" class="size-icon" /> <LoadingSpinner
v-if="processing"
class="size-icon"
/>
</div> </div>
<div class="grow">Name</div> <div class="grow">Name</div>
<SortCallbackButton initialFuncIsMine v-model="sortCallback" <SortCallbackButton
:compareFunc="sortCallbacks.name" /> initialFuncIsMine
v-model="sortCallback"
:compareFunc="sortCallbacks.name"
/>
</div> </div>
</th> </th>
<th class="last:border-r-2 last:border-r-transparent" <th
v-if="settings?.directoryView?.cols?.mode"> class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.mode"
>
Mode</th> Mode</th>
<th class="last:border-r-2 last:border-r-transparent" <th
v-if="settings?.directoryView?.cols?.owner"> 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="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Owner</div> <div class="grow">Owner</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.owner" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.owner"
/>
</div> </div>
</th> </th>
<th class="last:border-r-2 last:border-r-transparent" <th
v-if="settings?.directoryView?.cols?.group"> 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="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Group</div> <div class="grow">Group</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.group" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.group"
/>
</div> </div>
</th> </th>
<th class="last:border-r-2 last:border-r-transparent" <th
v-if="settings?.directoryView?.cols?.size"> 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="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow text-right">Size</div> <div class="grow text-right">Size</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.size" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.size"
/>
</div> </div>
</th> </th>
<th class="last:border-r-2 last:border-r-transparent" <th
v-if="settings?.directoryView?.cols?.btime"> class="last:border-r-2 last:border-r-transparent"
v-if="settings?.directoryView?.cols?.btime"
>
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Created</div> <div class="grow">Created</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.btime" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.btime"
/>
</div> </div>
</th> </th>
<th class="last:border-r-2 last:border-r-transparent" <th
v-if="settings?.directoryView?.cols?.mtime"> 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="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Modified</div> <div class="grow">Modified</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.mtime" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.mtime"
/>
</div> </div>
</th> </th>
<th class="last:border-r-2 last:border-r-transparent" <th
v-if="settings?.directoryView?.cols?.atime"> 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="flex flex-row flex-nowrap gap-2 items-center">
<div class="grow">Accessed</div> <div class="grow">Accessed</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.atime" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.atime"
/>
</div> </div>
</th> </th>
</tr> </tr>
</template> </template>
<template #tbody> <template #tbody>
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback" <DirectoryEntryList
:host="host"
:path="path"
:sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" :searchFilterRegExp="searchFilterRegExp"
@startProcessing="processing++" @stopProcessing="processing--" :level="0"
@browserAction="(...args) => $emit('browserAction', ...args)" :cols="1"
@directoryViewAction="handleAction" ref="directoryEntryListRef" :selectedCount="selectedCount"
@tallySelected="tallySelected" @tallySelected="tallySelected"
:level="0" :cols="1" :selectedCount="selectedCount" /> @startProcessing="processing++"
@stopProcessing="processing--"
@browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="handleAction"
ref="directoryEntryListRef"
/>
</template> </template>
</Table> </Table>
<div v-else class="h-full flex flex-col items-stretch"> <div
<div class="grow-0 flex flex-row justify-start items-center px-6 py-2 gap-4 text-sm border-t border-default"> v-else
class="h-full flex flex-col items-stretch"
>
<div
class="grow-0 flex flex-row justify-start items-center px-6 py-2 gap-4 text-sm border-t border-default">
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div>Name</div> <div>Name</div>
<SortCallbackButton initialFuncIsMine v-model="sortCallback" <SortCallbackButton
:compareFunc="sortCallbacks.name" /> initialFuncIsMine
v-model="sortCallback"
:compareFunc="sortCallbacks.name"
/>
</div> </div>
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div>Owner</div> <div>Owner</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.owner" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.owner"
/>
</div> </div>
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div>Group</div> <div>Group</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.group" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.group"
/>
</div> </div>
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div>Size</div> <div>Size</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.size" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.size"
/>
</div> </div>
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div>Created</div> <div>Created</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.btime" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.btime"
/>
</div> </div>
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div>Modified</div> <div>Modified</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.mtime" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.mtime"
/>
</div> </div>
<div class="flex flex-row flex-nowrap gap-2 items-center"> <div class="flex flex-row flex-nowrap gap-2 items-center">
<div>Accessed</div> <div>Accessed</div>
<SortCallbackButton v-model="sortCallback" :compareFunc="sortCallbacks.atime" /> <SortCallbackButton
v-model="sortCallback"
:compareFunc="sortCallbacks.atime"
/>
</div> </div>
</div> </div>
<div :key="host + path" class="grow flex flex-wrap bg-well overflow-y-auto content-start" ref="gridRef" <div
@wheel="scrollHandler"> :key="host + path"
<DirectoryEntryList :host="host" :path="path" :sortCallback="sortCallback" class="grow flex flex-wrap bg-well overflow-y-auto content-start"
ref="gridRef"
@wheel="scrollHandler"
>
<DirectoryEntryList
:host="host"
:path="path"
:sortCallback="sortCallback"
:searchFilterRegExp="searchFilterRegExp" :searchFilterRegExp="searchFilterRegExp"
@startProcessing="processing++" @stopProcessing="processing--" :level="0"
:cols="cols"
:selectedCount="selectedCount"
@startProcessing="processing++"
@stopProcessing="processing--"
@browserAction="(...args) => $emit('browserAction', ...args)" @browserAction="(...args) => $emit('browserAction', ...args)"
@directoryViewAction="handleAction" @directoryViewAction="handleAction"
@tallySelected="tallySelected" ref="directoryEntryListRef" @tallySelected="tallySelected"
:level="0" :cols="cols" :selectedCount="selectedCount" /> ref="directoryEntryListRef"
/>
</div> </div>
</div> </div>
</DragSelectArea> </DragSelectArea>
@ -125,7 +242,7 @@
</template> </template>
<script> <script>
import { ref, inject, watch, onMounted, computed, onBeforeUnmount, onUpdated, nextTick } from 'vue'; import { ref, inject, watch, onMounted, onBeforeUnmount, onUpdated } from 'vue';
import Table from './Table.vue'; import Table from './Table.vue';
import { clipboardInjectionKey, notificationsInjectionKey, settingsInjectionKey } from '../keys'; import { clipboardInjectionKey, notificationsInjectionKey, settingsInjectionKey } from '../keys';
import LoadingSpinner from './LoadingSpinner.vue'; import LoadingSpinner from './LoadingSpinner.vue';
@ -279,7 +396,7 @@ export default {
}; };
}); });
if (event.shiftKey) if (event.shiftKey)
clipboard.content = [ ...newContent, ...clipboard.content ].filter((a, index, arr) => arr.findIndex(b => b.uniqueId === a.uniqueId) === index); clipboard.content = [...newContent, ...clipboard.content].filter((a, index, arr) => arr.findIndex(b => b.uniqueId === a.uniqueId) === index);
else else
clipboard.content = newContent; clipboard.content = newContent;
const message = event.shiftKey const message = event.shiftKey
@ -397,11 +514,11 @@ export default {
<style> <style>
tr.dir-entry>td { 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; @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 { tr.dir-entry-selected>td {
@apply border-y first:border-l-red-600/50 last:border-red-600/50 bg-red-600/10; @apply border-y first: border-l-red-600/50 last:border-red-600/50 bg-red-600/10;
} }
div.dir-entry { div.dir-entry {
@ -413,19 +530,19 @@ div.dir-entry-selected {
} }
tr.dir-entry-selected.suppress-border-t>td { tr.dir-entry-selected.suppress-border-t>td {
@apply !border-t-0; @apply !border-t-0;
} }
tr.dir-entry-selected.suppress-border-b>td { tr.dir-entry-selected.suppress-border-b>td {
@apply !border-b-0; @apply !border-b-0;
} }
tr.dir-entry-selected.suppress-border-l>td { tr.dir-entry-selected.suppress-border-l>td {
@apply !border-l-0; @apply !border-l-0;
} }
tr.dir-entry-selected.suppress-border-r>td { tr.dir-entry-selected.suppress-border-r>td {
@apply !border-r-0; @apply !border-r-0;
} }
div.dir-entry-width { div.dir-entry-width {

View File

@ -1,12 +1,36 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<div v-show="dragging" class="fixed border-red-600/50 border border-solid bg-red-600/10 rounded-lg" ref="selectionBox"></div> <div
<div @mousedown="startDrag" v-bind="$attrs"> v-show="dragging"
class="fixed border-red-600/50 border border-solid bg-red-600/10 rounded-lg"
ref="selectionBox"
></div>
<div
v-bind="$attrs"
@mousedown="startDrag"
>
<slot /> <slot />
</div> </div>
</template> </template>
<script> <script>
import { ref, reactive, watch, onMounted, nextTick, onBeforeUnmount } from 'vue' import { ref, reactive, watch, onMounted } from 'vue'
export default { export default {
props: { props: {
areaThreshold: { areaThreshold: {

View File

@ -1,17 +1,17 @@
<!-- <!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com> Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit File Sharing. This file is part of Cockpit Navigator.
Cockpit File Sharing is free software: you can redistribute it and/or modify it under the terms Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3 of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version. of the License, or (at your option) any later version.
Cockpit File Sharing is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit File Sharing. You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>. If not, see <https://www.gnu.org/licenses/>.
--> -->
@ -23,19 +23,55 @@ If not, see <https://www.gnu.org/licenses/>.
<label class="text-label">Execute</label> <label class="text-label">Execute</label>
<label class="justify-self-start text-label">Owner</label> <label class="justify-self-start text-label">Owner</label>
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.owner.read" /> <input
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.owner.write" /> type="checkbox"
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.owner.execute" /> class="input-checkbox"
v-model="modeMatrix.owner.read"
/>
<input
type="checkbox"
class="input-checkbox"
v-model="modeMatrix.owner.write"
/>
<input
type="checkbox"
class="input-checkbox"
v-model="modeMatrix.owner.execute"
/>
<label class="justify-self-start text-label">Group</label> <label class="justify-self-start text-label">Group</label>
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.group.read" /> <input
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.group.write" /> type="checkbox"
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.group.execute" /> class="input-checkbox"
v-model="modeMatrix.group.read"
/>
<input
type="checkbox"
class="input-checkbox"
v-model="modeMatrix.group.write"
/>
<input
type="checkbox"
class="input-checkbox"
v-model="modeMatrix.group.execute"
/>
<label class="justify-self-start text-label">Other</label> <label class="justify-self-start text-label">Other</label>
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.other.read" /> <input
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.other.write" /> type="checkbox"
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.other.execute" /> class="input-checkbox"
v-model="modeMatrix.other.read"
/>
<input
type="checkbox"
class="input-checkbox"
v-model="modeMatrix.other.write"
/>
<input
type="checkbox"
class="input-checkbox"
v-model="modeMatrix.other.execute"
/>
<label class="justify-self-start text-label">Mode</label> <label class="justify-self-start text-label">Mode</label>
<span class="col-span-3 font-mono font-medium whitespace-nowrap">{{ modeStr }}</span> <span class="col-span-3 font-mono font-medium whitespace-nowrap">{{ modeStr }}</span>

View File

@ -1,34 +1,53 @@
<!-- <!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com> Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit File Sharing. This file is part of Cockpit Navigator.
Cockpit File Sharing is free software: you can redistribute it and/or modify it under the terms Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3 of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version. of the License, or (at your option) any later version.
Cockpit File Sharing is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit File Sharing. You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>. If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<ModalPopup :showModal="show" :headerText="entry?.nameHTML ?? entry?.name" @apply="apply" @cancel="$emit('hide')"> <ModalPopup
:showModal="show"
:headerText="entry?.nameHTML ?? entry?.name"
@apply="apply"
@cancel="$emit('hide')"
>
<div class="flex flex-col space-y-content items-start"> <div class="flex flex-col space-y-content items-start">
<FileModeMatrix v-model="mode" /> <FileModeMatrix v-model="mode" />
<div> <div>
<label class="block text-sm font-medium">Owner</label> <label class="block text-sm font-medium">Owner</label>
<select class="input-textlike" v-model="owner"> <select
<option v-for="user in users" :key="user.pretty" :value="user.user">{{ user.pretty }}</option> class="input-textlike"
v-model="owner"
>
<option
v-for="user in users"
:key="user.pretty"
:value="user.user"
>{{ user.pretty }}</option>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium">Group</label> <label class="block text-sm font-medium">Group</label>
<select class="input-textlike" v-model="group"> <select
<option v-for="group in groups" :key="group.pretty" :value="group.group">{{ group.pretty }}</option> class="input-textlike"
v-model="group"
>
<option
v-for="group in groups"
:key="group.pretty"
:value="group.group"
>{{ group.pretty }}</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -1,5 +1,22 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<button @click="$emit('update:modelValue', modelValue === trueValue ? falseValue: trueValue)"> <button @click="$emit('update:modelValue', modelValue === trueValue ? falseValue : trueValue)">
<slot :value="modelValue === trueValue"> <slot :value="modelValue === trueValue">
</slot> </slot>

View File

@ -16,19 +16,29 @@ If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<SwitchGroup as="div" :class="[labelRight ? 'flex-row-reverse' : 'flex-row', 'inline-flex items-center']"> <SwitchGroup
as="div"
:class="[labelRight ? 'flex-row-reverse' : 'flex-row', 'inline-flex items-center']"
>
<span :class="[labelRight ? 'grow' : '', 'flex flex-col']"> <span :class="[labelRight ? 'grow' : '', 'flex flex-col']">
<SwitchLabel as="div" class="text-label"><slot /></SwitchLabel> <SwitchLabel
as="div"
class="text-label"
>
<slot />
</SwitchLabel>
<SwitchDescription <SwitchDescription
as="div" as="div"
class="text-sm text-muted" class="text-sm text-muted"
><slot name="description" /></SwitchDescription> >
<slot name="description" />
</SwitchDescription>
</span> </span>
<div :class="[labelRight ? '' : 'grow', 'w-4']"></div> <!-- spacer --> <div :class="[labelRight ? '' : 'grow', 'w-4']"></div> <!-- spacer -->
<Switch <Switch
:modelValue="modelValue" :modelValue="modelValue"
@update:modelValue="newValue => { $emit('update:modelValue', newValue); $emit('change', newValue); $emit('input', newValue); }"
:class="[modelValue ? 'bg-45d' : 'bg-well', 'inline-flex shrink-0 h-6 w-11 p-[2px] rounded-full cursor-pointer shadow-inner']" :class="[modelValue ? 'bg-45d' : 'bg-well', 'inline-flex shrink-0 h-6 w-11 p-[2px] rounded-full cursor-pointer shadow-inner']"
@update:modelValue="newValue => { $emit('update:modelValue', newValue); $emit('change', newValue); $emit('input', newValue); }"
> >
<span <span
aria-hidden="true" aria-hidden="true"
@ -40,7 +50,6 @@ If not, see <https://www.gnu.org/licenses/>.
<script> <script>
import { Switch, SwitchDescription, SwitchGroup, SwitchLabel } from '@headlessui/vue' import { Switch, SwitchDescription, SwitchGroup, SwitchLabel } from '@headlessui/vue'
import { watch, ref } from 'vue';
export default { export default {
props: { props: {
modelValue: Boolean, modelValue: Boolean,

View File

@ -16,5 +16,6 @@ If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div class="aspect-square animate-spin border-neutral-300 border-t-neutral-500 dark:border-neutral-500 dark:border-t-neutral-200 rounded-full"></div> <div
class="aspect-square animate-spin border-neutral-300 border-t-neutral-500 dark:border-neutral-500 dark:border-t-neutral-200 rounded-full" />
</template> </template>

View File

@ -16,25 +16,45 @@ If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<TransitionRoot as="div" class="fixed inset-0 z-10 overflow-visible" :show="showModal"> <TransitionRoot
<TransitionChild as="template" enter="ease-out duration-500" enter-from="opacity-0" enter-to="opacity-100" as="div"
leave="ease-in duration-500" leave-from="opacity-100" leave-to="opacity-0"> class="fixed inset-0 z-10 overflow-visible"
:show="showModal"
>
<TransitionChild
as="template"
enter="ease-out duration-500"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in duration-500"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed z-10 inset-0 bg-neutral-500/75 dark:bg-black/50 transition-opacity pointer" /> <div class="fixed z-10 inset-0 bg-neutral-500/75 dark:bg-black/50 transition-opacity pointer" />
</TransitionChild> </TransitionChild>
<div @click.self="$emit('close')" <div
class="fixed z-10 inset-0 overflow-hidden flex items-end sm:items-center justify-center px-4 pb-20 sm:p-0"> class="fixed z-10 inset-0 overflow-hidden flex items-end sm:items-center justify-center px-4 pb-20 sm:p-0"
<TransitionChild as="template" enter="ease-out duration-300" @click.self="$emit('close')"
>
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-90" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-90"
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-100" enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-100"
leave-from="opacity-100 translate-y-0 sm:scale-100" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-75"> leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-75"
>
<div <div
:class="[autoWidth ? 'sm:max-w-full' : 'sm:max-w-lg', 'inline-flex flex-col items-stretch overflow-hidden transform transition-all text-left z-10']"> :class="[autoWidth ? 'sm:max-w-full' : 'sm:max-w-lg', 'inline-flex flex-col items-stretch overflow-hidden transform transition-all text-left z-10']">
<div class="block w-[512px]" /> <!-- set min width of div --> <div class="block w-[512px]" /> <!-- set min width of div -->
<div class="card flex flex-col items-stretch overflow-hidden"> <div class="card flex flex-col items-stretch overflow-hidden">
<div class="card-header"> <div class="card-header">
<slot name="header"> <slot name="header">
<h3 class="text-header" v-html="headerText" /> <h3
class="text-header"
v-html="headerText"
/>
</slot> </slot>
</div> </div>
<div class="card-body flex flex-row items-center gap-2"> <div class="card-body flex flex-row items-center gap-2">
@ -45,12 +65,20 @@ If not, see <https://www.gnu.org/licenses/>.
</div> </div>
<div class="card-footer button-group-row justify-end"> <div class="card-footer button-group-row justify-end">
<slot name="footer"> <slot name="footer">
<button v-if="!noCancel" type="button" class="btn btn-secondary" <button
@click="$emit('cancel'); $emit('close')"> v-if="!noCancel"
type="button"
class="btn btn-secondary"
@click="$emit('cancel'); $emit('close')"
>
{{ cancelText }} {{ cancelText }}
</button> </button>
<button type="button" :class="['btn', applyDangerous ? 'btn-danger' : 'btn-primary']" <button
@click="$emit('apply'); $emit('close')" :disabled="disableContinue"> type="button"
:class="['btn', applyDangerous ? 'btn-danger' : 'btn-primary']"
:disabled="disableContinue"
@click="$emit('apply'); $emit('close')"
>
{{ applyText }} {{ applyText }}
</button> </button>
</slot> </slot>

View File

@ -16,49 +16,93 @@ If not, see <https://www.gnu.org/licenses/>.
--> -->
<template> <template>
<div aria-live="assertive" <div
class="fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6 sm:items-start z-20 h-screen overflow-y-auto"> aria-live="assertive"
<transition-group tag="div" class="w-full flex flex-col-reverse items-center sm:items-end sm:flex-col space-y-content" class="fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6 sm:items-start z-20 h-screen overflow-y-auto"
>
<transition-group
tag="div"
class="w-full flex flex-col-reverse items-center sm:items-end sm:flex-col space-y-content"
enter-active-class="transition-all transform ease-out duration-300" enter-active-class="transition-all transform ease-out duration-300"
enter-from-class="translate-y-8 opacity-0 scale-95 sm:translate-y-0 sm:translate-x-8" enter-from-class="translate-y-8 opacity-0 scale-95 sm:translate-y-0 sm:translate-x-8"
enter-to-class="translate-y-0 opacity-100 scale-100 sm:translate-x-0" enter-to-class="translate-y-0 opacity-100 scale-100 sm:translate-x-0"
leave-active-class="transition-all transform ease-in duration-100" leave-active-class="transition-all transform ease-in duration-100"
leave-from-class="opacity-100 scale-100 sm:translate-x-0" leave-from-class="opacity-100 scale-100 sm:translate-x-0"
leave-to-class="opacity-0 scale-95 sm:translate-x-8"> leave-to-class="opacity-0 scale-95 sm:translate-x-8"
<div v-for="notification in notificationList" :key="notification.id" >
<div
v-for="notification in notificationList"
:key="notification.id"
class="max-w-sm w-full shadow-lg pointer-events-auto overflow-hidden bg-default text-default" class="max-w-sm w-full shadow-lg pointer-events-auto overflow-hidden bg-default text-default"
@mouseenter="notification.clearTimeouts?.()" @mouseleave="notification.setTimeouts?.()"> @mouseenter="notification.clearTimeouts?.()"
@mouseleave="notification.setTimeouts?.()"
>
<div class="p-4"> <div class="p-4">
<div class="flex items-start"> <div class="flex items-start">
<div class="flex-shrink-0" aria-hidden="true"> <div
<ExclamationCircleIcon v-if="notification.level === 'error'" class="icon-error size-icon-lg" class="flex-shrink-0"
aria-hidden="true" /> aria-hidden="true"
<ExclamationCircleIcon v-else-if="notification.level === 'warning'" >
class="icon-warning size-icon-lg" aria-hidden="true" /> <ExclamationCircleIcon
<CheckCircleIcon v-else-if="notification.level === 'success'" v-if="notification.level === 'error'"
class="icon-success size-icon-lg" aria-hidden="true" /> class="icon-error size-icon-lg"
<MinusCircleIcon v-else-if="notification.level === 'denied'" class="icon-error size-icon-lg" aria-hidden="true"
aria-hidden="true" /> />
<InformationCircleIcon v-else class="icon-info size-icon-lg" /> <ExclamationCircleIcon
v-else-if="notification.level === 'warning'"
class="icon-warning size-icon-lg"
aria-hidden="true"
/>
<CheckCircleIcon
v-else-if="notification.level === 'success'"
class="icon-success size-icon-lg"
aria-hidden="true"
/>
<MinusCircleIcon
v-else-if="notification.level === 'denied'"
class="icon-error size-icon-lg"
aria-hidden="true"
/>
<InformationCircleIcon
v-else
class="icon-info size-icon-lg"
/>
</div> </div>
<div class="ml-3 w-0 flex-1 pt-0.5"> <div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium">{{ notification.title }}</p> <p class="text-sm font-medium">{{ notification.title }}</p>
<p class="mt-1 text-sm text-muted whitespace-pre-wrap" v-html="notification.body"> <p
class="mt-1 text-sm text-muted whitespace-pre-wrap"
v-html="notification.body"
>
</p> </p>
<div v-if="notification.actions?.length" class="mt-3 flex space-x-7"> <div
<button v-for="action in notification.actions" @click="action.callback" v-if="notification.actions?.length"
class="rounded-md text-sm font-medium text-primary"> class="mt-3 flex space-x-7"
>
<button
v-for="action in notification.actions"
class="rounded-md text-sm font-medium text-primary"
@click="action.callback"
>
{{ action.text }} {{ action.text }}
</button> </button>
<button @click="notification.show = false" type="button" <button
class="rounded-md text-sm font-medium text-secondary">Dismiss</button> type="button"
class="rounded-md text-sm font-medium text-secondary"
@click="notification.show = false"
>Dismiss</button>
</div> </div>
</div> </div>
<div class="ml-4 flex-shrink-0 flex"> <div class="ml-4 flex-shrink-0 flex">
<button @click="notification.show = false" <button
class="icon-default"> class="icon-default"
@click="notification.show = false"
>
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
<XIcon class="size-icon" aria-hidden="true" /> <XIcon
class="size-icon"
aria-hidden="true"
/>
</button> </button>
</div> </div>
</div> </div>
@ -69,7 +113,7 @@ If not, see <https://www.gnu.org/licenses/>.
</template> </template>
<script> <script>
import { ref, watch, reactive, onUnmounted } from 'vue'; import { ref, watch, reactive } from 'vue';
import { InformationCircleIcon, ExclamationCircleIcon, MinusCircleIcon, CheckCircleIcon } from '@heroicons/vue/outline'; import { InformationCircleIcon, ExclamationCircleIcon, MinusCircleIcon, CheckCircleIcon } from '@heroicons/vue/outline';
import { XIcon } from '@heroicons/vue/solid'; import { XIcon } from '@heroicons/vue/solid';
import { FIFO } from '@45drives/cockpit-helpers'; import { FIFO } from '@45drives/cockpit-helpers';

View File

@ -1,26 +1,73 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="inline relative"> <div class="inline relative">
<input type="text" class="input-textlike pr-10" v-model="hostInput" @change="hostChangeCallback" <input
ref="hostInputRef" /> type="text"
class="input-textlike pr-10"
v-model="hostInput"
ref="hostInputRef"
@change="hostChangeCallback"
/>
<div class="absolute right-0 inset-y-0 pr-3 flex items-center pointer-events-none"> <div class="absolute right-0 inset-y-0 pr-3 flex items-center pointer-events-none">
<ServerIcon class="size-icon icon-default" /> <ServerIcon class="size-icon icon-default" />
</div> </div>
</div> </div>
<div class="flex items-center cursor-text h-10 grow" @click="typing = true"> <div
class="flex items-center cursor-text h-10 grow"
@click="typing = true"
>
<template v-if="typing"> <template v-if="typing">
<input v-model="pathInput" type="text" list="pathInputSuggestions" class="block w-full input-textlike" <input
@change="pathChangeCallback" ref="pathInputRef" @focusout="typing = false" /> v-model="pathInput"
type="text"
list="pathInputSuggestions"
class="block w-full input-textlike"
@focusout="typing = false"
@change="pathChangeCallback"
ref="pathInputRef"
/>
<datalist id="pathInputSuggestions"> <datalist id="pathInputSuggestions">
<option v-for="path in pathSuggestions" :value="path" /> <option
v-for="path in pathSuggestions"
:value="path"
/>
</datalist> </datalist>
</template> </template>
<div v-else class="inline-flex items-center gap-1"> <div
<template v-for="segment, index in pathArr" :key="index"> v-else
<ChevronRightIcon v-if="index > 0" class="size-icon icon-default" /> class="inline-flex items-center gap-1"
<button @click.prevent.stop="$emit('cd', { path: `/${pathArr.slice(1, index + 1).join('/')}` })" >
class="p-2 hover:bg-accent rounded-lg cursor-pointer" v-html="escapeStringHTML(segment)" <template
:title="escapeString(`/${pathArr.slice(1, index + 1).join('/')}`)"></button> v-for="segment, index in pathArr"
:key="index"
>
<ChevronRightIcon
v-if="index > 0"
class="size-icon icon-default"
/>
<button
class="p-2 hover:bg-accent rounded-lg cursor-pointer"
v-html="escapeStringHTML(segment)"
:title="escapeString(`/${pathArr.slice(1, index + 1).join('/')}`)"
@click.prevent.stop="$emit('cd', { path: `/${pathArr.slice(1, index + 1).join('/')}` })"
></button>
</template> </template>
</div> </div>
</div> </div>

View File

@ -1,3 +1,20 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<button @click="showMenu = true"> <button @click="showMenu = true">
<AdjustmentsIcon class="size-icon icon-default" /> <AdjustmentsIcon class="size-icon icon-default" />
@ -16,15 +33,16 @@
<LabelledSwitch v-model="darkMode">Dark mode</LabelledSwitch> <LabelledSwitch v-model="darkMode">Dark mode</LabelledSwitch>
<LabelledSwitch v-model="settings.directoryView.showHidden">Show hidden files</LabelledSwitch> <LabelledSwitch v-model="settings.directoryView.showHidden">Show hidden files</LabelledSwitch>
<LabelledSwitch v-model="booleanAnalogs.directoryView.view.bool">List view</LabelledSwitch> <LabelledSwitch v-model="booleanAnalogs.directoryView.view.bool">List view</LabelledSwitch>
<LabelledSwitch <LabelledSwitch v-model="settings.directoryView.separateDirs">Separate directories while sorting
v-model="settings.directoryView.separateDirs" </LabelledSwitch>
>Separate directories while sorting</LabelledSwitch>
</div> </div>
<div v-if="booleanAnalogs.directoryView.view.bool" class="self-stretch"> <div
v-if="booleanAnalogs.directoryView.view.bool"
class="self-stretch"
>
<div>List view columns</div> <div>List view columns</div>
<div <div
class="flex justify-start text-sm rounded-lg divide-x divide-default border border-default shadow" class="flex justify-start text-sm rounded-lg divide-x divide-default border border-default shadow">
>
<div class="flex flex-col grow items-stretch"> <div class="flex flex-col grow items-stretch">
<div class="font-semibold px-2">Visible</div> <div class="font-semibold px-2">Visible</div>
<div <div

View File

@ -1,13 +1,35 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<button @click="updateModel()"> <button @click="updateModel()">
<SortDescendingIcon v-if="reverse" :class="[funcIsMine ? 'icon-45d' : 'icon-default', 'size-icon']" /> <SortDescendingIcon
<SortAscendingIcon v-else :class="[funcIsMine ? 'icon-45d' : 'icon-default', 'size-icon']" /> v-if="reverse"
:class="[funcIsMine ? 'icon-45d' : 'icon-default', 'size-icon']"
/>
<SortAscendingIcon
v-else
:class="[funcIsMine ? 'icon-45d' : 'icon-default', 'size-icon']"
/>
</button> </button>
</template> </template>
<script> <script>
import { nextTick, ref, watch } from 'vue'; import { SortDescendingIcon } from "@heroicons/vue/solid";
import { SortAscendingIcon, SortDescendingIcon } from "@heroicons/vue/solid";
export default { export default {
props: { props: {

View File

@ -40,7 +40,10 @@ If not, see <https://www.gnu.org/licenses/>.
<tbody class="bg-default w-full"> <tbody class="bg-default w-full">
<slot name="tbody"> <slot name="tbody">
<tr> <tr>
<td colspan="100%" class="text-center align-middle text-muted text-sm">{{ emptyText }}</td> <td
colspan="100%"
class="text-center align-middle text-muted text-sm"
>{{ emptyText }}</td>
</tr> </tr>
</slot> </slot>
</tbody> </tbody>
@ -97,7 +100,7 @@ table.houston-table thead.use-sticky tr th {
table.houston-table th, table.houston-table th,
table.houston-table td { table.houston-table td {
@apply py-2 px-4 lg:px-6 whitespace-nowrap text-sm; @apply py-2 px-4 lg: px-6 whitespace-nowrap text-sm;
} }
table.houston-table th:not(.text-right):not(.text-center), table.houston-table th:not(.text-right):not(.text-center),
@ -110,6 +113,6 @@ table.houston-table th {
} }
table.houston-table tr { table.houston-table tr {
@apply even:bg-accent; @apply even: bg-accent;
} }
</style> </style>

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
const isObject = (obj) => typeof obj === 'object' && !Array.isArray(obj) && obj !== null; const isObject = (obj) => typeof obj === 'object' && !Array.isArray(obj) && obj !== null;
/** /**

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
/** /**
* Replaces all non-printable characters with an orange span containing the escaped representation of the character * Replaces all non-printable characters with an orange span containing the escaped representation of the character
* *

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
import '../globalTypedefs'; import '../globalTypedefs';
/** /**

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
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, hashString } from "./szudzikPair"; import { szudzikPair, hashString } from "./szudzikPair";

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
import { useSpawn } from "@45drives/cockpit-helpers"; import { useSpawn } from "@45drives/cockpit-helpers";
export async function getGroups() { export async function getGroups() {

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
import { useSpawn } from "@45drives/cockpit-helpers"; import { useSpawn } from "@45drives/cockpit-helpers";
export async function getUsers() { export async function getUsers() {

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
/** /**
* Perform isqrt on BigInt * Perform isqrt on BigInt
* *

View File

@ -1,3 +1,20 @@
/*
* Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
*
* This file is part of Cockpit Navigator.
*
* Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Cockpit Navigator.
* If not, see <https://www.gnu.org/licenses/>.
*/
import { createApp, reactive } from 'vue'; import { createApp, reactive } from 'vue';
import App from './App.vue'; import App from './App.vue';
import { errorString, FIFO } from '@45drives/cockpit-helpers'; import { errorString, FIFO } from '@45drives/cockpit-helpers';

View File

@ -1,69 +0,0 @@
import { useSpawn, errorString } from '@45drives/cockpit-helpers';
/** Run test with given expression and return boolean result. Throws on abnormal errors.
*
* @param {String} check - Argument(s) to test for checking path (man test(1))
* @param {String} path - Path to check
* @param {Object} opts - Options for cockpit.spawn()
* @returns {Promise<Boolean>} Result of test
*/
export const test = async (check, path, opts = { superuser: 'try' }) => {
try {
await useSpawn(['test', ...check.split(/\s+/), path], opts).promise();
return true;
} catch (state) {
if (state.status === 1)
return false;
throw new Error("Failed to check path: " + path + ": " + errorString(state));
}
}
/** Returns true if path exists, false otherwise. Throws on abnormal errors.
*
* @param {String} path - Path to check
* @param {Object} opts - Options for cockpit.spawn()
* @returns {Promise<Boolean>} Result of test
*/
export const checkIfExists = (path, opts = { superuser: 'try' }) => {
return test('-e', path, opts);
}
/**
* @typedef {Object} PermissionsResult
* @property {Boolean} r - Read permission result
* @property {Boolean} w - Write permission result
* @property {Boolean} x - Execute/search permission result
*/
/** Get read, write, and execute permissions for user on path. Throws on abnormal errors.
*
* @param {String} path - Path to check
* @param {Object} opts - Options for cockpit.spawn()
* @returns {Promise<PermissionsResult>} - result of test
*/
export const testPerimssions = async (path, opts = { superuser: 'try' }) => {
return {
r: await test('-r', path, opts),
w: await test('-w', path, opts),
x: await test('-x', path, opts),
}
}
/** Check for rw permissions if path is file or rwx permissions if path is dir.
* Returns false if DNE. Throws on abnormal errors.
*
* @param {String} path - Path to check
* @param {Boolean} readOnly - Set to true to ignore write permissions
* @returns {Promise<Boolean>} Result of check
*/
export const checkIfAllowed = async (path, readOnly = false) => {
if (!await checkIfExists(path))
return false;
const permissions = await testPerimssions(path);
let result = permissions.r;
if (!readOnly)
result &= permissions.w;
if (await test('-d', path))
result &= permissions.x;
return result;
}

View File

@ -4,17 +4,28 @@
<div <div
class="grid grid-cols-[auto_1fr] grid-rows-[1fr 1fr] md:grid-cols-[auto_3fr_1fr] md:grid-row-[1fr] items-stretch divide-x divide-y divide-default"> class="grid grid-cols-[auto_1fr] grid-rows-[1fr 1fr] md:grid-cols-[auto_3fr_1fr] md:grid-row-[1fr] items-stretch divide-x divide-y divide-default">
<div class="button-group-row p-1 md:px-4 md:py-2 border-t border-default"> <div class="button-group-row p-1 md:px-4 md:py-2 border-t border-default">
<button class="p-2 rounded-lg hover:bg-accent relative" :disabled="!pathHistory.backAllowed()" <button
@click="back()" @mouseenter="backHistoryDropdown.mouseEnter" class="p-2 rounded-lg hover:bg-accent relative"
@mouseleave="backHistoryDropdown.mouseLeave"> :disabled="!pathHistory.backAllowed()"
@click="back()"
@mouseenter="backHistoryDropdown.mouseEnter"
@mouseleave="backHistoryDropdown.mouseLeave"
>
<ArrowLeftIcon class="size-icon icon-default" /> <ArrowLeftIcon class="size-icon icon-default" />
<ChevronDownIcon class="w-3 h-3 icon-default absolute bottom-1 right-1" <ChevronDownIcon
v-if="pathHistory.backAllowed()" /> class="w-3 h-3 icon-default absolute bottom-1 right-1"
<div v-if="backHistoryDropdown.showDropdown" v-if="pathHistory.backAllowed()"
class="absolute top-full left-0 flex flex-col items-stretch z-50 bg-default shadow-lg rounded-lg overflow-y-auto max-h-80"> />
<div v-for="item, index in pathHistory.stack.slice(0, pathHistory.index).reverse()" <div
:key="index" @click="pathHistory.index = pathHistory.index - index" v-if="backHistoryDropdown.showDropdown"
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"> class="absolute top-full left-0 flex flex-col items-stretch z-50 bg-default shadow-lg rounded-lg overflow-y-auto max-h-80"
>
<div
v-for="item, index in pathHistory.stack.slice(0, pathHistory.index).reverse()"
:key="index"
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"
@click="pathHistory.index = pathHistory.index - index"
>
<span v-if="item.host !== pathHistory.current()?.host"> <span v-if="item.host !== pathHistory.current()?.host">
{{ item.host ?? cockpit.transport.host }}: {{ item.host ?? cockpit.transport.host }}:
</span> </span>
@ -22,17 +33,28 @@
</div> </div>
</div> </div>
</button> </button>
<button class="p-2 rounded-lg hover:bg-accent relative" :disabled="!pathHistory.forwardAllowed()" <button
@click="forward()" @mouseenter="forwardHistoryDropdown.mouseEnter" class="p-2 rounded-lg hover:bg-accent relative"
@mouseleave="forwardHistoryDropdown.mouseLeave"> :disabled="!pathHistory.forwardAllowed()"
@click="forward()"
@mouseenter="forwardHistoryDropdown.mouseEnter"
@mouseleave="forwardHistoryDropdown.mouseLeave"
>
<ArrowRightIcon class="size-icon icon-default" /> <ArrowRightIcon class="size-icon icon-default" />
<ChevronDownIcon class="w-3 h-3 icon-default absolute bottom-1 right-1" <ChevronDownIcon
v-if="pathHistory.forwardAllowed()" /> class="w-3 h-3 icon-default absolute bottom-1 right-1"
<div v-if="forwardHistoryDropdown.showDropdown" v-if="pathHistory.forwardAllowed()"
class="absolute top-full left-0 flex flex-col items-stretch z-50 bg-default shadow-lg rounded-lg overflow-y-auto max-h-80"> />
<div v-for="item, index in pathHistory.stack.slice(pathHistory.index + 1)" :key="index" <div
v-if="forwardHistoryDropdown.showDropdown"
class="absolute top-full left-0 flex flex-col items-stretch z-50 bg-default shadow-lg rounded-lg overflow-y-auto max-h-80"
>
<div
v-for="item, index in pathHistory.stack.slice(pathHistory.index + 1)"
:key="index"
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"
@click="pathHistory.index = pathHistory.index + index" @click="pathHistory.index = pathHistory.index + index"
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"> >
<span v-if="item.host !== pathHistory.current()?.host"> <span v-if="item.host !== pathHistory.current()?.host">
{{ item.host ?? cockpit.transport.host }}: {{ item.host ?? cockpit.transport.host }}:
</span> </span>
@ -40,72 +62,153 @@
</div> </div>
</div> </div>
</button> </button>
<button class="p-2 rounded-lg hover:bg-accent" @click="up()" <button
:disabled="pathHistory.current() === '/'"> class="p-2 rounded-lg hover:bg-accent"
:disabled="pathHistory.current() === '/'"
@click="up()"
>
<ArrowUpIcon class="size-icon icon-default" /> <ArrowUpIcon class="size-icon icon-default" />
</button> </button>
<button class="p-2 rounded-lg hover:bg-accent" @click="directoryViewRef.refresh()"> <button
class="p-2 rounded-lg hover:bg-accent"
@click="directoryViewRef.refresh()"
>
<RefreshIcon class="size-icon icon-default" /> <RefreshIcon class="size-icon icon-default" />
</button> </button>
</div> </div>
<div <div
class="p-1 md:px-4 md:py-2 col-start-1 col-end-3 row-start-2 row-end-3 md:col-start-auto md:col-end-auto md:row-start-auto md:row-end-auto"> class="p-1 md:px-4 md:py-2 col-start-1 col-end-3 row-start-2 row-end-3 md:col-start-auto md:col-end-auto md:row-start-auto md:row-end-auto">
<PathBreadCrumbs :host="pathHistory.current()?.host" :path="pathHistory.current()?.path ?? '/'" <PathBreadCrumbs
@cd="cd" /> :host="pathHistory.current()?.host"
:path="pathHistory.current()?.path ?? '/'"
@cd="cd"
/>
</div> </div>
<div class="p-1 md:px-4 md:py-2"> <div class="p-1 md:px-4 md:py-2">
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon class="size-icon icon-default" aria-hidden="true" /> <SearchIcon
class="size-icon icon-default"
aria-hidden="true"
/>
</div> </div>
<input type="text" class="block input-textlike w-full pl-10" v-model="searchFilterStr" <input
placeholder="Search in directory (foo*, b?r, *.jpg)" /> type="text"
class="block input-textlike w-full pl-10"
v-model="searchFilterStr"
placeholder="Search in directory (foo*, b?r, *.jpg)"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<DirectoryView :host="pathHistory.current()?.host" :path="pathHistory.current()?.path" <DirectoryView
:searchFilterRegExp="searchFilterRegExp" @cd="path => cd({ path })" @edit="openEditor" :host="pathHistory.current()?.host"
@browserAction="handleAction" ref="directoryViewRef" /> :path="pathHistory.current()?.path"
:searchFilterRegExp="searchFilterRegExp"
@cd="path => cd({ path })"
@edit="openEditor"
@browserAction="handleAction"
ref="directoryViewRef"
/>
</div> </div>
</div> </div>
</div> </div>
<ModalPopup :showModal="openFilePromptModal.show" :headerText="openFilePromptModal.entry?.name ?? 'NULL'" <ModalPopup
@close="() => openFilePromptModal.close()" autoWidth> :showModal="openFilePromptModal.show"
:headerText="openFilePromptModal.entry?.name ?? 'NULL'"
autoWidth
@close="() => openFilePromptModal.close()"
>
What would you like to do with this {{ openFilePromptModal.entry?.resolvedTypeHuman }}? What would you like to do with this {{ openFilePromptModal.entry?.resolvedTypeHuman }}?
<template #footer> <template #footer>
<button type="button" class="btn btn-secondary" @click="() => openFilePromptModal.close()"> <button
type="button"
class="btn btn-secondary"
@click="() => openFilePromptModal.close()"
>
Cancel Cancel
</button> </button>
<button type="button" class="btn btn-primary" @click="() => openFilePromptModal.action('editPermissions')"> <button
type="button"
class="btn btn-primary"
@click="() => openFilePromptModal.action('editPermissions')"
>
Edit permissions Edit permissions
</button> </button>
<button v-if="openFilePromptModal.entry?.resolvedType === 'f'" type="button" class="btn btn-primary" <button
@click="() => openFilePromptModal.action('edit')"> v-if="openFilePromptModal.entry?.resolvedType === 'f'"
type="button"
class="btn btn-primary"
@click="() => openFilePromptModal.action('edit')"
>
Open for editing Open for editing
</button> </button>
<button v-if="openFilePromptModal.entry?.resolvedType === 'f'" type="button" class="btn btn-primary" <button
@click="() => openFilePromptModal.action('download')"> v-if="openFilePromptModal.entry?.resolvedType === 'f'"
type="button"
class="btn btn-primary"
@click="() => openFilePromptModal.action('download')"
>
Download Download
</button> </button>
</template> </template>
</ModalPopup> </ModalPopup>
<FilePermissions :show="filePermissions.show" @hide="filePermissions.close" :entry="filePermissions.entry" /> <FilePermissions
<ContextMenu @browserAction="handleAction" :show="contextMenu.show" @hide="contextMenu.close" :entry="contextMenu.entry" :event="contextMenu.event" /> :show="filePermissions.show"
:entry="filePermissions.entry"
@hide="filePermissions.close"
/>
<ContextMenu
:show="contextMenu.show"
:entry="contextMenu.entry"
:event="contextMenu.event"
@browserAction="handleAction"
@hide="contextMenu.close"
/>
<Teleport to="#footer-buttons"> <Teleport to="#footer-buttons">
<IconToggle v-model="darkMode" v-slot="{ value }"> <IconToggle
<MoonIcon v-if="value" class="size-icon icon-default" /> v-model="darkMode"
<SunIcon v-else class="size-icon icon-default" /> v-slot="{ value }"
>
<MoonIcon
v-if="value"
class="size-icon icon-default"
/>
<SunIcon
v-else
class="size-icon icon-default"
/>
</IconToggle> </IconToggle>
<IconToggle v-model="settings.directoryView.showHidden" v-slot="{ value }"> <IconToggle
<EyeIcon v-if="value" class="size-icon icon-default" /> v-model="settings.directoryView.showHidden"
<EyeOffIcon v-else class="size-icon icon-default" /> v-slot="{ value }"
>
<EyeIcon
v-if="value"
class="size-icon icon-default"
/>
<EyeOffIcon
v-else
class="size-icon icon-default"
/>
</IconToggle> </IconToggle>
<IconToggle v-model="settings.directoryView.view" trueValue="list" falseValue="grid" v-slot="{ value }"> <IconToggle
<ViewListIcon v-if="value" class="size-icon icon-default" /> v-model="settings.directoryView.view"
<ViewGridIcon v-else class="size-icon icon-default" /> trueValue="list"
falseValue="grid"
v-slot="{ value }"
>
<ViewListIcon
v-if="value"
class="size-icon icon-default"
/>
<ViewGridIcon
v-else
class="size-icon icon-default"
/>
</IconToggle> </IconToggle>
</Teleport> </Teleport>
</template> </template>

View File

@ -1,3 +1,20 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<div class="grow">Editing {{ path }}</div> <div class="grow">Editing {{ path }}</div>
</template> </template>

View File

@ -1,8 +1,25 @@
<!--
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Cockpit Navigator.
If not, see <https://www.gnu.org/licenses/>.
-->
<template> <template>
<div class="grow flex flex-col items-center justify-center gap-10"> <div class="grow flex flex-col items-center justify-center gap-10">
<div class="text-header">{{title}}</div> <div class="text-header">{{ title }}</div>
<div class="text-muted">{{message}}</div> <div class="text-muted">{{ message }}</div>
<div class="text-muted">Redirecting in {{counter}}...</div> <div class="text-muted">Redirecting in {{ counter }}...</div>
</div> </div>
</template> </template>