mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-28 16:14:25 +02:00
clean up navigation with router
This commit is contained in:
parent
523166d424
commit
a716954e1f
@ -7,7 +7,7 @@
|
|||||||
<component :is="icon" class="size-icon icon-default" />
|
<component :is="icon" class="size-icon icon-default" />
|
||||||
<LinkIcon v-if="entry.type === 'symbolic link'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
|
<LinkIcon v-if="entry.type === 'symbolic link'" class="w-2 h-2 absolute right-0 bottom-0 text-default" />
|
||||||
</div>
|
</div>
|
||||||
<button v-if="directoryLike" @click.stop="() => showEntries = !showEntries">
|
<button v-if="directoryLike" @click.stop="toggleShowEntries">
|
||||||
<ChevronDownIcon v-if="!showEntries" class="size-icon icon-default" />
|
<ChevronDownIcon v-if="!showEntries" class="size-icon icon-default" />
|
||||||
<ChevronUpIcon v-else class="size-icon icon-default" />
|
<ChevronUpIcon v-else class="size-icon icon-default" />
|
||||||
</button>
|
</button>
|
||||||
@ -107,6 +107,14 @@ export default {
|
|||||||
return directoryViewRef.value?.getEntries?.();
|
return directoryViewRef.value?.getEntries?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleShowEntries = () => {
|
||||||
|
emit('startProcessing');
|
||||||
|
nextTick(() => {
|
||||||
|
showEntries.value = !showEntries.value;
|
||||||
|
nextTick(() => emit('stopProcessing'));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
watch(props.entry, () => {
|
watch(props.entry, () => {
|
||||||
if (props.entry.type === 'directory' || (props.entry.type === 'symbolic link' && props.entry.target?.type === 'directory')) {
|
if (props.entry.type === 'directory' || (props.entry.type === 'symbolic link' && props.entry.target?.type === 'directory')) {
|
||||||
icon.value = FolderIcon;
|
icon.value = FolderIcon;
|
||||||
@ -125,6 +133,7 @@ export default {
|
|||||||
directoryViewRef,
|
directoryViewRef,
|
||||||
doubleClickCallback,
|
doubleClickCallback,
|
||||||
getEntries,
|
getEntries,
|
||||||
|
toggleShowEntries,
|
||||||
DirectoryEntryList,
|
DirectoryEntryList,
|
||||||
nextTick,
|
nextTick,
|
||||||
}
|
}
|
||||||
|
@ -90,30 +90,40 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getEntries = async () => {
|
const getEntries = async () => {
|
||||||
processingHandler.start();
|
if (!props.path) {
|
||||||
const readLink = (target, cwd, symlinkStr) => {
|
return;
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const linkTargetRaw = symlinkStr.split(/\s*->\s*/)[1].trim().replace(/^['"]|['"]$/g, '');
|
|
||||||
Object.assign(target, {
|
|
||||||
rawPath: linkTargetRaw,
|
|
||||||
path: canonicalPath(linkTargetRaw.replace(/^(?!\/)/, `${cwd}/`)),
|
|
||||||
});
|
|
||||||
useSpawn(['stat', '-c', '%F', target.path], { superuser: 'try' }).promise()
|
|
||||||
.then(state => {
|
|
||||||
target.type = state.stdout.trim();
|
|
||||||
target.broken = false;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
target.broken = true;
|
|
||||||
})
|
|
||||||
.finally(resolve);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
processingHandler.start();
|
||||||
const US = '\x1F';
|
const US = '\x1F';
|
||||||
const RS = '\x1E';
|
const RS = '\x1E';
|
||||||
|
const processLinks = (linkTargets) => {
|
||||||
|
if (linkTargets.length === 0)
|
||||||
|
return null;
|
||||||
|
const callback = state => state.stdout
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(record => record)
|
||||||
|
.map((record, index) => {
|
||||||
|
if (record.includes(US)) {
|
||||||
|
const [type, mode] = record.split(US);
|
||||||
|
linkTargets[index].type = type;
|
||||||
|
linkTargets[index].mode = mode;
|
||||||
|
linkTargets[index].broken = false;
|
||||||
|
} else { // error
|
||||||
|
linkTargets[index].broken = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
useSpawn(['stat', `--printf=%F${US}%f\n`, ...linkTargets.map(target => target.path)], { superuser: 'try', err: 'out' }).promise()
|
||||||
|
.then(callback)
|
||||||
|
.catch(callback)
|
||||||
|
.finally(resolve)
|
||||||
|
)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const cwd = props.path;
|
const cwd = props.path;
|
||||||
const procs = [];
|
const procs = [];
|
||||||
|
let tmpEntries;
|
||||||
procs.push(...entryRefs.value.filter(entryRef => entryRef.showEntries).map(entryRef => entryRef.getEntries()));
|
procs.push(...entryRefs.value.filter(entryRef => entryRef.showEntries).map(entryRef => entryRef.getEntries()));
|
||||||
const entryNames =
|
const entryNames =
|
||||||
(await useSpawn(['dir', '--almost-all', '--dereference-command-line-symlink-to-dir', '--quoting-style=c', '-1', cwd], { superuser: 'try' }).promise()).stdout
|
(await useSpawn(['dir', '--almost-all', '--dereference-command-line-symlink-to-dir', '--quoting-style=c', '-1', cwd], { superuser: 'try' }).promise()).stdout
|
||||||
@ -123,7 +133,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(escaped);
|
return JSON.parse(escaped);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.constructNotification("Failed to parse file name", `${errorStringHTML(error)}\ncaused by ${escaped}`, 'error');
|
notifications.value.constructNotification("Failed to parse file name", `${errorStringHTML(error)}\ncaused by ${escaped}`, 'error');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -141,7 +151,7 @@ export default {
|
|||||||
'%F', // type
|
'%F', // type
|
||||||
'%N', // quoted name with symlink
|
'%N', // quoted name with symlink
|
||||||
]
|
]
|
||||||
entries.value =
|
tmpEntries =
|
||||||
entryNames.length
|
entryNames.length
|
||||||
? (await useSpawn(['stat', `--printf=${fields.join(US)}${RS}`, ...entryNames], { superuser: 'try', directory: cwd }).promise()).stdout
|
? (await useSpawn(['stat', `--printf=${fields.join(US)}${RS}`, ...entryNames], { superuser: 'try', directory: cwd }).promise()).stdout
|
||||||
.split(RS)
|
.split(RS)
|
||||||
@ -154,7 +164,7 @@ export default {
|
|||||||
mode = parseInt(mode, 16);
|
mode = parseInt(mode, 16);
|
||||||
const entry = reactive({
|
const entry = reactive({
|
||||||
name,
|
name,
|
||||||
path: canonicalPath(`/${cwd}/${name}`),
|
path: `${cwd}/${name}`.replace(/\/+/g, '/'),
|
||||||
mode,
|
mode,
|
||||||
modeStr,
|
modeStr,
|
||||||
size,
|
size,
|
||||||
@ -167,8 +177,10 @@ export default {
|
|||||||
type,
|
type,
|
||||||
target: {},
|
target: {},
|
||||||
});
|
});
|
||||||
if (type === 'symbolic link')
|
if (type === 'symbolic link') {
|
||||||
procs.push(readLink(entry.target, cwd, symlinkStr));
|
entry.target.rawPath = symlinkStr.split(/\s*->\s*/)[1].trim().replace(/^['"]|['"]$/g, '');
|
||||||
|
entry.target.path = entry.target.rawPath.replace(/^(?!\/)/, `${cwd}/`);
|
||||||
|
}
|
||||||
return entry;
|
return entry;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(errorString(error));
|
console.error(errorString(error));
|
||||||
@ -176,10 +188,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}).filter(entry => entry !== null)
|
}).filter(entry => entry !== null)
|
||||||
: [];
|
: [];
|
||||||
|
procs.push(processLinks(tmpEntries.filter(entry => entry.type === 'symbolic link').map(entry => entry.target)));
|
||||||
processingHandler.start();
|
processingHandler.start();
|
||||||
console.log("resolving", procs.length, 'symlinks');
|
|
||||||
return Promise.all(procs)
|
return Promise.all(procs)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
if (props.path !== cwd)
|
||||||
|
return;
|
||||||
|
entries.value = [...tmpEntries];
|
||||||
emitStats();
|
emitStats();
|
||||||
sortEntries();
|
sortEntries();
|
||||||
})
|
})
|
||||||
@ -223,7 +238,11 @@ export default {
|
|||||||
watch(() => props.sortCallback, sortEntries);
|
watch(() => props.sortCallback, sortEntries);
|
||||||
watch(() => settings.directoryView?.separateDirs, sortEntries);
|
watch(() => settings.directoryView?.separateDirs, sortEntries);
|
||||||
|
|
||||||
watch(() => props.path, getEntries, { immediate: true });
|
watch(() => props.path, (current, old) => {
|
||||||
|
if (current === old)
|
||||||
|
return;
|
||||||
|
getEntries();
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<template v-for="segment, index in pathArr" :key="index">
|
<template v-for="segment, index in pathArr" :key="index">
|
||||||
<ChevronRightIcon v-if="index > 0" class="size-icon icon-default" />
|
<ChevronRightIcon v-if="index > 0" class="size-icon icon-default" />
|
||||||
<button
|
<button
|
||||||
@click.prevent.stop="$emit('cd', canonicalPath(pathArr.slice(0, index + 1).join('/')))"
|
@click.prevent.stop="$emit('cd', `/${pathArr.slice(1, index + 1).join('/')}`)"
|
||||||
class="p-2 hover:bg-accent rounded-lg cursor-pointer"
|
class="p-2 hover:bg-accent rounded-lg cursor-pointer"
|
||||||
>{{ segment }}</button>
|
>{{ segment }}</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { createApp, reactive } from 'vue';
|
import { createApp, reactive } from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { FIFO } from '@45drives/cockpit-helpers';
|
import { errorString, FIFO } from '@45drives/cockpit-helpers';
|
||||||
import '@45drives/cockpit-css/src/index.css';
|
import '@45drives/cockpit-css/src/index.css';
|
||||||
|
import { useSpawn, errorStringHTML } from '@45drives/cockpit-helpers';
|
||||||
|
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
||||||
const notificationFIFO = reactive(new FIFO());
|
const notificationFIFO = reactive(new FIFO());
|
||||||
|
|
||||||
const errorHandler = (error) => {
|
const errorHandler = (error, title = "System Error") => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const notificationObj = {
|
const notificationObj = {
|
||||||
title: "System Error",
|
title,
|
||||||
body: "",
|
body: "",
|
||||||
show: true,
|
show: true,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@ -32,6 +33,33 @@ const errorHandler = (error) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from) => {
|
||||||
|
if (to.name === 'browse') {
|
||||||
|
if (!to.params.path)
|
||||||
|
return "/browse/"; // force / for opening root
|
||||||
|
try {
|
||||||
|
let realPath = (await useSpawn(['realpath', '--canonicalize-existing', to.params.path], { superuser: 'try' }).promise()).stdout.trim();
|
||||||
|
if (to.params.path !== realPath)
|
||||||
|
return `/browse${realPath}`;
|
||||||
|
try {
|
||||||
|
await useSpawn(['test', '-r', to.params.path, '-a', '-x', to.params.path], { superuser: 'try' }).promise();
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Permission denied for ${to.params.path}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (from.name === undefined)
|
||||||
|
return { name: 'errorRedirect', query: { title: "Error opening path", message: errorString(error) } }
|
||||||
|
errorHandler(errorStringHTML(error), "Failed to open path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cockpit.location.href !== to.fullPath.replace(/\/$/, '') && to.name !== 'errorRedirect') {
|
||||||
|
cockpit.location.replace(to.fullPath);
|
||||||
|
return false; // avoid double render from router change and cockpit path change
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
|
||||||
const app = createApp(App, { notificationFIFO }).use(router);
|
const app = createApp(App, { notificationFIFO }).use(router);
|
||||||
|
|
||||||
app.config.errorHandler = (error) => errorHandler(error);
|
app.config.errorHandler = (error) => errorHandler(error);
|
||||||
|
@ -1,21 +1,27 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
import { lastPathStorageKey } from '../keys';
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/browse:path(.+)',
|
path: '/browse:path(/.*)?',
|
||||||
name: 'browse',
|
name: 'browse',
|
||||||
component: () => import('../views/Browser.vue'),
|
component: () => import('../views/Browser.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit/:path(.*)',
|
path: '/edit:path(/.+)',
|
||||||
name: 'edit',
|
name: 'edit',
|
||||||
component: () => import('../views/Editor.vue'),
|
component: () => import('../views/Editor.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/errorRedirect',
|
||||||
name: 'redirectToBrowse',
|
name: 'errorRedirect',
|
||||||
|
component: () => import('../views/ErrorRedirect.vue'),
|
||||||
|
props: route => ({ title: route.query.title, message: route.query.message }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)',
|
||||||
|
name: 'notFound',
|
||||||
|
redirect: route => ({ name: 'errorRedirect', query: { title: 'Not found', message: `${route.href} is not a valid location.` }}),
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@ -23,14 +29,4 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
if (to.name === 'redirectToBrowse') {
|
|
||||||
const lastLocation = localStorage.getItem(lastPathStorageKey) ?? '/';
|
|
||||||
next(`/browse${lastLocation}`);
|
|
||||||
cockpit.location.go(`/browse${lastLocation}`);
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<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">
|
<div class="button-group-row p-1 md:px-4 md:py-2 border border-default">
|
||||||
<button
|
<button
|
||||||
class="p-2 rounded-lg hover:bg-accent relative"
|
class="p-2 rounded-lg hover:bg-accent relative"
|
||||||
:disabled="!pathHistory.backAllowed()"
|
:disabled="!pathHistory.backAllowed()"
|
||||||
@ -53,7 +53,11 @@
|
|||||||
>{{ item }}</div>
|
>{{ item }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button class="p-2 rounded-lg hover:bg-accent" @click="up()">
|
<button
|
||||||
|
class="p-2 rounded-lg hover:bg-accent"
|
||||||
|
@click="up()"
|
||||||
|
:disabled="pathHistory.current() === '/'"
|
||||||
|
>
|
||||||
<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.getEntries()">
|
<button class="p-2 rounded-lg hover:bg-accent" @click="directoryViewRef.getEntries()">
|
||||||
@ -64,7 +68,7 @@
|
|||||||
<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 :path="pathHistory.current() ?? '/'" @cd="newPath => cd(newPath, newPath !== pathHistory.current())" />
|
<PathBreadCrumbs :path="pathHistory.current() ?? '/'" @cd="newPath => cd(newPath)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-1 md:px-4 md:py-2">
|
<div class="p-1 md:px-4 md:py-2">
|
||||||
@ -83,10 +87,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<DirectoryView
|
<DirectoryView
|
||||||
:path="pathHistory.current() ?? '/'"
|
:path="pathHistory.current()"
|
||||||
:searchFilterRegExp="searchFilterRegExp"
|
:searchFilterRegExp="searchFilterRegExp"
|
||||||
@cd="newPath => cd(newPath)"
|
@cd="newPath => cd(newPath)"
|
||||||
@edit="(...args) => console.log('edit', ...args)"
|
@edit="openEditor"
|
||||||
@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'}`)"
|
||||||
ref="directoryViewRef"
|
ref="directoryViewRef"
|
||||||
/>
|
/>
|
||||||
@ -97,11 +101,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { inject, ref, reactive, watch, nextTick } from 'vue';
|
import { inject, ref, reactive, watch, nextTick } from 'vue';
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import DirectoryView from "../components/DirectoryView.vue";
|
import DirectoryView from "../components/DirectoryView.vue";
|
||||||
import { errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers';
|
import { useSpawn, errorString, errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers';
|
||||||
import PathBreadCrumbs from '../components/PathBreadCrumbs.vue';
|
import PathBreadCrumbs from '../components/PathBreadCrumbs.vue';
|
||||||
import { checkIfExists, checkIfAllowed } from '../mode';
|
|
||||||
import { notificationsInjectionKey, pathHistoryInjectionKey, lastPathStorageKey, settingsInjectionKey } 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';
|
||||||
|
|
||||||
@ -110,6 +113,7 @@ export default {
|
|||||||
const settings = inject(settingsInjectionKey);
|
const settings = inject(settingsInjectionKey);
|
||||||
const notifications = inject(notificationsInjectionKey);
|
const notifications = inject(notificationsInjectionKey);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const pathHistory = inject(pathHistoryInjectionKey);
|
const pathHistory = inject(pathHistoryInjectionKey);
|
||||||
const directoryViewRef = ref();
|
const directoryViewRef = ref();
|
||||||
const searchFilterStr = ref("");
|
const searchFilterStr = ref("");
|
||||||
@ -143,66 +147,63 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cd = (newPath, saveHistory = true) => {
|
const cd = (newPath) => {
|
||||||
localStorage.setItem(lastPathStorageKey, newPath);
|
router.push(`/browse${newPath}`);
|
||||||
if (saveHistory)
|
|
||||||
pathHistory.push(newPath);
|
|
||||||
cockpit.location.go(`/browse${newPath}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const back = (saveHistory = false) => {
|
const back = () => {
|
||||||
cd(pathHistory.back() ?? '/', saveHistory);
|
cd(pathHistory.back() ?? '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
const forward = (saveHistory = false) => {
|
const forward = () => {
|
||||||
cd(pathHistory.forward() ?? '/', saveHistory);
|
cd(pathHistory.forward() ?? '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
const up = (saveHistory = true) => {
|
const up = () => {
|
||||||
cd(canonicalPath((pathHistory.current() ?? "") + '/..'), saveHistory);
|
cd((pathHistory.current() ?? "") + '/..');
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditor = (path) => {
|
||||||
|
router.push(`/edit${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(searchFilterStr, () => {
|
watch(searchFilterStr, () => {
|
||||||
searchFilterRegExp.value = new RegExp(
|
searchFilterRegExp.value = new RegExp(
|
||||||
`^${
|
`^${searchFilterStr.value
|
||||||
searchFilterStr.value
|
.replace(/[.+^${}()|[\]]|\\(?![*?])/g, '\\$&') // escape special chars, \\ only if not before * or ?
|
||||||
.replace(/[.+^${}()|[\]]|\\(?![*?])/g, '\\$&') // escape special chars, \\ only if not before * or ?
|
.replace(/(?<!\\)\*/g, '.*') // replace * with .* if not escaped
|
||||||
.replace(/(?<!\\)\*/g, '.*') // replace * with .* if not escaped
|
.replace(/(?<!\\)\?/g, '.') // replace ? with . if not escaped
|
||||||
.replace(/(?<!\\)\?/g, '.') // replace ? with . if not escaped
|
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(() => route.params.path, async () => {
|
watch(() => route.params.path, async (current, last) => {
|
||||||
if (!route.params.path)
|
if (!last) {
|
||||||
return cockpit.location.go('/browse/');
|
console.log("First watch execute", last);
|
||||||
const tmpPath = canonicalPath(route.params.path);
|
|
||||||
if (pathHistory.current() !== tmpPath) {
|
|
||||||
pathHistory.push(tmpPath);
|
|
||||||
}
|
}
|
||||||
|
if (route.name !== 'browse' || current === last)
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
let badPath = false;
|
const tmpPath = route.params.path;
|
||||||
if (!await checkIfExists(tmpPath)) {
|
// let realPath = (await useSpawn(['realpath', '--canonicalize-existing', tmpPath], { superuser: 'try' }).promise()).stdout.trim();
|
||||||
notifications.value.constructNotification("Failed to open path", `${tmpPath} does not exist.`, 'error');
|
// if (tmpPath !== realPath)
|
||||||
badPath = true;
|
// return cd(realPath);
|
||||||
} else if (!await checkIfAllowed(tmpPath, true)) {
|
try {
|
||||||
notifications.value.constructNotification("Failed to open path", `Permission denied for ${tmpPath}`, 'denied');
|
await useSpawn(['test', '-r', tmpPath, '-a', '-x', tmpPath], { superuser: 'try' }).promise();
|
||||||
badPath = true;
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`Permission denied for ${tmpPath}`);
|
||||||
}
|
}
|
||||||
if (badPath) {
|
localStorage.setItem(lastPathStorageKey, tmpPath);
|
||||||
if (pathHistory.backAllowed())
|
if (pathHistory.current() !== tmpPath) {
|
||||||
back();
|
pathHistory.push(tmpPath); // updates actual view
|
||||||
else
|
|
||||||
up(false);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(lastPathStorageKey, tmpPath);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.value.constructNotification("Failed to open path", errorStringHTML(error), 'error');
|
notifications.value.constructNotification("Failed to open path", errorStringHTML(error), 'error');
|
||||||
if (pathHistory.backAllowed())
|
if (pathHistory.backAllowed())
|
||||||
back();
|
back();
|
||||||
else
|
else
|
||||||
up(false);
|
up();
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
@ -219,6 +220,7 @@ export default {
|
|||||||
back,
|
back,
|
||||||
forward,
|
forward,
|
||||||
up,
|
up,
|
||||||
|
openEditor,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grow">Editing {{ path }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
const path = ref("");
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
watch(() => route.params.path, (filePath) => path.value = filePath, { immediate: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
42
navigator-vue/src/views/ErrorRedirect.vue
Normal file
42
navigator-vue/src/views/ErrorRedirect.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grow flex flex-col items-center justify-center gap-10">
|
||||||
|
<div class="text-header">{{title}}</div>
|
||||||
|
<div class="text-muted">{{message}}</div>
|
||||||
|
<div class="text-muted">Redirecting in {{counter}}...</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { lastPathStorageKey } from '../keys';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "A system error has occured.",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "Error",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const counter = ref(3);
|
||||||
|
|
||||||
|
const countdown = () => {
|
||||||
|
counter.value--;
|
||||||
|
if (counter.value > 0)
|
||||||
|
setTimeout(countdown, 1000);
|
||||||
|
else {
|
||||||
|
const lastLocation = localStorage.getItem(lastPathStorageKey) ?? '/';
|
||||||
|
router.push(`/browse${lastLocation}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(countdown, 1000);
|
||||||
|
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user