using dialogs in file browser now
This commit is contained in:
@@ -317,7 +317,7 @@ function renderFileManagerPage(title, body) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
:root { --ink:#0b0f19; --muted:#5b6470; --line:#d8dde4; --bg:#eef1f5; --card:#ffffff; --accent:#0f766e; --accent-strong:#0a5b55; }
|
||||
:root { --ink:#0b0f19; --muted:#5b6470; --line:#d8dde4; --bg:#eef1f5; --card:#ffffff; --accent:#0f766e; --accent-strong:#0a5b55; --accent-soft:#e6f4f2; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "IBM Plex Sans", "Noto Sans", sans-serif; background: var(--bg); color: var(--ink); }
|
||||
main { max-width: 1280px; margin: 0 auto; padding: 26px 18px 70px; }
|
||||
@@ -327,8 +327,14 @@ function renderFileManagerPage(title, body) {
|
||||
.muted { color: var(--muted); font-size: 0.95rem; }
|
||||
.card { margin-top: 18px; padding: 16px; background: var(--card); border-radius: 16px; border: 1px solid var(--line); box-shadow: 0 8px 26px rgba(12, 18, 28, 0.08); }
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||
.tag { display: inline-flex; align-items: center; gap: 8px; padding: 4px 12px; border-radius: 999px; background: #f1f5f9; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; }
|
||||
.tag { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px; border-radius: 999px; background: #f1f5f9; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; }
|
||||
.tag.primary { background: var(--accent-soft); border-color: #bfe6e0; color: var(--accent-strong); }
|
||||
.tag span { color: var(--muted); font-weight: 500; }
|
||||
.browser-shell { display: grid; gap: 16px; }
|
||||
.browser-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||
.crumbs { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||
.crumbs a { color: var(--accent-strong); text-decoration: none; font-weight: 600; }
|
||||
.crumbs span { color: var(--muted); }
|
||||
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
||||
form { display: grid; gap: 10px; }
|
||||
label { display: grid; gap: 6px; font-weight: 600; }
|
||||
@@ -339,12 +345,17 @@ function renderFileManagerPage(title, body) {
|
||||
button.secondary { background: transparent; color: var(--accent); }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
|
||||
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||
tbody tr:nth-child(even) { background: #f9fbfc; }
|
||||
tbody tr:hover { background: #f2f8f7; }
|
||||
.name { display: inline-flex; align-items: center; gap: 10px; }
|
||||
.name strong { font-weight: 700; }
|
||||
.folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; }
|
||||
.actions { display: grid; gap: 6px; }
|
||||
.actions input, .actions button { font-size: 0.9rem; padding: 7px 10px; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.actions button { font-size: 0.9rem; padding: 7px 10px; }
|
||||
dialog { border: none; border-radius: 16px; padding: 0; width: min(480px, 92vw); }
|
||||
dialog::backdrop { background: rgba(15, 23, 42, 0.4); }
|
||||
.dialog-card { padding: 18px; display: grid; gap: 12px; }
|
||||
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.dialog-actions .secondary { border-color: var(--line); color: var(--accent-strong); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -743,8 +754,8 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
|
||||
<div class="muted">Systemstatistiken und Logs</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<a class="tag" href="${baseUrl('/admin/files')}">Dateimanager</a>
|
||||
<a class="tag" href="${baseUrl('/admin/users')}">Benutzer verwalten</a>
|
||||
<a class="tag primary" href="${baseUrl('/admin/files')}">Dateimanager</a>
|
||||
<a class="tag primary" href="${baseUrl('/admin/users')}">Benutzer verwalten</a>
|
||||
<form method="post" action="${baseUrl('/admin/logout')}">
|
||||
${csrfField(res.locals.csrfToken)}
|
||||
<button type="submit" class="secondary">Abmelden</button>
|
||||
@@ -937,11 +948,31 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
|
||||
const parentPath = relativePath ? relativePath.split('/').slice(0, -1).join('/') : '';
|
||||
|
||||
const dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== '_share');
|
||||
const files = entries.filter((entry) => !entry.isDirectory() && entry.name !== '_share');
|
||||
|
||||
const rowForEntry = (entry, isDir) => {
|
||||
const filtered = entries.filter((entry) => entry.name !== '_share');
|
||||
const details = await Promise.all(
|
||||
filtered.map(async (entry) => {
|
||||
const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
let stat = null;
|
||||
try {
|
||||
stat = await fs.promises.stat(path.join(resolved, entry.name));
|
||||
} catch (err) {
|
||||
stat = null;
|
||||
}
|
||||
return {
|
||||
entry,
|
||||
childPath,
|
||||
isDir: entry.isDirectory(),
|
||||
size: stat && stat.isFile() ? stat.size : null,
|
||||
modifiedAt: stat ? stat.mtimeMs : null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const dirs = details.filter((item) => item.isDir);
|
||||
const files = details.filter((item) => !item.isDir);
|
||||
|
||||
const rowForEntry = (item) => {
|
||||
const { entry, childPath, isDir, size, modifiedAt } = item;
|
||||
const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`);
|
||||
const escapedName = escapeHtml(entry.name);
|
||||
const escapedPath = escapeHtml(childPath);
|
||||
@@ -954,26 +985,19 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
</span>
|
||||
</td>
|
||||
<td>${isDir ? 'Ordner' : 'Datei'}</td>
|
||||
<td>${size ? formatBytes(size) : '—'}</td>
|
||||
<td>${modifiedAt ? formatTimestamp(modifiedAt) : '—'}</td>
|
||||
<td class="actions">
|
||||
<form method="post" action="${baseUrl('/admin/files/rename')}">
|
||||
${csrfField(res.locals.csrfToken)}
|
||||
<input type="hidden" name="path" value="${escapedPath}" />
|
||||
<input name="newName" placeholder="Neuer Name" required />
|
||||
<button type="submit">Umbenennen</button>
|
||||
</form>
|
||||
<form method="post" action="${baseUrl('/admin/files/delete')}">
|
||||
${csrfField(res.locals.csrfToken)}
|
||||
<input type="hidden" name="path" value="${escapedPath}" />
|
||||
<button type="submit" class="secondary">Löschen</button>
|
||||
</form>
|
||||
<button type="button" class="secondary rename-trigger" data-path="${escapedPath}" data-name="${escapedName}">Umbenennen</button>
|
||||
<button type="button" class="secondary delete-trigger" data-path="${escapedPath}" data-name="${escapedName}">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
};
|
||||
|
||||
const tableRows = [
|
||||
...dirs.map((entry) => rowForEntry(entry, true)),
|
||||
...files.map((entry) => rowForEntry(entry, false)),
|
||||
...dirs.map((entry) => rowForEntry(entry)),
|
||||
...files.map((entry) => rowForEntry(entry)),
|
||||
].join('');
|
||||
|
||||
const body = `
|
||||
@@ -991,14 +1015,19 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<div class="toolbar">
|
||||
<section class="card browser-shell">
|
||||
<div class="browser-bar">
|
||||
<span class="tag">Pfad <span>/${escapeHtml(relativePath || '')}</span></span>
|
||||
${relativePath ? `<a class="tag" href="${baseUrl(`/admin/files?path=${encodeURIComponent(parentPath)}`)}">← Zurück</a>` : ''}
|
||||
${relativePath ? `<a class="tag primary" href="${baseUrl(`/admin/files?path=${encodeURIComponent(parentPath)}`)}">← Zurück</a>` : ''}
|
||||
<div class="crumbs">
|
||||
<span>Position:</span>
|
||||
${relativePath ? relativePath.split('/').map((segment, idx, parts) => {
|
||||
const crumbPath = parts.slice(0, idx + 1).join('/');
|
||||
return `<a href="${baseUrl(`/admin/files?path=${encodeURIComponent(crumbPath)}`)}">${escapeHtml(segment)}</a>`;
|
||||
}).join('<span>/</span>') : '<span>Root</span>'}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card grid">
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h2>Ordner erstellen</h2>
|
||||
<form method="post" action="${baseUrl('/admin/files/mkdir')}">
|
||||
@@ -1023,6 +1052,7 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
<button type="submit">Hochladen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -1033,6 +1063,8 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Typ</th>
|
||||
<th>Größe</th>
|
||||
<th>Geändert</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1042,6 +1074,70 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
</table>
|
||||
` : '<div class="muted">Keine Eintraege in diesem Ordner.</div>'}
|
||||
</section>
|
||||
|
||||
<dialog id="rename-dialog">
|
||||
<form method="post" action="${baseUrl('/admin/files/rename')}" class="dialog-card">
|
||||
${csrfField(res.locals.csrfToken)}
|
||||
<input type="hidden" name="path" id="rename-path" />
|
||||
<h2>Umbenennen</h2>
|
||||
<div class="muted" id="rename-current"></div>
|
||||
<label>
|
||||
Neuer Name
|
||||
<input name="newName" id="rename-name" required />
|
||||
</label>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="secondary dialog-close">Abbrechen</button>
|
||||
<button type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="delete-dialog">
|
||||
<form method="post" action="${baseUrl('/admin/files/delete')}" class="dialog-card">
|
||||
${csrfField(res.locals.csrfToken)}
|
||||
<input type="hidden" name="path" id="delete-path" />
|
||||
<h2>Löschen</h2>
|
||||
<div class="muted" id="delete-current"></div>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="secondary dialog-close">Abbrechen</button>
|
||||
<button type="submit">Löschen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
const renameDialog = document.getElementById('rename-dialog');
|
||||
const deleteDialog = document.getElementById('delete-dialog');
|
||||
const renamePath = document.getElementById('rename-path');
|
||||
const renameName = document.getElementById('rename-name');
|
||||
const renameCurrent = document.getElementById('rename-current');
|
||||
const deletePath = document.getElementById('delete-path');
|
||||
const deleteCurrent = document.getElementById('delete-current');
|
||||
|
||||
document.querySelectorAll('.rename-trigger').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
renamePath.value = btn.dataset.path || '';
|
||||
renameName.value = btn.dataset.name || '';
|
||||
renameCurrent.textContent = `Aktuell: ${btn.dataset.name || ''}`;
|
||||
renameDialog.showModal();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-trigger').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
deletePath.value = btn.dataset.path || '';
|
||||
deleteCurrent.textContent = `Datei/Ordner: ${btn.dataset.name || ''}`;
|
||||
deleteDialog.showModal();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.dialog-close').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
renameDialog.close();
|
||||
deleteDialog.close();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
`;
|
||||
|
||||
res.send(renderFileManagerPage('Admin-Dateien', body));
|
||||
|
||||
Reference in New Issue
Block a user