Improve navigator UI and context actions
This commit is contained in:
+3
-3
@@ -2,7 +2,7 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
PLUGIN="u-navigator"
|
PLUGIN="u-navigator"
|
||||||
VERSION="0.2.2"
|
VERSION="0.2.3"
|
||||||
PKG_DIR="packages"
|
PKG_DIR="packages"
|
||||||
WORK_DIR=".plugin-build"
|
WORK_DIR=".plugin-build"
|
||||||
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
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"
|
mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets" "$PKG_DIR"
|
||||||
|
|
||||||
cp -R plugin-root/usr "$WORK_DIR/"
|
cp -R plugin-root/usr "$WORK_DIR/"
|
||||||
cp public/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js"
|
cp server/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/styles.css "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/styles.css"
|
||||||
|
|
||||||
tar -czf "$PKG_DIR/$PKG_NAME" -C "$WORK_DIR" usr
|
tar -czf "$PKG_DIR/$PKG_NAME" -C "$WORK_DIR" usr
|
||||||
rm -rf "$WORK_DIR"
|
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 wrapper = toolsLink.parentElement?.tagName === 'LI' ? toolsLink.parentElement : toolsLink;
|
||||||
const item = wrapper.cloneNode(true);
|
const isListItem = wrapper.tagName === 'LI';
|
||||||
const link = item.tagName === 'A' ? item : item.querySelector('a');
|
const item = isListItem ? document.createElement('li') : document.createElement('a');
|
||||||
if (!link) {
|
const link = isListItem ? document.createElement('a') : item;
|
||||||
return;
|
link.className = toolsLink.className || '';
|
||||||
}
|
|
||||||
|
|
||||||
link.href = targetHref;
|
link.href = targetHref;
|
||||||
link.textContent = targetText;
|
link.textContent = targetText;
|
||||||
link.setAttribute('data-u-navigator-nav', 'true');
|
link.setAttribute('data-u-navigator-nav', 'true');
|
||||||
link.classList.remove('active', 'selected');
|
link.classList.remove('active', 'selected', 'on');
|
||||||
item.classList?.remove('active', 'selected');
|
|
||||||
|
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);
|
wrapper.insertAdjacentElement('afterend', item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const state = {
|
|||||||
windows: [],
|
windows: [],
|
||||||
focusedId: null,
|
focusedId: null,
|
||||||
selectedEntry: null,
|
selectedEntry: null,
|
||||||
|
contextMenu: null,
|
||||||
jobs: new Map(),
|
jobs: new Map(),
|
||||||
nextWindowId: 1,
|
nextWindowId: 1,
|
||||||
zIndex: 20
|
zIndex: 20
|
||||||
@@ -106,6 +107,9 @@ function render() {
|
|||||||
for (const win of state.windows) {
|
for (const win of state.windows) {
|
||||||
workspace.appendChild(renderWindow(win));
|
workspace.appendChild(renderWindow(win));
|
||||||
}
|
}
|
||||||
|
if (state.contextMenu) {
|
||||||
|
workspace.appendChild(renderContextMenu());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWindow(win) {
|
function renderWindow(win) {
|
||||||
@@ -181,6 +185,7 @@ function renderExplorer(body, win) {
|
|||||||
const dropZone = body.querySelector('.drop-zone');
|
const dropZone = body.querySelector('.drop-zone');
|
||||||
dropZone.addEventListener('dragover', (event) => {
|
dropZone.addEventListener('dragover', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move';
|
||||||
data.dragOver = true;
|
data.dragOver = true;
|
||||||
dropZone.classList.add('drag-over');
|
dropZone.classList.add('drag-over');
|
||||||
});
|
});
|
||||||
@@ -194,6 +199,10 @@ function renderExplorer(body, win) {
|
|||||||
dropZone.classList.remove('drag-over');
|
dropZone.classList.remove('drag-over');
|
||||||
await handleDrop(event, win, data.path);
|
await handleDrop(event, win, data.path);
|
||||||
});
|
});
|
||||||
|
dropZone.addEventListener('contextmenu', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
showContextMenu(event, win, null);
|
||||||
|
});
|
||||||
|
|
||||||
for (const row of body.querySelectorAll('.file-row')) {
|
for (const row of body.querySelectorAll('.file-row')) {
|
||||||
const entry = data.entries.find((item) => item.path === row.dataset.path);
|
const entry = data.entries.find((item) => item.path === row.dataset.path);
|
||||||
@@ -205,7 +214,12 @@ function renderExplorer(body, win) {
|
|||||||
});
|
});
|
||||||
row.addEventListener('dragstart', (event) => {
|
row.addEventListener('dragstart', (event) => {
|
||||||
event.dataTransfer.setData('application/x-u-navigator-path', entry.path);
|
event.dataTransfer.setData('application/x-u-navigator-path', entry.path);
|
||||||
|
event.dataTransfer.setData('text/plain', entry.path);
|
||||||
event.dataTransfer.effectAllowed = 'copyMove';
|
event.dataTransfer.effectAllowed = 'copyMove';
|
||||||
|
row.classList.add('dragging');
|
||||||
|
});
|
||||||
|
row.addEventListener('dragend', () => {
|
||||||
|
row.classList.remove('dragging');
|
||||||
});
|
});
|
||||||
row.addEventListener('dragover', (event) => {
|
row.addEventListener('dragover', (event) => {
|
||||||
if (entry.type === 'directory') {
|
if (entry.type === 'directory') {
|
||||||
@@ -218,6 +232,11 @@ function renderExplorer(body, win) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await handleDrop(event, win, entry.path);
|
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();
|
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) {
|
async function pollJob(id) {
|
||||||
const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
|
const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
|
||||||
state.jobs.set(id, job);
|
state.jobs.set(id, job);
|
||||||
@@ -429,6 +535,12 @@ function parentPath(value) {
|
|||||||
return `/${parts.slice(0, -1).join('/')}`;
|
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) {
|
function formatBytes(bytes) {
|
||||||
const value = Number(bytes || 0);
|
const value = Number(bytes || 0);
|
||||||
if (value < 1024) return `${value} B`;
|
if (value < 1024) return `${value} B`;
|
||||||
@@ -197,32 +197,46 @@
|
|||||||
|
|
||||||
.u-nav .file-table {
|
.u-nav .file-table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.u-nav .file-table th,
|
.u-nav .file-table th,
|
||||||
.u-nav .file-table td {
|
.u-nav .file-table td {
|
||||||
|
background: var(--surface) !important;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
|
color: var(--text) !important;
|
||||||
padding: 8px 7px;
|
padding: 8px 7px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.u-nav .file-table th {
|
.u-nav .file-table th {
|
||||||
|
background: var(--surface-2) !important;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.u-nav .file-row {
|
.u-nav .file-row {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.u-nav .file-row:hover,
|
.u-nav .file-row:hover,
|
||||||
.u-nav .file-row.selected {
|
.u-nav .file-row.selected,
|
||||||
|
.u-nav .file-row.dragging {
|
||||||
background: var(--surface-2);
|
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 {
|
.u-nav .name-cell {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -241,6 +255,30 @@
|
|||||||
padding: 2px 8px;
|
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 {
|
.u-nav .queue-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
+1
-1
@@ -9,7 +9,7 @@ import { Sandbox, rootsFromEnv } from './sandbox.js';
|
|||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const projectRoot = path.resolve(__dirname, '..');
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
const publicDir = path.join(projectRoot, 'public');
|
const publicDir = path.join(projectRoot, 'server', 'public');
|
||||||
|
|
||||||
const MIME = {
|
const MIME = {
|
||||||
'.html': 'text/html; charset=utf-8',
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "u-navigator">
|
<!ENTITY name "u-navigator">
|
||||||
<!ENTITY author "michael">
|
<!ENTITY author "michael">
|
||||||
<!ENTITY version "0.2.2">
|
<!ENTITY version "0.2.3">
|
||||||
<!ENTITY package "&name;-&version;.tgz">
|
<!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">
|
<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;">
|
<FILE Name="/boot/config/plugins/&name;/&package;">
|
||||||
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
||||||
<MD5>0cb8fe513addbd7435a8c79107ae7973</MD5>
|
<MD5>8d05b440bd2ad3e20b8d900238642c3f</MD5>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Run="/bin/bash">
|
<FILE Run="/bin/bash">
|
||||||
|
|||||||
Reference in New Issue
Block a user