Improve navigator UI and context actions
This commit is contained in:
+3
-3
@@ -2,7 +2,7 @@
|
||||
set -eu
|
||||
|
||||
PLUGIN="u-navigator"
|
||||
VERSION="0.2.2"
|
||||
VERSION="0.2.3"
|
||||
PKG_DIR="packages"
|
||||
WORK_DIR=".plugin-build"
|
||||
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
||||
@@ -11,8 +11,8 @@ rm -rf "$WORK_DIR"
|
||||
mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets" "$PKG_DIR"
|
||||
|
||||
cp -R plugin-root/usr "$WORK_DIR/"
|
||||
cp public/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js"
|
||||
cp public/styles.css "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/styles.css"
|
||||
cp server/public/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js"
|
||||
cp server/public/styles.css "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/styles.css"
|
||||
|
||||
tar -czf "$PKG_DIR/$PKG_NAME" -C "$WORK_DIR" usr
|
||||
rm -rf "$WORK_DIR"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -29,17 +29,28 @@ Code="f07b"
|
||||
}
|
||||
|
||||
const wrapper = toolsLink.parentElement?.tagName === 'LI' ? toolsLink.parentElement : toolsLink;
|
||||
const item = wrapper.cloneNode(true);
|
||||
const link = item.tagName === 'A' ? item : item.querySelector('a');
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isListItem = wrapper.tagName === 'LI';
|
||||
const item = isListItem ? document.createElement('li') : document.createElement('a');
|
||||
const link = isListItem ? document.createElement('a') : item;
|
||||
link.className = toolsLink.className || '';
|
||||
link.href = targetHref;
|
||||
link.textContent = targetText;
|
||||
link.setAttribute('data-u-navigator-nav', 'true');
|
||||
link.classList.remove('active', 'selected');
|
||||
item.classList?.remove('active', 'selected');
|
||||
link.classList.remove('active', 'selected', 'on');
|
||||
|
||||
if (isListItem) {
|
||||
item.className = wrapper.className || '';
|
||||
item.classList.remove('active', 'selected', 'on');
|
||||
item.appendChild(link);
|
||||
}
|
||||
|
||||
if (location.pathname === targetHref) {
|
||||
toolsLink.classList.remove('active', 'selected', 'on');
|
||||
wrapper.classList?.remove('active', 'selected', 'on');
|
||||
link.classList.add('active');
|
||||
item.classList?.add('active');
|
||||
}
|
||||
|
||||
wrapper.insertAdjacentElement('afterend', item);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const state = {
|
||||
windows: [],
|
||||
focusedId: null,
|
||||
selectedEntry: null,
|
||||
contextMenu: null,
|
||||
jobs: new Map(),
|
||||
nextWindowId: 1,
|
||||
zIndex: 20
|
||||
@@ -106,6 +107,9 @@ function render() {
|
||||
for (const win of state.windows) {
|
||||
workspace.appendChild(renderWindow(win));
|
||||
}
|
||||
if (state.contextMenu) {
|
||||
workspace.appendChild(renderContextMenu());
|
||||
}
|
||||
}
|
||||
|
||||
function renderWindow(win) {
|
||||
@@ -181,6 +185,7 @@ function renderExplorer(body, win) {
|
||||
const dropZone = body.querySelector('.drop-zone');
|
||||
dropZone.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move';
|
||||
data.dragOver = true;
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
@@ -194,6 +199,10 @@ function renderExplorer(body, win) {
|
||||
dropZone.classList.remove('drag-over');
|
||||
await handleDrop(event, win, data.path);
|
||||
});
|
||||
dropZone.addEventListener('contextmenu', (event) => {
|
||||
event.preventDefault();
|
||||
showContextMenu(event, win, null);
|
||||
});
|
||||
|
||||
for (const row of body.querySelectorAll('.file-row')) {
|
||||
const entry = data.entries.find((item) => item.path === row.dataset.path);
|
||||
@@ -205,7 +214,12 @@ function renderExplorer(body, win) {
|
||||
});
|
||||
row.addEventListener('dragstart', (event) => {
|
||||
event.dataTransfer.setData('application/x-u-navigator-path', entry.path);
|
||||
event.dataTransfer.setData('text/plain', entry.path);
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
row.classList.add('dragging');
|
||||
});
|
||||
row.addEventListener('dragend', () => {
|
||||
row.classList.remove('dragging');
|
||||
});
|
||||
row.addEventListener('dragover', (event) => {
|
||||
if (entry.type === 'directory') {
|
||||
@@ -218,6 +232,11 @@ function renderExplorer(body, win) {
|
||||
event.preventDefault();
|
||||
await handleDrop(event, win, entry.path);
|
||||
});
|
||||
row.addEventListener('contextmenu', (event) => {
|
||||
event.preventDefault();
|
||||
selectEntry(win, entry);
|
||||
showContextMenu(event, win, entry);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +353,93 @@ async function createJob(type, source, destination) {
|
||||
render();
|
||||
}
|
||||
|
||||
async function renameEntry(win, entry) {
|
||||
const nextName = prompt('Neuer Name', entry.name);
|
||||
if (!nextName || nextName === entry.name) {
|
||||
return;
|
||||
}
|
||||
if (nextName.includes('/')) {
|
||||
alert('Der Name darf keinen Slash enthalten.');
|
||||
return;
|
||||
}
|
||||
await createJob('move', entry.path, `${dirname(entry.path)}/${nextName}`);
|
||||
await loadExplorer(win, win.data.path);
|
||||
}
|
||||
|
||||
async function promptTransfer(win, entry, type) {
|
||||
const targetDir = prompt(type === 'copy' ? 'Kopieren nach Ordner' : 'Verschieben nach Ordner', win.data.path);
|
||||
if (!targetDir) {
|
||||
return;
|
||||
}
|
||||
await createJob(type, entry.path, `${targetDir.replace(/\/$/, '')}/${entry.name}`);
|
||||
await loadExplorer(win, win.data.path);
|
||||
}
|
||||
|
||||
function showContextMenu(event, win, entry) {
|
||||
const rect = workspace.getBoundingClientRect();
|
||||
state.contextMenu = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
winId: win.id,
|
||||
entryPath: entry?.path || null
|
||||
};
|
||||
render();
|
||||
}
|
||||
|
||||
function renderContextMenu() {
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'context-menu';
|
||||
menu.style.left = `${state.contextMenu.x}px`;
|
||||
menu.style.top = `${state.contextMenu.y}px`;
|
||||
|
||||
const win = state.windows.find((item) => item.id === state.contextMenu.winId);
|
||||
const entry = win?.data.entries?.find((item) => item.path === state.contextMenu.entryPath) || null;
|
||||
const actions = [];
|
||||
|
||||
if (entry) {
|
||||
if (entry.type === 'directory') {
|
||||
actions.push(['Öffnen', () => loadExplorer(win, entry.path)]);
|
||||
}
|
||||
actions.push(['Umbenennen', () => renameEntry(win, entry)]);
|
||||
actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]);
|
||||
actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]);
|
||||
} else {
|
||||
actions.push(['Neu laden', () => loadExplorer(win, win.data.path)]);
|
||||
}
|
||||
|
||||
for (const [label, action] of actions) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = label;
|
||||
button.addEventListener('click', async () => {
|
||||
state.contextMenu = null;
|
||||
render();
|
||||
try {
|
||||
await action();
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
});
|
||||
menu.appendChild(button);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const close = () => {
|
||||
state.contextMenu = null;
|
||||
render();
|
||||
document.removeEventListener('click', close);
|
||||
document.removeEventListener('keydown', keyClose);
|
||||
};
|
||||
const keyClose = (event) => {
|
||||
if (event.key === 'Escape') close();
|
||||
};
|
||||
document.addEventListener('click', close);
|
||||
document.addEventListener('keydown', keyClose);
|
||||
}, 0);
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
async function pollJob(id) {
|
||||
const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
|
||||
state.jobs.set(id, job);
|
||||
@@ -429,6 +535,12 @@ function parentPath(value) {
|
||||
return `/${parts.slice(0, -1).join('/')}`;
|
||||
}
|
||||
|
||||
function dirname(value) {
|
||||
const parts = String(value || '').split('/').filter(Boolean);
|
||||
if (parts.length <= 1) return '/';
|
||||
return `/${parts.slice(0, -1).join('/')}`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
const value = Number(bytes || 0);
|
||||
if (value < 1024) return `${value} B`;
|
||||
@@ -197,32 +197,46 @@
|
||||
|
||||
.u-nav .file-table {
|
||||
border-collapse: collapse;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.u-nav .file-table th,
|
||||
.u-nav .file-table td {
|
||||
background: var(--surface) !important;
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--text) !important;
|
||||
padding: 8px 7px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.u-nav .file-table th {
|
||||
background: var(--surface-2) !important;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.u-nav .file-row {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.u-nav .file-row:hover,
|
||||
.u-nav .file-row.selected {
|
||||
.u-nav .file-row.selected,
|
||||
.u-nav .file-row.dragging {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.u-nav .file-row:hover td,
|
||||
.u-nav .file-row.selected td,
|
||||
.u-nav .file-row.dragging td {
|
||||
background: var(--surface-2) !important;
|
||||
}
|
||||
|
||||
.u-nav .name-cell {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -241,6 +255,30 @@
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.u-nav .context-menu {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
display: grid;
|
||||
min-width: 190px;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.u-nav .context-menu button {
|
||||
border-color: transparent;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.u-nav .context-menu button:hover {
|
||||
background: var(--surface-2);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.u-nav .queue-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { Sandbox, rootsFromEnv } from './sandbox.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
const publicDir = path.join(projectRoot, 'public');
|
||||
const publicDir = path.join(projectRoot, 'server', 'public');
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "u-navigator">
|
||||
<!ENTITY author "michael">
|
||||
<!ENTITY version "0.2.2">
|
||||
<!ENTITY version "0.2.3">
|
||||
<!ENTITY package "&name;-&version;.tgz">
|
||||
]>
|
||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/&package;">
|
||||
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
||||
<MD5>0cb8fe513addbd7435a8c79107ae7973</MD5>
|
||||
<MD5>8d05b440bd2ad3e20b8d900238642c3f</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Run="/bin/bash">
|
||||
|
||||
Reference in New Issue
Block a user