diff --git a/navigator/src/components/FileNameEditor.vue b/navigator/src/components/FileNameEditor.vue new file mode 100644 index 0000000..a063e43 --- /dev/null +++ b/navigator/src/components/FileNameEditor.vue @@ -0,0 +1,135 @@ + + $emit('hide')" + @apply="apply" + > + + + + {{ feedback }} + + + + + + + + + diff --git a/navigator/src/components/ModalPrompt.vue b/navigator/src/components/ModalPrompt.vue new file mode 100644 index 0000000..d30daae --- /dev/null +++ b/navigator/src/components/ModalPrompt.vue @@ -0,0 +1,130 @@ + + + + {{ input.label }} + { output[input.key] = event.target.value; validate(); }" + /> + + + {{ input.feedback }} + + + + + + diff --git a/navigator/src/views/Browser.vue b/navigator/src/views/Browser.vue index 4ebdb63..85606d2 100644 --- a/navigator/src/views/Browser.vue +++ b/navigator/src/views/Browser.vue @@ -118,7 +118,7 @@ openFilePromptModal.close()" > @@ -133,42 +133,62 @@ openFilePromptModal.action('editPermissions')" > - Edit permissions + Edit permissions + openFilePromptModal.action('edit')" > - Open for editing + Open for editing + openFilePromptModal.action('download')" > - Download + Download + + + {{ confirm.body }} + + + encodeURIComponent(string) @@ -292,6 +330,36 @@ export default { 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({ show: false, entry: null, @@ -306,6 +374,24 @@ export default { 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({ show: false, selection: [], @@ -325,6 +411,7 @@ export default { }, 500); }, }); + const modalPromptRef = ref(); const cd = ({ path, host }) => { const newHost = host ?? (pathHistory.current().host); @@ -356,10 +443,90 @@ export default { let { path, name, host } = items[0]; 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 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) => { switch (action) { case 'cd': @@ -389,6 +556,24 @@ export default { case 'up': up(); 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: console.error('Unknown browserAction:', action, args); break; @@ -427,8 +612,11 @@ export default { backHistoryDropdown, forwardHistoryDropdown, openFilePromptModal, + confirm, filePermissions, + nameEditor, contextMenu, + modalPromptRef, cd, back, forward, @@ -457,7 +645,12 @@ export default { ViewGridIcon, ModalPopup, FilePermissions, + FileNameEditor, ContextMenu, + KeyIcon, + PencilAltIcon, + DownloadIcon, + ModalPrompt, }, }