iobroker.staticsfilefolder 0.0.10 → 0.0.12
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/io-package.json +7 -1
- package/lib/web.js +12 -0
- package/package.json +1 -1
- package/www/app.js +185 -10
- package/www/index.html +20 -19
package/io-package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "staticsfilefolder",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.12",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.0.12": {
|
|
7
|
+
"en": "Automatically detect ioBroker system language. Added German and Russian translations."
|
|
8
|
+
},
|
|
9
|
+
"0.0.11": {
|
|
10
|
+
"en": "Added multi-language support (English and Azerbaijani) with English as the default."
|
|
11
|
+
},
|
|
6
12
|
"0.0.10": {
|
|
7
13
|
"en": "Updated README.md with detailed instructions for instance configuration settings."
|
|
8
14
|
},
|
package/lib/web.js
CHANGED
|
@@ -12,6 +12,7 @@ class ExtensionExample {
|
|
|
12
12
|
*/
|
|
13
13
|
constructor(server, webSettings, adapter, instanceSettings, app) {
|
|
14
14
|
this.app = app;
|
|
15
|
+
this.adapter = adapter;
|
|
15
16
|
this.config = instanceSettings ? instanceSettings.native : {};
|
|
16
17
|
this.config.route = this.config.route || "demo";
|
|
17
18
|
|
|
@@ -27,6 +28,17 @@ class ExtensionExample {
|
|
|
27
28
|
this.app.use("/" + this.config.route + "/files", express.static(this.config.dirname));
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// API Endpoint for configuration and system language
|
|
32
|
+
this.app.get("/" + this.config.route + "/api/config", async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const systemConfig = await this.adapter.getForeignObjectAsync("system.config");
|
|
35
|
+
const systemLanguage = (systemConfig && systemConfig.common && systemConfig.common.language) || "en";
|
|
36
|
+
res.json({ success: true, language: systemLanguage });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
res.json({ success: false, language: "en" });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
30
42
|
// API Endpoint for listing files
|
|
31
43
|
this.app.get("/" + this.config.route + "/api/list", (req, res) => {
|
|
32
44
|
try {
|
package/package.json
CHANGED
package/www/app.js
CHANGED
|
@@ -16,6 +16,7 @@ const btnBack = document.getElementById('btn-back');
|
|
|
16
16
|
const btnForward = document.getElementById('btn-forward');
|
|
17
17
|
const btnHome = document.getElementById('btn-home');
|
|
18
18
|
const btnTheme = document.getElementById('btn-theme');
|
|
19
|
+
const btnLang = document.getElementById('btn-lang');
|
|
19
20
|
const searchBox = document.getElementById('search-box');
|
|
20
21
|
const typeFilter = document.getElementById('type-filter');
|
|
21
22
|
const sortSelect = document.getElementById('sort-select');
|
|
@@ -33,11 +34,176 @@ const modalBtnDownload = document.getElementById('modal-btn-download');
|
|
|
33
34
|
let currentOpenItem = null;
|
|
34
35
|
let currentFilesList = [];
|
|
35
36
|
|
|
37
|
+
// Localization Settings
|
|
38
|
+
const translations = {
|
|
39
|
+
en: {
|
|
40
|
+
back: "← Back",
|
|
41
|
+
forward: "Forward →",
|
|
42
|
+
home: "Home",
|
|
43
|
+
searchPlaceholder: "Search...",
|
|
44
|
+
allTypes: "All Types",
|
|
45
|
+
sortAz: "Sort A-Z",
|
|
46
|
+
sortZa: "Sort Z-A",
|
|
47
|
+
sortNewest: "Newest First",
|
|
48
|
+
sortOldest: "Oldest First",
|
|
49
|
+
title: "Omni Reports",
|
|
50
|
+
loadingPdf: "Loading PDF...",
|
|
51
|
+
loadingExcel: "Loading Excel...",
|
|
52
|
+
loadingWord: "Loading Word...",
|
|
53
|
+
emptyFolder: "This folder is empty.",
|
|
54
|
+
root: "Root",
|
|
55
|
+
themeTooltip: "Toggle Dark/Light Mode",
|
|
56
|
+
langTooltip: "Change Language / Dili Dəyiş",
|
|
57
|
+
modalTitle: "File Viewer",
|
|
58
|
+
modalBack: "← Back",
|
|
59
|
+
modalForward: "Next →",
|
|
60
|
+
modalPrint: "🖨️ Print / PDF",
|
|
61
|
+
modalDownload: "📥 Download",
|
|
62
|
+
todayBadge: "Today",
|
|
63
|
+
errorLoading: "An error occurred: "
|
|
64
|
+
},
|
|
65
|
+
az: {
|
|
66
|
+
back: "← Geri",
|
|
67
|
+
forward: "İrəli →",
|
|
68
|
+
home: "Ev (Home)",
|
|
69
|
+
searchPlaceholder: "Axtarış...",
|
|
70
|
+
allTypes: "Bütün Tiplər",
|
|
71
|
+
sortAz: "A-Z Sırala",
|
|
72
|
+
sortZa: "Z-A Sırala",
|
|
73
|
+
sortNewest: "Ən Yeni",
|
|
74
|
+
sortOldest: "Ən Köhnə",
|
|
75
|
+
title: "Omni Hesabatlar",
|
|
76
|
+
loadingPdf: "PDF Yüklənir...",
|
|
77
|
+
loadingExcel: "Excel Yüklənir...",
|
|
78
|
+
loadingWord: "Word Yüklənir...",
|
|
79
|
+
emptyFolder: "Bu qovluq boşdur.",
|
|
80
|
+
root: "Kök",
|
|
81
|
+
themeTooltip: "Gecə/Gündüz Rejimi",
|
|
82
|
+
langTooltip: "Change Language / Dili Dəyiş",
|
|
83
|
+
modalTitle: "Fayl Baxışı",
|
|
84
|
+
modalBack: "← Geri",
|
|
85
|
+
modalForward: "İrəli →",
|
|
86
|
+
modalPrint: "🖨️ Çap / PDF",
|
|
87
|
+
modalDownload: "📥 Yüklə",
|
|
88
|
+
todayBadge: "Bu gün",
|
|
89
|
+
errorLoading: "Xəta baş verdi: "
|
|
90
|
+
},
|
|
91
|
+
de: {
|
|
92
|
+
back: "← Zurück",
|
|
93
|
+
forward: "Weiter →",
|
|
94
|
+
home: "Startseite",
|
|
95
|
+
searchPlaceholder: "Suchen...",
|
|
96
|
+
allTypes: "Alle Typen",
|
|
97
|
+
sortAz: "A-Z sortieren",
|
|
98
|
+
sortZa: "Z-A sortieren",
|
|
99
|
+
sortNewest: "Neueste zuerst",
|
|
100
|
+
sortOldest: "Älteste zuerst",
|
|
101
|
+
title: "Omni Berichte",
|
|
102
|
+
loadingPdf: "PDF wird geladen...",
|
|
103
|
+
loadingExcel: "Excel wird geladen...",
|
|
104
|
+
loadingWord: "Word wird geladen...",
|
|
105
|
+
emptyFolder: "Dieser Ordner ist leer.",
|
|
106
|
+
root: "Hauptverzeichnis",
|
|
107
|
+
themeTooltip: "Dunkel/Hell-Modus umschalten",
|
|
108
|
+
langTooltip: "Sprache ändern",
|
|
109
|
+
modalTitle: "Dateibetrachter",
|
|
110
|
+
modalBack: "← Zurück",
|
|
111
|
+
modalForward: "Weiter →",
|
|
112
|
+
modalPrint: "🖨️ Drucken / PDF",
|
|
113
|
+
modalDownload: "📥 Herunterladen",
|
|
114
|
+
todayBadge: "Heute",
|
|
115
|
+
errorLoading: "Ein Fehler ist aufgetreten: "
|
|
116
|
+
},
|
|
117
|
+
ru: {
|
|
118
|
+
back: "← Назад",
|
|
119
|
+
forward: "Вперед →",
|
|
120
|
+
home: "Главная",
|
|
121
|
+
searchPlaceholder: "Поиск...",
|
|
122
|
+
allTypes: "Все типы",
|
|
123
|
+
sortAz: "Сортировка А-Я",
|
|
124
|
+
sortZa: "Сортировка Я-А",
|
|
125
|
+
sortNewest: "Сначала новые",
|
|
126
|
+
sortOldest: "Сначала старые",
|
|
127
|
+
title: "Omni Отчеты",
|
|
128
|
+
loadingPdf: "Загрузка PDF...",
|
|
129
|
+
loadingExcel: "Загрузка Excel...",
|
|
130
|
+
loadingWord: "Загрузка Word...",
|
|
131
|
+
emptyFolder: "Эта папка пуста.",
|
|
132
|
+
root: "Корень",
|
|
133
|
+
themeTooltip: "Переключить тему",
|
|
134
|
+
langTooltip: "Изменить язык",
|
|
135
|
+
modalTitle: "Просмотр файла",
|
|
136
|
+
modalBack: "← Назад",
|
|
137
|
+
modalForward: "Вперед →",
|
|
138
|
+
modalPrint: "🖨️ Печать / PDF",
|
|
139
|
+
modalDownload: "📥 Скачать",
|
|
140
|
+
todayBadge: "Сегодня",
|
|
141
|
+
errorLoading: "Произошла ошибка: "
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const supportedLangs = ['en', 'az', 'de', 'ru'];
|
|
146
|
+
let currentLang = 'en'; // default English
|
|
147
|
+
|
|
36
148
|
// Setup PDF.js worker
|
|
37
149
|
if (window.pdfjsLib) {
|
|
38
150
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'libs/pdfjs/pdf.worker.min.mjs';
|
|
39
151
|
}
|
|
40
152
|
|
|
153
|
+
// Language Initialization
|
|
154
|
+
async function initLang() {
|
|
155
|
+
let savedLang = localStorage.getItem('staticsfilefolder_lang');
|
|
156
|
+
if (!savedLang) {
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(`${BASE_ROUTE}/api/config`);
|
|
159
|
+
const data = await res.json();
|
|
160
|
+
if (data && data.language) {
|
|
161
|
+
savedLang = data.language;
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
// ignore
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
currentLang = savedLang || navigator.language.split('-')[0] || 'en';
|
|
169
|
+
if (!translations[currentLang]) {
|
|
170
|
+
currentLang = 'en';
|
|
171
|
+
}
|
|
172
|
+
applyTranslations();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toggleLang() {
|
|
176
|
+
let index = supportedLangs.indexOf(currentLang);
|
|
177
|
+
index = (index + 1) % supportedLangs.length;
|
|
178
|
+
currentLang = supportedLangs[index];
|
|
179
|
+
localStorage.setItem('staticsfilefolder_lang', currentLang);
|
|
180
|
+
applyTranslations();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function applyTranslations() {
|
|
184
|
+
const t = translations[currentLang];
|
|
185
|
+
|
|
186
|
+
// Update elements with data-i18n attribute
|
|
187
|
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
188
|
+
const key = el.getAttribute('data-i18n');
|
|
189
|
+
if (t[key]) {
|
|
190
|
+
if (el.tagName === 'INPUT') {
|
|
191
|
+
el.placeholder = t[key];
|
|
192
|
+
} else {
|
|
193
|
+
el.textContent = t[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Update search placeholder and other specific inputs
|
|
199
|
+
searchBox.placeholder = t.searchPlaceholder;
|
|
200
|
+
btnLang.textContent = `🌐 ${currentLang.toUpperCase()}`;
|
|
201
|
+
|
|
202
|
+
// Refresh UI to update dynamic content like dates/breadcrumbs
|
|
203
|
+
updateBreadcrumb();
|
|
204
|
+
renderItems();
|
|
205
|
+
}
|
|
206
|
+
|
|
41
207
|
// Theme Initialization
|
|
42
208
|
function initTheme() {
|
|
43
209
|
const savedTheme = localStorage.getItem('staticsfilefolder_theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
@@ -56,6 +222,7 @@ function toggleTheme() {
|
|
|
56
222
|
// Initialize
|
|
57
223
|
document.addEventListener('DOMContentLoaded', () => {
|
|
58
224
|
initTheme();
|
|
225
|
+
initLang();
|
|
59
226
|
loadPath('');
|
|
60
227
|
|
|
61
228
|
// Event Listeners
|
|
@@ -63,6 +230,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
63
230
|
btnForward.addEventListener('click', goForward);
|
|
64
231
|
btnHome.addEventListener('click', () => navigateTo(''));
|
|
65
232
|
btnTheme.addEventListener('click', toggleTheme);
|
|
233
|
+
btnLang.addEventListener('click', toggleLang);
|
|
66
234
|
|
|
67
235
|
searchBox.addEventListener('input', renderItems);
|
|
68
236
|
typeFilter.addEventListener('change', renderItems);
|
|
@@ -132,13 +300,14 @@ function updateNavButtons() {
|
|
|
132
300
|
}
|
|
133
301
|
|
|
134
302
|
function updateBreadcrumb() {
|
|
303
|
+
const rootName = translations[currentLang]?.root || 'Root';
|
|
135
304
|
if (!currentPath) {
|
|
136
|
-
breadcrumbEl.innerHTML =
|
|
305
|
+
breadcrumbEl.innerHTML = `<span><i class="icon">🏠</i> ${rootName}</span>`;
|
|
137
306
|
return;
|
|
138
307
|
}
|
|
139
308
|
|
|
140
309
|
const parts = currentPath.split('/');
|
|
141
|
-
let html =
|
|
310
|
+
let html = `<span data-path="">🏠 ${rootName}</span>`;
|
|
142
311
|
let buildPath = '';
|
|
143
312
|
|
|
144
313
|
parts.forEach((part, index) => {
|
|
@@ -216,7 +385,8 @@ function renderItems() {
|
|
|
216
385
|
const isToday = itemDate.setHours(0,0,0,0) === today;
|
|
217
386
|
if (isToday) div.classList.add('is-today');
|
|
218
387
|
|
|
219
|
-
const
|
|
388
|
+
const locale = currentLang === 'en' ? 'en-US' : 'az-AZ';
|
|
389
|
+
const dateStr = new Date(item.mtime).toLocaleString(locale);
|
|
220
390
|
|
|
221
391
|
let sizeStr = '';
|
|
222
392
|
if (!item.isDirectory) {
|
|
@@ -224,11 +394,13 @@ function renderItems() {
|
|
|
224
394
|
else sizeStr = (item.size / 1024).toFixed(2) + ' KB';
|
|
225
395
|
}
|
|
226
396
|
|
|
397
|
+
const todayText = translations[currentLang]?.todayBadge || 'Today';
|
|
398
|
+
|
|
227
399
|
div.innerHTML = `
|
|
228
400
|
<div class="file-icon">${icon}</div>
|
|
229
401
|
<div class="file-name" title="${item.name}">${item.name}</div>
|
|
230
402
|
<div class="file-meta">${dateStr}<br>${sizeStr}</div>
|
|
231
|
-
${isToday ?
|
|
403
|
+
${isToday ? `<div class="today-badge">${todayText}</div>` : ''}
|
|
232
404
|
`;
|
|
233
405
|
|
|
234
406
|
div.addEventListener('click', () => handleItemClick(item));
|
|
@@ -352,7 +524,7 @@ function downloadCurrentFile() {
|
|
|
352
524
|
|
|
353
525
|
async function openPdf(url) {
|
|
354
526
|
viewerModal.style.display = 'block';
|
|
355
|
-
viewerBody.innerHTML = '
|
|
527
|
+
viewerBody.innerHTML = `<h2>${translations[currentLang]?.loadingPdf || 'Loading PDF...'}</h2>`;
|
|
356
528
|
|
|
357
529
|
try {
|
|
358
530
|
const loadingTask = window.pdfjsLib.getDocument({ url: url });
|
|
@@ -379,13 +551,14 @@ async function openPdf(url) {
|
|
|
379
551
|
await page.render(renderContext).promise;
|
|
380
552
|
}
|
|
381
553
|
} catch (e) {
|
|
382
|
-
|
|
554
|
+
const errorText = translations[currentLang]?.errorLoading || 'An error occurred: ';
|
|
555
|
+
viewerBody.innerHTML = `<h2 style="color:red">${errorText}${e.message}</h2>`;
|
|
383
556
|
}
|
|
384
557
|
}
|
|
385
558
|
|
|
386
559
|
async function openExcel(url) {
|
|
387
560
|
viewerModal.style.display = 'block';
|
|
388
|
-
viewerBody.innerHTML = '
|
|
561
|
+
viewerBody.innerHTML = `<h2>${translations[currentLang]?.loadingExcel || 'Loading Excel...'}</h2>`;
|
|
389
562
|
|
|
390
563
|
try {
|
|
391
564
|
const res = await fetch(url);
|
|
@@ -407,13 +580,14 @@ async function openExcel(url) {
|
|
|
407
580
|
if(table) table.className = 'sheetjs-table';
|
|
408
581
|
|
|
409
582
|
} catch (e) {
|
|
410
|
-
|
|
583
|
+
const errorText = translations[currentLang]?.errorLoading || 'An error occurred: ';
|
|
584
|
+
viewerBody.innerHTML = `<h2 style="color:red">${errorText}${e.message}</h2>`;
|
|
411
585
|
}
|
|
412
586
|
}
|
|
413
587
|
|
|
414
588
|
async function openWord(url) {
|
|
415
589
|
viewerModal.style.display = 'block';
|
|
416
|
-
viewerBody.innerHTML =
|
|
590
|
+
viewerBody.innerHTML = `<h2>${translations[currentLang]?.loadingWord || 'Loading Word...'}</h2>`;
|
|
417
591
|
|
|
418
592
|
try {
|
|
419
593
|
const res = await fetch(url);
|
|
@@ -425,6 +599,7 @@ async function openWord(url) {
|
|
|
425
599
|
${result.value}
|
|
426
600
|
</div>`;
|
|
427
601
|
} catch (e) {
|
|
428
|
-
|
|
602
|
+
const errorText = translations[currentLang]?.errorLoading || 'An error occurred: ';
|
|
603
|
+
viewerBody.innerHTML = `<h2 style="color:red">${errorText}${e.message}</h2>`;
|
|
429
604
|
}
|
|
430
605
|
}
|
package/www/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -16,31 +16,32 @@
|
|
|
16
16
|
<div class="app-container">
|
|
17
17
|
<!-- Header & Toolbar -->
|
|
18
18
|
<header class="app-header">
|
|
19
|
-
<div class="header-title">Omni Reports</div>
|
|
19
|
+
<div class="header-title" id="app-title" data-i18n="title">Omni Reports</div>
|
|
20
20
|
|
|
21
21
|
<div class="toolbar">
|
|
22
|
-
<button id="btn-back" class="nav-btn" disabled>←
|
|
23
|
-
<button id="btn-forward" class="nav-btn" disabled
|
|
24
|
-
<button id="btn-home" class="nav-btn">
|
|
22
|
+
<button id="btn-back" class="nav-btn" data-i18n="back" disabled>← Back</button>
|
|
23
|
+
<button id="btn-forward" class="nav-btn" data-i18n="forward" disabled>Forward →</button>
|
|
24
|
+
<button id="btn-home" class="nav-btn" data-i18n="home">Home</button>
|
|
25
25
|
|
|
26
26
|
<div class="breadcrumb" id="breadcrumb">
|
|
27
27
|
<!-- Path will be injected here -->
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<div class="filters">
|
|
31
|
-
<button id="btn-
|
|
32
|
-
<
|
|
31
|
+
<button id="btn-lang" class="nav-btn" title="Change Language / Dili Dəyiş">🌐 AZ</button>
|
|
32
|
+
<button id="btn-theme" class="nav-btn" title="Toggle Dark/Light Mode">🌙</button>
|
|
33
|
+
<input type="text" id="search-box" placeholder="Search...">
|
|
33
34
|
<select id="type-filter">
|
|
34
|
-
<option value="all">
|
|
35
|
+
<option value="all" data-i18n="allTypes">All Types</option>
|
|
35
36
|
<option value=".pdf">PDF</option>
|
|
36
37
|
<option value=".xlsx">Excel</option>
|
|
37
38
|
<option value=".docx">Word</option>
|
|
38
39
|
</select>
|
|
39
40
|
<select id="sort-select">
|
|
40
|
-
<option value="name-asc">A-Z
|
|
41
|
-
<option value="name-desc">Z-A
|
|
42
|
-
<option value="date-desc"
|
|
43
|
-
<option value="date-asc"
|
|
41
|
+
<option value="name-asc" data-i18n="sortAz">Sort A-Z</option>
|
|
42
|
+
<option value="name-desc" data-i18n="sortZa">Sort Z-A</option>
|
|
43
|
+
<option value="date-desc" data-i18n="sortNewest">Newest First</option>
|
|
44
|
+
<option value="date-asc" data-i18n="sortOldest">Oldest First</option>
|
|
44
45
|
</select>
|
|
45
46
|
</div>
|
|
46
47
|
</div>
|
|
@@ -51,8 +52,8 @@
|
|
|
51
52
|
<div id="file-list" class="file-list">
|
|
52
53
|
<!-- File items will be injected here -->
|
|
53
54
|
</div>
|
|
54
|
-
<div id="empty-state" class="empty-state" style="display: none;">
|
|
55
|
-
|
|
55
|
+
<div id="empty-state" class="empty-state" data-i18n="emptyFolder" style="display: none;">
|
|
56
|
+
This folder is empty.
|
|
56
57
|
</div>
|
|
57
58
|
</main>
|
|
58
59
|
</div>
|
|
@@ -61,12 +62,12 @@
|
|
|
61
62
|
<div id="viewer-modal" class="modal">
|
|
62
63
|
<div class="modal-content">
|
|
63
64
|
<header class="modal-header">
|
|
64
|
-
<div class="modal-title" id="modal-title">
|
|
65
|
+
<div class="modal-title" id="modal-title" data-i18n="modalTitle">File Viewer</div>
|
|
65
66
|
<div class="modal-toolbar">
|
|
66
|
-
<button id="modal-btn-prev" class="nav-btn" title="
|
|
67
|
-
<button id="modal-btn-next" class="nav-btn" title="
|
|
68
|
-
<button id="modal-btn-print" class="nav-btn" title="
|
|
69
|
-
<button id="modal-btn-download" class="nav-btn" title="
|
|
67
|
+
<button id="modal-btn-prev" class="nav-btn" data-i18n="modalBack" title="Previous">← Back</button>
|
|
68
|
+
<button id="modal-btn-next" class="nav-btn" data-i18n="modalForward" title="Next">Next →</button>
|
|
69
|
+
<button id="modal-btn-print" class="nav-btn" data-i18n="modalPrint" title="Print / Save as PDF">🖨️ Print / PDF</button>
|
|
70
|
+
<button id="modal-btn-download" class="nav-btn" data-i18n="modalDownload" title="Download File">📥 Download</button>
|
|
70
71
|
<span id="close-modal" class="close-button">×</span>
|
|
71
72
|
</div>
|
|
72
73
|
</header>
|