- Checkbox-Spalte in der Dateitabelle (inkl. "alle auswaehlen" im Tabellen- kopf) -- sobald mindestens ein Eintrag markiert ist, erscheint eine Aktionsleiste mit "Herunterladen (ZIP)" und (mit fileshare.edit) "Loeschen". - Herunterladen buendelt die Auswahl serverseitig zu EINEM ZIP statt mehrerer einzelner Downloads (vermeidet Browser-Blockaden bei vielen gleichzeitigen Downloads) -- neue Route /fileshare/download-multi, Ordner werden dabei rekursiv mit aufgenommen (relativer Pfad als Archivpfad). - Loeschen mehrerer Elemente auf einmal ueber eine neue Route /fileshare/delete-multi (ein Bestaetigungsdialog fuer die ganze Auswahl), mit Sammel-Erfolgsmeldung analog zum Mehrfach-Upload. - Die Pfadaufloesung+Validierung fuer ein einzelnes Kind-Element wurde aus dem bestehenden Einzel-Loeschen in _fileshare_resolve_child() extrahiert und von Einzel-Loeschen, Mehrfach-Loeschen UND Mehrfach-Download gemeinsam genutzt (kein duplizierter Sicherheitscode). Live auf POETEST verifiziert: Mehrfachauswahl inkl. "alle auswaehlen", ZIP-Download zweier Dateien (Inhalt geprueft), ZIP-Download eines Ordners (rekursiv, korrekter Archivpfad), Mehrfach-Loeschen mit Sammelmeldung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
618 lines
28 KiB
HTML
618 lines
28 KiB
HTML
{% extends "base.html" %}
|
||
{% set active_page = "fileshare" %}
|
||
{% block page_title %}Dateifreigaben{% endblock %}
|
||
{% block page_sub %}<div class="topbar-sub">{{ selected_share }}{% if rel_path %} / {{ rel_path }}{% endif %}</div>{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
{% macro render_tree_node(share, path, name, expanded_map, active_path) %}
|
||
<li class="tree-node" data-share="{{ share }}" data-path="{{ path }}">
|
||
<div class="tree-row{{ ' active' if path == active_path else '' }}">
|
||
{% if path in expanded_map %}
|
||
<button type="button" class="tree-toggle" aria-expanded="true">▼</button>
|
||
{% else %}
|
||
<button type="button" class="tree-toggle" aria-expanded="false">▶</button>
|
||
{% endif %}
|
||
<span class="tree-label">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px; margin-right:5px; vertical-align:-2px; color:var(--accent);"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||
{{ name }}
|
||
</span>
|
||
</div>
|
||
<ul class="tree-children{{ '' if path in expanded_map else ' hidden' }}"{% if path in expanded_map %} data-loaded="1"{% endif %}>
|
||
{% if path in expanded_map %}
|
||
{% for child in expanded_map[path] %}
|
||
{{ render_tree_node(share, (path ~ '/' ~ child) if path else child, child, expanded_map, active_path) }}
|
||
{% endfor %}
|
||
{% endif %}
|
||
</ul>
|
||
</li>
|
||
{% endmacro %}
|
||
|
||
<div class="section-head">
|
||
<div>
|
||
<h2 style="font-size:16px;">Dateifreigaben</h2>
|
||
<div class="hint">Freigaben je nach AD-Gruppenmitgliedschaft für diese Sitzung gemountet — wird beim Abmelden wieder ausgehängt.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fileshare-layout">
|
||
<div class="fileshare-tree card">
|
||
<div class="fileshare-tree-title">Freigaben</div>
|
||
<ul class="tree-root" id="fileshareTree">
|
||
{% for s in shares %}
|
||
{{ render_tree_node(s, '', s, tree_expanded if s == selected_share else {}, rel_path if s == selected_share else None) }}
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="fileshare-main">
|
||
<div class="table-wrap">
|
||
<div class="table-toolbar">
|
||
<div class="search-input">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||
<input type="text" id="fileSearch" placeholder="Datei/Ordner suchen..." oninput="filterTable('fileSearch', 'fileTable')">
|
||
</div>
|
||
<div class="flex gap-2">
|
||
{% if can_create %}
|
||
<button type="button" class="btn btn-secondary btn-sm" data-open-modal="mkdirModal">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/><path d="M12 11v4M10 13h4"/></svg>
|
||
Neuer Ordner
|
||
</button>
|
||
<button type="button" class="btn btn-primary btn-sm" data-open-modal="uploadModal">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/></svg>
|
||
Hochladen
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div id="bulkActionsBar" class="flex gap-2 hidden" data-share="{{ selected_share }}" data-path="{{ rel_path }}" style="align-items:center; margin-bottom:10px; flex-wrap:wrap;">
|
||
<span id="bulkSelectedCount" class="text-faint" style="font-size:12.5px; font-weight:600;"></span>
|
||
<button type="button" class="btn btn-secondary btn-sm" onclick="bulkDownload()">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
||
Herunterladen (ZIP)
|
||
</button>
|
||
{% if can_edit %}
|
||
<button type="button" class="btn btn-sm" style="color:var(--danger); background:transparent; border-color:var(--danger-dim);" onclick="bulkDelete()">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
|
||
Löschen
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if entries %}
|
||
<div style="overflow-x:auto;">
|
||
<table class="data-table" id="fileTable" data-sortable>
|
||
<thead><tr>
|
||
<th style="width:1%;"><input type="checkbox" id="selectAllFiles"></th>
|
||
<th data-sort-key="name">Name</th>
|
||
<th data-sort-key="size">Größe</th>
|
||
<th data-sort-key="mtime">Geändert</th>
|
||
<th style="width:1%;">Aktionen</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for e in entries %}
|
||
<tr data-sort-name="{{ e.name|lower }}" data-sort-size="{{ 0 if e.is_dir else e.size_str }}" data-sort-mtime="{{ e.mtime_str }}">
|
||
<td><input type="checkbox" class="row-select" value="{{ e.name }}"></td>
|
||
<td class="cell-name">
|
||
{% if e.is_dir %}
|
||
<a href="{{ url_for('fileshare', share=selected_share, path=(rel_path ~ '/' ~ e.name) if rel_path else e.name) }}">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:15px; height:15px; margin-right:6px; vertical-align:-2px; color:var(--accent);"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||
{{ e.name }}
|
||
</a>
|
||
{% else %}
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:15px; height:15px; margin-right:6px; vertical-align:-2px; color:var(--text-faint);"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
|
||
{{ e.name }}
|
||
{% endif %}
|
||
</td>
|
||
<td class="text-faint">{{ e.size_str }}</td>
|
||
<td class="text-faint">{{ e.mtime_str }}</td>
|
||
<td>
|
||
<div class="row-actions">
|
||
{% if e.preview_kind %}
|
||
<button type="button" class="icon-btn" title="Vorschau"
|
||
data-preview-kind="{{ e.preview_kind }}"
|
||
data-preview-name="{{ e.name }}"
|
||
data-preview-url="{{ url_for('fileshare_view', share=selected_share, path=(rel_path ~ '/' ~ e.name) if rel_path else e.name) }}">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||
</button>
|
||
{% endif %}
|
||
{% if not e.is_dir %}
|
||
<a class="icon-btn" title="Herunterladen" href="{{ url_for('fileshare_download', share=selected_share, path=(rel_path ~ '/' ~ e.name) if rel_path else e.name) }}">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
||
</a>
|
||
{% endif %}
|
||
{% if can_edit %}
|
||
<button type="button" class="icon-btn" title="Umbenennen" onclick="openRenameModal('{{ e.name|e }}')">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||
</button>
|
||
<form method="post" action="{{ url_for('fileshare_delete') }}" data-confirm="„{{ e.name }}“ wirklich endgültig löschen?">
|
||
<input type="hidden" name="share" value="{{ selected_share }}">
|
||
<input type="hidden" name="path" value="{{ rel_path }}">
|
||
<input type="hidden" name="name" value="{{ e.name }}">
|
||
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div style="padding:40px 16px; text-align:center; color:var(--text-faint);">Dieser Ordner ist leer.</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if can_create %}
|
||
<div class="modal-overlay" id="uploadModal">
|
||
<div class="modal">
|
||
<form method="post" action="{{ url_for('fileshare_upload') }}" enctype="multipart/form-data">
|
||
<input type="hidden" name="share" value="{{ selected_share }}">
|
||
<input type="hidden" name="path" value="{{ rel_path }}">
|
||
<div class="modal-header">
|
||
<h3>Datei(en) hochladen</h3>
|
||
<button type="button" class="modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="field">
|
||
<label>Datei(en)</label>
|
||
<input type="file" name="file" id="uploadFileInput" multiple required>
|
||
<div id="uploadFileList" class="upload-file-list"></div>
|
||
<div class="field-hint">Insgesamt maximal 15 MB pro Upload-Vorgang. Mehrfachauswahl möglich (auch mehrmals nacheinander — bereits hinzugefügte Dateien bleiben dabei erhalten).</div>
|
||
</div>
|
||
<div class="field-hint">Wird in „{{ selected_share }}{% if rel_path %} / {{ rel_path }}{% endif %}“ hochgeladen. Eine bereits vorhandene Datei gleichen Namens wird überschrieben.</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Hochladen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="mkdirModal">
|
||
<div class="modal">
|
||
<form method="post" action="{{ url_for('fileshare_mkdir') }}">
|
||
<input type="hidden" name="share" value="{{ selected_share }}">
|
||
<input type="hidden" name="path" value="{{ rel_path }}">
|
||
<div class="modal-header">
|
||
<h3>Neuer Ordner</h3>
|
||
<button type="button" class="modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="field">
|
||
<label>Ordnername</label>
|
||
<input type="text" name="name" required autofocus>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Anlegen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if can_edit %}
|
||
<div class="modal-overlay" id="renameModal">
|
||
<div class="modal">
|
||
<form method="post" action="{{ url_for('fileshare_rename') }}">
|
||
<input type="hidden" name="share" value="{{ selected_share }}">
|
||
<input type="hidden" name="path" value="{{ rel_path }}">
|
||
<input type="hidden" name="old_name" id="renameOldName">
|
||
<div class="modal-header">
|
||
<h3>Umbenennen</h3>
|
||
<button type="button" class="modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="field">
|
||
<label>Neuer Name</label>
|
||
<input type="text" name="new_name" id="renameNewName" required>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Umbenennen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="modal-overlay" id="previewModal">
|
||
<div class="modal" style="max-width:900px; width:90vw;">
|
||
<div class="modal-header">
|
||
<h3 id="previewTitle">Vorschau</h3>
|
||
<button type="button" class="modal-close" data-close-modal>×</button>
|
||
</div>
|
||
<div class="modal-body" id="previewBody" style="max-height:75vh; overflow:auto;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script src="{{ url_for('static', filename='js/vendor/mammoth.browser.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/vendor/xlsx.full.min.js') }}"></script>
|
||
<script>
|
||
const FILESHARE_SUBFOLDERS_URL = "{{ url_for('fileshare_subfolders') }}";
|
||
const FILESHARE_BASE_URL = "{{ url_for('fileshare') }}";
|
||
const FILESHARE_DOWNLOAD_MULTI_URL = "{{ url_for('fileshare_download_multi') }}";
|
||
const FILESHARE_DELETE_MULTI_URL = "{{ url_for('fileshare_delete_multi') }}";
|
||
|
||
function openRenameModal(name) {
|
||
document.getElementById("renameOldName").value = name;
|
||
document.getElementById("renameNewName").value = name;
|
||
PoeUI.openModal("renameModal");
|
||
}
|
||
|
||
function filterTable(inputId, tableId) {
|
||
const q = document.getElementById(inputId).value.trim().toLowerCase();
|
||
document.querySelectorAll(`#${tableId} tbody tr`).forEach(row => {
|
||
if (row.classList.contains("empty-row")) return;
|
||
row.style.display = row.innerText.toLowerCase().includes(q) ? "" : "none";
|
||
});
|
||
}
|
||
|
||
/* ---------------- Mehrfachauswahl (Herunterladen als ZIP / Löschen) ---------------- */
|
||
|
||
function getSelectedFileNames() {
|
||
return Array.from(document.querySelectorAll("#fileTable .row-select:checked")).map(function (cb) { return cb.value; });
|
||
}
|
||
|
||
function updateBulkBar() {
|
||
const names = getSelectedFileNames();
|
||
const bar = document.getElementById("bulkActionsBar");
|
||
const countEl = document.getElementById("bulkSelectedCount");
|
||
if (!bar) return;
|
||
if (names.length > 0) {
|
||
bar.classList.remove("hidden");
|
||
countEl.textContent = names.length + " ausgewählt";
|
||
} else {
|
||
bar.classList.add("hidden");
|
||
}
|
||
const selectAll = document.getElementById("selectAllFiles");
|
||
const allBoxes = document.querySelectorAll("#fileTable .row-select");
|
||
if (selectAll && allBoxes.length) {
|
||
selectAll.checked = names.length === allBoxes.length;
|
||
selectAll.indeterminate = names.length > 0 && names.length < allBoxes.length;
|
||
}
|
||
}
|
||
|
||
function submitBulkForm(actionUrl, names) {
|
||
// data-share/data-path statt Jinja-Werte direkt in einen JS-String-
|
||
// Literal zu setzen -- Ordnernamen kommen von der echten Freigabe und
|
||
// koennten Anfuehrungszeichen o.ae. enthalten, ueber HTML-Attribute
|
||
// (von Jinja automatisch escaped) ist das unproblematisch.
|
||
const bar = document.getElementById("bulkActionsBar");
|
||
const form = document.createElement("form");
|
||
form.method = "post";
|
||
form.action = actionUrl;
|
||
form.style.display = "none";
|
||
[["share", bar.dataset.share], ["path", bar.dataset.path]].forEach(function (pair) {
|
||
const input = document.createElement("input");
|
||
input.type = "hidden";
|
||
input.name = pair[0];
|
||
input.value = pair[1];
|
||
form.appendChild(input);
|
||
});
|
||
names.forEach(function (name) {
|
||
const input = document.createElement("input");
|
||
input.type = "hidden";
|
||
input.name = "names";
|
||
input.value = name;
|
||
form.appendChild(input);
|
||
});
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
|
||
function bulkDownload() {
|
||
const names = getSelectedFileNames();
|
||
if (!names.length) return;
|
||
submitBulkForm(FILESHARE_DOWNLOAD_MULTI_URL, names);
|
||
}
|
||
|
||
function bulkDelete() {
|
||
const names = getSelectedFileNames();
|
||
if (!names.length) return;
|
||
window.confirmAction(
|
||
names.length + " ausgewählte Elemente wirklich endgültig löschen?",
|
||
function () { submitBulkForm(FILESHARE_DELETE_MULTI_URL, names); },
|
||
"Auswahl löschen?"
|
||
);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
const selectAll = document.getElementById("selectAllFiles");
|
||
if (selectAll) {
|
||
selectAll.addEventListener("change", function () {
|
||
document.querySelectorAll("#fileTable .row-select").forEach(function (cb) { cb.checked = selectAll.checked; });
|
||
updateBulkBar();
|
||
});
|
||
}
|
||
document.querySelectorAll("#fileTable .row-select").forEach(function (cb) {
|
||
cb.addEventListener("change", updateBulkBar);
|
||
});
|
||
});
|
||
|
||
/* ---------------- Mehrfach-Upload (Multiauswahl + mehrmals nacheinander) ---------------- */
|
||
/* Ein <input type=file multiple> ERSETZT bei jeder erneuten Dateiauswahl
|
||
die vorherige -- fuer "mehrmals nacheinander hinzufuegen" wird deshalb
|
||
selbst eine "angesammelte" Auswahl per DataTransfer gepflegt und nach
|
||
jeder Aenderung zurueck auf das Input-Feld geschrieben, sodass das
|
||
normale <form>-Submit (kein fetch() noetig) am Ende alle gesammelten
|
||
Dateien mitschickt. DataTransfer-Zuweisung an .files wird von allen
|
||
gaengigen Mobil-Browsern (Android Chrome, iOS Safari) mitgetragen; falls
|
||
nicht, faellt es einfach auf das native Verhalten (letzte Auswahl zaehlt)
|
||
zurueck, ohne den Upload an sich zu verhindern. */
|
||
(function () {
|
||
const input = document.getElementById("uploadFileInput");
|
||
const listEl = document.getElementById("uploadFileList");
|
||
if (!input || !listEl) return;
|
||
let staged = null;
|
||
try { staged = new DataTransfer(); } catch (e) { staged = null; }
|
||
|
||
function render() {
|
||
listEl.innerHTML = "";
|
||
if (!staged) return;
|
||
Array.from(staged.files).forEach(function (file, idx) {
|
||
const row = document.createElement("div");
|
||
row.className = "upload-file-row";
|
||
const name = document.createElement("span");
|
||
name.textContent = file.name;
|
||
const removeBtn = document.createElement("button");
|
||
removeBtn.type = "button";
|
||
removeBtn.className = "upload-file-remove";
|
||
removeBtn.setAttribute("aria-label", "Entfernen");
|
||
removeBtn.textContent = "×";
|
||
removeBtn.addEventListener("click", function () {
|
||
const dt = new DataTransfer();
|
||
Array.from(staged.files).forEach(function (f, i) {
|
||
if (i !== idx) dt.items.add(f);
|
||
});
|
||
staged = dt;
|
||
input.files = staged.files;
|
||
render();
|
||
});
|
||
row.appendChild(name);
|
||
row.appendChild(removeBtn);
|
||
listEl.appendChild(row);
|
||
});
|
||
}
|
||
|
||
input.addEventListener("change", function () {
|
||
if (!staged) return; // kein DataTransfer-Support -- natives Verhalten greift
|
||
Array.from(input.files).forEach(function (file) { staged.items.add(file); });
|
||
input.files = staged.files;
|
||
render();
|
||
});
|
||
|
||
// Beim (Wieder-)Oeffnen des Modals eine frische Sammlung starten, statt
|
||
// Dateien aus einem vorherigen, bereits abgeschickten Upload-Vorgang
|
||
// versehentlich mitzuschleppen.
|
||
document.querySelectorAll('[data-open-modal="uploadModal"]').forEach(function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
try { staged = new DataTransfer(); } catch (e) { staged = null; }
|
||
input.value = "";
|
||
render();
|
||
});
|
||
});
|
||
})();
|
||
|
||
/* ---------------- Baum-Navigation (Freigaben links) ---------------- */
|
||
|
||
function buildTreeNode(share, path, name) {
|
||
const li = document.createElement("li");
|
||
li.className = "tree-node";
|
||
li.dataset.share = share;
|
||
li.dataset.path = path;
|
||
|
||
const row = document.createElement("div");
|
||
row.className = "tree-row";
|
||
|
||
const toggle = document.createElement("button");
|
||
toggle.type = "button";
|
||
toggle.className = "tree-toggle";
|
||
toggle.textContent = "▶";
|
||
toggle.setAttribute("aria-expanded", "false");
|
||
|
||
const label = document.createElement("span");
|
||
label.className = "tree-label";
|
||
label.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;margin-right:5px;vertical-align:-2px;color:var(--accent);"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>';
|
||
label.append(document.createTextNode(name));
|
||
|
||
row.appendChild(toggle);
|
||
row.appendChild(label);
|
||
|
||
const childUl = document.createElement("ul");
|
||
childUl.className = "tree-children hidden";
|
||
|
||
li.appendChild(row);
|
||
li.appendChild(childUl);
|
||
return li;
|
||
}
|
||
|
||
function toggleTreeNode(btn) {
|
||
const li = btn.closest(".tree-node");
|
||
const childUl = li.querySelector(":scope > .tree-children");
|
||
if (childUl.classList.contains("hidden")) {
|
||
if (childUl.dataset.loaded === "1") {
|
||
childUl.classList.remove("hidden");
|
||
btn.textContent = "▼";
|
||
btn.setAttribute("aria-expanded", "true");
|
||
} else {
|
||
loadTreeChildren(li, childUl, btn);
|
||
}
|
||
} else {
|
||
childUl.classList.add("hidden");
|
||
btn.textContent = "▶";
|
||
btn.setAttribute("aria-expanded", "false");
|
||
}
|
||
}
|
||
|
||
function loadTreeChildren(li, childUl, btn) {
|
||
const share = li.dataset.share;
|
||
const path = li.dataset.path;
|
||
const prevLabel = btn.textContent;
|
||
btn.textContent = "…";
|
||
fetch(FILESHARE_SUBFOLDERS_URL + "?share=" + encodeURIComponent(share) + "&path=" + encodeURIComponent(path))
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
childUl.innerHTML = "";
|
||
(data.folders || []).forEach(name => {
|
||
const childPath = path ? path + "/" + name : name;
|
||
childUl.appendChild(buildTreeNode(share, childPath, name));
|
||
});
|
||
childUl.dataset.loaded = "1";
|
||
childUl.classList.remove("hidden");
|
||
btn.textContent = "▼";
|
||
btn.setAttribute("aria-expanded", "true");
|
||
})
|
||
.catch(() => { btn.textContent = prevLabel; });
|
||
}
|
||
|
||
function navigateTree(share, path) {
|
||
window.location.href = FILESHARE_BASE_URL + "?share=" + encodeURIComponent(share) + "&path=" + encodeURIComponent(path);
|
||
}
|
||
|
||
document.getElementById("fileshareTree").addEventListener("click", function (e) {
|
||
const toggle = e.target.closest(".tree-toggle");
|
||
if (toggle) { toggleTreeNode(toggle); return; }
|
||
const label = e.target.closest(".tree-label");
|
||
if (label) {
|
||
const li = label.closest(".tree-node");
|
||
navigateTree(li.dataset.share, li.dataset.path);
|
||
}
|
||
});
|
||
|
||
/* ---------------- Datei-Vorschau ---------------- */
|
||
|
||
const fileTableEl = document.getElementById("fileTable");
|
||
if (fileTableEl) {
|
||
fileTableEl.addEventListener("click", function (e) {
|
||
const btn = e.target.closest("[data-preview-kind]");
|
||
if (!btn) return;
|
||
openPreview(btn.dataset.previewKind, btn.dataset.previewName, btn.dataset.previewUrl);
|
||
});
|
||
}
|
||
|
||
function openPreview(kind, name, url) {
|
||
document.getElementById("previewTitle").textContent = name;
|
||
const body = document.getElementById("previewBody");
|
||
body.innerHTML = "";
|
||
PoeUI.openModal("previewModal");
|
||
|
||
if (kind === "pdf") {
|
||
const iframe = document.createElement("iframe");
|
||
iframe.src = url;
|
||
iframe.style.width = "100%";
|
||
iframe.style.height = "70vh";
|
||
iframe.style.border = "0";
|
||
body.appendChild(iframe);
|
||
} else if (kind === "image") {
|
||
const img = document.createElement("img");
|
||
img.src = url;
|
||
img.alt = name;
|
||
img.style.maxWidth = "100%";
|
||
img.style.display = "block";
|
||
img.style.margin = "0 auto";
|
||
body.appendChild(img);
|
||
} else if (kind === "text") {
|
||
body.textContent = "Lade …";
|
||
// Nur die ersten 512KB anfordern -- bei sehr großen Textdateien
|
||
// (Logs etc.) reicht das für eine Vorschau, ohne alles auf einmal
|
||
// laden zu müssen. Server unterstützt Range ueber send_file(conditional=True).
|
||
fetch(url, { headers: { "Range": "bytes=0-524287" } })
|
||
.then(r => {
|
||
// r.status ist bei einem Range-Request praktisch immer 206,
|
||
// auch wenn die Datei kleiner als die angefragten 512KB ist
|
||
// (der Server liefert dann trotzdem "206" mit der kompletten
|
||
// Datei) -- ob wirklich abgeschnitten wurde, steht nur im
|
||
// Content-Range-Header ("bytes 0-524287/<Gesamtgroesse>").
|
||
const contentRange = r.headers.get("Content-Range") || "";
|
||
const m = contentRange.match(/\/(\d+)$/);
|
||
const truncated = !!m && parseInt(m[1], 10) > 524288;
|
||
return r.text().then(text => ({ text: text, truncated: truncated }));
|
||
})
|
||
.then(({ text, truncated }) => {
|
||
body.innerHTML = "";
|
||
const pre = document.createElement("pre");
|
||
pre.style.whiteSpace = "pre-wrap";
|
||
pre.style.wordBreak = "break-word";
|
||
pre.style.fontSize = "12.5px";
|
||
pre.textContent = text;
|
||
body.appendChild(pre);
|
||
if (truncated) {
|
||
const hint = document.createElement("div");
|
||
hint.className = "text-faint";
|
||
hint.style.marginTop = "10px";
|
||
hint.style.fontSize = "12px";
|
||
hint.textContent = "Nur die ersten 512 KB angezeigt — bitte herunterladen für die komplette Datei.";
|
||
body.appendChild(hint);
|
||
}
|
||
})
|
||
.catch(() => { body.textContent = "Vorschau konnte nicht geladen werden."; });
|
||
} else if (kind === "docx") {
|
||
body.textContent = "Lade …";
|
||
fetch(url).then(r => r.arrayBuffer())
|
||
.then(buf => mammoth.convertToHtml({ arrayBuffer: buf }))
|
||
.then(result => {
|
||
body.innerHTML = "";
|
||
const wrap = document.createElement("div");
|
||
wrap.className = "docx-preview";
|
||
// mammoth erzeugt kontrolliertes HTML aus Words eigenem,
|
||
// begrenztem Formatierungsmodell (Absätze/Tabellen/Listen/
|
||
// Bilder) -- kein beliebiges, aus der Datei uebernommenes
|
||
// Skript kann darin stecken, anders als bei generischem
|
||
// "fremdes HTML direkt einbetten".
|
||
wrap.innerHTML = result.value;
|
||
body.appendChild(wrap);
|
||
})
|
||
.catch(() => { body.textContent = "Vorschau konnte nicht geladen werden (Format evtl. nicht unterstützt)."; });
|
||
} else if (kind === "xlsx") {
|
||
body.textContent = "Lade …";
|
||
fetch(url).then(r => r.arrayBuffer())
|
||
.then(buf => {
|
||
const wb = XLSX.read(buf, { type: "array" });
|
||
body.innerHTML = "";
|
||
wb.SheetNames.forEach(function (sheetName, idx) {
|
||
const rows = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header: 1, defval: "" });
|
||
const h4 = document.createElement("div");
|
||
h4.className = "xlsx-preview-sheet-title";
|
||
h4.textContent = sheetName;
|
||
body.appendChild(h4);
|
||
const wrap = document.createElement("div");
|
||
wrap.style.overflowX = "auto";
|
||
const table = document.createElement("table");
|
||
table.className = "data-table";
|
||
// Zellenwerte bewusst per textContent statt ueber die
|
||
// eingebaute HTML-Ausgabe von SheetJS gesetzt -- so ist
|
||
// die Vorschau unabhaengig von deren Escaping-Verhalten
|
||
// garantiert sicher gegen Inhalte in den Zellen.
|
||
rows.forEach(function (row) {
|
||
const tr = document.createElement("tr");
|
||
row.forEach(function (cell) {
|
||
const td = document.createElement("td");
|
||
td.textContent = (cell === null || cell === undefined) ? "" : String(cell);
|
||
tr.appendChild(td);
|
||
});
|
||
table.appendChild(tr);
|
||
});
|
||
wrap.appendChild(table);
|
||
body.appendChild(wrap);
|
||
});
|
||
})
|
||
.catch(() => { body.textContent = "Vorschau konnte nicht geladen werden."; });
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|