mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-28 16:14:25 +02:00
fix processing spinner and search filtering
This commit is contained in:
parent
8850189771
commit
636df9e14c
@ -1,71 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<tr
|
<template v-if="settings.directoryView?.view === 'list'">
|
||||||
v-show="!/^\./.test(entry.name) || settings?.directoryView?.showHidden"
|
<tr v-show="show || showEntries" @dblclick="doubleClickCallback" class="hover:!bg-red-600/10">
|
||||||
v-if="settings.directoryView?.view === 'list'"
|
<td class="flex items-center gap-1 !pl-1">
|
||||||
@dblclick="doubleClickCallback"
|
<div :style="{ width: `${24 * level}px` }"></div>
|
||||||
class="hover:!bg-red-600/10"
|
<div class="relative w-6">
|
||||||
>
|
<component :is="icon" class="size-icon icon-default" />
|
||||||
<td class="flex items-center gap-1 !pl-1">
|
<LinkIcon v-if="entry.type === 'link'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
|
||||||
<div :style="{ width: `${24 * level}px` }"></div>
|
|
||||||
<div class="relative w-6">
|
|
||||||
<component :is="icon" class="size-icon icon-default" />
|
|
||||||
<LinkIcon v-if="entry.type === 'link'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
|
|
||||||
</div>
|
|
||||||
<button v-if="directoryLike" @click.stop="showEntries = !showEntries">
|
|
||||||
<ChevronDownIcon v-if="!showEntries" class="size-icon icon-default" />
|
|
||||||
<ChevronUpIcon v-else class="size-icon icon-default" />
|
|
||||||
</button>
|
|
||||||
<div>{{ entry.name }}</div>
|
|
||||||
<div v-if="entry.type === 'link'" class="inline-flex gap-1 items-center">
|
|
||||||
<div class="inline relative">
|
|
||||||
<ArrowNarrowRightIcon class="text-default size-icon-sm inline" />
|
|
||||||
<XIcon
|
|
||||||
v-if="entry.target?.broken"
|
|
||||||
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>{{ entry.target?.rawPath ?? '' }}</div>
|
<button v-if="directoryLike" @click.stop="() => showEntries = !showEntries">
|
||||||
</div>
|
<ChevronDownIcon v-if="!showEntries" class="size-icon icon-default" />
|
||||||
</td>
|
<ChevronUpIcon v-else class="size-icon icon-default" />
|
||||||
<td v-if="settings?.directoryView?.cols?.mode" class="font-mono">{{ entry.modeStr }}</td>
|
</button>
|
||||||
<td v-if="settings?.directoryView?.cols?.owner">{{ entry.owner }}</td>
|
<div>{{ entry.name }}</div>
|
||||||
<td v-if="settings?.directoryView?.cols?.group">{{ entry.group }}</td>
|
<div v-if="entry.type === 'link'" class="inline-flex gap-1 items-center">
|
||||||
<td v-if="settings?.directoryView?.cols?.size">{{ entry.sizeHuman }}</td>
|
<div class="inline relative">
|
||||||
<td v-if="settings?.directoryView?.cols?.ctime">{{ entry.ctime?.toLocaleString() ?? '-' }}</td>
|
<ArrowNarrowRightIcon class="text-default size-icon-sm inline" />
|
||||||
<td v-if="settings?.directoryView?.cols?.mtime">{{ entry.mtime?.toLocaleString() ?? '-' }}</td>
|
<XIcon
|
||||||
<td v-if="settings?.directoryView?.cols?.atime">{{ entry.atime?.toLocaleString() ?? '-' }}</td>
|
v-if="entry.target?.broken"
|
||||||
</tr>
|
class="icon-danger size-icon-sm absolute inset-x-0 bottom-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>{{ entry.target?.rawPath ?? '' }}</div>
|
||||||
|
</div>
|
||||||
|
</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?.group">{{ entry.group }}</td>
|
||||||
|
<td v-if="settings?.directoryView?.cols?.size" class="text-right">{{ entry.sizeHuman }}</td>
|
||||||
|
<td v-if="settings?.directoryView?.cols?.ctime">{{ entry.ctime?.toLocaleString() ?? '-' }}</td>
|
||||||
|
<td v-if="settings?.directoryView?.cols?.mtime">{{ entry.mtime?.toLocaleString() ?? '-' }}</td>
|
||||||
|
<td v-if="settings?.directoryView?.cols?.atime">{{ entry.atime?.toLocaleString() ?? '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<component
|
||||||
|
:show="show || showEntries"
|
||||||
|
:is="DirectoryEntryList"
|
||||||
|
v-if="directoryLike && showEntries"
|
||||||
|
:path="entry.path"
|
||||||
|
:isChild="true"
|
||||||
|
:inheritedSortCallback="inheritedSortCallback"
|
||||||
|
:searchFilterRegExp="searchFilterRegExp"
|
||||||
|
@cd="(...args) => $emit('cd', ...args)"
|
||||||
|
@edit="(...args) => $emit('edit', ...args)"
|
||||||
|
@startProcessing="(...args) => $emit('startProcessing', ...args)"
|
||||||
|
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
|
||||||
|
ref="directoryViewRef"
|
||||||
|
:level="level + 1"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
v-show="!/^\./.test(entry.name) || settings?.directoryView?.showHidden"
|
v-show="show"
|
||||||
@dblclick="doubleClickCallback"
|
@dblclick="doubleClickCallback"
|
||||||
class="flex flex-col items-center w-20 overflow-hidden"
|
class="flex flex-col items-center w-20 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="relative w-20">
|
<div class="relative w-20">
|
||||||
<component :is="icon" class="icon-default w-20 h-auto" />
|
<component :is="icon" class="icon-default w-20 h-auto" />
|
||||||
<div :class="[directoryLike ? 'right-3 bottom-5' : 'right-5 bottom-3', 'inline absolute']" :title="`-> ${entry.target?.rawPath ?? '?'}`">
|
<div
|
||||||
<LinkIcon v-if="entry.type === 'link'" :class="[entry.target?.broken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900','w-4 h-auto']" />
|
:class="[directoryLike ? 'right-3 bottom-5' : 'right-5 bottom-3', 'inline absolute']"
|
||||||
|
:title="`-> ${entry.target?.rawPath ?? '?'}`"
|
||||||
|
>
|
||||||
|
<LinkIcon
|
||||||
|
v-if="entry.type === 'link'"
|
||||||
|
:class="[entry.target?.broken ? 'text-red-300 dark:text-red-800' : 'text-gray-100 dark:text-gray-900', 'w-4 h-auto']"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center w-full" style="overflow-wrap: break-word;">{{ entry.name }}</div>
|
<div class="text-center w-full" style="overflow-wrap: break-word;">{{ entry.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
|
||||||
:is="DirectoryEntryList"
|
|
||||||
v-if="directoryLike && showEntries"
|
|
||||||
:path="entry.path"
|
|
||||||
:isChild="true"
|
|
||||||
:inheritedSortCallback="inheritedSortCallback"
|
|
||||||
@cd="(...args) => $emit('cd', ...args)"
|
|
||||||
@edit="(...args) => $emit('edit', ...args)"
|
|
||||||
@startProcessing="(...args) => $emit('startProcessing', ...args)"
|
|
||||||
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
|
|
||||||
ref="directoryViewRef"
|
|
||||||
:level="level + 1"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, inject, watch } from 'vue';
|
import { ref, inject, watch, nextTick } 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 { settingsInjectionKey } from '../keys';
|
||||||
import DirectoryEntryList from './DirectoryEntryList.vue';
|
import DirectoryEntryList from './DirectoryEntryList.vue';
|
||||||
@ -74,6 +79,8 @@ export default {
|
|||||||
name: 'DirectoryEntry',
|
name: 'DirectoryEntry',
|
||||||
props: {
|
props: {
|
||||||
entry: Object,
|
entry: Object,
|
||||||
|
show: Boolean,
|
||||||
|
searchFilterRegExp: RegExp,
|
||||||
inheritedSortCallback: {
|
inheritedSortCallback: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: false,
|
required: false,
|
||||||
@ -119,6 +126,7 @@ export default {
|
|||||||
doubleClickCallback,
|
doubleClickCallback,
|
||||||
getEntries,
|
getEntries,
|
||||||
DirectoryEntryList,
|
DirectoryEntryList,
|
||||||
|
nextTick,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -1,22 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<DirectoryEntry
|
<template v-for="entry, index in entries" :key="entry.path">
|
||||||
v-for="entry, index in entries"
|
<DirectoryEntry
|
||||||
:key="entry.path"
|
:show="entryFilterCallback(entry)"
|
||||||
:entry="entry"
|
:entry="entry"
|
||||||
:inheritedSortCallback="sortCallback"
|
:inheritedSortCallback="sortCallback"
|
||||||
@cd="(...args) => $emit('cd', ...args)"
|
:searchFilterRegExp="searchFilterRegExp"
|
||||||
@edit="(...args) => $emit('edit', ...args)"
|
@cd="(...args) => $emit('cd', ...args)"
|
||||||
@sortEntries="sortEntries"
|
@edit="(...args) => $emit('edit', ...args)"
|
||||||
@updateStats="emitStats"
|
@sortEntries="sortEntries"
|
||||||
@startProcessing="(...args) => $emit('startProcessing', ...args)"
|
@updateStats="emitStats"
|
||||||
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
|
@startProcessing="(...args) => $emit('startProcessing', ...args)"
|
||||||
ref="entryRefs"
|
@stopProcessing="(...args) => $emit('stopProcessing', ...args)"
|
||||||
:level="level"
|
ref="entryRefs"
|
||||||
/>
|
:level="level"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<tr
|
||||||
|
v-if="show && entries.reduce((sum, entry) => entryFilterCallback(entry) ? sum + 1 : sum, 0) === 0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
:colspan="Object.values(settings?.directoryView?.cols ?? {}).reduce((sum, current) => current ? sum + 1 : sum, 1) ?? 100"
|
||||||
|
class="!pl-1 text-muted text-sm"
|
||||||
|
>
|
||||||
|
<div class="inline-block" :style="{ width: `${24 * level}px` }"></div>
|
||||||
|
<div class="inline-block">No entries.</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, reactive, computed, inject, watch } from 'vue';
|
import { ref, reactive, computed, inject, watch, onBeforeUnmount, onMounted } from 'vue';
|
||||||
import { useSpawn, errorString, errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers';
|
import { useSpawn, errorString, errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers';
|
||||||
import { notificationsInjectionKey, settingsInjectionKey } from '../keys';
|
import { notificationsInjectionKey, settingsInjectionKey } from '../keys';
|
||||||
import DirectoryEntry from './DirectoryEntry.vue';
|
import DirectoryEntry from './DirectoryEntry.vue';
|
||||||
@ -25,6 +38,12 @@ export default {
|
|||||||
name: 'DirectoryEntryList',
|
name: 'DirectoryEntryList',
|
||||||
props: {
|
props: {
|
||||||
path: String,
|
path: String,
|
||||||
|
searchFilterRegExp: RegExp,
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
sortCallback: {
|
sortCallback: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: false,
|
required: false,
|
||||||
@ -52,6 +71,23 @@ export default {
|
|||||||
return props.sortCallback(a, b);
|
return props.sortCallback(a, b);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const processingHandler = {
|
||||||
|
count: 0,
|
||||||
|
start: () => {
|
||||||
|
emit('startProcessing');
|
||||||
|
processingHandler.count++;
|
||||||
|
},
|
||||||
|
stop: () => {
|
||||||
|
if (processingHandler.count > 0) {
|
||||||
|
emit('stopProcessing');
|
||||||
|
processingHandler.count--;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolveDangling: () => {
|
||||||
|
for (; processingHandler.count > 0; processingHandler.count--)
|
||||||
|
emit('stopProcessing');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getAsyncEntryStats = (cwd, entry, modeStr, path, linkTargetRaw) => {
|
const getAsyncEntryStats = (cwd, entry, modeStr, path, linkTargetRaw) => {
|
||||||
const procs = [];
|
const procs = [];
|
||||||
@ -90,9 +126,8 @@ export default {
|
|||||||
if (linkTargetRaw) {
|
if (linkTargetRaw) {
|
||||||
entry.target = {
|
entry.target = {
|
||||||
rawPath: linkTargetRaw,
|
rawPath: linkTargetRaw,
|
||||||
path: canonicalPath(linkTargetRaw.replace(/^(?!=\/)/, cwd + '/')),
|
path: canonicalPath(linkTargetRaw.replace(/^(?!\/)/, cwd + '/')),
|
||||||
};
|
};
|
||||||
emit('startProcessing');
|
|
||||||
procs.push(useSpawn(['stat', '-c', '%A', entry.target.path]).promise()
|
procs.push(useSpawn(['stat', '-c', '%A', entry.target.path]).promise()
|
||||||
.then(state => {
|
.then(state => {
|
||||||
getAsyncEntryStats(cwd, entry.target, state.stdout.trim());
|
getAsyncEntryStats(cwd, entry.target, state.stdout.trim());
|
||||||
@ -101,7 +136,6 @@ export default {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
entry.target.broken = true;
|
entry.target.broken = true;
|
||||||
})
|
})
|
||||||
.finally(() => emit('stopProcessing'))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -119,7 +153,6 @@ export default {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (entry.permissions.acl && path) {
|
if (entry.permissions.acl && path) {
|
||||||
emit('startProcessing');
|
|
||||||
procs.push(useSpawn(['getfacl', '--omit-header', '--no-effective', path], { superuser: 'try' }).promise()
|
procs.push(useSpawn(['getfacl', '--omit-header', '--no-effective', path], { superuser: 'try' }).promise()
|
||||||
.then(state => {
|
.then(state => {
|
||||||
entry.permissions.acl = state.stdout
|
entry.permissions.acl = state.stdout
|
||||||
@ -139,31 +172,28 @@ export default {
|
|||||||
.catch(state => {
|
.catch(state => {
|
||||||
console.error(`failed to get ACL for ${path}:`, errorString(state));
|
console.error(`failed to get ACL for ${path}:`, errorString(state));
|
||||||
})
|
})
|
||||||
.finally(() => emit('stopProcessing'))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (path) {
|
if (path) {
|
||||||
emit('startProcessing');
|
|
||||||
procs.push(useSpawn(['stat', '-c', '%W:%Y:%X', path], { superuser: 'try' }).promise() // birth:modification:access
|
procs.push(useSpawn(['stat', '-c', '%W:%Y:%X', path], { superuser: 'try' }).promise() // birth:modification:access
|
||||||
.then(state => {
|
.then(state => {
|
||||||
const [ctimeStr, mtimeStr, atimeStr] = state.stdout.trim().split(':');
|
const [ctime, mtime, atime] = state.stdout.trim().split(':').map(str => parseInt(str));
|
||||||
Object.assign(entry, {
|
Object.assign(entry, {
|
||||||
ctime: new Date(parseInt(ctimeStr) * 1000),
|
ctime: ctime ? new Date(ctime * 1000) : null,
|
||||||
mtime: new Date(parseInt(mtimeStr) * 1000),
|
mtime: mtime ? new Date(mtime * 1000) : null,
|
||||||
atime: new Date(parseInt(atimeStr) * 1000),
|
atime: atime ? new Date(atime * 1000) : null,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(state =>
|
.catch(state =>
|
||||||
notifications.value.constructNotification(`Failed to get stats for ${path}`, errorStringHTML(state), 'error')
|
notifications.value.constructNotification(`Failed to get stats for ${path}`, errorStringHTML(state), 'error')
|
||||||
)
|
)
|
||||||
.finally(() => emit('stopProcessing'))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Promise.all(procs);
|
return Promise.all(procs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEntries = async () => {
|
const getEntries = async () => {
|
||||||
emit('startProcessing');
|
processingHandler.start();
|
||||||
try {
|
try {
|
||||||
const cwd = props.path;
|
const cwd = props.path;
|
||||||
const procs = [];
|
const procs = [];
|
||||||
@ -215,15 +245,16 @@ export default {
|
|||||||
})
|
})
|
||||||
.filter(entry => entry !== null)
|
.filter(entry => entry !== null)
|
||||||
?? [];
|
?? [];
|
||||||
|
processingHandler.start();
|
||||||
return Promise.all(procs).then(() => {
|
return Promise.all(procs).then(() => {
|
||||||
emitStats();
|
emitStats();
|
||||||
sortEntries();
|
sortEntries();
|
||||||
})
|
}).finally(() => processingHandler.stop());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
entries.value = [];
|
entries.value = [];
|
||||||
notifications.value.constructNotification("Error getting directory entries", errorStringHTML(error), 'error');
|
notifications.value.constructNotification("Error getting directory entries", errorStringHTML(error), 'error');
|
||||||
} finally {
|
} finally {
|
||||||
emit('stopProcessing');
|
processingHandler.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,11 +269,19 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sortEntries = () => {
|
const sortEntries = () => {
|
||||||
emit('startProcessing');
|
processingHandler.start();
|
||||||
entries.value.sort(sortCallbackComputed.value);
|
entries.value.sort(sortCallbackComputed.value);
|
||||||
emit('stopProcessing');
|
processingHandler.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entryFilterCallback = (entry) =>
|
||||||
|
(!/^\./.test(entry.name) || settings?.directoryView?.showHidden)
|
||||||
|
&& (props.searchFilterRegExp?.test(entry.name) ?? true);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
processingHandler.resolveDangling();
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.sortCallback, sortEntries);
|
watch(() => props.sortCallback, sortEntries);
|
||||||
watch(entries, sortEntries);
|
watch(entries, sortEntries);
|
||||||
watch(() => settings.directoryView?.separateDirs, sortEntries);
|
watch(() => settings.directoryView?.separateDirs, sortEntries);
|
||||||
@ -256,6 +295,7 @@ export default {
|
|||||||
getEntries,
|
getEntries,
|
||||||
emitStats,
|
emitStats,
|
||||||
sortEntries,
|
sortEntries,
|
||||||
|
entryFilterCallback,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th v-if="settings?.directoryView?.cols?.size">
|
<th 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">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>
|
||||||
@ -66,6 +66,7 @@
|
|||||||
<DirectoryEntryList
|
<DirectoryEntryList
|
||||||
:path="path"
|
:path="path"
|
||||||
:sortCallback="sortCallback"
|
:sortCallback="sortCallback"
|
||||||
|
:searchFilterRegExp="searchFilterRegExp"
|
||||||
@cd="(...args) => $emit('cd', ...args)"
|
@cd="(...args) => $emit('cd', ...args)"
|
||||||
@edit="(...args) => $emit('edit', ...args)"
|
@edit="(...args) => $emit('edit', ...args)"
|
||||||
@updateStats="(...args) => $emit('updateStats', ...args)"
|
@updateStats="(...args) => $emit('updateStats', ...args)"
|
||||||
@ -80,6 +81,7 @@
|
|||||||
<DirectoryEntryList
|
<DirectoryEntryList
|
||||||
:path="path"
|
:path="path"
|
||||||
:sortCallback="sortCallback"
|
:sortCallback="sortCallback"
|
||||||
|
:searchFilterRegExp="searchFilterRegExp"
|
||||||
@cd="(...args) => $emit('cd', ...args)"
|
@cd="(...args) => $emit('cd', ...args)"
|
||||||
@edit="(...args) => $emit('edit', ...args)"
|
@edit="(...args) => $emit('edit', ...args)"
|
||||||
@updateStats="(...args) => $emit('updateStats', ...args)"
|
@updateStats="(...args) => $emit('updateStats', ...args)"
|
||||||
@ -101,6 +103,7 @@ import DirectoryEntryList from './DirectoryEntryList.vue';
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
path: String,
|
path: String,
|
||||||
|
searchFilterRegExp: RegExp,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const settings = inject(settingsInjectionKey);
|
const settings = inject(settingsInjectionKey);
|
||||||
|
@ -1,70 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<div class="h-full flex flex-col items-stretch">
|
<div class="h-full flex flex-col items-stretch">
|
||||||
<div class="flex items-stretch divide-x divide-y divide-default flex-wrap">
|
<div
|
||||||
<div class="flex items-stretch divide-x divide-default grow-[6] basis-0">
|
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 px-4 py-2">
|
>
|
||||||
<button
|
<div class="button-group-row p-1 md:px-4 md:py-2">
|
||||||
class="p-2 rounded-lg hover:bg-accent relative"
|
<button
|
||||||
:disabled="!pathHistory.backAllowed()"
|
class="p-2 rounded-lg hover:bg-accent relative"
|
||||||
@click="back()"
|
:disabled="!pathHistory.backAllowed()"
|
||||||
@mouseenter="backHistoryDropdown.mouseEnter"
|
@click="back()"
|
||||||
@mouseleave="backHistoryDropdown.mouseLeave"
|
@mouseenter="backHistoryDropdown.mouseEnter"
|
||||||
|
@mouseleave="backHistoryDropdown.mouseLeave"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon class="size-icon icon-default" />
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="w-3 h-3 icon-default absolute bottom-1 right-1"
|
||||||
|
v-if="pathHistory.backAllowed()"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="backHistoryDropdown.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"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon class="size-icon icon-default" />
|
|
||||||
<ChevronDownIcon
|
|
||||||
class="w-3 h-3 icon-default absolute bottom-1 right-1"
|
|
||||||
v-if="pathHistory.backAllowed()"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
v-if="backHistoryDropdown.showDropdown"
|
v-for="item, index in pathHistory.stack.slice(0, pathHistory.index).reverse()"
|
||||||
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"
|
:key="index"
|
||||||
>
|
@click="pathHistory.index = pathHistory.index - index"
|
||||||
<div
|
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"
|
||||||
v-for="item, index in pathHistory.stack.slice(0, pathHistory.index).reverse()"
|
>{{ item }}</div>
|
||||||
:key="index"
|
</div>
|
||||||
@click="pathHistory.index = pathHistory.index - index"
|
</button>
|
||||||
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"
|
<button
|
||||||
>{{ item }}</div>
|
class="p-2 rounded-lg hover:bg-accent relative"
|
||||||
</div>
|
:disabled="!pathHistory.forwardAllowed()"
|
||||||
</button>
|
@click="forward()"
|
||||||
<button
|
@mouseenter="forwardHistoryDropdown.mouseEnter"
|
||||||
class="p-2 rounded-lg hover:bg-accent relative"
|
@mouseleave="forwardHistoryDropdown.mouseLeave"
|
||||||
:disabled="!pathHistory.forwardAllowed()"
|
>
|
||||||
@click="forward()"
|
<ArrowRightIcon class="size-icon icon-default" />
|
||||||
@mouseenter="forwardHistoryDropdown.mouseEnter"
|
<ChevronDownIcon
|
||||||
@mouseleave="forwardHistoryDropdown.mouseLeave"
|
class="w-3 h-3 icon-default absolute bottom-1 right-1"
|
||||||
|
v-if="pathHistory.forwardAllowed()"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
>
|
>
|
||||||
<ArrowRightIcon class="size-icon icon-default" />
|
|
||||||
<ChevronDownIcon
|
|
||||||
class="w-3 h-3 icon-default absolute bottom-1 right-1"
|
|
||||||
v-if="pathHistory.forwardAllowed()"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
v-if="forwardHistoryDropdown.showDropdown"
|
v-for="item, index in pathHistory.stack.slice(pathHistory.index + 1)"
|
||||||
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"
|
:key="index"
|
||||||
>
|
@click="pathHistory.index = pathHistory.index + index"
|
||||||
<div
|
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"
|
||||||
v-for="item, index in pathHistory.stack.slice(pathHistory.index + 1)"
|
>{{ item }}</div>
|
||||||
:key="index"
|
</div>
|
||||||
@click="pathHistory.index = pathHistory.index + index"
|
</button>
|
||||||
class="hover:text-white hover:bg-red-600 px-4 py-2 text-sm text-left whitespace-nowrap"
|
<button class="p-2 rounded-lg hover:bg-accent" @click="up()">
|
||||||
>{{ item }}</div>
|
<ArrowUpIcon class="size-icon icon-default" />
|
||||||
</div>
|
</button>
|
||||||
</button>
|
<button class="p-2 rounded-lg hover:bg-accent" @click="directoryViewRef.getEntries()">
|
||||||
<button class="p-2 rounded-lg hover:bg-accent" @click="up()">
|
<RefreshIcon class="size-icon icon-default" />
|
||||||
<ArrowUpIcon class="size-icon icon-default" />
|
</button>
|
||||||
</button>
|
|
||||||
<button class="p-2 rounded-lg hover:bg-accent" @click="directoryViewRef.getEntries()">
|
|
||||||
<RefreshIcon class="size-icon icon-default" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="grow px-4 py-2">
|
|
||||||
<PathBreadCrumbs :path="pathHistory.current() ?? '/'" @cd="newPath => cd(newPath)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow shrink-0 px-4 py-2">
|
<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"
|
||||||
|
>
|
||||||
|
<PathBreadCrumbs :path="pathHistory.current() ?? '/'" @cd="newPath => cd(newPath)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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" />
|
||||||
@ -72,8 +75,8 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="block input-textlike w-full pl-10"
|
class="block input-textlike w-full pl-10"
|
||||||
v-model="searchFilter"
|
v-model="searchFilterStr"
|
||||||
placeholder="Search in directory"
|
placeholder="Search in directory (foo*, b?r, *.jpg)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,6 +84,7 @@
|
|||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<DirectoryView
|
<DirectoryView
|
||||||
:path="pathHistory.current() ?? '/'"
|
:path="pathHistory.current() ?? '/'"
|
||||||
|
:searchFilterRegExp="searchFilterRegExp"
|
||||||
@cd="newPath => cd(newPath)"
|
@cd="newPath => cd(newPath)"
|
||||||
@edit="(...args) => console.log('edit', ...args)"
|
@edit="(...args) => console.log('edit', ...args)"
|
||||||
@updateStats="stats => $emit('updateFooterText', `${stats.files} file${stats.files === 1 ? '' : 's'}, ${stats.dirs} director${stats.dirs === 1 ? 'y' : 'ies'}`)"
|
@updateStats="stats => $emit('updateFooterText', `${stats.files} file${stats.files === 1 ? '' : 's'}, ${stats.dirs} director${stats.dirs === 1 ? 'y' : 'ies'}`)"
|
||||||
@ -98,16 +102,18 @@ import DirectoryView from "../components/DirectoryView.vue";
|
|||||||
import { errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers';
|
import { errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers';
|
||||||
import PathBreadCrumbs from '../components/PathBreadCrumbs.vue';
|
import PathBreadCrumbs from '../components/PathBreadCrumbs.vue';
|
||||||
import { checkIfExists, checkIfAllowed } from '../mode';
|
import { checkIfExists, checkIfAllowed } from '../mode';
|
||||||
import { notificationsInjectionKey, pathHistoryInjectionKey, lastPathStorageKey } from '../keys';
|
import { notificationsInjectionKey, pathHistoryInjectionKey, lastPathStorageKey, settingsInjectionKey } from '../keys';
|
||||||
import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, RefreshIcon, ChevronDownIcon, SearchIcon } from '@heroicons/vue/solid';
|
import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, RefreshIcon, ChevronDownIcon, SearchIcon } from '@heroicons/vue/solid';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
|
const settings = inject(settingsInjectionKey);
|
||||||
const notifications = inject(notificationsInjectionKey);
|
const notifications = inject(notificationsInjectionKey);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const pathHistory = inject(pathHistoryInjectionKey);
|
const pathHistory = inject(pathHistoryInjectionKey);
|
||||||
const directoryViewRef = ref();
|
const directoryViewRef = ref();
|
||||||
const searchFilter = ref("");
|
const searchFilterStr = ref("");
|
||||||
|
const searchFilterRegExp = ref(/^/g);
|
||||||
const backHistoryDropdown = reactive({
|
const backHistoryDropdown = reactive({
|
||||||
showDropdown: false,
|
showDropdown: false,
|
||||||
timeoutHandle: null,
|
timeoutHandle: null,
|
||||||
@ -156,6 +162,17 @@ export default {
|
|||||||
cd(canonicalPath((pathHistory.current() ?? "") + '/..'), saveHistory);
|
cd(canonicalPath((pathHistory.current() ?? "") + '/..'), saveHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(searchFilterStr, () => {
|
||||||
|
searchFilterRegExp.value = new RegExp(
|
||||||
|
`^${
|
||||||
|
searchFilterStr.value
|
||||||
|
.replace(/[.+^${}()|[\]]|\\(?![*?])/g, '\\$&') // escape special chars, \\ only if not before * or ?
|
||||||
|
.replace(/(?<!\\)\*/g, '.*') // replace * with .* if not escaped
|
||||||
|
.replace(/(?<!\\)\?/g, '.') // replace ? with . if not escaped
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(() => route.params.path, async () => {
|
watch(() => route.params.path, async () => {
|
||||||
if (!route.params.path)
|
if (!route.params.path)
|
||||||
return cockpit.location.go('/browse/');
|
return cockpit.location.go('/browse/');
|
||||||
@ -194,7 +211,8 @@ export default {
|
|||||||
console,
|
console,
|
||||||
pathHistory,
|
pathHistory,
|
||||||
directoryViewRef,
|
directoryViewRef,
|
||||||
searchFilter,
|
searchFilterStr,
|
||||||
|
searchFilterRegExp,
|
||||||
backHistoryDropdown,
|
backHistoryDropdown,
|
||||||
forwardHistoryDropdown,
|
forwardHistoryDropdown,
|
||||||
cd,
|
cd,
|
||||||
|
@ -245,9 +245,9 @@ camelcase-css@^2.0.1:
|
|||||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335:
|
caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335:
|
||||||
version "1.0.30001340"
|
version "1.0.30001341"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz#029a2f8bfc025d4820fafbfaa6259fd7778340c7"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498"
|
||||||
integrity sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==
|
integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA==
|
||||||
|
|
||||||
chokidar@^3.5.3:
|
chokidar@^3.5.3:
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user