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.
@@ -1,165 +1,123 @@
1
1
  /**
2
- * Client-side controller for the SambaFileManager view (v0.3.0).
2
+ * saltcorn-samba – Client-Controller für die `SambaFileManager`-View.
3
3
  *
4
- * Adds upload, rename, delete and mkdir on top of the read-only browser.
5
- * Every write action goes through /sambaupload | /sambadelete | /sambarename
6
- * | /sambamkdir with a CSRF token (rendered into the shell) and expects
7
- * JSON responses.
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
- // ---- tiny DOM helper ----------------------------------------------------
13
- function h(tag, attrs, children) {
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 (k) {
17
- if (k === "class") el.className = attrs[k];
18
- else if (k === "text") el.textContent = attrs[k];
19
- else if (k === "html") el.innerHTML = attrs[k];
20
- else if (k.slice(0, 2) === "on")
21
- el.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
22
- else el.setAttribute(k, attrs[k]);
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
- (children || []).forEach(function (c) {
25
- if (c == null) return;
26
- el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
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
- // ---- helpers ------------------------------------------------------------
32
- function extOf(name) {
33
- var s = String(name || "");
34
- var d = s.lastIndexOf(".");
35
- return d >= 0 ? s.slice(d + 1).toLowerCase() : "";
36
- }
37
- function iconFor(item) {
38
- if (item.isDir) return "📁";
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 r = await fetch(url, { credentials: "same-origin" });
108
- if (!r.ok) {
53
+ var response = await fetch(url, { credentials: "same-origin" });
54
+ if (!response.ok) {
109
55
  var msg;
110
- try { msg = (await r.json()).error; } catch (_) { msg = "HTTP " + r.status; }
111
- throw new Error(msg || "HTTP " + r.status);
56
+ try { msg = (await response.json()).error; } catch (_) { msg = "HTTP " + response.status; }
57
+ throw new Error(msg || "HTTP " + response.status);
112
58
  }
113
- return r.json();
59
+ return response.json();
114
60
  }
115
- async function postJson(url, body, csrf) {
116
- var r = await fetch(url, {
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": csrf || "",
72
+ "X-CSRF-Token": csrfToken || "",
122
73
  },
123
74
  body: JSON.stringify(body || {}),
124
75
  });
125
76
  var data = null;
126
- try { data = await r.json(); } catch (_) { data = null; }
127
- if (!r.ok) {
128
- var msg = (data && data.error) || ("HTTP " + r.status);
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 = r.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
- // ---- modal & toast ------------------------------------------------------
138
- function toast(msg, type) {
139
- var box = h("div", {
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: msg,
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
- function modal(title, bodyEl, footerEls) {
148
- var backdrop = h("div", { class: "samba-modal-backdrop" }, []);
149
- var modalEl = h("div", { class: "samba-modal card" }, [
150
- h("div", { class: "samba-modal-header card-header d-flex align-items-center" }, [
151
- h("strong", { text: title }, []),
152
- h("button", {
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": "Close",
111
+ "aria-label": C.t("ui.close"),
155
112
  onclick: function () { close(); },
156
113
  }, []),
157
114
  ]),
158
- h("div", { class: "samba-modal-body card-body" }, [bodyEl]),
159
- h("div", { class: "samba-modal-footer card-footer d-flex justify-content-end gap-2" }, footerEls || []),
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(modalEl);
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: modalEl };
130
+ return { close: close, root: modalRoot };
173
131
  }
174
132
 
175
- // ---- component ----------------------------------------------------------
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 root = document.getElementById(id + "-list");
179
- if (!root) return;
180
- if (root.dataset.mounted === "1") return;
181
- root.dataset.mounted = "1";
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 toolbar = document.getElementById(id + "-toolbar");
184
- var counter = document.getElementById(id + "-count");
185
- var viewer = document.getElementById(id + "-viewer");
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
- // ---- toolbar --------------------------------------------------------
167
+ // ---- Toolbar --------------------------------------------------------
168
+ /** Rendert Up/Home/Refresh + Breadcrumb + Toggle "show hidden" + Write-Buttons. */
205
169
  function renderToolbar() {
206
- toolbar.innerHTML = "";
207
- var upBtn = h("button", {
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: "Up one directory",
212
- }, ["⬆ Up"]);
213
- var homeBtn = h("button", {
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: "Go to root",
182
+ title: C.t("ui.home_title"),
218
183
  }, ["🏠"]);
219
- var reloadBtn = h("button", {
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: "Refresh",
189
+ title: C.t("ui.refresh_title"),
224
190
  }, ["↻"]);
225
191
 
226
- var breadcrumb = h("nav", { class: "d-inline-block", "aria-label": "breadcrumb" }, [
227
- (function () {
228
- var ol = h("ol", { class: "breadcrumb mb-0 d-inline-flex" }, []);
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 parts = rel ? rel.split("/") : [];
231
- ol.appendChild(h("li", {
232
- class: "breadcrumb-item" + (parts.length ? "" : " active"),
196
+ var segments = rel ? rel.split("/") : [];
197
+ list.appendChild(element("li", {
198
+ class: "breadcrumb-item" + (segments.length ? "" : " active"),
233
199
  }, [
234
- parts.length
235
- ? h("a", { href: "#", onclick: function (e) { e.preventDefault(); navigate(state.rootPath); } }, ["/"])
200
+ segments.length
201
+ ? element("a", { href: "#", onclick: function (e) { e.preventDefault(); navigate(state.rootPath); } }, ["/"])
236
202
  : "/",
237
203
  ]));
238
- var cur = state.rootPath;
239
- parts.forEach(function (seg, idx) {
240
- cur = joinPath(cur, seg);
241
- var isLast = idx === parts.length - 1;
242
- var target = cur;
243
- ol.appendChild(h("li", { class: "breadcrumb-item" + (isLast ? " active" : "") }, [
244
- isLast ? seg : h("a", {
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
- }, [seg]),
213
+ }, [segment]),
248
214
  ]));
249
215
  });
250
- return ol;
216
+ return list;
251
217
  })(),
252
218
  ]);
253
219
 
254
- var hiddenToggle = h("label", { class: "form-check form-check-inline ms-3 mb-0 align-middle" }, [
255
- h("input", {
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
- h("span", { class: "form-check-label small ms-1", text: "Show hidden" }, []),
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
- toolbar.appendChild(upBtn);
264
- toolbar.appendChild(homeBtn);
265
- toolbar.appendChild(reloadBtn);
266
- toolbar.appendChild(breadcrumb);
267
- toolbar.appendChild(hiddenToggle);
229
+ toolbarRoot.appendChild(upButton);
230
+ toolbarRoot.appendChild(homeButton);
231
+ toolbarRoot.appendChild(reloadButton);
232
+ toolbarRoot.appendChild(breadcrumb);
233
+ toolbarRoot.appendChild(hiddenToggle);
268
234
 
269
- // Write buttons on the right
270
- var right = h("span", { class: "ms-auto d-inline-flex gap-1" }, []);
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
- right.appendChild(h("button", {
238
+ rightGroup.appendChild(element("button", {
273
239
  type: "button",
274
240
  class: "btn btn-sm btn-primary",
275
241
  onclick: openUploadDialog,
276
- title: "Upload files to this directory",
277
- }, ["⬆ Upload"]));
242
+ title: C.t("fm.upload_title"),
243
+ }, [C.t("fm.upload")]));
278
244
  }
279
245
  if (state.perms.allowMkdir) {
280
- right.appendChild(h("button", {
246
+ rightGroup.appendChild(element("button", {
281
247
  type: "button",
282
248
  class: "btn btn-sm btn-outline-primary",
283
249
  onclick: openMkdirDialog,
284
- title: "Create new folder",
285
- }, ["+ Folder"]));
250
+ title: C.t("fm.new_folder_title"),
251
+ }, [C.t("fm.new_folder")]));
286
252
  }
287
- if (right.childNodes.length) {
288
- toolbar.appendChild(h("span", { class: "flex-grow-1" }, []));
289
- toolbar.appendChild(right);
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
- // ---- table ----------------------------------------------------------
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 by = state.sortBy, dir = state.sortDir;
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 (by === "size") { va = a.size || 0; vb = b.size || 0; }
300
- else if (by === "mtime") {
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 (by === "type") { va = mediaTypeFor(a); vb = mediaTypeFor(b); }
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 * dir;
306
- if (va > vb) return 1 * dir;
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 h("th", {
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
- root.innerHTML = "";
299
+ listRoot.innerHTML = "";
324
300
  var showWriteActions = state.perms.allowDelete || state.perms.allowRename;
325
- var table = h("table", {
301
+ var table = element("table", {
326
302
  class: "table table-sm table-hover mb-0 samba-fm-table align-middle",
327
303
  }, [
328
- h("thead", {}, [
329
- h("tr", {}, [
330
- h("th", { style: "width:2.4rem;" }, []),
331
- sortHeader("Filename", "name"),
332
- sortHeader("Media type", "type"),
333
- sortHeader("Size", "size"),
334
- sortHeader("Modified", "mtime"),
335
- h("th", { style: "width:" + (showWriteActions ? "18rem" : "14rem") + ";" }, ["Actions"]),
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 = h("tbody", {}, []);
315
+ var tbody = element("tbody", {}, []);
340
316
  var sorted = sortItems(items);
341
- var slice = sorted;
317
+ var pageSlice = sorted;
342
318
  if (state.pageSize > 0) {
343
319
  var from = (state.page - 1) * state.pageSize;
344
- slice = sorted.slice(from, from + state.pageSize);
320
+ pageSlice = sorted.slice(from, from + state.pageSize);
345
321
  }
346
322
 
347
- slice.forEach(function (item) {
348
- var full = joinPath(state.path, item.name);
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 = h("td", {}, [
352
- h("a", {
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(full); else openFile(item, full);
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(h("button", {
339
+ actions.push(element("button", {
364
340
  type: "button", class: "btn btn-sm btn-outline-secondary me-1",
365
- onclick: function () { navigate(full); },
366
- }, ["Open"]));
341
+ onclick: function () { navigate(fullPath); },
342
+ }, [C.t("fm.open")]));
367
343
  } else {
368
- actions.push(h("a", {
344
+ actions.push(element("a", {
369
345
  class: "btn btn-sm btn-outline-secondary me-1",
370
- href: "/sambafile?path=" + encodeURIComponent(full) + "&disposition=inline",
371
- target: "_blank", title: "Open in new tab",
372
- }, ["View"]));
373
- actions.push(h("a", {
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(full) + "&disposition=attachment",
376
- title: "Download",
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(h("a", {
356
+ actions.push(element("a", {
381
357
  class: "btn btn-sm btn-outline-primary me-1",
382
- href: "/sambalink?path=" + encodeURIComponent(full),
358
+ href: "/sambalink?path=" + encodeURIComponent(fullPath),
383
359
  target: "_blank",
384
- title: "Open in file manager (Nemo/Nautilus/Explorer)",
360
+ title: C.t("fm.smb_link_title"),
385
361
  }, ["↗"]));
386
362
  }
387
363
  if (state.perms.allowRename) {
388
- actions.push(h("button", {
364
+ actions.push(element("button", {
389
365
  type: "button", class: "btn btn-sm btn-outline-secondary me-1",
390
- title: "Rename",
391
- onclick: function () { openRenameDialog(item, full); },
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(h("button", {
371
+ actions.push(element("button", {
396
372
  type: "button", class: "btn btn-sm btn-outline-danger",
397
- title: "Delete",
398
- onclick: function () { confirmDelete(item, full); },
373
+ title: C.t("fm.delete_title"),
374
+ onclick: function () { confirmDelete(item, fullPath); },
399
375
  }, ["🗑"]));
400
376
  }
401
377
 
402
- tbody.appendChild(h("tr", {}, [
403
- h("td", { class: "samba-fm-icon", style: "font-size:1.2rem;" }, [iconFor(item)]),
378
+ tbody.appendChild(element("tr", {}, [
379
+ element("td", { class: "samba-fm-icon", style: "font-size:1.2rem;" }, [C.iconFor(item)]),
404
380
  nameCell,
405
- h("td", { class: "text-muted small" }, [mediaTypeFor(item)]),
406
- h("td", { class: "text-muted small" }, [isDir ? "" : fmtSize(item.size)]),
407
- h("td", { class: "text-muted small" }, [fmtDate(item.mtime)]),
408
- h("td", {}, actions),
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
- root.appendChild(table);
388
+ listRoot.appendChild(table);
413
389
 
414
390
  if (!items.length) {
415
- root.appendChild(h("div", {
391
+ listRoot.appendChild(element("div", {
416
392
  class: "text-muted fst-italic p-3 text-center",
417
- text: "(empty directory)",
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
- root.appendChild(h("div", {
399
+ listRoot.appendChild(element("div", {
424
400
  class: "d-flex align-items-center justify-content-end p-2 border-top",
425
401
  }, [
426
- h("span", { class: "text-muted small me-2", text: "Page " + state.page + " / " + totalPages }, []),
427
- h("button", {
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
- h("button", {
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 (!counter) return;
442
- var dirs = items.filter(function (i) { return i.isDir; }).length;
443
- var files = items.length - dirs;
444
- counter.textContent = dirs + " folders · " + files + " files";
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
- // ---- navigation & viewer -------------------------------------------
433
+ // ---- Navigation & Viewer -------------------------------------------
453
434
  function navigate(path) { state.page = 1; load(path); }
454
- function openFile(item, full) {
455
- if (!viewer) {
456
- window.open("/sambafile?path=" + encodeURIComponent(full) + "&disposition=inline", "_blank");
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
- viewer.innerHTML = "";
461
- var isImg = /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(item.name);
462
- var url = "/sambafile?path=" + encodeURIComponent(full) + "&disposition=inline";
463
- var header = h("div", {
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
- h("strong", { text: item.name }, []),
467
- h("span", { class: "text-muted small ms-2", text: fmtSize(item.size) }, []),
468
- h("button", {
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 () { viewer.innerHTML = ""; },
471
- }, ["Close"]),
453
+ onclick: function () { viewerRoot.innerHTML = ""; },
454
+ }, [C.t("ui.close")]),
472
455
  ]);
473
- viewer.appendChild(header);
474
- if (isImg) {
475
- viewer.appendChild(h("img", {
476
- src: url,
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
- viewer.appendChild(h("iframe", {
481
- src: url, style: "width:100%;height:70vh;border:0;display:block;",
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(full) + "&disposition=inline", "_blank");
468
+ window.open("/sambafile?path=" + encodeURIComponent(fullPath) + "&disposition=inline", "_blank");
486
469
  }
487
470
  }
488
471
 
489
- // ---- write actions --------------------------------------------------
490
- function confirmDelete(item, full) {
491
- var body = h("div", {}, [
492
- h("p", {}, [
493
- "Delete ",
494
- h("strong", { text: item.name }, []),
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
- ? h("div", { class: "alert alert-warning small mb-0" }, [
499
- "This will only succeed if the directory is empty.",
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 m = modal("Confirm delete", body, [
504
- h("button", {
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 () { m.close(); },
507
- }, ["Cancel"]),
508
- h("button", {
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: full, isDir: item.isDir ? "1" : "0",
495
+ path: fullPath, isDir: item.isDir ? "1" : "0",
514
496
  }, opts.csrfToken);
515
- m.close();
516
- toast("Deleted " + item.name, "success");
497
+ dialog.close();
498
+ toast(C.t("fm.deleted_ok", { name: item.name }), "success");
517
499
  load(state.path);
518
- } catch (e) {
519
- toast("Delete failed: " + e.message, "error");
500
+ } catch (err) {
501
+ toast(C.t("fm.deleted_fail", { msg: err.message }), "error");
520
502
  }
521
503
  },
522
- }, ["Delete"]),
504
+ }, [C.t("fm.delete")]),
523
505
  ]);
524
506
  }
525
507
 
526
- function openRenameDialog(item, full) {
527
- var input = h("input", {
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 = h("div", {}, [
531
- h("label", { class: "form-label small text-muted", text: "New name" }, []),
513
+ var body = element("div", {}, [
514
+ element("label", { class: "form-label small text-muted", text: C.t("fm.rename.label") }, []),
532
515
  input,
533
- h("div", { class: "form-text small", text: "Slashes are not allowed. Move by editing folder path? Use the ↗ browse feature instead." }, []),
516
+ element("div", { class: "form-text small", text: C.t("fm.rename.hint") }, []),
534
517
  ]);
535
- var m = modal("Rename '" + item.name + "'", body, [
536
- h("button", {
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 () { m.close(); },
539
- }, ["Cancel"]),
540
- h("button", {
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 v = input.value.trim();
544
- if (!v || v === item.name) { m.close(); return; }
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: full, newName: v,
530
+ from: fullPath, newName: newName,
548
531
  }, opts.csrfToken);
549
- m.close();
550
- toast("Renamed to " + v, "success");
532
+ dialog.close();
533
+ toast(C.t("fm.renamed_ok", { name: newName }), "success");
551
534
  load(state.path);
552
- } catch (e) {
553
- toast("Rename failed: " + e.message, "error");
535
+ } catch (err) {
536
+ toast(C.t("fm.rename_fail", { msg: err.message }), "error");
554
537
  }
555
538
  },
556
- }, ["Rename"]),
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 = h("input", {
563
- type: "text", class: "form-control", placeholder: "New folder name",
546
+ var input = element("input", {
547
+ type: "text", class: "form-control", placeholder: C.t("fm.mkdir.placeholder"),
564
548
  }, []);
565
- var body = h("div", {}, [
566
- h("label", { class: "form-label small text-muted", text: "Folder name" }, []),
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 m = modal("Create folder in " + (state.path || "/"), body, [
570
- h("button", {
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 () { m.close(); },
573
- }, ["Cancel"]),
574
- h("button", {
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 v = input.value.trim();
578
- if (!v) return;
561
+ var name = input.value.trim();
562
+ if (!name) return;
579
563
  try {
580
564
  await postJson("/sambamkdir", {
581
- path: state.path, name: v,
565
+ path: state.path, name: name,
582
566
  }, opts.csrfToken);
583
- m.close();
584
- toast("Created folder " + v, "success");
567
+ dialog.close();
568
+ toast(C.t("fm.mkdir_ok", { name: name }), "success");
585
569
  load(state.path);
586
- } catch (e) {
587
- toast("Create folder failed: " + e.message, "error");
570
+ } catch (err) {
571
+ toast(C.t("fm.mkdir_fail", { msg: err.message }), "error");
588
572
  }
589
573
  },
590
- }, ["Create"]),
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 = h("input", {
586
+ var fileInput = element("input", {
598
587
  type: "file", class: "form-control", multiple: "multiple",
599
588
  }, []);
600
- var overwriteCb = h("input", { type: "checkbox", class: "form-check-input" }, []);
601
- var dropZone = h("div", {
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: "Drop files here or click to select",
592
+ text: C.t("fm.upload.dropzone"),
604
593
  onclick: function () { fileInput.click(); },
605
594
  }, []);
606
- var picked = h("div", { class: "samba-picked small mt-2" }, []);
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
- picked.innerHTML = "";
599
+ pickedList.innerHTML = "";
611
600
  if (!fileInput.__files.length) return;
612
- var ul = h("ul", { class: "list-unstyled mb-0" }, []);
613
- fileInput.__files.forEach(function (f) {
614
- ul.appendChild(h("li", { text: f.name + " (" + fmtSize(f.size) + ")" }, []));
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
- picked.appendChild(ul);
605
+ pickedList.appendChild(ul);
617
606
  }
618
607
  fileInput.addEventListener("change", function () { setFiles(fileInput.files); });
619
- ["dragenter", "dragover"].forEach(function (ev) {
620
- dropZone.addEventListener(ev, function (e) {
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 (ev) {
626
- dropZone.addEventListener(ev, function (e) {
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 = h("div", { class: "samba-upload-progress mt-2" }, []);
636
- var body = h("div", {}, [
637
- h("div", { class: "small text-muted mb-2" }, [
638
- "Uploading to ",
639
- h("code", { text: state.path || "/" }, []),
640
- ". Max " + maxMb + " MiB per file.",
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
- h("div", { class: "mt-2" }, [
644
- h("label", { class: "form-label small text-muted", text: "…or pick files" }, []),
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
- picked,
648
- h("label", { class: "form-check mt-2" }, [
649
- overwriteCb,
650
- h("span", { class: "form-check-label ms-1 small", text: "Overwrite existing files" }, []),
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 uploadBtn;
656
- var m = modal("Upload files", body, [
657
- h("button", {
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 () { m.close(); },
660
- }, ["Cancel"]),
661
- (uploadBtn = h("button", {
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("No files selected", "error"); return; }
666
- uploadBtn.setAttribute("disabled", "disabled");
654
+ if (!files.length) { toast(C.t("fm.upload.no_files"), "error"); return; }
655
+ uploadButton.setAttribute("disabled", "disabled");
667
656
  try {
668
- var fd = new FormData();
669
- fd.append("path", state.path);
670
- fd.append("overwrite", overwriteCb.checked ? "1" : "0");
671
- files.forEach(function (f) { fd.append("file", f); });
672
- progress.innerHTML = "Uploading…";
673
- var r = await fetch("/sambaupload", {
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: fd,
666
+ body: formData,
678
667
  });
679
668
  var data = null;
680
- try { data = await r.json(); } catch (_) { data = null; }
681
- if (!r.ok && r.status !== 207) {
682
- throw new Error((data && data.error) || ("HTTP " + r.status));
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 (x) { return x.ok; }).length;
686
- var failed = results.filter(function (x) { return !x.ok; });
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(h("div", {
678
+ progress.appendChild(element("div", {
690
679
  class: "alert alert-warning small mb-0",
691
680
  }, [
692
- "Uploaded " + okCount + " / " + results.length + " files. Failures:",
693
- h("ul", { class: "mb-0" }, failed.map(function (r) {
694
- return h("li", { text: r.name + ": " + r.error }, []);
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("Some uploads failed", "error");
686
+ toast(C.t("fm.upload.some_failed"), "error");
698
687
  } else {
699
- toast("Uploaded " + okCount + " file(s)", "success");
700
- m.close();
688
+ toast(C.t("fm.upload.ok", { n: okCount }), "success");
689
+ dialog.close();
701
690
  }
702
691
  load(state.path);
703
- } catch (e) {
692
+ } catch (err) {
704
693
  progress.innerHTML = "";
705
- progress.appendChild(h("div", {
706
- class: "alert alert-danger small mb-0", text: "Upload failed: " + e.message,
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("Upload failed", "error");
698
+ toast(C.t("fm.upload.failed_short"), "error");
709
699
  } finally {
710
- uploadBtn.removeAttribute("disabled");
700
+ uploadButton.removeAttribute("disabled");
711
701
  }
712
702
  },
713
- }, ["Upload"])),
703
+ }, [C.t("fm.upload.button")])),
714
704
  ]);
715
705
  }
716
706
 
717
- // ---- data ------------------------------------------------------------
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
- root.innerHTML = '<div class="text-muted p-3">Loading…</div>';
711
+ listRoot.innerHTML = '<div class="text-muted p-3">' + C.t("ui.loading") + "</div>";
721
712
  try {
722
- var data = await fetchDir(path, state.showHidden);
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 (e) {
727
- root.innerHTML =
728
- '<div class="alert alert-danger m-2">Samba: ' + (e.message || String(e)) + "</div>";
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
  }