iobroker.script-restore 0.0.1
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/LICENSE +21 -0
- package/README.md +159 -0
- package/admin/index_m.html +74 -0
- package/admin/script-restore.png +0 -0
- package/admin/style.css +32 -0
- package/admin/tab_m.html +898 -0
- package/admin/words.js +19 -0
- package/build/main.js +254 -0
- package/build/main.js.map +7 -0
- package/io-package.json +95 -0
- package/package.json +74 -0
package/admin/tab_m.html
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="de">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Script Restore</title>
|
|
7
|
+
<link rel="icon" href="data:,">
|
|
8
|
+
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--primary: #0d6efd;
|
|
12
|
+
--primary-hover: #0b5ed7;
|
|
13
|
+
--success: #198754;
|
|
14
|
+
--success-hover: #157347;
|
|
15
|
+
--bg-light: #f8f9fa;
|
|
16
|
+
--bg-dark: #212529;
|
|
17
|
+
--bg-panel: #1e1e1e;
|
|
18
|
+
--border: #dee2e6;
|
|
19
|
+
--font-main: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
22
|
+
|
|
23
|
+
html, body {
|
|
24
|
+
height: 100%;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
font-family: var(--font-main);
|
|
28
|
+
background: var(--bg-light);
|
|
29
|
+
color: #212529;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Toolbar */
|
|
36
|
+
.toolbar {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 8px;
|
|
40
|
+
padding: 8px 12px;
|
|
41
|
+
background: white;
|
|
42
|
+
border-bottom: 1px solid var(--border);
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
}
|
|
46
|
+
.toolbar-left { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
|
|
47
|
+
.toolbar-right { display: flex; align-items: center; gap: 6px; }
|
|
48
|
+
|
|
49
|
+
.btn {
|
|
50
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
51
|
+
font-weight: 500; cursor: pointer; padding: 0.35rem 0.75rem;
|
|
52
|
+
font-size: 0.875rem; border-radius: 4px; transition: 0.15s;
|
|
53
|
+
border: 1px solid transparent; background: transparent; white-space: nowrap;
|
|
54
|
+
font-family: var(--font-main);
|
|
55
|
+
}
|
|
56
|
+
.btn-primary { background: var(--primary); color: white; border-color: var(--primary); }
|
|
57
|
+
.btn-primary:hover { background: var(--primary-hover); }
|
|
58
|
+
.btn-outline { color: #495057; border-color: #ced4da; background: white; }
|
|
59
|
+
.btn-outline:hover { background: #f0f0f0; }
|
|
60
|
+
.btn-outline-light { color: #f8f9fa; border-color: #f8f9fa; }
|
|
61
|
+
.btn-outline-light:hover, .btn-outline-light.active { background: #f8f9fa; color: #000; }
|
|
62
|
+
.btn-outline-primary { color: var(--primary); border-color: var(--primary); }
|
|
63
|
+
.btn-outline-primary:hover, .btn-outline-primary.active { background: var(--primary); color: white; }
|
|
64
|
+
.btn-outline-success { color: var(--success); border-color: var(--success); }
|
|
65
|
+
.btn-outline-success:hover, .btn-outline-success.active { background: var(--success); color: white; }
|
|
66
|
+
.btn-group { display: flex; }
|
|
67
|
+
.btn-group .btn { border-radius: 0; border-right-width: 0; }
|
|
68
|
+
.btn-group .btn:first-child { border-radius: 4px 0 0 4px; }
|
|
69
|
+
.btn-group .btn:last-child { border-radius: 0 4px 4px 0; border-right-width: 1px; }
|
|
70
|
+
|
|
71
|
+
.file-input-label {
|
|
72
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
73
|
+
font-weight: 500; cursor: pointer; padding: 0.35rem 0.75rem;
|
|
74
|
+
font-size: 0.875rem; border-radius: 4px; transition: 0.15s;
|
|
75
|
+
background: var(--primary); color: white; border: 1px solid var(--primary);
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
}
|
|
78
|
+
.file-input-label:hover { background: var(--primary-hover); }
|
|
79
|
+
#fileInput { display: none; }
|
|
80
|
+
|
|
81
|
+
/* Status message */
|
|
82
|
+
.status-msg {
|
|
83
|
+
font-size: 0.8rem;
|
|
84
|
+
color: #6c757d;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
text-overflow: ellipsis;
|
|
88
|
+
flex: 1;
|
|
89
|
+
min-width: 0;
|
|
90
|
+
}
|
|
91
|
+
.status-msg.error { color: #dc3545; }
|
|
92
|
+
.status-msg.success { color: var(--success); }
|
|
93
|
+
|
|
94
|
+
/* Main Layout */
|
|
95
|
+
.main-container {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex: 1;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Sidebar */
|
|
102
|
+
.sidebar {
|
|
103
|
+
width: 300px; min-width: 180px; max-width: 60vw;
|
|
104
|
+
display: flex; flex-direction: column;
|
|
105
|
+
background: white; height: 100%; flex-shrink: 0;
|
|
106
|
+
border-right: 1px solid var(--border);
|
|
107
|
+
}
|
|
108
|
+
.sidebar-header {
|
|
109
|
+
padding: 6px 8px; border-bottom: 1px solid var(--border);
|
|
110
|
+
display: flex; gap: 5px; flex-shrink: 0;
|
|
111
|
+
}
|
|
112
|
+
.sidebar-header input {
|
|
113
|
+
flex: 1; padding: 0.375rem 0.75rem; border: 1px solid #ced4da;
|
|
114
|
+
border-radius: 4px; font-size: 0.9rem; outline: none; min-width: 0;
|
|
115
|
+
font-family: var(--font-main);
|
|
116
|
+
}
|
|
117
|
+
.sidebar-header input:focus { border-color: var(--primary); }
|
|
118
|
+
.btn-icon {
|
|
119
|
+
background: #e9ecef; border: 1px solid #ced4da; border-radius: 4px;
|
|
120
|
+
padding: 0 10px; cursor: pointer; color: #444; transition: 0.2s;
|
|
121
|
+
display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
|
|
122
|
+
}
|
|
123
|
+
.btn-icon:hover { background: #dde0e3; }
|
|
124
|
+
|
|
125
|
+
.script-list { flex: 1; overflow-y: auto; padding: 4px 0; }
|
|
126
|
+
|
|
127
|
+
/* Tree */
|
|
128
|
+
.tree-folder {
|
|
129
|
+
cursor: pointer; padding: 7px 12px; font-weight: 500; color: #333;
|
|
130
|
+
display: flex; align-items: center; user-select: none; transition: 0.1s;
|
|
131
|
+
}
|
|
132
|
+
.tree-folder:hover { background-color: #f0f7ff; }
|
|
133
|
+
.folder-icon { display: inline-block; width: 14px; margin-right: 6px; transition: transform 0.2s; opacity: 0.7; font-size: 0.8em; flex-shrink: 0; }
|
|
134
|
+
.folder-icon.open { transform: rotate(90deg); }
|
|
135
|
+
.tree-children { display: none; }
|
|
136
|
+
.tree-children.open { display: block; }
|
|
137
|
+
|
|
138
|
+
.script-item {
|
|
139
|
+
cursor: pointer; padding: 8px 12px; transition: background-color 0.1s;
|
|
140
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
141
|
+
border-bottom: 1px solid #f0f0f0;
|
|
142
|
+
}
|
|
143
|
+
.tree-script { padding-left: 32px; border-bottom: none; }
|
|
144
|
+
.tree-children .tree-children .tree-script { padding-left: 50px; }
|
|
145
|
+
.tree-children .tree-children .tree-children .tree-script { padding-left: 68px; }
|
|
146
|
+
.tree-children .tree-children .tree-folder { padding-left: 28px; }
|
|
147
|
+
.tree-children .tree-children .tree-children .tree-folder { padding-left: 44px; }
|
|
148
|
+
|
|
149
|
+
.script-item:hover { background-color: #f0f7ff; }
|
|
150
|
+
.script-item.active { background-color: #e7f1ff; border-left: 3px solid var(--primary); padding-left: calc(12px - 3px); }
|
|
151
|
+
.tree-script.active { padding-left: calc(32px - 3px); }
|
|
152
|
+
.tree-children .tree-children .tree-script.active { padding-left: calc(50px - 3px); }
|
|
153
|
+
.tree-children .tree-children .tree-children .tree-script.active { padding-left: calc(68px - 3px); }
|
|
154
|
+
|
|
155
|
+
.script-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; font-size: 0.9rem; }
|
|
156
|
+
.type-badge { font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: bold; text-transform: uppercase; margin-left: 8px; flex-shrink: 0; }
|
|
157
|
+
.badge-Blockly { background: #61696e; color: white; }
|
|
158
|
+
.badge-JS { background: #f4d436; color: #212529; }
|
|
159
|
+
.badge-TypeScript { background: #3375c2; color: white; }
|
|
160
|
+
.badge-Rules { background: #05194a; color: white; }
|
|
161
|
+
|
|
162
|
+
/* Resizer */
|
|
163
|
+
.resizer {
|
|
164
|
+
width: 4px; background-color: var(--border); cursor: col-resize;
|
|
165
|
+
transition: background-color 0.2s; z-index: 10; flex-shrink: 0;
|
|
166
|
+
touch-action: none;
|
|
167
|
+
}
|
|
168
|
+
.resizer:hover, .resizer.resizing { background-color: var(--primary); width: 5px; }
|
|
169
|
+
|
|
170
|
+
/* Content Area */
|
|
171
|
+
.content-area {
|
|
172
|
+
flex: 1; display: flex; flex-direction: column;
|
|
173
|
+
background: var(--bg-panel); overflow: hidden; min-width: 0;
|
|
174
|
+
}
|
|
175
|
+
.action-bar {
|
|
176
|
+
background: #2d2d2d; padding: 8px 16px; border-bottom: 1px solid #444;
|
|
177
|
+
flex-shrink: 0; display: none;
|
|
178
|
+
}
|
|
179
|
+
.action-bar-inner {
|
|
180
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
181
|
+
flex-wrap: wrap; gap: 8px;
|
|
182
|
+
}
|
|
183
|
+
.code-display {
|
|
184
|
+
font-family: 'Consolas', 'Courier New', monospace;
|
|
185
|
+
font-size: 13px; color: #d4d4d4; flex: 1; overflow: auto;
|
|
186
|
+
margin: 0; padding: 12px 0; counter-reset: line;
|
|
187
|
+
line-height: 1.5; white-space: pre;
|
|
188
|
+
}
|
|
189
|
+
.code-line { display: block; padding-right: 15px; }
|
|
190
|
+
.code-line:hover { background: rgba(255,255,255,0.03); }
|
|
191
|
+
.code-line::before {
|
|
192
|
+
counter-increment: line; content: counter(line);
|
|
193
|
+
display: inline-block; width: 3.5em; margin-right: 1.5em;
|
|
194
|
+
color: #6c757d; text-align: right; border-right: 1px solid #444;
|
|
195
|
+
padding-right: 1em; user-select: none;
|
|
196
|
+
}
|
|
197
|
+
.code-empty {
|
|
198
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
199
|
+
height: 100%; color: #6a9955; font-family: 'Consolas', monospace;
|
|
200
|
+
font-size: 14px; flex: 1; text-align: center; padding: 20px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Loader overlay */
|
|
204
|
+
#loader {
|
|
205
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
206
|
+
background: rgba(248,249,250,0.95); z-index: 9999;
|
|
207
|
+
display: none; flex-direction: column; align-items: center; justify-content: center;
|
|
208
|
+
}
|
|
209
|
+
#progressContainer { position: relative; width: 4rem; height: 4rem; margin-bottom: 1rem; }
|
|
210
|
+
#progressCircle {
|
|
211
|
+
width: 100%; height: 100%; border-radius: 50%;
|
|
212
|
+
background: conic-gradient(var(--primary) 0%, #e9ecef 0%); transition: background 0.1s;
|
|
213
|
+
}
|
|
214
|
+
.progress-inner {
|
|
215
|
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
216
|
+
width: 3.2rem; height: 3.2rem; background: rgba(248,249,250,0.95);
|
|
217
|
+
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
218
|
+
}
|
|
219
|
+
#progressPercent { font-size: 0.9rem; font-weight: bold; color: var(--primary); }
|
|
220
|
+
.spinner {
|
|
221
|
+
width: 3.5rem; height: 3.5rem; border: 0.35em solid rgba(13,110,253,0.2);
|
|
222
|
+
border-top-color: var(--primary); border-radius: 50%;
|
|
223
|
+
animation: spin 1s linear infinite; margin-bottom: 1rem;
|
|
224
|
+
}
|
|
225
|
+
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
226
|
+
#loaderText { color: #495057; font-size: 0.95rem; }
|
|
227
|
+
|
|
228
|
+
/* Local files dropdown */
|
|
229
|
+
.dropdown-wrapper { position: relative; }
|
|
230
|
+
.dropdown-menu {
|
|
231
|
+
display: none; position: absolute; top: 100%; left: 0; z-index: 1000;
|
|
232
|
+
background: white; border: 1px solid var(--border); border-radius: 4px;
|
|
233
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 320px; max-width: 480px;
|
|
234
|
+
max-height: 300px; overflow-y: auto; margin-top: 2px;
|
|
235
|
+
}
|
|
236
|
+
.dropdown-menu.open { display: block; }
|
|
237
|
+
.dropdown-item {
|
|
238
|
+
padding: 8px 12px; cursor: pointer; font-size: 0.875rem;
|
|
239
|
+
border-bottom: 1px solid #f0f0f0; white-space: nowrap; overflow: hidden;
|
|
240
|
+
text-overflow: ellipsis; transition: background 0.1s;
|
|
241
|
+
}
|
|
242
|
+
.dropdown-item:hover { background: #f0f7ff; }
|
|
243
|
+
.dropdown-item:last-child { border-bottom: none; }
|
|
244
|
+
.dropdown-empty { padding: 12px; color: #6c757d; font-size: 0.875rem; text-align: center; }
|
|
245
|
+
.dropdown-loading { padding: 12px; color: #6c757d; font-size: 0.875rem; text-align: center; }
|
|
246
|
+
|
|
247
|
+
/* Responsive */
|
|
248
|
+
@media (max-width: 768px) {
|
|
249
|
+
.toolbar { padding: 6px 8px; }
|
|
250
|
+
.main-container { flex-direction: column; }
|
|
251
|
+
.sidebar { width: 100% !important; max-width: 100%; height: 40vh; min-height: 120px; border-right: none; border-bottom: 1px solid var(--border); }
|
|
252
|
+
.resizer { width: 100% !important; height: 4px; cursor: row-resize; }
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
255
|
+
</head>
|
|
256
|
+
<body>
|
|
257
|
+
<div id="loader">
|
|
258
|
+
<div id="progressContainer">
|
|
259
|
+
<div id="progressCircle"></div>
|
|
260
|
+
<div class="progress-inner"><span id="progressPercent">0%</span></div>
|
|
261
|
+
</div>
|
|
262
|
+
<div id="spinnerEl" class="spinner" style="display:none;"></div>
|
|
263
|
+
<div id="loaderText">Lade Backup...</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="toolbar">
|
|
267
|
+
<div class="toolbar-left">
|
|
268
|
+
<label class="file-input-label">
|
|
269
|
+
📂 Backup hochladen
|
|
270
|
+
<input type="file" id="fileInput" accept=".tar,.gz,.tar.gz,.json,.jsonl">
|
|
271
|
+
</label>
|
|
272
|
+
<div class="dropdown-wrapper" id="localDropdown">
|
|
273
|
+
<button class="btn btn-outline" onclick="toggleLocalFiles()">
|
|
274
|
+
🗂️ Lokale Backups ▾
|
|
275
|
+
</button>
|
|
276
|
+
<div class="dropdown-menu" id="localMenu"></div>
|
|
277
|
+
</div>
|
|
278
|
+
<span class="status-msg" id="statusMsg">Backup laden oder lokale Datei wählen</span>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div class="main-container">
|
|
283
|
+
<div class="sidebar" id="sidebar">
|
|
284
|
+
<div class="sidebar-header">
|
|
285
|
+
<input type="text" id="q" placeholder="Suche in Namen, Ordner & Code...">
|
|
286
|
+
<button class="btn-icon" id="expandToggleBtn" onclick="toggleExpandAll()" title="Alle Ordner aufklappen">📂</button>
|
|
287
|
+
</div>
|
|
288
|
+
<div id="list" class="script-list"></div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="resizer" id="resizer"></div>
|
|
292
|
+
|
|
293
|
+
<div class="content-area">
|
|
294
|
+
<div class="action-bar" id="actionBar">
|
|
295
|
+
<div class="action-bar-inner">
|
|
296
|
+
<div class="btn-group" id="viewSwitcher"></div>
|
|
297
|
+
<div style="display:flex;gap:6px;">
|
|
298
|
+
<button onclick="copyCode(this)" class="btn btn-outline-light">Code Kopieren</button>
|
|
299
|
+
<button onclick="downloadActive()" class="btn btn-primary" id="dlBtn">Download</button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div id="codeContainer" class="code-empty">
|
|
304
|
+
<div>
|
|
305
|
+
<strong>ioBroker Script Restore</strong><br><br>
|
|
306
|
+
Lade ein Backup hoch oder wähle eine lokale Datei,<br>
|
|
307
|
+
um Skripte anzuzeigen und wiederherzustellen.
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<script>
|
|
314
|
+
// === ioBroker Socket ===
|
|
315
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
316
|
+
const instance = urlParams.get('instance') || '0';
|
|
317
|
+
const adapterInst = 'script-restore.' + instance;
|
|
318
|
+
|
|
319
|
+
let socket = null;
|
|
320
|
+
let socketReady = false;
|
|
321
|
+
|
|
322
|
+
function initSocket() {
|
|
323
|
+
// Try 1: admin 7 Connection object on parent (has .sendTo method)
|
|
324
|
+
try {
|
|
325
|
+
const p = window.parent;
|
|
326
|
+
if (p && p.socket) {
|
|
327
|
+
socket = p.socket;
|
|
328
|
+
socketReady = true;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
} catch(e) {}
|
|
332
|
+
|
|
333
|
+
// Try 2: servConn._socket (older admin)
|
|
334
|
+
try {
|
|
335
|
+
const p = window.parent;
|
|
336
|
+
if (p && p.servConn && p.servConn._socket) {
|
|
337
|
+
socket = p.servConn._socket;
|
|
338
|
+
socketReady = socket.connected;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
} catch(e) {}
|
|
342
|
+
|
|
343
|
+
// Try 3: create own socket.io connection (same origin)
|
|
344
|
+
try {
|
|
345
|
+
if (typeof io !== 'undefined') {
|
|
346
|
+
const raw = io.connect(window.location.origin, { path: '/socket.io' });
|
|
347
|
+
raw.on('connect', () => { socket = raw; socketReady = true; });
|
|
348
|
+
raw.on('disconnect', () => { socketReady = false; });
|
|
349
|
+
}
|
|
350
|
+
} catch(e) {}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function sendTo(command, message, callback) {
|
|
354
|
+
if (!socket) {
|
|
355
|
+
callback({ error: 'Kein Socket. Bitte prüfen ob script-restore.' + instance + ' läuft.' });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let done = false;
|
|
359
|
+
const timer = setTimeout(() => {
|
|
360
|
+
if (done) return;
|
|
361
|
+
done = true;
|
|
362
|
+
callback({ error: 'Timeout: Adapter antwortet nicht. Läuft script-restore.' + instance + '?' });
|
|
363
|
+
}, 30000);
|
|
364
|
+
const wrapped = (result) => {
|
|
365
|
+
if (done) return;
|
|
366
|
+
done = true;
|
|
367
|
+
clearTimeout(timer);
|
|
368
|
+
callback(result);
|
|
369
|
+
};
|
|
370
|
+
// Admin 7 Connection wrapper has sendTo as async method
|
|
371
|
+
if (typeof socket.sendTo === 'function') {
|
|
372
|
+
Promise.resolve(socket.sendTo(adapterInst, command, message))
|
|
373
|
+
.then(wrapped)
|
|
374
|
+
.catch(err => wrapped({ error: String(err) }));
|
|
375
|
+
} else {
|
|
376
|
+
socket.emit('sendTo', adapterInst, command, message, wrapped);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
initSocket();
|
|
381
|
+
|
|
382
|
+
// === Resizer ===
|
|
383
|
+
const resizerEl = document.getElementById('resizer');
|
|
384
|
+
const sidebarEl = document.getElementById('sidebar');
|
|
385
|
+
let startX = 0, startY = 0, startW = 0, startH = 0, isMobile = false;
|
|
386
|
+
|
|
387
|
+
resizerEl.addEventListener('mousedown', startResize);
|
|
388
|
+
resizerEl.addEventListener('touchstart', startResize, { passive: true });
|
|
389
|
+
|
|
390
|
+
function startResize(e) {
|
|
391
|
+
isMobile = window.innerWidth <= 768;
|
|
392
|
+
startX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
393
|
+
startY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
|
|
394
|
+
const styles = window.getComputedStyle(sidebarEl);
|
|
395
|
+
startW = parseInt(styles.width, 10);
|
|
396
|
+
startH = parseInt(styles.height, 10);
|
|
397
|
+
document.body.style.userSelect = 'none';
|
|
398
|
+
document.body.style.cursor = isMobile ? 'row-resize' : 'col-resize';
|
|
399
|
+
resizerEl.classList.add('resizing');
|
|
400
|
+
document.addEventListener('mousemove', doResize);
|
|
401
|
+
document.addEventListener('mouseup', stopResize);
|
|
402
|
+
document.addEventListener('touchmove', doResize, { passive: false });
|
|
403
|
+
document.addEventListener('touchend', stopResize);
|
|
404
|
+
}
|
|
405
|
+
function doResize(e) {
|
|
406
|
+
const cx = e.clientX || (e.touches && e.touches[0].clientX) || 0;
|
|
407
|
+
const cy = e.clientY || (e.touches && e.touches[0].clientY) || 0;
|
|
408
|
+
if (isMobile) sidebarEl.style.height = Math.max(80, startH + cy - startY) + 'px';
|
|
409
|
+
else sidebarEl.style.width = Math.max(150, startW + cx - startX) + 'px';
|
|
410
|
+
}
|
|
411
|
+
function stopResize() {
|
|
412
|
+
resizerEl.classList.remove('resizing');
|
|
413
|
+
document.body.style.removeProperty('cursor');
|
|
414
|
+
document.body.style.removeProperty('user-select');
|
|
415
|
+
document.removeEventListener('mousemove', doResize);
|
|
416
|
+
document.removeEventListener('mouseup', stopResize);
|
|
417
|
+
document.removeEventListener('touchmove', doResize);
|
|
418
|
+
document.removeEventListener('touchend', stopResize);
|
|
419
|
+
}
|
|
420
|
+
window.addEventListener('resize', () => {
|
|
421
|
+
sidebarEl.style.width = '';
|
|
422
|
+
sidebarEl.style.height = '';
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// === Script Data ===
|
|
426
|
+
let scriptsData = [];
|
|
427
|
+
let cur = { index: -1 };
|
|
428
|
+
const openFolders = new Set();
|
|
429
|
+
let isAllExpanded = false;
|
|
430
|
+
|
|
431
|
+
function escapeHTML(str) {
|
|
432
|
+
if (!str) return '';
|
|
433
|
+
return String(str).replace(/[&<>'"]/g, t => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[t]));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function setStatus(msg, type) {
|
|
437
|
+
const el = document.getElementById('statusMsg');
|
|
438
|
+
el.textContent = msg;
|
|
439
|
+
el.className = 'status-msg' + (type ? ' ' + type : '');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// === Browser-side JSON/JSONL parser (same logic as adapter backend) ===
|
|
443
|
+
function processItem(key, val, scripts) {
|
|
444
|
+
if (!key || typeof val !== 'object' || val === null) return;
|
|
445
|
+
const v = val;
|
|
446
|
+
if (['channel','device','folder','meta'].includes(v.type)) return;
|
|
447
|
+
if (v.type !== 'script' && !key.startsWith('script.js.')) return;
|
|
448
|
+
const c = v.common;
|
|
449
|
+
if (!c || (c.engineType === undefined && c.source === undefined)) return;
|
|
450
|
+
const et = (typeof c.engineType === 'string' ? c.engineType : 'JS').toLowerCase();
|
|
451
|
+
let stype = 'JS';
|
|
452
|
+
if (et.includes('ts') || et.includes('typescript')) stype = 'TypeScript';
|
|
453
|
+
else if (et.includes('blockly')) stype = 'Blockly';
|
|
454
|
+
else if (et.includes('rules')) stype = 'Rules';
|
|
455
|
+
let name;
|
|
456
|
+
const no = c.name;
|
|
457
|
+
if (typeof no === 'object' && no !== null) name = no.de || no.en || Object.values(no)[0] || key.split('.').pop();
|
|
458
|
+
else name = (typeof no === 'string' && no) ? no : (key.split('.').pop() || key);
|
|
459
|
+
scripts.push({ name, path: key.startsWith('script.js.') ? key.slice(10) : key, type: stype, source: typeof c.source === 'string' ? c.source : '' });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function parseJsonContent(text, filename) {
|
|
463
|
+
const scripts = [];
|
|
464
|
+
const trimmed = text.trimStart();
|
|
465
|
+
const isJsonl = filename.endsWith('.jsonl') || (trimmed.startsWith('{') && !trimmed.startsWith('{\n "') && trimmed.includes('\n{'));
|
|
466
|
+
if (isJsonl) {
|
|
467
|
+
for (const line of text.split('\n')) {
|
|
468
|
+
const l = line.trim(); if (!l) continue;
|
|
469
|
+
try { const item = JSON.parse(l); processItem(item._id || item.id, item.value || item.doc || item, scripts); } catch {}
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
const data = JSON.parse(text);
|
|
473
|
+
for (const [k, v] of Object.entries(data)) processItem(k, v, scripts);
|
|
474
|
+
}
|
|
475
|
+
return scripts.sort((a,b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// === Browser-side tar.gz parser ===
|
|
479
|
+
async function decompressGzip(uint8arr) {
|
|
480
|
+
const ds = new DecompressionStream('gzip');
|
|
481
|
+
const writer = ds.writable.getWriter();
|
|
482
|
+
writer.write(uint8arr);
|
|
483
|
+
writer.close();
|
|
484
|
+
const chunks = [];
|
|
485
|
+
const reader = ds.readable.getReader();
|
|
486
|
+
while (true) {
|
|
487
|
+
const { value, done } = await reader.read();
|
|
488
|
+
if (done) break;
|
|
489
|
+
chunks.push(value);
|
|
490
|
+
}
|
|
491
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
492
|
+
const out = new Uint8Array(total);
|
|
493
|
+
let pos = 0;
|
|
494
|
+
for (const c of chunks) { out.set(c, pos); pos += c.length; }
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function parseTar(buf) {
|
|
499
|
+
const dec = new TextDecoder('utf-8', { fatal: false });
|
|
500
|
+
const files = [];
|
|
501
|
+
let offset = 0;
|
|
502
|
+
while (offset + 512 <= buf.length) {
|
|
503
|
+
// name field: bytes 0-99
|
|
504
|
+
let end = offset;
|
|
505
|
+
while (end < offset + 100 && buf[end] !== 0) end++;
|
|
506
|
+
const name = dec.decode(buf.slice(offset, end)).replace(/^\.\//, '');
|
|
507
|
+
if (!name) break;
|
|
508
|
+
// size field: bytes 124-135 (octal)
|
|
509
|
+
const sizeStr = dec.decode(buf.slice(offset + 124, offset + 136)).replace(/\0/g, '').trim();
|
|
510
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
511
|
+
// typeflag byte 156: '0' or '\0' = regular file
|
|
512
|
+
const type = String.fromCharCode(buf[offset + 156]);
|
|
513
|
+
if (type === '0' || type === '\0') {
|
|
514
|
+
files.push({ name, content: buf.slice(offset + 512, offset + 512 + size) });
|
|
515
|
+
}
|
|
516
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
517
|
+
}
|
|
518
|
+
return files;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function parseArchiveInBrowser(arrayBuffer, filename) {
|
|
522
|
+
const nameLow = filename.toLowerCase();
|
|
523
|
+
let tarBuf;
|
|
524
|
+
if (nameLow.endsWith('.tar.gz') || nameLow.endsWith('.tgz')) {
|
|
525
|
+
tarBuf = await decompressGzip(new Uint8Array(arrayBuffer));
|
|
526
|
+
} else {
|
|
527
|
+
tarBuf = new Uint8Array(arrayBuffer);
|
|
528
|
+
}
|
|
529
|
+
const entries = parseTar(tarBuf);
|
|
530
|
+
const targets = ['objects.jsonl', 'objects.json', 'scripts.json', 'script.json'];
|
|
531
|
+
const dec = new TextDecoder('utf-8', { fatal: false });
|
|
532
|
+
for (const target of targets) {
|
|
533
|
+
const entry = entries.find(e => e.name === target || e.name.endsWith('/' + target));
|
|
534
|
+
if (entry) return parseJsonContent(dec.decode(entry.content), target);
|
|
535
|
+
}
|
|
536
|
+
throw new Error('Keine passende Datei im Archiv gefunden (objects.json, objects.jsonl, scripts.json)');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// === File Upload ===
|
|
540
|
+
document.getElementById('fileInput').addEventListener('change', function() {
|
|
541
|
+
if (!this.files || !this.files.length) return;
|
|
542
|
+
const file = this.files[0];
|
|
543
|
+
const name = file.name.toLowerCase();
|
|
544
|
+
const isArchive = name.endsWith('.tar.gz') || name.endsWith('.tgz') || name.endsWith('.tar');
|
|
545
|
+
|
|
546
|
+
// JSON/JSONL: parse directly in browser, no server needed
|
|
547
|
+
if (!isArchive) {
|
|
548
|
+
showLoader('Lese Datei...');
|
|
549
|
+
const reader = new FileReader();
|
|
550
|
+
reader.onprogress = ev => { if (ev.lengthComputable) updateProgress(Math.round(ev.loaded / ev.total * 90)); };
|
|
551
|
+
reader.onload = function() {
|
|
552
|
+
try {
|
|
553
|
+
updateProgress(100);
|
|
554
|
+
const scripts = parseJsonContent(reader.result, file.name);
|
|
555
|
+
hideLoader();
|
|
556
|
+
loadScripts(scripts);
|
|
557
|
+
setStatus(scripts.length + ' Skripte geladen aus: ' + file.name, 'success');
|
|
558
|
+
} catch(e) {
|
|
559
|
+
hideLoader();
|
|
560
|
+
setStatus('Fehler beim Parsen: ' + e.message, 'error');
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
reader.onerror = () => { hideLoader(); setStatus('Fehler beim Lesen der Datei.', 'error'); };
|
|
564
|
+
reader.readAsText(file, 'utf-8');
|
|
565
|
+
this.value = '';
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Archive: parse fully in browser using DecompressionStream + tar parser
|
|
570
|
+
showLoader('Lese Archiv...');
|
|
571
|
+
const archiveReader = new FileReader();
|
|
572
|
+
archiveReader.onprogress = ev => { if (ev.lengthComputable) updateProgress(Math.round(ev.loaded / ev.total * 60)); };
|
|
573
|
+
archiveReader.onload = async function() {
|
|
574
|
+
try {
|
|
575
|
+
updateProgress(70);
|
|
576
|
+
showLoaderSpinner('Entpacke Archiv...');
|
|
577
|
+
const scripts = await parseArchiveInBrowser(archiveReader.result, file.name); // result is ArrayBuffer
|
|
578
|
+
hideLoader();
|
|
579
|
+
loadScripts(scripts);
|
|
580
|
+
setStatus(scripts.length + ' Skripte geladen aus: ' + file.name, 'success');
|
|
581
|
+
} catch(e) {
|
|
582
|
+
hideLoader();
|
|
583
|
+
setStatus('Fehler: ' + e.message, 'error');
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
archiveReader.onerror = function() {
|
|
587
|
+
hideLoader();
|
|
588
|
+
setStatus('Fehler beim Lesen der Datei.', 'error');
|
|
589
|
+
};
|
|
590
|
+
archiveReader.readAsArrayBuffer(file);
|
|
591
|
+
this.value = '';
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// === Local Files ===
|
|
595
|
+
let localMenuOpen = false;
|
|
596
|
+
|
|
597
|
+
function toggleLocalFiles() {
|
|
598
|
+
const menu = document.getElementById('localMenu');
|
|
599
|
+
localMenuOpen = !localMenuOpen;
|
|
600
|
+
if (localMenuOpen) {
|
|
601
|
+
menu.classList.add('open');
|
|
602
|
+
menu.innerHTML = '<div class="dropdown-loading">⏳ Lade Dateiliste...</div>';
|
|
603
|
+
sendTo('listLocalFiles', {}, function(result) {
|
|
604
|
+
if (result && result.error) {
|
|
605
|
+
menu.innerHTML = '<div class="dropdown-empty">⚠️ ' + escapeHTML(result.error) + '</div>';
|
|
606
|
+
} else if (result && result.files && result.files.length > 0) {
|
|
607
|
+
menu.innerHTML = result.files.map(f =>
|
|
608
|
+
'<div class="dropdown-item" data-file="' + escapeHTML(f) + '">' +
|
|
609
|
+
escapeHTML(f) + '</div>'
|
|
610
|
+
).join('');
|
|
611
|
+
menu.querySelectorAll('.dropdown-item').forEach(el => {
|
|
612
|
+
el.addEventListener('click', function() { loadLocalFile(this.dataset.file); });
|
|
613
|
+
});
|
|
614
|
+
} else {
|
|
615
|
+
menu.innerHTML = '<div class="dropdown-empty">Keine Dateien gefunden in:<br>' + escapeHTML((result && result.path) || '') + '</div>';
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
// Close when clicking outside
|
|
619
|
+
setTimeout(() => document.addEventListener('click', closeLocalMenuOutside), 0);
|
|
620
|
+
} else {
|
|
621
|
+
menu.classList.remove('open');
|
|
622
|
+
document.removeEventListener('click', closeLocalMenuOutside);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function closeLocalMenuOutside(e) {
|
|
627
|
+
const wrapper = document.getElementById('localDropdown');
|
|
628
|
+
if (!wrapper.contains(e.target)) {
|
|
629
|
+
document.getElementById('localMenu').classList.remove('open');
|
|
630
|
+
localMenuOpen = false;
|
|
631
|
+
document.removeEventListener('click', closeLocalMenuOutside);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function loadLocalFile(filename) {
|
|
636
|
+
document.getElementById('localMenu').classList.remove('open');
|
|
637
|
+
localMenuOpen = false;
|
|
638
|
+
document.removeEventListener('click', closeLocalMenuOutside);
|
|
639
|
+
showLoaderSpinner('Lade und verarbeite ' + filename + '...');
|
|
640
|
+
sendTo('parseLocalFile', { filename: filename }, function(result) {
|
|
641
|
+
hideLoader();
|
|
642
|
+
if (result && result.error) {
|
|
643
|
+
setStatus('Fehler: ' + result.error, 'error');
|
|
644
|
+
} else if (result && result.scripts) {
|
|
645
|
+
loadScripts(result.scripts);
|
|
646
|
+
setStatus(result.scripts.length + ' Skripte geladen aus: ' + filename, 'success');
|
|
647
|
+
} else {
|
|
648
|
+
setStatus('Keine Skripte gefunden.', 'error');
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function loadScripts(scripts) {
|
|
654
|
+
scriptsData = scripts;
|
|
655
|
+
cur = { index: -1 };
|
|
656
|
+
openFolders.clear();
|
|
657
|
+
isAllExpanded = false;
|
|
658
|
+
document.getElementById('expandToggleBtn').innerHTML = '📂';
|
|
659
|
+
renderList();
|
|
660
|
+
document.getElementById('actionBar').style.display = 'none';
|
|
661
|
+
document.getElementById('codeContainer').className = 'code-empty';
|
|
662
|
+
document.getElementById('codeContainer').innerHTML = scripts.length > 0
|
|
663
|
+
? '// Skript im Baum links auswählen...'
|
|
664
|
+
: '<span style="color:#dc3545">Keine Skripte in diesem Backup gefunden.</span>';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// === Loader ===
|
|
668
|
+
function showLoader(text) {
|
|
669
|
+
document.getElementById('progressContainer').style.display = 'block';
|
|
670
|
+
document.getElementById('spinnerEl').style.display = 'none';
|
|
671
|
+
document.getElementById('progressCircle').style.background = 'conic-gradient(var(--primary) 0%, #e9ecef 0%)';
|
|
672
|
+
document.getElementById('progressPercent').textContent = '0%';
|
|
673
|
+
document.getElementById('loaderText').textContent = text || 'Laden...';
|
|
674
|
+
document.getElementById('loader').style.display = 'flex';
|
|
675
|
+
}
|
|
676
|
+
function showLoaderSpinner(text) {
|
|
677
|
+
document.getElementById('progressContainer').style.display = 'none';
|
|
678
|
+
document.getElementById('spinnerEl').style.display = 'block';
|
|
679
|
+
document.getElementById('loaderText').textContent = text || 'Verarbeite...';
|
|
680
|
+
document.getElementById('loader').style.display = 'flex';
|
|
681
|
+
}
|
|
682
|
+
function updateProgress(pct) {
|
|
683
|
+
document.getElementById('progressCircle').style.background =
|
|
684
|
+
'conic-gradient(var(--primary) ' + pct + '%, #e9ecef ' + pct + '%)';
|
|
685
|
+
document.getElementById('progressPercent').textContent = pct + '%';
|
|
686
|
+
}
|
|
687
|
+
function hideLoader() {
|
|
688
|
+
document.getElementById('loader').style.display = 'none';
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// === Tree ===
|
|
692
|
+
function toggleExpandAll() {
|
|
693
|
+
isAllExpanded = !isAllExpanded;
|
|
694
|
+
const btn = document.getElementById('expandToggleBtn');
|
|
695
|
+
btn.innerHTML = isAllExpanded ? '📁' : '📂';
|
|
696
|
+
btn.title = isAllExpanded ? 'Alle einklappen' : 'Alle aufklappen';
|
|
697
|
+
document.querySelectorAll('.tree-folder').forEach(el => {
|
|
698
|
+
const p = el.dataset.path;
|
|
699
|
+
const icon = el.querySelector('.folder-icon');
|
|
700
|
+
const children = el.nextElementSibling;
|
|
701
|
+
if (isAllExpanded) { icon.classList.add('open'); children.classList.add('open'); openFolders.add(p); }
|
|
702
|
+
else { icon.classList.remove('open'); children.classList.remove('open'); openFolders.delete(p); }
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function buildTree(data) {
|
|
707
|
+
const root = { children: {} };
|
|
708
|
+
data.forEach((s, idx) => {
|
|
709
|
+
const parts = s.path.split('.');
|
|
710
|
+
let node = root;
|
|
711
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
712
|
+
const p = parts[i];
|
|
713
|
+
if (!node.children[p]) node.children[p] = { isDir: true, name: p, children: {} };
|
|
714
|
+
node = node.children[p];
|
|
715
|
+
}
|
|
716
|
+
node.children[parts[parts.length - 1]] = { isDir: false, script: s, index: idx };
|
|
717
|
+
});
|
|
718
|
+
return root;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function renderTree(nodes, container, currentPath) {
|
|
722
|
+
currentPath = currentPath || '';
|
|
723
|
+
const keys = Object.keys(nodes).sort((a, b) => {
|
|
724
|
+
const nA = nodes[a], nB = nodes[b];
|
|
725
|
+
if (nA.isDir && !nB.isDir) return -1;
|
|
726
|
+
if (!nA.isDir && nB.isDir) return 1;
|
|
727
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
728
|
+
});
|
|
729
|
+
keys.forEach(k => {
|
|
730
|
+
const node = nodes[k];
|
|
731
|
+
const fullPath = currentPath ? currentPath + '.' + k : k;
|
|
732
|
+
if (node.isDir) {
|
|
733
|
+
const fd = document.createElement('div');
|
|
734
|
+
fd.className = 'tree-folder';
|
|
735
|
+
fd.dataset.path = fullPath;
|
|
736
|
+
const isOpen = openFolders.has(fullPath);
|
|
737
|
+
fd.innerHTML = '<span class="folder-icon' + (isOpen ? ' open' : '') + '">▶</span> 📁 ' + escapeHTML(node.name);
|
|
738
|
+
const cc = document.createElement('div');
|
|
739
|
+
cc.className = 'tree-children' + (isOpen ? ' open' : '');
|
|
740
|
+
fd.onclick = e => {
|
|
741
|
+
e.stopPropagation();
|
|
742
|
+
const nowOpen = cc.classList.toggle('open');
|
|
743
|
+
fd.querySelector('.folder-icon').classList.toggle('open');
|
|
744
|
+
if (nowOpen) openFolders.add(fullPath); else openFolders.delete(fullPath);
|
|
745
|
+
};
|
|
746
|
+
container.appendChild(fd);
|
|
747
|
+
renderTree(node.children, cc, fullPath);
|
|
748
|
+
container.appendChild(cc);
|
|
749
|
+
} else {
|
|
750
|
+
const sd = createScriptNode(node.script, node.index);
|
|
751
|
+
sd.classList.add('tree-script');
|
|
752
|
+
container.appendChild(sd);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function createScriptNode(s, idx) {
|
|
758
|
+
const badgeText = s.type === 'TypeScript' ? 'TS' : (s.type === 'Blockly' ? 'Blockly' : (s.type === 'Rules' ? 'RULES' : 'JS'));
|
|
759
|
+
const div = document.createElement('div');
|
|
760
|
+
div.className = 'script-item' + (cur.index === idx ? ' active' : '');
|
|
761
|
+
div.dataset.index = idx;
|
|
762
|
+
div.onclick = () => selectScript(idx);
|
|
763
|
+
div.innerHTML = '<div class="script-name" title="' + escapeHTML(s.path) + '">📄 ' + escapeHTML(s.name) + '</div>' +
|
|
764
|
+
'<span class="type-badge badge-' + s.type + '">' + badgeText + '</span>';
|
|
765
|
+
return div;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function renderList() {
|
|
769
|
+
const list = document.getElementById('list');
|
|
770
|
+
list.innerHTML = '';
|
|
771
|
+
const q = document.getElementById('q').value.toLowerCase();
|
|
772
|
+
const toggleBtn = document.getElementById('expandToggleBtn');
|
|
773
|
+
const tree = buildTree(scriptsData);
|
|
774
|
+
|
|
775
|
+
if (q) {
|
|
776
|
+
toggleBtn.style.display = 'none';
|
|
777
|
+
const filter = (node, path) => {
|
|
778
|
+
if (!node.isDir) {
|
|
779
|
+
return node.script.name.toLowerCase().includes(q) ||
|
|
780
|
+
node.script.path.toLowerCase().includes(q) ||
|
|
781
|
+
(node.script.source && node.script.source.toLowerCase().includes(q));
|
|
782
|
+
}
|
|
783
|
+
let has = false;
|
|
784
|
+
for (const k in node.children) {
|
|
785
|
+
const cp = path ? path + '.' + k : k;
|
|
786
|
+
if (filter(node.children[k], cp)) { has = true; openFolders.add(cp); }
|
|
787
|
+
else delete node.children[k];
|
|
788
|
+
}
|
|
789
|
+
return has;
|
|
790
|
+
};
|
|
791
|
+
for (const k in tree.children) {
|
|
792
|
+
if (!filter(tree.children[k], k)) delete tree.children[k];
|
|
793
|
+
else openFolders.add(k);
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
toggleBtn.style.display = 'flex';
|
|
797
|
+
}
|
|
798
|
+
renderTree(tree.children, list);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// === Script Viewer ===
|
|
802
|
+
function formatXml(xml) {
|
|
803
|
+
const PAD = ' ';
|
|
804
|
+
xml = xml.replace(/(>)\s*(<)(\/*)/g, '$1\n$2$3');
|
|
805
|
+
let pad = 0, out = '';
|
|
806
|
+
xml.split('\n').forEach(n => {
|
|
807
|
+
let indent = 0;
|
|
808
|
+
if (n.match(/.+<\/\w[^>]*>$/)) indent = 0;
|
|
809
|
+
else if (n.match(/^<\/\w/)) { if (pad) pad--; }
|
|
810
|
+
else if (n.match(/^<\w[^>]*[^\/]>.*$/)) indent = 1;
|
|
811
|
+
out += PAD.repeat(pad) + n + '\n';
|
|
812
|
+
pad += indent;
|
|
813
|
+
});
|
|
814
|
+
return out.trim();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function formatJson(s) {
|
|
818
|
+
try { return JSON.stringify(JSON.parse(s), null, 2); } catch { return s; }
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function selectScript(idx) {
|
|
822
|
+
cur.index = idx;
|
|
823
|
+
document.querySelectorAll('.script-item').forEach(el => {
|
|
824
|
+
el.classList.toggle('active', parseInt(el.dataset.index) === idx);
|
|
825
|
+
});
|
|
826
|
+
const data = scriptsData[idx];
|
|
827
|
+
let src = data.source || '', xmlStr = null, ruleStr = null;
|
|
828
|
+
|
|
829
|
+
if (data.type === 'Blockly') {
|
|
830
|
+
const m = src.match(/\/\/(JTND[A-Za-z0-9+/=%]+)/);
|
|
831
|
+
if (m) { try { xmlStr = formatXml(decodeURIComponent(atob(m[1]))); } catch {} }
|
|
832
|
+
} else if (data.type === 'Rules') {
|
|
833
|
+
const m = src.match(/\/\/({.*"triggers".*})/);
|
|
834
|
+
if (m) ruleStr = formatJson(m[1]);
|
|
835
|
+
else {
|
|
836
|
+
const m2 = src.match(/\/\* const demo = ({[\s\S]*?}); \*\//);
|
|
837
|
+
if (m2) ruleStr = formatJson(m2[1]);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
cur.name = data.name; cur.type = data.type;
|
|
842
|
+
cur.src = src; cur.xml = xmlStr; cur.rule = ruleStr;
|
|
843
|
+
|
|
844
|
+
document.getElementById('actionBar').style.display = 'block';
|
|
845
|
+
document.getElementById('codeContainer').className = 'code-display';
|
|
846
|
+
|
|
847
|
+
const vs = document.getElementById('viewSwitcher');
|
|
848
|
+
vs.innerHTML = '<button onclick="showView(\'src\')" class="btn btn-outline-light" id="btn-src">' + (cur.type === 'TypeScript' ? 'TS' : 'JS') + '</button>';
|
|
849
|
+
if (cur.xml) vs.innerHTML += '<button onclick="showView(\'xml\')" class="btn btn-outline-primary" id="btn-xml">BLOCKLY</button>';
|
|
850
|
+
if (cur.rule) vs.innerHTML += '<button onclick="showView(\'rule\')" class="btn btn-outline-success" id="btn-rule">RULES</button>';
|
|
851
|
+
|
|
852
|
+
showView(cur.xml ? 'xml' : (cur.rule ? 'rule' : 'src'));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function showView(v) {
|
|
856
|
+
cur.activeView = v;
|
|
857
|
+
document.querySelectorAll('#viewSwitcher button').forEach(b => b.classList.remove('active'));
|
|
858
|
+
const btn = document.getElementById('btn-' + v);
|
|
859
|
+
if (btn) btn.classList.add('active');
|
|
860
|
+
const txt = cur[v] || '';
|
|
861
|
+
document.getElementById('codeContainer').innerHTML =
|
|
862
|
+
txt.split('\n').map(l => '<span class="code-line">' + (escapeHTML(l) || ' ') + '</span>').join('');
|
|
863
|
+
const dlBtn = document.getElementById('dlBtn');
|
|
864
|
+
if (v === 'xml') dlBtn.textContent = 'DL .xml';
|
|
865
|
+
else if (v === 'rule') dlBtn.textContent = 'DL .json';
|
|
866
|
+
else dlBtn.textContent = cur.type === 'TypeScript' ? 'DL .ts' : 'DL .js';
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function copyCode(btn) {
|
|
870
|
+
const txt = cur[cur.activeView] || '';
|
|
871
|
+
const ta = document.createElement('textarea');
|
|
872
|
+
ta.value = txt; document.body.appendChild(ta); ta.select();
|
|
873
|
+
try {
|
|
874
|
+
document.execCommand('copy');
|
|
875
|
+
btn.textContent = '✓ Kopiert!';
|
|
876
|
+
btn.classList.replace('btn-outline-light', 'btn-outline-success');
|
|
877
|
+
} finally {
|
|
878
|
+
document.body.removeChild(ta);
|
|
879
|
+
setTimeout(() => {
|
|
880
|
+
btn.textContent = 'Code Kopieren';
|
|
881
|
+
btn.classList.replace('btn-outline-success', 'btn-outline-light');
|
|
882
|
+
}, 1500);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function downloadActive() {
|
|
887
|
+
const txt = cur[cur.activeView] || '';
|
|
888
|
+
const ext = cur.activeView === 'xml' ? '.xml' : (cur.activeView === 'rule' ? '.json' : (cur.type === 'TypeScript' ? '.ts' : '.js'));
|
|
889
|
+
const a = document.createElement('a');
|
|
890
|
+
a.href = URL.createObjectURL(new Blob([txt], { type: 'text/plain' }));
|
|
891
|
+
a.download = (cur.name || 'script') + ext;
|
|
892
|
+
a.click();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
document.getElementById('q').addEventListener('keyup', renderList);
|
|
896
|
+
</script>
|
|
897
|
+
</body>
|
|
898
|
+
</html>
|