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
package/i18n.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* saltcorn-samba – Minimales i18n-Modul (Server-Seite).
|
|
3
|
+
*
|
|
4
|
+
* Design-Ziele:
|
|
5
|
+
* - Keine externe Dependency (kein i18next, keine intl-messageformat).
|
|
6
|
+
* - JSON-Kataloge unter ./i18n/<locale>.json.
|
|
7
|
+
* - Lazy load + Cache; unbekannte Locales fallen automatisch auf `en`.
|
|
8
|
+
* - Placeholder-Syntax {name} wird durch die übergebenen Werte ersetzt.
|
|
9
|
+
*
|
|
10
|
+
* Locale-Auflösung (in dieser Reihenfolge):
|
|
11
|
+
* 1. Explizit übergebener Wert (Request-Query ?locale=de).
|
|
12
|
+
* 2. `req.getLocale()` (Saltcorn setzt das aus Cookie / Accept-Language).
|
|
13
|
+
* 3. `req.headers["accept-language"]` – erste passende Locale.
|
|
14
|
+
* 4. Fallback = `en`.
|
|
15
|
+
*
|
|
16
|
+
* Die Client-Seite bekommt den passenden Katalog via Route
|
|
17
|
+
* GET /samba-i18n.json?locale=xx
|
|
18
|
+
* (siehe index.js). Dort ruft samba-common.js `SambaCommon.setCatalog(...)` auf.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
const fs = require("fs");
|
|
24
|
+
const path = require("path");
|
|
25
|
+
|
|
26
|
+
const CATALOG_DIR = path.join(__dirname, "i18n");
|
|
27
|
+
const DEFAULT_LOCALE = "en";
|
|
28
|
+
const catalogs = Object.create(null); // locale -> { key: str }
|
|
29
|
+
const availableLocales = new Set(["de", "en"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load a catalogue from disk. Cached forever (plugin restart to reload).
|
|
33
|
+
* Missing files silently return {} — the caller falls back to the key itself.
|
|
34
|
+
*/
|
|
35
|
+
function loadCatalog(locale) {
|
|
36
|
+
if (catalogs[locale]) return catalogs[locale];
|
|
37
|
+
const file = path.join(CATALOG_DIR, locale + ".json");
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
delete parsed.$meta; // metadata is not a translation
|
|
42
|
+
catalogs[locale] = parsed;
|
|
43
|
+
} catch (_) {
|
|
44
|
+
catalogs[locale] = {};
|
|
45
|
+
}
|
|
46
|
+
return catalogs[locale];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a locale string to one we actually ship a catalogue for.
|
|
51
|
+
* Accepts full IETF tags ("de-DE", "en-US") and language-only ("de", "en").
|
|
52
|
+
*/
|
|
53
|
+
function normaliseLocale(raw) {
|
|
54
|
+
if (!raw) return DEFAULT_LOCALE;
|
|
55
|
+
const lc = String(raw).toLowerCase().trim();
|
|
56
|
+
if (availableLocales.has(lc)) return lc;
|
|
57
|
+
const short = lc.split(/[-_]/)[0];
|
|
58
|
+
if (availableLocales.has(short)) return short;
|
|
59
|
+
return DEFAULT_LOCALE;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract the caller's preferred locale from an Express-like request object.
|
|
64
|
+
* Never throws; falls back to DEFAULT_LOCALE.
|
|
65
|
+
*/
|
|
66
|
+
function resolveLocaleFromReq(req, explicit) {
|
|
67
|
+
if (explicit) return normaliseLocale(explicit);
|
|
68
|
+
try {
|
|
69
|
+
if (req) {
|
|
70
|
+
// Saltcorn / i18next-express-middleware
|
|
71
|
+
if (typeof req.getLocale === "function") {
|
|
72
|
+
const l = req.getLocale();
|
|
73
|
+
if (l) return normaliseLocale(l);
|
|
74
|
+
}
|
|
75
|
+
// Explicit query ?locale=de
|
|
76
|
+
if (req.query && req.query.locale) {
|
|
77
|
+
return normaliseLocale(req.query.locale);
|
|
78
|
+
}
|
|
79
|
+
// Accept-Language header – take the first tag we understand.
|
|
80
|
+
const hdr = req.headers && req.headers["accept-language"];
|
|
81
|
+
if (hdr) {
|
|
82
|
+
const first = String(hdr).split(",")[0].split(";")[0];
|
|
83
|
+
return normaliseLocale(first);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (_) {
|
|
87
|
+
// fall through
|
|
88
|
+
}
|
|
89
|
+
return DEFAULT_LOCALE;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Substitute {placeholder} tokens in the message. Missing values render as "".
|
|
94
|
+
*/
|
|
95
|
+
function interpolate(msg, params) {
|
|
96
|
+
if (!params) return msg;
|
|
97
|
+
return String(msg).replace(/\{(\w+)\}/g, (_, key) =>
|
|
98
|
+
Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : ""
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Translate a single key. Falls back to English, then to the key itself so
|
|
104
|
+
* a missing translation never breaks the UI.
|
|
105
|
+
*
|
|
106
|
+
* t("fm.upload.button", { locale: "de" }) → "Hochladen"
|
|
107
|
+
* t("fm.deleted_ok", { locale: "de", name: "foo.txt" }) → "„foo.txt\" gelöscht"
|
|
108
|
+
*/
|
|
109
|
+
function t(key, params) {
|
|
110
|
+
const opts = params || {};
|
|
111
|
+
const locale = normaliseLocale(opts.locale);
|
|
112
|
+
const primary = loadCatalog(locale);
|
|
113
|
+
if (Object.prototype.hasOwnProperty.call(primary, key)) {
|
|
114
|
+
return interpolate(primary[key], opts);
|
|
115
|
+
}
|
|
116
|
+
if (locale !== DEFAULT_LOCALE) {
|
|
117
|
+
const fallback = loadCatalog(DEFAULT_LOCALE);
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(fallback, key)) {
|
|
119
|
+
return interpolate(fallback[key], opts);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return key;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Bind a locale so callers can write `const _ = tFor("de"); _("ui.close")`.
|
|
127
|
+
*/
|
|
128
|
+
function tFor(locale) {
|
|
129
|
+
const loc = normaliseLocale(locale);
|
|
130
|
+
return function boundTranslate(key, params) {
|
|
131
|
+
return t(key, Object.assign({}, params || {}, { locale: loc }));
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Return the raw catalogue for a locale, merged over the English defaults.
|
|
137
|
+
* Used to ship a single JSON blob to the browser.
|
|
138
|
+
*/
|
|
139
|
+
function catalogFor(locale) {
|
|
140
|
+
const loc = normaliseLocale(locale);
|
|
141
|
+
const en = loadCatalog(DEFAULT_LOCALE);
|
|
142
|
+
if (loc === DEFAULT_LOCALE) return Object.assign({}, en);
|
|
143
|
+
const merged = Object.assign({}, en, loadCatalog(loc));
|
|
144
|
+
return merged;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
DEFAULT_LOCALE,
|
|
149
|
+
availableLocales: Array.from(availableLocales),
|
|
150
|
+
t,
|
|
151
|
+
tFor,
|
|
152
|
+
catalogFor,
|
|
153
|
+
resolveLocaleFromReq,
|
|
154
|
+
normaliseLocale,
|
|
155
|
+
};
|
package/index.js
CHANGED
|
@@ -50,6 +50,11 @@ const {
|
|
|
50
50
|
} = require("./smb-client");
|
|
51
51
|
const treeView = require("./tree-view");
|
|
52
52
|
const fileManagerView = require("./filemanager-view");
|
|
53
|
+
const {
|
|
54
|
+
catalogFor,
|
|
55
|
+
resolveLocaleFromReq,
|
|
56
|
+
availableLocales,
|
|
57
|
+
} = require("./i18n");
|
|
53
58
|
// pdf-view is intentionally NOT wired into the manifest (see note at bottom).
|
|
54
59
|
// The file is kept in the package so the DB-linkage release can revive it.
|
|
55
60
|
|
|
@@ -519,22 +524,18 @@ function jsonOk(res, extra) {
|
|
|
519
524
|
}
|
|
520
525
|
|
|
521
526
|
/**
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
527
|
+
* CSRF verification is handled by Saltcorn's global csurf middleware
|
|
528
|
+
* (packages/server/app.js). By the time our route handler runs, csurf
|
|
529
|
+
* has already validated req.body._csrf / X-CSRF-Token / csrf-token /
|
|
530
|
+
* xsrf-token headers against the session secret. A manual re-check
|
|
531
|
+
* here would be wrong: csrf-tokens returns a NEW salted token on
|
|
532
|
+
* every call to req.csrfToken(), so `provided !== req.csrfToken()`
|
|
533
|
+
* would fail even for perfectly valid requests.
|
|
534
|
+
*
|
|
535
|
+
* We keep this stub as the single documented spot to attach future
|
|
536
|
+
* pre-flight authorisation checks if ever needed.
|
|
525
537
|
*/
|
|
526
|
-
function checkCsrf(
|
|
527
|
-
if (typeof req.csrfToken !== "function") return true; // CSRF disabled globally
|
|
528
|
-
const expected = req.csrfToken();
|
|
529
|
-
const provided =
|
|
530
|
-
(req.body && req.body._csrf) ||
|
|
531
|
-
req.headers["x-csrf-token"] ||
|
|
532
|
-
req.headers["csrf-token"] ||
|
|
533
|
-
req.query._csrf;
|
|
534
|
-
if (!provided || provided !== expected) {
|
|
535
|
-
jsonError(res, 403, "Invalid CSRF token");
|
|
536
|
-
return false;
|
|
537
|
-
}
|
|
538
|
+
function checkCsrf(_req, _res) {
|
|
538
539
|
return true;
|
|
539
540
|
}
|
|
540
541
|
|
|
@@ -1267,6 +1268,35 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
1267
1268
|
},
|
|
1268
1269
|
},
|
|
1269
1270
|
|
|
1271
|
+
{
|
|
1272
|
+
// GET /samba-i18n.json?locale=xx
|
|
1273
|
+
//
|
|
1274
|
+
// Liefert den vollen \u00dcbersetzungskatalog f\u00fcr die Client-Seite. Wird
|
|
1275
|
+
// aktuell NICHT vom Standard-Bootstrap benutzt (die View-Shells injizieren
|
|
1276
|
+
// den Katalog inline, spart einen Roundtrip). Die Route ist f\u00fcr
|
|
1277
|
+
// externe Consumer / Debugging / dynamisches Nachladen einer anderen
|
|
1278
|
+
// Sprache im Browser gedacht.
|
|
1279
|
+
//
|
|
1280
|
+
// Keine Authentifizierung: Katalog enth\u00e4lt nur \u00dcbersetzungstext,
|
|
1281
|
+
// keine Konfiguration und keine share-spezifischen Daten.
|
|
1282
|
+
url: "/samba-i18n.json",
|
|
1283
|
+
method: "get",
|
|
1284
|
+
callback: async (req, res) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const locale = resolveLocaleFromReq(req, req.query && req.query.locale);
|
|
1287
|
+
const catalog = catalogFor(locale);
|
|
1288
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1289
|
+
// Kurzer Cache: Katalog \u00e4ndert sich nur bei Plugin-Release.
|
|
1290
|
+
res.setHeader("Cache-Control", "public, max-age=300");
|
|
1291
|
+
res.setHeader("X-Samba-Locale", locale);
|
|
1292
|
+
res.setHeader("X-Samba-Available-Locales", availableLocales.join(","));
|
|
1293
|
+
res.json(catalog);
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
res.status(500).json({ error: "i18n: " + (e.message || String(e)) });
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
|
|
1270
1300
|
{
|
|
1271
1301
|
url: "/sambamkdir",
|
|
1272
1302
|
method: "post",
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
4
4
|
"description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share via SMB 3.1.1 (AES-CMAC signing, optional encryption). File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node test/sanitize.test.js",
|
|
8
|
-
"lint": "node --check index.js && node --check smb-client.js && node --check readdir-compat.js && node --check tree-view.js && node --check filemanager-view.js && node --check pdf-view.js && node --check public/samba-tree.js && node --check public/samba-filemanager.js",
|
|
8
|
+
"lint": "node --check index.js && node --check smb-client.js && node --check readdir-compat.js && node --check tree-view.js && node --check filemanager-view.js && node --check pdf-view.js && node --check i18n.js && node --check public/samba-tree.js && node --check public/samba-filemanager.js && node --check public/samba-common.js",
|
|
9
9
|
"prepublishOnly": "npm run lint && npm test"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"tree-view.js",
|
|
44
44
|
"filemanager-view.js",
|
|
45
45
|
"pdf-view.js",
|
|
46
|
+
"i18n.js",
|
|
47
|
+
"i18n/",
|
|
46
48
|
"public/",
|
|
47
49
|
"tools/",
|
|
48
50
|
"README.md",
|
package/pdf-view.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
button,
|
|
21
21
|
} = require("@saltcorn/markup/tags");
|
|
22
22
|
const { text } = require("@saltcorn/markup");
|
|
23
|
+
const { tFor, resolveLocaleFromReq } = require("./i18n");
|
|
23
24
|
|
|
24
25
|
/** Extract the file extension in lowercase, without the dot. */
|
|
25
26
|
function extOf(name) {
|
|
@@ -88,7 +89,9 @@ const samba_pdf = {
|
|
|
88
89
|
},
|
|
89
90
|
],
|
|
90
91
|
run: (value, req, options = {}) => {
|
|
91
|
-
|
|
92
|
+
// Locale aus dem Saltcorn-Request; bei fehlendem req greift automatisch "en".
|
|
93
|
+
const _t = tFor(resolveLocaleFromReq(req));
|
|
94
|
+
if (!value) return span({ class: "text-muted" }, _t("pdf.empty"));
|
|
92
95
|
const safeVal = text(String(value));
|
|
93
96
|
const height = Number(options.height) > 0 ? Number(options.height) : 700;
|
|
94
97
|
|
|
@@ -101,7 +104,7 @@ const samba_pdf = {
|
|
|
101
104
|
class: "btn btn-sm btn-outline-secondary me-2",
|
|
102
105
|
},
|
|
103
106
|
i({ class: "fas fa-download me-1" }),
|
|
104
|
-
"
|
|
107
|
+
_t("pdf.download")
|
|
105
108
|
)
|
|
106
109
|
);
|
|
107
110
|
}
|
|
@@ -111,10 +114,10 @@ const samba_pdf = {
|
|
|
111
114
|
{
|
|
112
115
|
href: smbUrl(safeVal),
|
|
113
116
|
class: "btn btn-sm btn-outline-primary me-2",
|
|
114
|
-
title: "
|
|
117
|
+
title: _t("pdf.open_in_fm_title"),
|
|
115
118
|
},
|
|
116
119
|
i({ class: "fas fa-external-link-alt me-1" }),
|
|
117
|
-
"
|
|
120
|
+
_t("pdf.open_in_fm")
|
|
118
121
|
)
|
|
119
122
|
);
|
|
120
123
|
}
|
|
@@ -126,7 +129,7 @@ const samba_pdf = {
|
|
|
126
129
|
class: "btn btn-sm btn-outline-secondary",
|
|
127
130
|
},
|
|
128
131
|
i({ class: "fas fa-eye me-1" }),
|
|
129
|
-
"
|
|
132
|
+
_t("pdf.open_new_tab")
|
|
130
133
|
)
|
|
131
134
|
);
|
|
132
135
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* saltcorn-samba – Gemeinsame Client-Utilities für den Browser.
|
|
3
|
+
*
|
|
4
|
+
* Wird von samba-filemanager.js und samba-tree.js benutzt. Enthält:
|
|
5
|
+
* - iconFor(item) : Emoji-Icon für Verzeichnisse/Dateien
|
|
6
|
+
* (früher in beiden JS-Dateien dupliziert)
|
|
7
|
+
* - extOf(name) : Extension in lower-case, ohne Punkt
|
|
8
|
+
* - joinPath(a, b), parentOf : simple posix-artige Pfad-Helfer
|
|
9
|
+
* - fmtSize(bytes), fmtDate(v): menschenlesbare Formatierung
|
|
10
|
+
* - isViewable(name) : PDFs, Bilder, Text-artige Dateien
|
|
11
|
+
* - mediaTypeFor(item) : grober MIME-Typ (auch nur für File-Manager)
|
|
12
|
+
* - t(key, params) : i18n-Übersetzung. Katalog wird nach dem
|
|
13
|
+
* Laden asynchron nachgeschoben; solange nur
|
|
14
|
+
* der Key nicht auffindbar ist, gilt der Key
|
|
15
|
+
* selbst als Rückfalltext.
|
|
16
|
+
* - setCatalog(obj) : setzt den i18n-Katalog (aus dem Server-JSON)
|
|
17
|
+
* - loadCatalog(url) : lädt einen JSON-Katalog per fetch()
|
|
18
|
+
*
|
|
19
|
+
* Alles wird an `window.SambaCommon` gehängt. Die Datei ist absichtlich in
|
|
20
|
+
* ES5 gehalten (kein Bundler-Zwang, funktioniert in älteren Browsern).
|
|
21
|
+
*/
|
|
22
|
+
(function () {
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
// ---- i18n-State ---------------------------------------------------------
|
|
26
|
+
var catalog = {};
|
|
27
|
+
|
|
28
|
+
function setCatalog(obj) {
|
|
29
|
+
catalog = obj && typeof obj === "object" ? obj : {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Löst {name}-Platzhalter im Muster auf. */
|
|
33
|
+
function interpolate(msg, params) {
|
|
34
|
+
if (!params) return msg;
|
|
35
|
+
return String(msg).replace(/\{(\w+)\}/g, function (_, k) {
|
|
36
|
+
return Object.prototype.hasOwnProperty.call(params, k) ? String(params[k]) : "";
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Übersetzt einen Key. Fällt auf den Key selbst zurück, falls kein Eintrag. */
|
|
41
|
+
function t(key, params) {
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(catalog, key)) {
|
|
43
|
+
return interpolate(catalog[key], params);
|
|
44
|
+
}
|
|
45
|
+
return interpolate(key, params);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Lädt den Katalog von der übergebenen URL (z. B. /samba-i18n.json?locale=de).
|
|
50
|
+
* Ergebnis wird gecacht; scheitert der Fetch, bleibt der aktuelle Katalog stehen.
|
|
51
|
+
*/
|
|
52
|
+
function loadCatalog(url) {
|
|
53
|
+
return fetch(url, { credentials: "same-origin" })
|
|
54
|
+
.then(function (r) { return r.ok ? r.json() : {}; })
|
|
55
|
+
.then(function (data) { setCatalog(data); return data; })
|
|
56
|
+
.catch(function () { return {}; });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---- reine Utilities ---------------------------------------------------
|
|
60
|
+
/** Extension in lower-case, ohne führenden Punkt. */
|
|
61
|
+
function extOf(name) {
|
|
62
|
+
var s = String(name || "");
|
|
63
|
+
var dot = s.lastIndexOf(".");
|
|
64
|
+
return dot >= 0 ? s.slice(dot + 1).toLowerCase() : "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Einheitliches Icon für Verzeichnisse und Dateien.
|
|
69
|
+
* Die Kategorien decken die häufigsten Office-, Bild-, Archiv-, Audio- und
|
|
70
|
+
* Video-Formate ab. Alles Unbekannte bekommt 📎.
|
|
71
|
+
*/
|
|
72
|
+
function iconFor(item) {
|
|
73
|
+
if (item && item.isDir) return "📁";
|
|
74
|
+
var e = extOf(item && item.name);
|
|
75
|
+
if (e === "pdf") return "📄";
|
|
76
|
+
if (["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"].indexOf(e) >= 0) return "🖼️";
|
|
77
|
+
if (["doc", "docx", "odt", "rtf", "txt", "md"].indexOf(e) >= 0) return "📝";
|
|
78
|
+
if (["xls", "xlsx", "ods", "csv"].indexOf(e) >= 0) return "📊";
|
|
79
|
+
if (["ppt", "pptx", "odp"].indexOf(e) >= 0) return "📽️";
|
|
80
|
+
if (["zip", "tar", "gz", "7z", "rar"].indexOf(e) >= 0) return "🗜️";
|
|
81
|
+
if (["mp3", "wav", "ogg", "flac", "m4a"].indexOf(e) >= 0) return "🎵";
|
|
82
|
+
if (["mp4", "mkv", "mov", "avi", "webm"].indexOf(e) >= 0) return "🎬";
|
|
83
|
+
return "📎";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Erlaubt inline-Anzeige im integrierten Viewer (PDF, Bilder, Text). */
|
|
87
|
+
function isViewable(name) {
|
|
88
|
+
var n = String(name || "").toLowerCase();
|
|
89
|
+
return (
|
|
90
|
+
n.endsWith(".pdf") ||
|
|
91
|
+
/\.(png|jpe?g|gif|webp|svg|bmp)$/.test(n) ||
|
|
92
|
+
/\.(txt|md|json|xml|csv|html?)$/.test(n)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Grober MIME-Typ nur anhand der Extension – ausreichend zum Sortieren und
|
|
98
|
+
* für die "Media type"-Spalte. Der Server liefert für den echten Stream
|
|
99
|
+
* seinen eigenen (genaueren) Content-Type.
|
|
100
|
+
*/
|
|
101
|
+
function mediaTypeFor(item) {
|
|
102
|
+
if (item && item.isDir) return "folder";
|
|
103
|
+
var e = extOf(item && item.name);
|
|
104
|
+
var map = {
|
|
105
|
+
pdf: "application/pdf",
|
|
106
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
107
|
+
gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
108
|
+
txt: "text/plain", md: "text/markdown", csv: "text/csv",
|
|
109
|
+
json: "application/json", xml: "application/xml",
|
|
110
|
+
html: "text/html", htm: "text/html",
|
|
111
|
+
zip: "application/zip", doc: "application/msword",
|
|
112
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
113
|
+
xls: "application/vnd.ms-excel",
|
|
114
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
115
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
116
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
117
|
+
mp3: "audio/mpeg", mp4: "video/mp4", mkv: "video/x-matroska",
|
|
118
|
+
mov: "video/quicktime", avi: "video/x-msvideo",
|
|
119
|
+
};
|
|
120
|
+
return map[e] || (e ? "application/" + e : "application/octet-stream");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Größe in KiB/MiB/GiB. Kompakter Format-String für Tabellen und Bäume. */
|
|
124
|
+
function fmtSize(n) {
|
|
125
|
+
if (!n || n < 0) return "";
|
|
126
|
+
if (n < 1024) return n + " B";
|
|
127
|
+
var kib = n / 1024;
|
|
128
|
+
if (kib < 1024) return kib.toFixed(kib < 10 ? 1 : 0) + " KiB";
|
|
129
|
+
var mib = kib / 1024;
|
|
130
|
+
if (mib < 1024) return mib.toFixed(mib < 10 ? 1 : 0) + " MiB";
|
|
131
|
+
return (mib / 1024).toFixed(2) + " GiB";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** ISO-artig, ohne Sekunden ("2026-07-08 22:31"). Leere/ungültige Werte → "". */
|
|
135
|
+
function fmtDate(v) {
|
|
136
|
+
if (!v) return "";
|
|
137
|
+
var d = new Date(v);
|
|
138
|
+
if (isNaN(d.getTime())) return "";
|
|
139
|
+
var pad = function (x) { return String(x).padStart(2, "0"); };
|
|
140
|
+
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) +
|
|
141
|
+
" " + pad(d.getHours()) + ":" + pad(d.getMinutes());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Fügt zwei Pfad-Fragmente mit genau einem "/" zusammen. */
|
|
145
|
+
function joinPath(a, b) {
|
|
146
|
+
if (!a) return b;
|
|
147
|
+
if (!b) return a;
|
|
148
|
+
return (a.replace(/\/+$/, "") + "/" + b.replace(/^\/+/, "")).replace(/\/+/g, "/");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Übergeordneter Pfad. `parentOf("a/b/c")` = "a/b", `parentOf("a")` = "". */
|
|
152
|
+
function parentOf(p) {
|
|
153
|
+
if (!p) return "";
|
|
154
|
+
var i = p.lastIndexOf("/");
|
|
155
|
+
return i < 0 ? "" : p.slice(0, i);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- Export -------------------------------------------------------------
|
|
159
|
+
window.SambaCommon = {
|
|
160
|
+
// i18n
|
|
161
|
+
t: t,
|
|
162
|
+
setCatalog: setCatalog,
|
|
163
|
+
loadCatalog: loadCatalog,
|
|
164
|
+
// icons / classification
|
|
165
|
+
iconFor: iconFor,
|
|
166
|
+
isViewable: isViewable,
|
|
167
|
+
mediaTypeFor: mediaTypeFor,
|
|
168
|
+
// strings & paths
|
|
169
|
+
extOf: extOf,
|
|
170
|
+
fmtSize: fmtSize,
|
|
171
|
+
fmtDate: fmtDate,
|
|
172
|
+
joinPath: joinPath,
|
|
173
|
+
parentOf: parentOf,
|
|
174
|
+
};
|
|
175
|
+
})();
|