saltcorn-samba 0.4.14 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -0
- package/README.md +75 -5
- package/filemanager-view.js +93 -17
- package/i18n/de.json +93 -0
- package/i18n/en.json +93 -0
- package/i18n.js +155 -0
- package/index.js +45 -15
- package/package.json +4 -2
- package/pdf-view.js +8 -5
- package/public/samba-common.js +175 -0
- package/public/samba-filemanager.js +373 -381
- package/public/samba-tree.js +126 -137
- package/tree-view.js +88 -20
|
@@ -1,165 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client-
|
|
2
|
+
* saltcorn-samba – Client-Controller für die `SambaFileManager`-View.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* | /
|
|
7
|
-
*
|
|
4
|
+
* Ergänzt zur reinen Anzeige aus samba-tree.js noch Upload, Rename, Delete
|
|
5
|
+
* und Mkdir. Jede Schreib-Aktion geht über eine der Routen
|
|
6
|
+
* /sambaupload | /sambadelete | /sambarename | /sambamkdir
|
|
7
|
+
* mit einem CSRF-Token, das in der View-Shell in den JavaScript-Kontext
|
|
8
|
+
* eingebettet wird und pro Request als X-CSRF-Token-Header mitgesendet wird.
|
|
9
|
+
*
|
|
10
|
+
* Gemeinsame Utilities (iconFor, joinPath, fmtSize, fmtDate, i18n) leben
|
|
11
|
+
* in public/samba-common.js und werden via window.SambaCommon konsumiert.
|
|
12
|
+
* Dadurch entfällt die frühere Duplizierung von iconFor() in beiden Client-JS.
|
|
8
13
|
*/
|
|
9
14
|
(function () {
|
|
10
15
|
"use strict";
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
var C = window.SambaCommon || {};
|
|
18
|
+
|
|
19
|
+
// ---- DOM-Helper --------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Minimaler DOM-Konstruktor. Attribute `class`, `text` und `on…`-Handler
|
|
22
|
+
* werden speziell behandelt; alles andere landet als HTML-Attribut. Kinder
|
|
23
|
+
* dürfen Strings (Textknoten) oder Elemente sein; `null` wird verworfen.
|
|
24
|
+
*/
|
|
25
|
+
function element(tag, attrs, children) {
|
|
14
26
|
var el = document.createElement(tag);
|
|
15
|
-
if (attrs)
|
|
16
|
-
Object.keys(attrs).forEach(function (
|
|
17
|
-
if (
|
|
18
|
-
else if (
|
|
19
|
-
else if (
|
|
20
|
-
else if (
|
|
21
|
-
el.addEventListener(
|
|
22
|
-
else el.setAttribute(
|
|
27
|
+
if (attrs) {
|
|
28
|
+
Object.keys(attrs).forEach(function (key) {
|
|
29
|
+
if (key === "class") el.className = attrs[key];
|
|
30
|
+
else if (key === "text") el.textContent = attrs[key];
|
|
31
|
+
else if (key === "html") el.innerHTML = attrs[key];
|
|
32
|
+
else if (key.slice(0, 2) === "on")
|
|
33
|
+
el.addEventListener(key.slice(2).toLowerCase(), attrs[key]);
|
|
34
|
+
else el.setAttribute(key, attrs[key]);
|
|
23
35
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
}
|
|
37
|
+
(children || []).forEach(function (child) {
|
|
38
|
+
if (child == null) return;
|
|
39
|
+
el.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
|
|
27
40
|
});
|
|
28
41
|
return el;
|
|
29
42
|
}
|
|
30
43
|
|
|
31
|
-
// ----
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
var e = extOf(item.name);
|
|
40
|
-
if (e === "pdf") return "📄";
|
|
41
|
-
if (["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"].indexOf(e) >= 0) return "🖼️";
|
|
42
|
-
if (["doc", "docx", "odt", "rtf", "txt", "md"].indexOf(e) >= 0) return "📝";
|
|
43
|
-
if (["xls", "xlsx", "ods", "csv"].indexOf(e) >= 0) return "📊";
|
|
44
|
-
if (["ppt", "pptx", "odp"].indexOf(e) >= 0) return "📽️";
|
|
45
|
-
if (["zip", "tar", "gz", "7z", "rar"].indexOf(e) >= 0) return "🗜️";
|
|
46
|
-
if (["mp3", "wav", "ogg", "flac", "m4a"].indexOf(e) >= 0) return "🎵";
|
|
47
|
-
if (["mp4", "mkv", "mov", "avi", "webm"].indexOf(e) >= 0) return "🎬";
|
|
48
|
-
return "📎";
|
|
49
|
-
}
|
|
50
|
-
function mediaTypeFor(item) {
|
|
51
|
-
if (item.isDir) return "folder";
|
|
52
|
-
var e = extOf(item.name);
|
|
53
|
-
var map = {
|
|
54
|
-
pdf: "application/pdf", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
55
|
-
gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
56
|
-
txt: "text/plain", md: "text/markdown", csv: "text/csv",
|
|
57
|
-
json: "application/json", xml: "application/xml", html: "text/html", htm: "text/html",
|
|
58
|
-
zip: "application/zip", doc: "application/msword",
|
|
59
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
60
|
-
xls: "application/vnd.ms-excel",
|
|
61
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
62
|
-
ppt: "application/vnd.ms-powerpoint",
|
|
63
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
64
|
-
mp3: "audio/mpeg", mp4: "video/mp4", mkv: "video/x-matroska",
|
|
65
|
-
mov: "video/quicktime", avi: "video/x-msvideo",
|
|
66
|
-
};
|
|
67
|
-
return map[e] || (e ? "application/" + e : "application/octet-stream");
|
|
68
|
-
}
|
|
69
|
-
function fmtSize(n) {
|
|
70
|
-
if (!n || n < 0) return "";
|
|
71
|
-
if (n < 1024) return n + " B";
|
|
72
|
-
var kib = n / 1024;
|
|
73
|
-
if (kib < 1024) return kib.toFixed(kib < 10 ? 1 : 0) + " KiB";
|
|
74
|
-
var mib = kib / 1024;
|
|
75
|
-
if (mib < 1024) return mib.toFixed(mib < 10 ? 1 : 0) + " MiB";
|
|
76
|
-
return (mib / 1024).toFixed(2) + " GiB";
|
|
77
|
-
}
|
|
78
|
-
function fmtDate(v) {
|
|
79
|
-
if (!v) return "";
|
|
80
|
-
var d = new Date(v);
|
|
81
|
-
if (isNaN(d.getTime())) return "";
|
|
82
|
-
var pad = function (x) { return String(x).padStart(2, "0"); };
|
|
83
|
-
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) +
|
|
84
|
-
" " + pad(d.getHours()) + ":" + pad(d.getMinutes());
|
|
85
|
-
}
|
|
86
|
-
function joinPath(a, b) {
|
|
87
|
-
if (!a) return b;
|
|
88
|
-
if (!b) return a;
|
|
89
|
-
return (a.replace(/\/+$/, "") + "/" + b.replace(/^\/+/, "")).replace(/\/+/g, "/");
|
|
90
|
-
}
|
|
91
|
-
function parentOf(p) {
|
|
92
|
-
if (!p) return "";
|
|
93
|
-
var i = p.lastIndexOf("/");
|
|
94
|
-
return i < 0 ? "" : p.slice(0, i);
|
|
95
|
-
}
|
|
96
|
-
function isViewable(name) {
|
|
97
|
-
var n = (name || "").toLowerCase();
|
|
98
|
-
return n.endsWith(".pdf") ||
|
|
99
|
-
/\.(png|jpe?g|gif|webp|svg|bmp)$/.test(n) ||
|
|
100
|
-
/\.(txt|md|json|xml|csv|html?)$/.test(n);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---- http helpers -------------------------------------------------------
|
|
104
|
-
async function fetchDir(path, showHidden) {
|
|
105
|
-
var url = "/sambadir?path=" + encodeURIComponent(path || "") +
|
|
44
|
+
// ---- HTTP-Helper -------------------------------------------------------
|
|
45
|
+
/**
|
|
46
|
+
* GET /sambadir mit definierten Fehlermeldungen: JSON-`error`-Feld
|
|
47
|
+
* schlägt HTTP-Status als Meldung.
|
|
48
|
+
*/
|
|
49
|
+
async function fetchDirectory(path, showHidden) {
|
|
50
|
+
var url =
|
|
51
|
+
"/sambadir?path=" + encodeURIComponent(path || "") +
|
|
106
52
|
(showHidden ? "&show_hidden=1" : "");
|
|
107
|
-
var
|
|
108
|
-
if (!
|
|
53
|
+
var response = await fetch(url, { credentials: "same-origin" });
|
|
54
|
+
if (!response.ok) {
|
|
109
55
|
var msg;
|
|
110
|
-
try { msg = (await
|
|
111
|
-
throw new Error(msg || "HTTP " +
|
|
56
|
+
try { msg = (await response.json()).error; } catch (_) { msg = "HTTP " + response.status; }
|
|
57
|
+
throw new Error(msg || "HTTP " + response.status);
|
|
112
58
|
}
|
|
113
|
-
return
|
|
59
|
+
return response.json();
|
|
114
60
|
}
|
|
115
|
-
|
|
116
|
-
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* POST als JSON mit CSRF-Token-Header. Erwartet JSON-Antwort mit optionalem
|
|
64
|
+
* `error`-Feld; Status-Code wird bei Fehlern als `.status` durchgereicht.
|
|
65
|
+
*/
|
|
66
|
+
async function postJson(url, body, csrfToken) {
|
|
67
|
+
var response = await fetch(url, {
|
|
117
68
|
method: "POST",
|
|
118
69
|
credentials: "same-origin",
|
|
119
70
|
headers: {
|
|
120
71
|
"Content-Type": "application/json",
|
|
121
|
-
"X-CSRF-Token":
|
|
72
|
+
"X-CSRF-Token": csrfToken || "",
|
|
122
73
|
},
|
|
123
74
|
body: JSON.stringify(body || {}),
|
|
124
75
|
});
|
|
125
76
|
var data = null;
|
|
126
|
-
try { data = await
|
|
127
|
-
if (!
|
|
128
|
-
var msg = (data && data.error) || ("HTTP " +
|
|
77
|
+
try { data = await response.json(); } catch (_) { data = null; }
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
var msg = (data && data.error) || ("HTTP " + response.status);
|
|
129
80
|
var err = new Error(msg);
|
|
130
|
-
err.status =
|
|
81
|
+
err.status = response.status;
|
|
131
82
|
err.body = data;
|
|
132
83
|
throw err;
|
|
133
84
|
}
|
|
134
85
|
return data || { ok: true };
|
|
135
86
|
}
|
|
136
87
|
|
|
137
|
-
// ----
|
|
138
|
-
|
|
139
|
-
|
|
88
|
+
// ---- Toast + Modal -----------------------------------------------------
|
|
89
|
+
/** Zeigt eine kurze Feedback-Nachricht am Bildschirmrand. */
|
|
90
|
+
function toast(message, type) {
|
|
91
|
+
var box = element("div", {
|
|
140
92
|
class: "samba-toast samba-toast-" + (type || "info"),
|
|
141
|
-
text:
|
|
93
|
+
text: message,
|
|
142
94
|
}, []);
|
|
143
95
|
document.body.appendChild(box);
|
|
144
96
|
setTimeout(function () { box.classList.add("samba-toast-hide"); }, 3200);
|
|
145
97
|
setTimeout(function () { if (box.parentNode) box.parentNode.removeChild(box); }, 3800);
|
|
146
98
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Baut einen einfachen Modal-Dialog auf. Rückgabe: `{ close, root }`;
|
|
102
|
+
* ESC und Klick auf den Backdrop schließen automatisch.
|
|
103
|
+
*/
|
|
104
|
+
function openModal(title, bodyElement, footerElements) {
|
|
105
|
+
var backdrop = element("div", { class: "samba-modal-backdrop" }, []);
|
|
106
|
+
var modalRoot = element("div", { class: "samba-modal card" }, [
|
|
107
|
+
element("div", { class: "samba-modal-header card-header d-flex align-items-center" }, [
|
|
108
|
+
element("strong", { text: title }, []),
|
|
109
|
+
element("button", {
|
|
153
110
|
type: "button", class: "btn-close ms-auto",
|
|
154
|
-
"aria-label": "
|
|
111
|
+
"aria-label": C.t("ui.close"),
|
|
155
112
|
onclick: function () { close(); },
|
|
156
113
|
}, []),
|
|
157
114
|
]),
|
|
158
|
-
|
|
159
|
-
|
|
115
|
+
element("div", { class: "samba-modal-body card-body" }, [bodyElement]),
|
|
116
|
+
element("div", { class: "samba-modal-footer card-footer d-flex justify-content-end gap-2" }, footerElements || []),
|
|
160
117
|
]);
|
|
161
|
-
backdrop.appendChild(
|
|
118
|
+
backdrop.appendChild(modalRoot);
|
|
162
119
|
document.body.appendChild(backdrop);
|
|
120
|
+
|
|
163
121
|
function close() {
|
|
164
122
|
if (backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
|
|
165
123
|
document.removeEventListener("keydown", onKey);
|
|
@@ -169,20 +127,25 @@
|
|
|
169
127
|
backdrop.addEventListener("click", function (e) {
|
|
170
128
|
if (e.target === backdrop) close();
|
|
171
129
|
});
|
|
172
|
-
return { close: close, root:
|
|
130
|
+
return { close: close, root: modalRoot };
|
|
173
131
|
}
|
|
174
132
|
|
|
175
|
-
// ----
|
|
133
|
+
// ---- Komponente --------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* Baut den File-Manager-State auf und rendert Toolbar + Tabelle in die
|
|
136
|
+
* vom Server vorbereiteten `<div>`-Elemente. Wird von der View-Shell
|
|
137
|
+
* genau einmal pro Instanz mit `mount(id, opts)` aufgerufen.
|
|
138
|
+
*/
|
|
176
139
|
function mount(id, opts) {
|
|
177
140
|
opts = opts || {};
|
|
178
|
-
var
|
|
179
|
-
if (!
|
|
180
|
-
if (
|
|
181
|
-
|
|
141
|
+
var listRoot = document.getElementById(id + "-list");
|
|
142
|
+
if (!listRoot) return;
|
|
143
|
+
if (listRoot.dataset.mounted === "1") return;
|
|
144
|
+
listRoot.dataset.mounted = "1";
|
|
182
145
|
|
|
183
|
-
var
|
|
184
|
-
var
|
|
185
|
-
var
|
|
146
|
+
var toolbarRoot = document.getElementById(id + "-toolbar");
|
|
147
|
+
var counterEl = document.getElementById(id + "-count");
|
|
148
|
+
var viewerRoot = document.getElementById(id + "-viewer");
|
|
186
149
|
|
|
187
150
|
var state = {
|
|
188
151
|
path: opts.startPath || "",
|
|
@@ -201,115 +164,126 @@
|
|
|
201
164
|
return state.path.length > state.rootPath.length;
|
|
202
165
|
}
|
|
203
166
|
|
|
204
|
-
// ----
|
|
167
|
+
// ---- Toolbar --------------------------------------------------------
|
|
168
|
+
/** Rendert Up/Home/Refresh + Breadcrumb + Toggle "show hidden" + Write-Buttons. */
|
|
205
169
|
function renderToolbar() {
|
|
206
|
-
|
|
207
|
-
var
|
|
170
|
+
toolbarRoot.innerHTML = "";
|
|
171
|
+
var upButton = element("button", {
|
|
208
172
|
type: "button",
|
|
209
173
|
class: "btn btn-sm btn-outline-secondary me-1" + (canGoUp() ? "" : " disabled"),
|
|
210
|
-
onclick: function () { if (canGoUp()) navigate(parentOf(state.path)); },
|
|
211
|
-
title: "
|
|
212
|
-
}, ["
|
|
213
|
-
|
|
174
|
+
onclick: function () { if (canGoUp()) navigate(C.parentOf(state.path)); },
|
|
175
|
+
title: C.t("ui.up_title"),
|
|
176
|
+
}, [C.t("ui.up")]);
|
|
177
|
+
|
|
178
|
+
var homeButton = element("button", {
|
|
214
179
|
type: "button",
|
|
215
180
|
class: "btn btn-sm btn-outline-secondary me-1" + (state.path === state.rootPath ? " disabled" : ""),
|
|
216
181
|
onclick: function () { navigate(state.rootPath); },
|
|
217
|
-
title: "
|
|
182
|
+
title: C.t("ui.home_title"),
|
|
218
183
|
}, ["🏠"]);
|
|
219
|
-
|
|
184
|
+
|
|
185
|
+
var reloadButton = element("button", {
|
|
220
186
|
type: "button",
|
|
221
187
|
class: "btn btn-sm btn-outline-secondary me-2",
|
|
222
188
|
onclick: function () { load(state.path); },
|
|
223
|
-
title: "
|
|
189
|
+
title: C.t("ui.refresh_title"),
|
|
224
190
|
}, ["↻"]);
|
|
225
191
|
|
|
226
|
-
var breadcrumb =
|
|
227
|
-
(function () {
|
|
228
|
-
var
|
|
192
|
+
var breadcrumb = element("nav", { class: "d-inline-block", "aria-label": "breadcrumb" }, [
|
|
193
|
+
(function buildBreadcrumb() {
|
|
194
|
+
var list = element("ol", { class: "breadcrumb mb-0 d-inline-flex" }, []);
|
|
229
195
|
var rel = state.path.slice(state.rootPath.length).replace(/^\/+/, "");
|
|
230
|
-
var
|
|
231
|
-
|
|
232
|
-
class: "breadcrumb-item" + (
|
|
196
|
+
var segments = rel ? rel.split("/") : [];
|
|
197
|
+
list.appendChild(element("li", {
|
|
198
|
+
class: "breadcrumb-item" + (segments.length ? "" : " active"),
|
|
233
199
|
}, [
|
|
234
|
-
|
|
235
|
-
?
|
|
200
|
+
segments.length
|
|
201
|
+
? element("a", { href: "#", onclick: function (e) { e.preventDefault(); navigate(state.rootPath); } }, ["/"])
|
|
236
202
|
: "/",
|
|
237
203
|
]));
|
|
238
|
-
var
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
var isLast = idx ===
|
|
242
|
-
var target =
|
|
243
|
-
|
|
244
|
-
isLast ?
|
|
204
|
+
var cursor = state.rootPath;
|
|
205
|
+
segments.forEach(function (segment, idx) {
|
|
206
|
+
cursor = C.joinPath(cursor, segment);
|
|
207
|
+
var isLast = idx === segments.length - 1;
|
|
208
|
+
var target = cursor;
|
|
209
|
+
list.appendChild(element("li", { class: "breadcrumb-item" + (isLast ? " active" : "") }, [
|
|
210
|
+
isLast ? segment : element("a", {
|
|
245
211
|
href: "#",
|
|
246
212
|
onclick: function (e) { e.preventDefault(); navigate(target); },
|
|
247
|
-
}, [
|
|
213
|
+
}, [segment]),
|
|
248
214
|
]));
|
|
249
215
|
});
|
|
250
|
-
return
|
|
216
|
+
return list;
|
|
251
217
|
})(),
|
|
252
218
|
]);
|
|
253
219
|
|
|
254
|
-
var hiddenToggle =
|
|
255
|
-
|
|
220
|
+
var hiddenToggle = element("label", { class: "form-check form-check-inline ms-3 mb-0 align-middle" }, [
|
|
221
|
+
element("input", {
|
|
256
222
|
type: "checkbox", class: "form-check-input",
|
|
257
223
|
onchange: function (e) { state.showHidden = e.target.checked; load(state.path); },
|
|
258
224
|
}, []),
|
|
259
|
-
|
|
225
|
+
element("span", { class: "form-check-label small ms-1", text: C.t("ui.show_hidden") }, []),
|
|
260
226
|
]);
|
|
261
227
|
hiddenToggle.querySelector("input").checked = state.showHidden;
|
|
262
228
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
229
|
+
toolbarRoot.appendChild(upButton);
|
|
230
|
+
toolbarRoot.appendChild(homeButton);
|
|
231
|
+
toolbarRoot.appendChild(reloadButton);
|
|
232
|
+
toolbarRoot.appendChild(breadcrumb);
|
|
233
|
+
toolbarRoot.appendChild(hiddenToggle);
|
|
268
234
|
|
|
269
|
-
// Write
|
|
270
|
-
var
|
|
235
|
+
// Write-Buttons rechts angeschlagen.
|
|
236
|
+
var rightGroup = element("span", { class: "ms-auto d-inline-flex gap-1" }, []);
|
|
271
237
|
if (state.perms.allowUpload) {
|
|
272
|
-
|
|
238
|
+
rightGroup.appendChild(element("button", {
|
|
273
239
|
type: "button",
|
|
274
240
|
class: "btn btn-sm btn-primary",
|
|
275
241
|
onclick: openUploadDialog,
|
|
276
|
-
title: "
|
|
277
|
-
}, ["
|
|
242
|
+
title: C.t("fm.upload_title"),
|
|
243
|
+
}, [C.t("fm.upload")]));
|
|
278
244
|
}
|
|
279
245
|
if (state.perms.allowMkdir) {
|
|
280
|
-
|
|
246
|
+
rightGroup.appendChild(element("button", {
|
|
281
247
|
type: "button",
|
|
282
248
|
class: "btn btn-sm btn-outline-primary",
|
|
283
249
|
onclick: openMkdirDialog,
|
|
284
|
-
title: "
|
|
285
|
-
}, ["
|
|
250
|
+
title: C.t("fm.new_folder_title"),
|
|
251
|
+
}, [C.t("fm.new_folder")]));
|
|
286
252
|
}
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
253
|
+
if (rightGroup.childNodes.length) {
|
|
254
|
+
toolbarRoot.appendChild(element("span", { class: "flex-grow-1" }, []));
|
|
255
|
+
toolbarRoot.appendChild(rightGroup);
|
|
290
256
|
}
|
|
291
257
|
}
|
|
292
258
|
|
|
293
|
-
// ----
|
|
259
|
+
// ---- Tabelle --------------------------------------------------------
|
|
260
|
+
/**
|
|
261
|
+
* Sortiert Ordner immer vor Dateien; danach nach der ausgewählten Spalte.
|
|
262
|
+
* Kein stabiler Sort (Array.prototype.sort ist in modernen Browsern
|
|
263
|
+
* stabil, aber wir belasten uns nicht damit).
|
|
264
|
+
*/
|
|
294
265
|
function sortItems(items) {
|
|
295
|
-
var
|
|
266
|
+
var key = state.sortBy;
|
|
267
|
+
var direction = state.sortDir;
|
|
296
268
|
return items.slice().sort(function (a, b) {
|
|
297
269
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
298
270
|
var va, vb;
|
|
299
|
-
if (
|
|
300
|
-
else if (
|
|
271
|
+
if (key === "size") { va = a.size || 0; vb = b.size || 0; }
|
|
272
|
+
else if (key === "mtime") {
|
|
301
273
|
va = a.mtime ? new Date(a.mtime).getTime() : 0;
|
|
302
274
|
vb = b.mtime ? new Date(b.mtime).getTime() : 0;
|
|
303
|
-
} else if (
|
|
275
|
+
} else if (key === "type") { va = C.mediaTypeFor(a); vb = C.mediaTypeFor(b); }
|
|
304
276
|
else { va = String(a.name).toLowerCase(); vb = String(b.name).toLowerCase(); }
|
|
305
|
-
if (va < vb) return -1 *
|
|
306
|
-
if (va > vb) return 1 *
|
|
277
|
+
if (va < vb) return -1 * direction;
|
|
278
|
+
if (va > vb) return 1 * direction;
|
|
307
279
|
return 0;
|
|
308
280
|
});
|
|
309
281
|
}
|
|
282
|
+
|
|
283
|
+
/** Klickbarer Spaltenkopf: toggelt Richtung bzw. wechselt Sortierspalte. */
|
|
310
284
|
function sortHeader(label, key) {
|
|
311
285
|
var arrow = state.sortBy === key ? (state.sortDir > 0 ? " ▲" : " ▼") : "";
|
|
312
|
-
return
|
|
286
|
+
return element("th", {
|
|
313
287
|
class: "samba-fm-th",
|
|
314
288
|
style: "cursor:pointer;user-select:none;",
|
|
315
289
|
onclick: function () {
|
|
@@ -319,311 +293,326 @@
|
|
|
319
293
|
},
|
|
320
294
|
}, [label + arrow]);
|
|
321
295
|
}
|
|
296
|
+
|
|
297
|
+
/** Baut die Datei-Tabelle inkl. Pagination, wenn `pageSize` > 0 ist. */
|
|
322
298
|
function renderTable(items) {
|
|
323
|
-
|
|
299
|
+
listRoot.innerHTML = "";
|
|
324
300
|
var showWriteActions = state.perms.allowDelete || state.perms.allowRename;
|
|
325
|
-
var table =
|
|
301
|
+
var table = element("table", {
|
|
326
302
|
class: "table table-sm table-hover mb-0 samba-fm-table align-middle",
|
|
327
303
|
}, [
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
sortHeader("
|
|
332
|
-
sortHeader("
|
|
333
|
-
sortHeader("
|
|
334
|
-
sortHeader("
|
|
335
|
-
|
|
304
|
+
element("thead", {}, [
|
|
305
|
+
element("tr", {}, [
|
|
306
|
+
element("th", { style: "width:2.4rem;" }, []),
|
|
307
|
+
sortHeader(C.t("fm.col.filename"), "name"),
|
|
308
|
+
sortHeader(C.t("fm.col.media_type"), "type"),
|
|
309
|
+
sortHeader(C.t("fm.col.size"), "size"),
|
|
310
|
+
sortHeader(C.t("fm.col.modified"), "mtime"),
|
|
311
|
+
element("th", { style: "width:" + (showWriteActions ? "18rem" : "14rem") + ";" }, [C.t("fm.col.actions")]),
|
|
336
312
|
]),
|
|
337
313
|
]),
|
|
338
314
|
]);
|
|
339
|
-
var tbody =
|
|
315
|
+
var tbody = element("tbody", {}, []);
|
|
340
316
|
var sorted = sortItems(items);
|
|
341
|
-
var
|
|
317
|
+
var pageSlice = sorted;
|
|
342
318
|
if (state.pageSize > 0) {
|
|
343
319
|
var from = (state.page - 1) * state.pageSize;
|
|
344
|
-
|
|
320
|
+
pageSlice = sorted.slice(from, from + state.pageSize);
|
|
345
321
|
}
|
|
346
322
|
|
|
347
|
-
|
|
348
|
-
var
|
|
323
|
+
pageSlice.forEach(function (item) {
|
|
324
|
+
var fullPath = C.joinPath(state.path, item.name);
|
|
349
325
|
var isDir = item.isDir;
|
|
350
326
|
|
|
351
|
-
var nameCell =
|
|
352
|
-
|
|
327
|
+
var nameCell = element("td", {}, [
|
|
328
|
+
element("a", {
|
|
353
329
|
href: "#", class: "samba-fm-name",
|
|
354
330
|
onclick: function (e) {
|
|
355
331
|
e.preventDefault();
|
|
356
|
-
if (isDir) navigate(
|
|
332
|
+
if (isDir) navigate(fullPath); else openFile(item, fullPath);
|
|
357
333
|
},
|
|
358
334
|
}, [item.name]),
|
|
359
335
|
]);
|
|
360
336
|
|
|
361
337
|
var actions = [];
|
|
362
338
|
if (isDir) {
|
|
363
|
-
actions.push(
|
|
339
|
+
actions.push(element("button", {
|
|
364
340
|
type: "button", class: "btn btn-sm btn-outline-secondary me-1",
|
|
365
|
-
onclick: function () { navigate(
|
|
366
|
-
}, ["
|
|
341
|
+
onclick: function () { navigate(fullPath); },
|
|
342
|
+
}, [C.t("fm.open")]));
|
|
367
343
|
} else {
|
|
368
|
-
actions.push(
|
|
344
|
+
actions.push(element("a", {
|
|
369
345
|
class: "btn btn-sm btn-outline-secondary me-1",
|
|
370
|
-
href: "/sambafile?path=" + encodeURIComponent(
|
|
371
|
-
target: "_blank", title: "
|
|
372
|
-
}, ["
|
|
373
|
-
actions.push(
|
|
346
|
+
href: "/sambafile?path=" + encodeURIComponent(fullPath) + "&disposition=inline",
|
|
347
|
+
target: "_blank", title: C.t("fm.open_new_tab"),
|
|
348
|
+
}, [C.t("fm.view")]));
|
|
349
|
+
actions.push(element("a", {
|
|
374
350
|
class: "btn btn-sm btn-outline-secondary me-1",
|
|
375
|
-
href: "/sambafile?path=" + encodeURIComponent(
|
|
376
|
-
title: "
|
|
351
|
+
href: "/sambafile?path=" + encodeURIComponent(fullPath) + "&disposition=attachment",
|
|
352
|
+
title: C.t("fm.download"),
|
|
377
353
|
}, ["⬇"]));
|
|
378
354
|
}
|
|
379
355
|
if (opts.exposeSmbLink) {
|
|
380
|
-
actions.push(
|
|
356
|
+
actions.push(element("a", {
|
|
381
357
|
class: "btn btn-sm btn-outline-primary me-1",
|
|
382
|
-
href: "/sambalink?path=" + encodeURIComponent(
|
|
358
|
+
href: "/sambalink?path=" + encodeURIComponent(fullPath),
|
|
383
359
|
target: "_blank",
|
|
384
|
-
title: "
|
|
360
|
+
title: C.t("fm.smb_link_title"),
|
|
385
361
|
}, ["↗"]));
|
|
386
362
|
}
|
|
387
363
|
if (state.perms.allowRename) {
|
|
388
|
-
actions.push(
|
|
364
|
+
actions.push(element("button", {
|
|
389
365
|
type: "button", class: "btn btn-sm btn-outline-secondary me-1",
|
|
390
|
-
title: "
|
|
391
|
-
onclick: function () { openRenameDialog(item,
|
|
366
|
+
title: C.t("fm.rename_title"),
|
|
367
|
+
onclick: function () { openRenameDialog(item, fullPath); },
|
|
392
368
|
}, ["✎"]));
|
|
393
369
|
}
|
|
394
370
|
if (state.perms.allowDelete) {
|
|
395
|
-
actions.push(
|
|
371
|
+
actions.push(element("button", {
|
|
396
372
|
type: "button", class: "btn btn-sm btn-outline-danger",
|
|
397
|
-
title: "
|
|
398
|
-
onclick: function () { confirmDelete(item,
|
|
373
|
+
title: C.t("fm.delete_title"),
|
|
374
|
+
onclick: function () { confirmDelete(item, fullPath); },
|
|
399
375
|
}, ["🗑"]));
|
|
400
376
|
}
|
|
401
377
|
|
|
402
|
-
tbody.appendChild(
|
|
403
|
-
|
|
378
|
+
tbody.appendChild(element("tr", {}, [
|
|
379
|
+
element("td", { class: "samba-fm-icon", style: "font-size:1.2rem;" }, [C.iconFor(item)]),
|
|
404
380
|
nameCell,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
381
|
+
element("td", { class: "text-muted small" }, [C.mediaTypeFor(item)]),
|
|
382
|
+
element("td", { class: "text-muted small" }, [isDir ? "" : C.fmtSize(item.size)]),
|
|
383
|
+
element("td", { class: "text-muted small" }, [C.fmtDate(item.mtime)]),
|
|
384
|
+
element("td", {}, actions),
|
|
409
385
|
]));
|
|
410
386
|
});
|
|
411
387
|
table.appendChild(tbody);
|
|
412
|
-
|
|
388
|
+
listRoot.appendChild(table);
|
|
413
389
|
|
|
414
390
|
if (!items.length) {
|
|
415
|
-
|
|
391
|
+
listRoot.appendChild(element("div", {
|
|
416
392
|
class: "text-muted fst-italic p-3 text-center",
|
|
417
|
-
text: "
|
|
393
|
+
text: C.t("ui.empty"),
|
|
418
394
|
}, []));
|
|
419
395
|
}
|
|
420
396
|
|
|
421
397
|
if (state.pageSize > 0 && sorted.length > state.pageSize) {
|
|
422
398
|
var totalPages = Math.ceil(sorted.length / state.pageSize);
|
|
423
|
-
|
|
399
|
+
listRoot.appendChild(element("div", {
|
|
424
400
|
class: "d-flex align-items-center justify-content-end p-2 border-top",
|
|
425
401
|
}, [
|
|
426
|
-
|
|
427
|
-
|
|
402
|
+
element("span", { class: "text-muted small me-2",
|
|
403
|
+
text: C.t("ui.page_of", { page: state.page, total: totalPages }) }, []),
|
|
404
|
+
element("button", {
|
|
428
405
|
type: "button",
|
|
429
406
|
class: "btn btn-sm btn-outline-secondary me-1" + (state.page <= 1 ? " disabled" : ""),
|
|
430
407
|
onclick: function () { if (state.page > 1) { state.page--; render(); } },
|
|
431
|
-
}, ["
|
|
432
|
-
|
|
408
|
+
}, [C.t("ui.prev_page")]),
|
|
409
|
+
element("button", {
|
|
433
410
|
type: "button",
|
|
434
411
|
class: "btn btn-sm btn-outline-secondary" + (state.page >= totalPages ? " disabled" : ""),
|
|
435
412
|
onclick: function () { if (state.page < totalPages) { state.page++; render(); } },
|
|
436
|
-
}, ["
|
|
413
|
+
}, [C.t("ui.next_page")]),
|
|
437
414
|
]));
|
|
438
415
|
}
|
|
439
416
|
}
|
|
417
|
+
|
|
418
|
+
/** Aktualisiert den kleinen "3 folders · 12 files"-Text im Karten-Header. */
|
|
440
419
|
function updateCounter(items) {
|
|
441
|
-
if (!
|
|
442
|
-
var
|
|
443
|
-
var
|
|
444
|
-
|
|
420
|
+
if (!counterEl) return;
|
|
421
|
+
var dirCount = items.filter(function (i) { return i.isDir; }).length;
|
|
422
|
+
var fileCount = items.length - dirCount;
|
|
423
|
+
counterEl.textContent = C.t("fm.counter", { dirs: dirCount, files: fileCount });
|
|
445
424
|
}
|
|
425
|
+
|
|
426
|
+
/** Toolbar + Tabelle + Counter neu zeichnen. */
|
|
446
427
|
function render() {
|
|
447
428
|
renderToolbar();
|
|
448
429
|
renderTable(state.lastItems);
|
|
449
430
|
updateCounter(state.lastItems);
|
|
450
431
|
}
|
|
451
432
|
|
|
452
|
-
// ----
|
|
433
|
+
// ---- Navigation & Viewer -------------------------------------------
|
|
453
434
|
function navigate(path) { state.page = 1; load(path); }
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
435
|
+
|
|
436
|
+
/** Öffnet eine Datei entweder inline im Viewer oder in einem neuen Tab. */
|
|
437
|
+
function openFile(item, fullPath) {
|
|
438
|
+
if (!viewerRoot) {
|
|
439
|
+
window.open("/sambafile?path=" + encodeURIComponent(fullPath) + "&disposition=inline", "_blank");
|
|
457
440
|
return;
|
|
458
441
|
}
|
|
459
|
-
if (opts.pdfInline && isViewable(item.name)) {
|
|
460
|
-
|
|
461
|
-
var
|
|
462
|
-
var
|
|
463
|
-
var header =
|
|
442
|
+
if (opts.pdfInline && C.isViewable(item.name)) {
|
|
443
|
+
viewerRoot.innerHTML = "";
|
|
444
|
+
var isImage = /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(item.name);
|
|
445
|
+
var streamUrl = "/sambafile?path=" + encodeURIComponent(fullPath) + "&disposition=inline";
|
|
446
|
+
var header = element("div", {
|
|
464
447
|
class: "samba-fm-viewer-header d-flex align-items-center p-2 border-top",
|
|
465
448
|
}, [
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
449
|
+
element("strong", { text: item.name }, []),
|
|
450
|
+
element("span", { class: "text-muted small ms-2", text: C.fmtSize(item.size) }, []),
|
|
451
|
+
element("button", {
|
|
469
452
|
type: "button", class: "btn btn-sm btn-outline-secondary ms-auto",
|
|
470
|
-
onclick: function () {
|
|
471
|
-
}, ["
|
|
453
|
+
onclick: function () { viewerRoot.innerHTML = ""; },
|
|
454
|
+
}, [C.t("ui.close")]),
|
|
472
455
|
]);
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
src:
|
|
456
|
+
viewerRoot.appendChild(header);
|
|
457
|
+
if (isImage) {
|
|
458
|
+
viewerRoot.appendChild(element("img", {
|
|
459
|
+
src: streamUrl,
|
|
477
460
|
style: "max-width:100%;max-height:70vh;display:block;margin:0 auto;",
|
|
478
461
|
}, []));
|
|
479
462
|
} else {
|
|
480
|
-
|
|
481
|
-
src:
|
|
463
|
+
viewerRoot.appendChild(element("iframe", {
|
|
464
|
+
src: streamUrl, style: "width:100%;height:70vh;border:0;display:block;",
|
|
482
465
|
}, []));
|
|
483
466
|
}
|
|
484
467
|
} else {
|
|
485
|
-
window.open("/sambafile?path=" + encodeURIComponent(
|
|
468
|
+
window.open("/sambafile?path=" + encodeURIComponent(fullPath) + "&disposition=inline", "_blank");
|
|
486
469
|
}
|
|
487
470
|
}
|
|
488
471
|
|
|
489
|
-
// ----
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
"
|
|
472
|
+
// ---- Schreib-Aktionen ----------------------------------------------
|
|
473
|
+
/** Öffnet einen "Wirklich löschen?"-Dialog und ruft /sambadelete auf. */
|
|
474
|
+
function confirmDelete(item, fullPath) {
|
|
475
|
+
var body = element("div", {}, [
|
|
476
|
+
element("p", {}, [
|
|
477
|
+
C.t("fm.confirm_delete.prompt_prefix"),
|
|
478
|
+
element("strong", { text: item.name }, []),
|
|
479
|
+
C.t("fm.confirm_delete.prompt_suffix"),
|
|
496
480
|
]),
|
|
497
481
|
item.isDir
|
|
498
|
-
?
|
|
499
|
-
|
|
500
|
-
])
|
|
501
|
-
: h("div", { class: "text-muted small" }, ["This action cannot be undone."]),
|
|
482
|
+
? element("div", { class: "alert alert-warning small mb-0", text: C.t("fm.confirm_delete.dir_hint") }, [])
|
|
483
|
+
: element("div", { class: "text-muted small", text: C.t("fm.confirm_delete.file_hint") }, []),
|
|
502
484
|
]);
|
|
503
|
-
var
|
|
504
|
-
|
|
485
|
+
var dialog = openModal(C.t("fm.confirm_delete.title"), body, [
|
|
486
|
+
element("button", {
|
|
505
487
|
type: "button", class: "btn btn-outline-secondary",
|
|
506
|
-
onclick: function () {
|
|
507
|
-
}, ["
|
|
508
|
-
|
|
488
|
+
onclick: function () { dialog.close(); },
|
|
489
|
+
}, [C.t("ui.cancel")]),
|
|
490
|
+
element("button", {
|
|
509
491
|
type: "button", class: "btn btn-danger",
|
|
510
492
|
onclick: async function () {
|
|
511
493
|
try {
|
|
512
494
|
await postJson("/sambadelete", {
|
|
513
|
-
path:
|
|
495
|
+
path: fullPath, isDir: item.isDir ? "1" : "0",
|
|
514
496
|
}, opts.csrfToken);
|
|
515
|
-
|
|
516
|
-
toast("
|
|
497
|
+
dialog.close();
|
|
498
|
+
toast(C.t("fm.deleted_ok", { name: item.name }), "success");
|
|
517
499
|
load(state.path);
|
|
518
|
-
} catch (
|
|
519
|
-
toast("
|
|
500
|
+
} catch (err) {
|
|
501
|
+
toast(C.t("fm.deleted_fail", { msg: err.message }), "error");
|
|
520
502
|
}
|
|
521
503
|
},
|
|
522
|
-
}, ["
|
|
504
|
+
}, [C.t("fm.delete")]),
|
|
523
505
|
]);
|
|
524
506
|
}
|
|
525
507
|
|
|
526
|
-
|
|
527
|
-
|
|
508
|
+
/** Rename-Dialog. Der Server prüft nochmal via sanitizeFilename. */
|
|
509
|
+
function openRenameDialog(item, fullPath) {
|
|
510
|
+
var input = element("input", {
|
|
528
511
|
type: "text", class: "form-control", value: item.name,
|
|
529
512
|
}, []);
|
|
530
|
-
var body =
|
|
531
|
-
|
|
513
|
+
var body = element("div", {}, [
|
|
514
|
+
element("label", { class: "form-label small text-muted", text: C.t("fm.rename.label") }, []),
|
|
532
515
|
input,
|
|
533
|
-
|
|
516
|
+
element("div", { class: "form-text small", text: C.t("fm.rename.hint") }, []),
|
|
534
517
|
]);
|
|
535
|
-
var
|
|
536
|
-
|
|
518
|
+
var dialog = openModal(C.t("fm.rename.title", { name: item.name }), body, [
|
|
519
|
+
element("button", {
|
|
537
520
|
type: "button", class: "btn btn-outline-secondary",
|
|
538
|
-
onclick: function () {
|
|
539
|
-
}, ["
|
|
540
|
-
|
|
521
|
+
onclick: function () { dialog.close(); },
|
|
522
|
+
}, [C.t("ui.cancel")]),
|
|
523
|
+
element("button", {
|
|
541
524
|
type: "button", class: "btn btn-primary",
|
|
542
525
|
onclick: async function () {
|
|
543
|
-
var
|
|
544
|
-
if (!
|
|
526
|
+
var newName = input.value.trim();
|
|
527
|
+
if (!newName || newName === item.name) { dialog.close(); return; }
|
|
545
528
|
try {
|
|
546
529
|
await postJson("/sambarename", {
|
|
547
|
-
from:
|
|
530
|
+
from: fullPath, newName: newName,
|
|
548
531
|
}, opts.csrfToken);
|
|
549
|
-
|
|
550
|
-
toast("
|
|
532
|
+
dialog.close();
|
|
533
|
+
toast(C.t("fm.renamed_ok", { name: newName }), "success");
|
|
551
534
|
load(state.path);
|
|
552
|
-
} catch (
|
|
553
|
-
toast("
|
|
535
|
+
} catch (err) {
|
|
536
|
+
toast(C.t("fm.rename_fail", { msg: err.message }), "error");
|
|
554
537
|
}
|
|
555
538
|
},
|
|
556
|
-
}, ["
|
|
539
|
+
}, [C.t("fm.rename")]),
|
|
557
540
|
]);
|
|
558
541
|
setTimeout(function () { input.focus(); input.select(); }, 20);
|
|
559
542
|
}
|
|
560
543
|
|
|
544
|
+
/** Neuen Ordner im aktuellen Pfad anlegen. */
|
|
561
545
|
function openMkdirDialog() {
|
|
562
|
-
var input =
|
|
563
|
-
type: "text", class: "form-control", placeholder: "
|
|
546
|
+
var input = element("input", {
|
|
547
|
+
type: "text", class: "form-control", placeholder: C.t("fm.mkdir.placeholder"),
|
|
564
548
|
}, []);
|
|
565
|
-
var body =
|
|
566
|
-
|
|
549
|
+
var body = element("div", {}, [
|
|
550
|
+
element("label", { class: "form-label small text-muted", text: C.t("fm.mkdir.label") }, []),
|
|
567
551
|
input,
|
|
568
552
|
]);
|
|
569
|
-
var
|
|
570
|
-
|
|
553
|
+
var dialog = openModal(C.t("fm.mkdir.title", { path: state.path || "/" }), body, [
|
|
554
|
+
element("button", {
|
|
571
555
|
type: "button", class: "btn btn-outline-secondary",
|
|
572
|
-
onclick: function () {
|
|
573
|
-
}, ["
|
|
574
|
-
|
|
556
|
+
onclick: function () { dialog.close(); },
|
|
557
|
+
}, [C.t("ui.cancel")]),
|
|
558
|
+
element("button", {
|
|
575
559
|
type: "button", class: "btn btn-primary",
|
|
576
560
|
onclick: async function () {
|
|
577
|
-
var
|
|
578
|
-
if (!
|
|
561
|
+
var name = input.value.trim();
|
|
562
|
+
if (!name) return;
|
|
579
563
|
try {
|
|
580
564
|
await postJson("/sambamkdir", {
|
|
581
|
-
path: state.path, name:
|
|
565
|
+
path: state.path, name: name,
|
|
582
566
|
}, opts.csrfToken);
|
|
583
|
-
|
|
584
|
-
toast("
|
|
567
|
+
dialog.close();
|
|
568
|
+
toast(C.t("fm.mkdir_ok", { name: name }), "success");
|
|
585
569
|
load(state.path);
|
|
586
|
-
} catch (
|
|
587
|
-
toast("
|
|
570
|
+
} catch (err) {
|
|
571
|
+
toast(C.t("fm.mkdir_fail", { msg: err.message }), "error");
|
|
588
572
|
}
|
|
589
573
|
},
|
|
590
|
-
}, ["
|
|
574
|
+
}, [C.t("fm.mkdir.create")]),
|
|
591
575
|
]);
|
|
592
576
|
setTimeout(function () { input.focus(); }, 20);
|
|
593
577
|
}
|
|
594
578
|
|
|
579
|
+
/**
|
|
580
|
+
* Upload-Dialog mit Drag-&-Drop-Zone und File-Input. Sendet multipart
|
|
581
|
+
* mit CSRF-Header. 207 (Multi-Status) mit Teilfehlern wird als Teil-
|
|
582
|
+
* Erfolg dargestellt.
|
|
583
|
+
*/
|
|
595
584
|
function openUploadDialog() {
|
|
596
585
|
var maxMb = Number(state.perms.maxUploadMb) || 50;
|
|
597
|
-
var fileInput =
|
|
586
|
+
var fileInput = element("input", {
|
|
598
587
|
type: "file", class: "form-control", multiple: "multiple",
|
|
599
588
|
}, []);
|
|
600
|
-
var
|
|
601
|
-
var dropZone =
|
|
589
|
+
var overwriteCheckbox = element("input", { type: "checkbox", class: "form-check-input" }, []);
|
|
590
|
+
var dropZone = element("div", {
|
|
602
591
|
class: "samba-drop border rounded p-4 text-center text-muted",
|
|
603
|
-
text: "
|
|
592
|
+
text: C.t("fm.upload.dropzone"),
|
|
604
593
|
onclick: function () { fileInput.click(); },
|
|
605
594
|
}, []);
|
|
606
|
-
var
|
|
595
|
+
var pickedList = element("div", { class: "samba-picked small mt-2" }, []);
|
|
607
596
|
|
|
608
597
|
function setFiles(fileList) {
|
|
609
598
|
fileInput.__files = Array.prototype.slice.call(fileList);
|
|
610
|
-
|
|
599
|
+
pickedList.innerHTML = "";
|
|
611
600
|
if (!fileInput.__files.length) return;
|
|
612
|
-
var ul =
|
|
613
|
-
fileInput.__files.forEach(function (
|
|
614
|
-
ul.appendChild(
|
|
601
|
+
var ul = element("ul", { class: "list-unstyled mb-0" }, []);
|
|
602
|
+
fileInput.__files.forEach(function (file) {
|
|
603
|
+
ul.appendChild(element("li", { text: file.name + " (" + C.fmtSize(file.size) + ")" }, []));
|
|
615
604
|
});
|
|
616
|
-
|
|
605
|
+
pickedList.appendChild(ul);
|
|
617
606
|
}
|
|
618
607
|
fileInput.addEventListener("change", function () { setFiles(fileInput.files); });
|
|
619
|
-
["dragenter", "dragover"].forEach(function (
|
|
620
|
-
dropZone.addEventListener(
|
|
608
|
+
["dragenter", "dragover"].forEach(function (evt) {
|
|
609
|
+
dropZone.addEventListener(evt, function (e) {
|
|
621
610
|
e.preventDefault(); e.stopPropagation();
|
|
622
611
|
dropZone.classList.add("samba-drop-active");
|
|
623
612
|
});
|
|
624
613
|
});
|
|
625
|
-
["dragleave", "drop"].forEach(function (
|
|
626
|
-
dropZone.addEventListener(
|
|
614
|
+
["dragleave", "drop"].forEach(function (evt) {
|
|
615
|
+
dropZone.addEventListener(evt, function (e) {
|
|
627
616
|
e.preventDefault(); e.stopPropagation();
|
|
628
617
|
dropZone.classList.remove("samba-drop-active");
|
|
629
618
|
});
|
|
@@ -632,100 +621,103 @@
|
|
|
632
621
|
if (e.dataTransfer && e.dataTransfer.files) setFiles(e.dataTransfer.files);
|
|
633
622
|
});
|
|
634
623
|
|
|
635
|
-
var progress =
|
|
636
|
-
var body =
|
|
637
|
-
|
|
638
|
-
"
|
|
639
|
-
|
|
640
|
-
".
|
|
624
|
+
var progress = element("div", { class: "samba-upload-progress mt-2" }, []);
|
|
625
|
+
var body = element("div", {}, [
|
|
626
|
+
element("div", { class: "small text-muted mb-2" }, [
|
|
627
|
+
C.t("fm.upload.into_prefix"),
|
|
628
|
+
element("code", { text: state.path || "/" }, []),
|
|
629
|
+
C.t("fm.upload.limit", { mb: maxMb }),
|
|
641
630
|
]),
|
|
642
631
|
dropZone,
|
|
643
|
-
|
|
644
|
-
|
|
632
|
+
element("div", { class: "mt-2" }, [
|
|
633
|
+
element("label", { class: "form-label small text-muted", text: C.t("fm.upload.or_pick") }, []),
|
|
645
634
|
fileInput,
|
|
646
635
|
]),
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
636
|
+
pickedList,
|
|
637
|
+
element("label", { class: "form-check mt-2" }, [
|
|
638
|
+
overwriteCheckbox,
|
|
639
|
+
element("span", { class: "form-check-label ms-1 small", text: C.t("fm.upload.overwrite") }, []),
|
|
651
640
|
]),
|
|
652
641
|
progress,
|
|
653
642
|
]);
|
|
654
643
|
|
|
655
|
-
var
|
|
656
|
-
var
|
|
657
|
-
|
|
644
|
+
var uploadButton;
|
|
645
|
+
var dialog = openModal(C.t("fm.upload.title"), body, [
|
|
646
|
+
element("button", {
|
|
658
647
|
type: "button", class: "btn btn-outline-secondary",
|
|
659
|
-
onclick: function () {
|
|
660
|
-
}, ["
|
|
661
|
-
(
|
|
648
|
+
onclick: function () { dialog.close(); },
|
|
649
|
+
}, [C.t("ui.cancel")]),
|
|
650
|
+
(uploadButton = element("button", {
|
|
662
651
|
type: "button", class: "btn btn-primary",
|
|
663
652
|
onclick: async function () {
|
|
664
653
|
var files = fileInput.__files || Array.prototype.slice.call(fileInput.files || []);
|
|
665
|
-
if (!files.length) { toast("
|
|
666
|
-
|
|
654
|
+
if (!files.length) { toast(C.t("fm.upload.no_files"), "error"); return; }
|
|
655
|
+
uploadButton.setAttribute("disabled", "disabled");
|
|
667
656
|
try {
|
|
668
|
-
var
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
files.forEach(function (
|
|
672
|
-
progress.innerHTML = "
|
|
673
|
-
var
|
|
657
|
+
var formData = new FormData();
|
|
658
|
+
formData.append("path", state.path);
|
|
659
|
+
formData.append("overwrite", overwriteCheckbox.checked ? "1" : "0");
|
|
660
|
+
files.forEach(function (file) { formData.append("file", file); });
|
|
661
|
+
progress.innerHTML = C.t("fm.upload.uploading");
|
|
662
|
+
var response = await fetch("/sambaupload", {
|
|
674
663
|
method: "POST",
|
|
675
664
|
credentials: "same-origin",
|
|
676
665
|
headers: { "X-CSRF-Token": opts.csrfToken || "" },
|
|
677
|
-
body:
|
|
666
|
+
body: formData,
|
|
678
667
|
});
|
|
679
668
|
var data = null;
|
|
680
|
-
try { data = await
|
|
681
|
-
if (!
|
|
682
|
-
throw new Error((data && data.error) || ("HTTP " +
|
|
669
|
+
try { data = await response.json(); } catch (_) { data = null; }
|
|
670
|
+
if (!response.ok && response.status !== 207) {
|
|
671
|
+
throw new Error((data && data.error) || ("HTTP " + response.status));
|
|
683
672
|
}
|
|
684
673
|
var results = (data && data.results) || [];
|
|
685
|
-
var okCount = results.filter(function (
|
|
686
|
-
var failed = results.filter(function (
|
|
674
|
+
var okCount = results.filter(function (item) { return item.ok; }).length;
|
|
675
|
+
var failed = results.filter(function (item) { return !item.ok; });
|
|
687
676
|
if (failed.length) {
|
|
688
677
|
progress.innerHTML = "";
|
|
689
|
-
progress.appendChild(
|
|
678
|
+
progress.appendChild(element("div", {
|
|
690
679
|
class: "alert alert-warning small mb-0",
|
|
691
680
|
}, [
|
|
692
|
-
"
|
|
693
|
-
|
|
694
|
-
return
|
|
681
|
+
C.t("fm.upload.partial_summary", { ok: okCount, total: results.length }),
|
|
682
|
+
element("ul", { class: "mb-0" }, failed.map(function (item) {
|
|
683
|
+
return element("li", { text: item.name + ": " + item.error }, []);
|
|
695
684
|
})),
|
|
696
685
|
]));
|
|
697
|
-
toast("
|
|
686
|
+
toast(C.t("fm.upload.some_failed"), "error");
|
|
698
687
|
} else {
|
|
699
|
-
toast("
|
|
700
|
-
|
|
688
|
+
toast(C.t("fm.upload.ok", { n: okCount }), "success");
|
|
689
|
+
dialog.close();
|
|
701
690
|
}
|
|
702
691
|
load(state.path);
|
|
703
|
-
} catch (
|
|
692
|
+
} catch (err) {
|
|
704
693
|
progress.innerHTML = "";
|
|
705
|
-
progress.appendChild(
|
|
706
|
-
class: "alert alert-danger small mb-0",
|
|
694
|
+
progress.appendChild(element("div", {
|
|
695
|
+
class: "alert alert-danger small mb-0",
|
|
696
|
+
text: C.t("fm.upload.failed", { msg: err.message }),
|
|
707
697
|
}, []));
|
|
708
|
-
toast("
|
|
698
|
+
toast(C.t("fm.upload.failed_short"), "error");
|
|
709
699
|
} finally {
|
|
710
|
-
|
|
700
|
+
uploadButton.removeAttribute("disabled");
|
|
711
701
|
}
|
|
712
702
|
},
|
|
713
|
-
}, ["
|
|
703
|
+
}, [C.t("fm.upload.button")])),
|
|
714
704
|
]);
|
|
715
705
|
}
|
|
716
706
|
|
|
717
|
-
// ----
|
|
707
|
+
// ---- Datenzugriff --------------------------------------------------
|
|
708
|
+
/** Lädt den aktuellen Pfad neu, aktualisiert `state` und rendert. */
|
|
718
709
|
async function load(path) {
|
|
719
710
|
state.path = path;
|
|
720
|
-
|
|
711
|
+
listRoot.innerHTML = '<div class="text-muted p-3">' + C.t("ui.loading") + "</div>";
|
|
721
712
|
try {
|
|
722
|
-
var data = await
|
|
713
|
+
var data = await fetchDirectory(path, state.showHidden);
|
|
723
714
|
state.lastItems = data.items || [];
|
|
724
715
|
state.perms = data.perms || {};
|
|
725
716
|
render();
|
|
726
|
-
} catch (
|
|
727
|
-
|
|
728
|
-
'<div class="alert alert-danger m-2">
|
|
717
|
+
} catch (err) {
|
|
718
|
+
listRoot.innerHTML =
|
|
719
|
+
'<div class="alert alert-danger m-2">' +
|
|
720
|
+
C.t("tree.samba_prefix") + (err.message || String(err)) + "</div>";
|
|
729
721
|
renderToolbar();
|
|
730
722
|
}
|
|
731
723
|
}
|