fix processing spinner and search filtering

This commit is contained in:
joshuaboud 2022-05-16 12:23:06 -03:00
parent 8850189771
commit 636df9e14c
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
5 changed files with 219 additions and 150 deletions

View File

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

View File

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

View File

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

View File

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

View File

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