mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-30 00:55:30 +02:00
create/edit links, create dirs/files, edit dir/file names
This commit is contained in:
parent
d2deb01783
commit
075c1e7fc9
135
navigator/src/components/FileNameEditor.vue
Normal file
135
navigator/src/components/FileNameEditor.vue
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<ModalPopup
|
||||||
|
:showModal="show"
|
||||||
|
:headerText="createNew ? `New ${({f: 'file', d: 'directory'})[createNew]}` : `Rename ${entry?.name}`"
|
||||||
|
autoWidth
|
||||||
|
:disableContinue="!valid"
|
||||||
|
@cancel="() => $emit('hide')"
|
||||||
|
@apply="apply"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="name"
|
||||||
|
class="input-textlike w-full"
|
||||||
|
placeholder="Name"
|
||||||
|
@keypress.enter="apply"
|
||||||
|
ref="inputRef"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="feedback"
|
||||||
|
class="feedback-group"
|
||||||
|
>
|
||||||
|
<ExclamationCircleIcon class="size-icon icon-error" />
|
||||||
|
<span class="text-feedback text-error">{{ feedback }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="stderr"
|
||||||
|
class="feedback-group"
|
||||||
|
>
|
||||||
|
<ExclamationCircleIcon class="size-icon icon-error" />
|
||||||
|
<span
|
||||||
|
class="text-feedback text-error"
|
||||||
|
v-html="stderr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalPopup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { nextTick, ref, watch } from 'vue';
|
||||||
|
import { useSpawn, errorStringHTML } from "@45drives/cockpit-helpers";
|
||||||
|
import { ExclamationCircleIcon } from '@heroicons/vue/solid';
|
||||||
|
import ModalPopup from './ModalPopup.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
show: Boolean,
|
||||||
|
entry: Object,
|
||||||
|
createNew: String,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const name = ref("");
|
||||||
|
const valid = ref(false);
|
||||||
|
const feedback = ref("");
|
||||||
|
const stderr = ref("");
|
||||||
|
const inputRef = ref();
|
||||||
|
|
||||||
|
const apply = async () => {
|
||||||
|
if (!valid.value)
|
||||||
|
return;
|
||||||
|
stderr.value = "";
|
||||||
|
try {
|
||||||
|
if (!props.createNew) {
|
||||||
|
await (useSpawn(['mv', '-nT', props.entry.path, props.entry.path.split('/').slice(0, -1).concat(name.value).join('/')], { superuser: 'try' }).promise());
|
||||||
|
} else if (['f', 'd'].includes(props.createNew)) {
|
||||||
|
const parentPath = props.entry.resolvedType === 'd' ? props.entry.resolvedPath : props.entry.path.split('/').slice(0, -1).join('/');
|
||||||
|
const path = `${parentPath}/${name.value}`
|
||||||
|
await useSpawn(['test', '!', '(', '-e', path, '-o', '-L', path, ')'], { superuser: 'try' }).promise().catch(() => { throw new Error('File exists') });
|
||||||
|
if (props.createNew === 'f')
|
||||||
|
await useSpawn(['touch', '-h', path], { superuser: 'try' }).promise();
|
||||||
|
else
|
||||||
|
await useSpawn(['mkdir', path], { superuser: 'try' }).promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('hide');
|
||||||
|
setTimeout(() => {
|
||||||
|
name.value = feedback.value = "";
|
||||||
|
valid.value = false;
|
||||||
|
}, 500);
|
||||||
|
} catch (state) {
|
||||||
|
valid.value = false;
|
||||||
|
stderr.value = errorStringHTML(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch([() => props.entry, () => props.createNew], () => {
|
||||||
|
if (props.createNew)
|
||||||
|
name.value = "";
|
||||||
|
else
|
||||||
|
name.value = props.entry?.name ?? "";
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(name, () => {
|
||||||
|
let result = true;
|
||||||
|
let feedbackArr = [];
|
||||||
|
if (!name.value) {
|
||||||
|
feedbackArr.push('Name cannot be empty');
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (name.value.includes('/')) {
|
||||||
|
feedbackArr.push("Name cannot include '/'");
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (['.', '..'].includes(name.value)) {
|
||||||
|
feedbackArr.push(`Name cannot be '${name.value}'`);
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
feedback.value = feedbackArr.join(', ');
|
||||||
|
valid.value = result;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.show, () => {
|
||||||
|
if (props.show) {
|
||||||
|
feedback.value = stderr.value = "";
|
||||||
|
nextTick(() => inputRef.value.focus());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
valid,
|
||||||
|
feedback,
|
||||||
|
stderr,
|
||||||
|
inputRef,
|
||||||
|
apply,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
ModalPopup,
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'hide',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
130
navigator/src/components/ModalPrompt.vue
Normal file
130
navigator/src/components/ModalPrompt.vue
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<ModalPopup
|
||||||
|
:showModal="show"
|
||||||
|
:headerText="headerText"
|
||||||
|
autoWidth
|
||||||
|
:disableContinue="!valid"
|
||||||
|
@cancel="cancel()"
|
||||||
|
@apply="apply()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="input in inputs"
|
||||||
|
:key="input.key"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="input.label"
|
||||||
|
class="block text-label"
|
||||||
|
>{{ input.label }}</label>
|
||||||
|
<input
|
||||||
|
:class="[!['button', 'checkbox', 'color', 'file', 'radio', 'range'].includes(input.type) ? 'input-textlike w-full' : input.type == 'checkbox' ? 'input-checkbox' : '']"
|
||||||
|
v-bind="{...input.props}"
|
||||||
|
:type="input.type"
|
||||||
|
:value="output[input.key] ?? input.default"
|
||||||
|
:placeholder="input.placeholder"
|
||||||
|
@input="(event) => { output[input.key] = event.target.value; validate(); }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="input.feedback"
|
||||||
|
class="feedback-group"
|
||||||
|
>
|
||||||
|
<ExclamationCircleIcon class="size-icon icon-error" />
|
||||||
|
<span class="text-feedback text-error">{{ input.feedback }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalPopup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, computed, nextTick } from 'vue';
|
||||||
|
import { ExclamationCircleIcon } from '@heroicons/vue/solid';
|
||||||
|
import ModalPopup from './ModalPopup.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const show = ref(false);
|
||||||
|
const headerText = ref("");
|
||||||
|
const inputs = ref([]);
|
||||||
|
const output = ref({});
|
||||||
|
const valid = ref(false);
|
||||||
|
const apply = ref(() => null);
|
||||||
|
const cancel = ref(() => null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise with method to add input to prompt
|
||||||
|
* @typedef {Object} ModalPromptPromise
|
||||||
|
* @property {function} addInput
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt user for input
|
||||||
|
* @param {string} header - Text to display in popup header
|
||||||
|
* @returns {ModalPromptPromise} - Resolves object with responses or null if cancelled
|
||||||
|
*/
|
||||||
|
const prompt = (header) => {
|
||||||
|
headerText.value = header;
|
||||||
|
output.value = {};
|
||||||
|
inputs.value = [];
|
||||||
|
valid.value = false;
|
||||||
|
show.value = true;
|
||||||
|
const prom = new Promise((resolve, reject) => {
|
||||||
|
apply.value = () => resolve(output.value);
|
||||||
|
cancel.value = () => resolve(null);
|
||||||
|
}).finally(() => show.value = false);
|
||||||
|
/**
|
||||||
|
* Callback to validate input, cannot be arrow function
|
||||||
|
* @callback ModalPromptInputValidationCallback
|
||||||
|
* @param {any} - value of input
|
||||||
|
* @returns {boolean} - true if valid, false otherwise
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Add an input to the prompt
|
||||||
|
* @param {string} key - object key for result
|
||||||
|
* @param {string} type - input tag type prop value
|
||||||
|
* @param {string} label - label for input
|
||||||
|
* @param {string} placeholder - input placeholder if applicable
|
||||||
|
* @param {any} defaultValue - Default value of input
|
||||||
|
* @param {ModalPromptInputValidationCallback} validate - Validation callback for input
|
||||||
|
* @param {object} props - optional extra properties for input tag
|
||||||
|
* @returns {ModalPromptPromise} - Resolves object with responses or null if cancelled
|
||||||
|
*/
|
||||||
|
const addInput = (key, type, label, placeholder, defaultValue, validate, props) => {
|
||||||
|
const input = {
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
default: defaultValue,
|
||||||
|
props,
|
||||||
|
feedback: '',
|
||||||
|
}
|
||||||
|
output.value[key] = defaultValue;
|
||||||
|
input.validate = validate?.bind(input),
|
||||||
|
inputs.value.push(input);
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
prom.addInput = addInput;
|
||||||
|
return prom;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
valid.value = inputs.value.every(input => input.validate?.(output.value[input.key]) ?? true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
show,
|
||||||
|
headerText,
|
||||||
|
inputs,
|
||||||
|
output,
|
||||||
|
valid,
|
||||||
|
apply,
|
||||||
|
cancel,
|
||||||
|
prompt,
|
||||||
|
validate,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ModalPopup,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -118,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ModalPopup
|
<ModalPopup
|
||||||
:showModal="openFilePromptModal.show"
|
:showModal="openFilePromptModal.show"
|
||||||
:headerText="openFilePromptModal.entry?.name ?? 'NULL'"
|
:headerText="openFilePromptModal.entry?.linkRawPath || (openFilePromptModal.entry?.name ?? 'NULL')"
|
||||||
autoWidth
|
autoWidth
|
||||||
@close="() => openFilePromptModal.close()"
|
@close="() => openFilePromptModal.close()"
|
||||||
>
|
>
|
||||||
@ -133,42 +133,62 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary flex items-center gap-1"
|
||||||
@click="() => openFilePromptModal.action('editPermissions')"
|
@click="() => openFilePromptModal.action('editPermissions')"
|
||||||
>
|
>
|
||||||
Edit permissions
|
<span>Edit permissions</span>
|
||||||
|
<KeyIcon class="size-icon text-default" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="openFilePromptModal.entry?.resolvedType === 'f'"
|
v-if="openFilePromptModal.entry?.resolvedType === 'f'"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary flex items-center gap-1"
|
||||||
@click="() => openFilePromptModal.action('edit')"
|
@click="() => openFilePromptModal.action('edit')"
|
||||||
>
|
>
|
||||||
Open for editing
|
<span>Open for editing</span>
|
||||||
|
<PencilAltIcon class="size-icon text-default" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="openFilePromptModal.entry?.resolvedType === 'f'"
|
v-if="openFilePromptModal.entry?.resolvedType === 'f'"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary flex items-center gap-1"
|
||||||
@click="() => openFilePromptModal.action('download')"
|
@click="() => openFilePromptModal.action('download')"
|
||||||
>
|
>
|
||||||
Download
|
<span>Download</span>
|
||||||
|
<DownloadIcon class="size-icon text-default" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</ModalPopup>
|
</ModalPopup>
|
||||||
|
<ModalPopup
|
||||||
|
:showModal="confirm.show"
|
||||||
|
:headerText="confirm.header"
|
||||||
|
autoWidth
|
||||||
|
:applyDangerous="confirm.dangerous"
|
||||||
|
@apply="confirm.resolve(true)"
|
||||||
|
@close="confirm.close"
|
||||||
|
>
|
||||||
|
<div class="whitespace-pre">{{ confirm.body }}</div>
|
||||||
|
</ModalPopup>
|
||||||
<FilePermissions
|
<FilePermissions
|
||||||
:show="filePermissions.show"
|
:show="filePermissions.show"
|
||||||
:entry="filePermissions.entry"
|
:entry="filePermissions.entry"
|
||||||
@hide="filePermissions.close"
|
@hide="filePermissions.close"
|
||||||
/>
|
/>
|
||||||
|
<FileNameEditor
|
||||||
|
:show="nameEditor.show"
|
||||||
|
:entry="nameEditor.entry"
|
||||||
|
:createNew="nameEditor.createNew"
|
||||||
|
@hide="nameEditor.close"
|
||||||
|
/>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
:show="contextMenu.show"
|
:show="contextMenu.show"
|
||||||
:selection="contextMenu.selection"
|
:selection="contextMenu.selection"
|
||||||
:event="contextMenu.event"
|
:event="contextMenu.event"
|
||||||
:currentDirEntry="{...pathHistory.current(), name: `Current directory (${pathHistory.current().path.split('/').pop()})`}"
|
:currentDirEntry="{ ...pathHistory.current(), name: `Current directory (${pathHistory.current().path.split('/').pop()})` }"
|
||||||
@browserAction="handleAction"
|
@browserAction="handleAction"
|
||||||
@hide="contextMenu.close"
|
@hide="contextMenu.close"
|
||||||
/>
|
/>
|
||||||
|
<ModalPrompt ref="modalPromptRef" />
|
||||||
<Teleport to="#footer-buttons">
|
<Teleport to="#footer-buttons">
|
||||||
<IconToggle
|
<IconToggle
|
||||||
v-model="darkMode"
|
v-model="darkMode"
|
||||||
@ -220,12 +240,30 @@ import { useRoute, useRouter } from "vue-router";
|
|||||||
import DirectoryView from "../components/DirectoryView.vue";
|
import DirectoryView from "../components/DirectoryView.vue";
|
||||||
import PathBreadCrumbs from '../components/PathBreadCrumbs.vue';
|
import PathBreadCrumbs from '../components/PathBreadCrumbs.vue';
|
||||||
import { notificationsInjectionKey, pathHistoryInjectionKey, lastPathStorageKey, settingsInjectionKey } from '../keys';
|
import { notificationsInjectionKey, pathHistoryInjectionKey, lastPathStorageKey, settingsInjectionKey } from '../keys';
|
||||||
import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, RefreshIcon, ChevronDownIcon, SearchIcon, SunIcon, MoonIcon, EyeIcon, EyeOffIcon, ViewListIcon, ViewGridIcon } from '@heroicons/vue/solid';
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
SearchIcon,
|
||||||
|
SunIcon,
|
||||||
|
MoonIcon,
|
||||||
|
EyeIcon,
|
||||||
|
EyeOffIcon,
|
||||||
|
ViewListIcon,
|
||||||
|
ViewGridIcon,
|
||||||
|
KeyIcon,
|
||||||
|
PencilAltIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
} from '@heroicons/vue/solid';
|
||||||
import IconToggle from '../components/IconToggle.vue';
|
import IconToggle from '../components/IconToggle.vue';
|
||||||
import ModalPopup from '../components/ModalPopup.vue';
|
import ModalPopup from '../components/ModalPopup.vue';
|
||||||
import { fileDownload } from '@45drives/cockpit-helpers';
|
import { fileDownload, useSpawn, errorStringHTML } from '@45drives/cockpit-helpers';
|
||||||
import FilePermissions from '../components/FilePermissions.vue';
|
import FilePermissions from '../components/FilePermissions.vue';
|
||||||
|
import FileNameEditor from '../components/FileNameEditor.vue';
|
||||||
import ContextMenu from '../components/ContextMenu.vue';
|
import ContextMenu from '../components/ContextMenu.vue';
|
||||||
|
import ModalPrompt from '../components/ModalPrompt.vue';
|
||||||
|
|
||||||
const encodePartial = (string) =>
|
const encodePartial = (string) =>
|
||||||
encodeURIComponent(string)
|
encodeURIComponent(string)
|
||||||
@ -292,6 +330,36 @@ export default {
|
|||||||
openFilePromptModal.close();
|
openFilePromptModal.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const confirm = reactive({
|
||||||
|
show: false,
|
||||||
|
header: "",
|
||||||
|
body: "",
|
||||||
|
dangerous: false,
|
||||||
|
resolve: () => null,
|
||||||
|
reject: () => null,
|
||||||
|
resetTimeoutHandle: null,
|
||||||
|
ask: (header, body = "", dangerous = false) => {
|
||||||
|
clearTimeout(confirm.resetTimeoutHandle);
|
||||||
|
confirm.header = header;
|
||||||
|
confirm.body = body;
|
||||||
|
confirm.dangerous = dangerous;
|
||||||
|
confirm.show = true;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
confirm.resolve = resolve;
|
||||||
|
confirm.reject = reject;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
confirm.show = false;
|
||||||
|
confirm.resolve(false);
|
||||||
|
confirm.resetTimeoutHandle = setTimeout(() => {
|
||||||
|
confirm.resetTimeoutHandle = null;
|
||||||
|
confirm.header = confirm.body = "";
|
||||||
|
confirm.dangerous = false;
|
||||||
|
confirm.resolve = confirm.reject = () => null;
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
});
|
||||||
const filePermissions = reactive({
|
const filePermissions = reactive({
|
||||||
show: false,
|
show: false,
|
||||||
entry: null,
|
entry: null,
|
||||||
@ -306,6 +374,24 @@ export default {
|
|||||||
filePermissions.resetTimeoutHandle = setTimeout(() => filePermissions.resetTimeoutHandle = filePermissions.entry = null, 500);
|
filePermissions.resetTimeoutHandle = setTimeout(() => filePermissions.resetTimeoutHandle = filePermissions.entry = null, 500);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const nameEditor = reactive({
|
||||||
|
show: false,
|
||||||
|
entry: null,
|
||||||
|
createNew: null,
|
||||||
|
resetTimeoutHandle: null,
|
||||||
|
open: (entry, createNew) => {
|
||||||
|
clearTimeout(nameEditor.resetTimeoutHandle);
|
||||||
|
nameEditor.entry = entry;
|
||||||
|
nameEditor.createNew = createNew ?? null;
|
||||||
|
nameEditor.show = true;
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
nameEditor.show = false;
|
||||||
|
nameEditor.resetTimeoutHandle = setTimeout(() => {
|
||||||
|
nameEditor.resetTimeoutHandle = nameEditor.entry = nameEditor.createNew = null;
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
});
|
||||||
const contextMenu = reactive({
|
const contextMenu = reactive({
|
||||||
show: false,
|
show: false,
|
||||||
selection: [],
|
selection: [],
|
||||||
@ -325,6 +411,7 @@ export default {
|
|||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const modalPromptRef = ref();
|
||||||
|
|
||||||
const cd = ({ path, host }) => {
|
const cd = ({ path, host }) => {
|
||||||
const newHost = host ?? (pathHistory.current().host);
|
const newHost = host ?? (pathHistory.current().host);
|
||||||
@ -356,10 +443,90 @@ export default {
|
|||||||
let { path, name, host } = items[0];
|
let { path, name, host } = items[0];
|
||||||
fileDownload(path, name, host);
|
fileDownload(path, name, host);
|
||||||
}
|
}
|
||||||
|
// TODO: mutlifile & directory downloads
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItems = async (selection) => {
|
||||||
|
const items = [].concat(selection); // forces to be array
|
||||||
|
if (items.length === 0)
|
||||||
|
return;
|
||||||
|
if (!await confirm.ask(`Permanently delete ${items.length} item${items.length > 1 ? 's' : ''}?`, items.map(i => i.path).join('\n'), true))
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
await useSpawn(['rm', '-rf', '--', ...items.map(i => i.path)]).promise();
|
||||||
|
} catch (state) {
|
||||||
|
notifications.value.constructNotification("Failed to remove file(s)", errorStringHTML(state), 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSelected = () => directoryViewRef.value?.getSelected?.() ?? [];
|
const getSelected = () => directoryViewRef.value?.getSelected?.() ?? [];
|
||||||
|
|
||||||
|
const cwdAsEntryObj = () => {
|
||||||
|
const obj = { ...pathHistory.current() };
|
||||||
|
obj.name = obj.path.split('/').pop();
|
||||||
|
obj.resolvedPath = obj.path;
|
||||||
|
obj.type = obj.resolvedType = 'd';
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLink = async (parentEntry) => {
|
||||||
|
const result = await modalPromptRef.value.prompt("Create link")
|
||||||
|
.addInput(
|
||||||
|
'linkName',
|
||||||
|
'text',
|
||||||
|
'Name',
|
||||||
|
'Name',
|
||||||
|
'',
|
||||||
|
function (name) {
|
||||||
|
let result = true;
|
||||||
|
let feedbackArr = [];
|
||||||
|
if (!name) {
|
||||||
|
feedbackArr.push('Name cannot be empty');
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (name && name.includes('/')) {
|
||||||
|
feedbackArr.push("Name cannot include '/'");
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (['.', '..'].includes(name)) {
|
||||||
|
feedbackArr.push(`Name cannot be '${name}'`);
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
this.feedback = feedbackArr.join(', ');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.addInput(
|
||||||
|
'linkTarget',
|
||||||
|
'text',
|
||||||
|
'Link target',
|
||||||
|
'Target path',
|
||||||
|
parentEntry.resolvedType !== 'd' ? parentEntry.path : ''
|
||||||
|
);
|
||||||
|
if (!result)
|
||||||
|
return; // cancelled
|
||||||
|
console.log(result);
|
||||||
|
try {
|
||||||
|
const parentPath = parentEntry.resolvedType === 'd' ? parentEntry.resolvedPath : parentEntry.path.split('/').slice(0, -1).join('/');
|
||||||
|
const path = `${parentPath}/${result.linkName}`
|
||||||
|
await useSpawn(['test', '!', '-e', path], { superuser: 'try' }).promise().catch(() => { throw new Error('File exists') });
|
||||||
|
await useSpawn(['ln', '-snT', result.linkTarget, path], { superuser: 'try' }).promise();
|
||||||
|
} catch (state) {
|
||||||
|
notifications.value.constructNotification("Failed to create link", errorStringHTML(state), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editLink = async (linkEntry) => {
|
||||||
|
const { path, linkRawPath, name } = linkEntry;
|
||||||
|
const { newTarget } = await modalPromptRef.value.prompt(`Edit link target for ${name}`)
|
||||||
|
.addInput('newTarget', 'text', 'Link target', 'path/to/target', linkRawPath);
|
||||||
|
try {
|
||||||
|
await useSpawn(['ln', '-snfT', newTarget, path], { superuser: 'try' }).promise();
|
||||||
|
} catch (state) {
|
||||||
|
notifications.value.constructNotification("Failed to edit link", errorStringHTML(state), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleAction = (action, ...args) => {
|
const handleAction = (action, ...args) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'cd':
|
case 'cd':
|
||||||
@ -389,6 +556,24 @@ export default {
|
|||||||
case 'up':
|
case 'up':
|
||||||
up();
|
up();
|
||||||
break;
|
break;
|
||||||
|
case 'rename':
|
||||||
|
nameEditor.open(args[0], null);
|
||||||
|
break;
|
||||||
|
case 'createFile':
|
||||||
|
nameEditor.open(args[0] ?? cwdAsEntryObj(), 'f');
|
||||||
|
break;
|
||||||
|
case 'createLink':
|
||||||
|
createLink(args[0] ?? cwdAsEntryObj());
|
||||||
|
break;
|
||||||
|
case 'createDirectory':
|
||||||
|
nameEditor.open(args[0] ?? cwdAsEntryObj(), 'd');
|
||||||
|
break;
|
||||||
|
case 'editLink':
|
||||||
|
editLink(args[0]);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
deleteItems(...args);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('Unknown browserAction:', action, args);
|
console.error('Unknown browserAction:', action, args);
|
||||||
break;
|
break;
|
||||||
@ -427,8 +612,11 @@ export default {
|
|||||||
backHistoryDropdown,
|
backHistoryDropdown,
|
||||||
forwardHistoryDropdown,
|
forwardHistoryDropdown,
|
||||||
openFilePromptModal,
|
openFilePromptModal,
|
||||||
|
confirm,
|
||||||
filePermissions,
|
filePermissions,
|
||||||
|
nameEditor,
|
||||||
contextMenu,
|
contextMenu,
|
||||||
|
modalPromptRef,
|
||||||
cd,
|
cd,
|
||||||
back,
|
back,
|
||||||
forward,
|
forward,
|
||||||
@ -457,7 +645,12 @@ export default {
|
|||||||
ViewGridIcon,
|
ViewGridIcon,
|
||||||
ModalPopup,
|
ModalPopup,
|
||||||
FilePermissions,
|
FilePermissions,
|
||||||
|
FileNameEditor,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
KeyIcon,
|
||||||
|
PencilAltIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
ModalPrompt,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user