webtun 1.5.3 → 1.5.5
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/README.md +25 -5
- package/bin/webtun.js +12 -0
- package/electron/main.js +176 -0
- package/electron/preload.js +7 -0
- package/package.json +43 -4
- package/postinstall.js +38 -20
- package/public/commands.js +1 -1
- package/public/index.html +610 -214
- package/public/manifest.json +1 -1
- package/public/sw.js +16 -3
- package/server.js +651 -166
package/public/index.html
CHANGED
|
@@ -147,10 +147,10 @@
|
|
|
147
147
|
RESET & BASE
|
|
148
148
|
═══════════════════════════════════════════════════════ */
|
|
149
149
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
150
|
-
html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--fg); font-family: var(--font-ui); font-size: 13px; padding-bottom: env(safe-area-inset-bottom); touch-action: manipulation; -webkit-overflow-scrolling: touch; -webkit-tap-highlight-color: transparent; }
|
|
150
|
+
html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--fg); font-family: var(--font-ui); font-size: 13px; padding-bottom: env(safe-area-inset-bottom); touch-action: pan-y manipulation; -webkit-overflow-scrolling: touch; -webkit-tap-highlight-color: transparent; overscroll-behavior: contain; }
|
|
151
151
|
|
|
152
152
|
button { background: none; border: none; cursor: pointer; color: inherit; font-family: inherit; }
|
|
153
|
-
input, select, textarea { font-family: var(--font-ui); background: var(--bg3); border: 1px solid var(--border); color: var(--fg); border-radius: var(--radius); padding: 6px 10px; outline: none; font-size:
|
|
153
|
+
input, select, textarea { font-family: var(--font-ui); background: var(--bg3); border: 1px solid var(--border); color: var(--fg); border-radius: var(--radius); padding: 6px 10px; outline: none; font-size: 13px; }
|
|
154
154
|
@media (pointer: coarse) { input, select, textarea { font-size: 16px; } }
|
|
155
155
|
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
|
156
156
|
a { color: var(--accent); text-decoration: none; }
|
|
@@ -165,7 +165,7 @@ a { color: var(--accent); text-decoration: none; }
|
|
|
165
165
|
@media (pointer: coarse) {
|
|
166
166
|
.tab { height: 38px; }
|
|
167
167
|
.tab .tab-close { width: 44px; height: 44px; }
|
|
168
|
-
#new-tab-btn { width: 36px; height: 36px; }
|
|
168
|
+
#new-tab-btn { width: 36px; height: 36px; min-width: 36px; min-height: 36px; }
|
|
169
169
|
.icon-btn { width: 40px; height: 40px; }
|
|
170
170
|
.file-action-btn { width: 36px; height: 36px; }
|
|
171
171
|
.search-btn { width: 32px; height: 32px; }
|
|
@@ -196,20 +196,22 @@ svg { display: block; }
|
|
|
196
196
|
LAYOUT
|
|
197
197
|
═══════════════════════════════════════════════════════ */
|
|
198
198
|
#app { display: flex; flex-direction: column; height: 100dvh; }
|
|
199
|
-
#header { display: flex; align-items: center; height: var(--header-h); background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 8px; padding-top: env(safe-area-inset-top, 0px); gap: 6px; flex-shrink: 0; z-index: var(--z-header); position: relative; }
|
|
199
|
+
#header { display: flex; align-items: center; height: var(--header-h); background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 8px; padding-top: max(0px, env(safe-area-inset-top, 0px)); gap: 6px; flex-shrink: 0; z-index: var(--z-header); position: relative; box-sizing: border-box; }
|
|
200
200
|
|
|
201
201
|
/* Upload progress bar in header */
|
|
202
202
|
#upload-progress { position: absolute; bottom: 0; left: 0; right: 0; height: 0; background: var(--bg3); z-index: calc(var(--z-header) + 1); overflow: visible; transition: height var(--transition-normal) ease; }
|
|
203
|
-
#upload-progress.active { height:
|
|
203
|
+
#upload-progress.active { height: 6px; }
|
|
204
204
|
#upload-progress-bar { height: 100%; width: 0%; background: var(--accent); transition: width var(--transition-slow) ease; border-radius: 0 2px 2px 0; }
|
|
205
205
|
#upload-progress.success #upload-progress-bar { background: var(--green); }
|
|
206
206
|
#upload-progress.error #upload-progress-bar { background: var(--red); }
|
|
207
|
-
#upload-progress-text { position: absolute; right: 8px; top: -
|
|
207
|
+
#upload-progress-text { position: absolute; right: 8px; top: -18px; font-size: 12px; font-weight: 600; color: var(--fg); background: var(--bg3); padding: 2px 6px; border-radius: var(--radius-sm); border: 1px solid var(--border); white-space: nowrap; opacity: 0; transition: var(--transition-normal); pointer-events: none; line-height: 1; box-shadow: 0 2px 8px var(--shadow-color); }
|
|
208
208
|
#upload-progress.active #upload-progress-text { opacity: 1; }
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#sidebar
|
|
209
|
+
#upload-cancel-btn { position: absolute; right: 8px; top: -28px; height: 22px; padding: 0 8px; font-size: 11px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); color: var(--fg2); cursor: pointer; display: none; z-index: 2; }
|
|
210
|
+
#upload-progress.active #upload-cancel-btn { display: block; pointer-events: auto; }
|
|
211
|
+
#main { display: flex; flex: 1; overflow: hidden; min-height: 0; position: relative; isolation: isolate; }
|
|
212
|
+
#sidebar { position: relative; width: var(--sidebar-w); background: var(--bg2); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; flex-shrink: 0; transition: width var(--transition-normal) ease, transform var(--transition-normal) ease; will-change: width, transform; backface-visibility: hidden; }
|
|
213
|
+
#sidebar.dragging { transition: none; will-change: width; }
|
|
214
|
+
#sidebar.hidden { width: 0 !important; overflow: hidden; }
|
|
213
215
|
#content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; min-height: 0; position: relative; }
|
|
214
216
|
#tab-bar { display: flex; align-items: center; height: var(--tab-h); background: var(--bg2); border-bottom: 1px solid var(--border); flex-shrink: 0; gap: 2px; padding: 0 4px; }
|
|
215
217
|
#tab-scroll { display: flex; align-items: center; gap: 2px; height: 100%; flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; mask-image: linear-gradient(to right, black calc(100% - 20px), transparent); -webkit-mask-image: linear-gradient(to right, black calc(100% - 20px), transparent); scrollbar-width: none; }
|
|
@@ -234,14 +236,16 @@ svg { display: block; }
|
|
|
234
236
|
.xterm-viewport { border-radius: 0; }
|
|
235
237
|
.term-bell-flash { animation: bellFlash 0.15s ease-out; }
|
|
236
238
|
@keyframes bellFlash { 0% { filter: brightness(1.8); } 100% { filter: brightness(1); } }
|
|
237
|
-
#terminals.tiles-mode { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-auto-rows: 1fr; gap: 4px; padding: 4px; position: relative; overflow: auto; min-height: 0; overscroll-behavior: none; }
|
|
239
|
+
#terminals.tiles-mode { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); grid-auto-rows: 1fr; gap: 4px; padding: 4px; position: relative; overflow: auto; min-height: 0; overscroll-behavior: none; }
|
|
238
240
|
#terminals.tiles-mode .term-wrapper { position: relative; inset: unset; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; min-height: 150px; }
|
|
239
241
|
#terminals.tiles-mode .term-wrapper.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
240
242
|
#terminals.tiles-mode .term-wrapper.active .term-tile-header { background: var(--bg); color: var(--accent); }
|
|
241
243
|
.term-tile-header { display: flex; align-items: center; height: 24px; padding: 0 8px; background: var(--bg3); border-bottom: 1px solid var(--border); font-size: 11px; color: var(--fg2); flex-shrink: 0; gap: 6px; cursor: pointer; user-select: none; }
|
|
244
|
+
.term-tile-header:focus-visible, .term-tile-header .tth-name:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
242
245
|
.term-tile-header .tth-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
243
246
|
.term-tile-header .tth-close { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); opacity: 0.4; font-size: 12px; line-height: 1; }
|
|
244
247
|
.term-tile-header .tth-close:hover { opacity: 1; background: var(--border); }
|
|
248
|
+
.term-tile-header .tth-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
245
249
|
#terminals.tiles-mode .xterm { flex: 1; }
|
|
246
250
|
#mobile-keys { display: none; height: var(--mobilekey-h); background: var(--bg3); border-top: 1px solid var(--border); flex-shrink: 0; overflow-x: auto; overflow-y: hidden; white-space: nowrap; padding: 4px 6px; gap: 4px; padding-bottom: calc(4px + env(safe-area-inset-bottom)); min-height: calc(var(--mobilekey-h) + env(safe-area-inset-bottom)); }
|
|
247
251
|
/* ═══════════════════════════════════════════════════════
|
|
@@ -263,6 +267,11 @@ svg { display: block; }
|
|
|
263
267
|
}
|
|
264
268
|
#editor-view { display: none; flex-direction: column; background: var(--bg); overflow: hidden; }
|
|
265
269
|
#editor-view.open { display: flex; }
|
|
270
|
+
#editor-view.fullscreen { position: absolute; inset: 0; z-index: 30; width: 100%; height: 100%; background: var(--bg); border: none; }
|
|
271
|
+
#editor-view.fullscreen #editor-header { border-bottom: 1px solid var(--border); background: var(--bg2); }
|
|
272
|
+
#editor-view.fullscreen #editor-area { flex: 1; min-height: 0; }
|
|
273
|
+
#editor-view.fullscreen #editor-area .CodeMirror { height: 100%; }
|
|
274
|
+
#editor-split-area { position: relative; }
|
|
266
275
|
|
|
267
276
|
/* Desktop: split layout */
|
|
268
277
|
@media (min-width: 769px) {
|
|
@@ -335,8 +344,11 @@ svg { display: block; }
|
|
|
335
344
|
background: var(--bg2); border-left: 1px solid var(--border);
|
|
336
345
|
display: flex; flex-direction: column;
|
|
337
346
|
transform: translateX(100%);
|
|
338
|
-
transition: var(--transition-normal);
|
|
347
|
+
transition: transform var(--transition-normal) ease;
|
|
348
|
+
will-change: transform;
|
|
349
|
+
backface-visibility: hidden;
|
|
339
350
|
z-index: var(--z-drawer);
|
|
351
|
+
contain: layout style;
|
|
340
352
|
}
|
|
341
353
|
#settings-panel.open {
|
|
342
354
|
transform: translateX(0);
|
|
@@ -361,7 +373,7 @@ svg { display: block; }
|
|
|
361
373
|
═══════════════════════════════════════════════════════ */
|
|
362
374
|
#logo { font-weight: 700; color: var(--accent); font-size: 15px; letter-spacing: 1px; user-select: none; padding: 0 4px; }
|
|
363
375
|
.header-spacer { flex: 1; }
|
|
364
|
-
.icon-btn { width:
|
|
376
|
+
.icon-btn { width: 36px; height: 36px; min-width: 36px; min-height: 36px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius); color: var(--fg2); transition: background var(--transition-fast), color var(--transition-fast); }
|
|
365
377
|
.icon-btn:hover { background: var(--bg4); color: var(--fg); }
|
|
366
378
|
.icon-btn.active { color: var(--accent); }
|
|
367
379
|
|
|
@@ -384,9 +396,9 @@ svg { display: block; }
|
|
|
384
396
|
.tunnel-inner { display: flex; align-items: center; gap: 5px; min-width: 0; }
|
|
385
397
|
.tunnel-url { font-size: 11px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
386
398
|
.tunnel-status, .tunnel-target { font-size: 9px; padding: 1px 5px; border-radius: 4px; flex-shrink: 0; display: inline-flex; align-items: center; gap: 2px; }
|
|
387
|
-
.tunnel-btn { width:
|
|
399
|
+
.tunnel-btn { width: 28px; height: 28px; min-width: 28px; min-height: 28px; flex-shrink: 0; }
|
|
388
400
|
.tunnel-btn.stop { color: var(--red); }
|
|
389
|
-
#conn-status { transition: background var(--transition-slow); }
|
|
401
|
+
#conn-status { transition: background var(--transition-slow); margin-left: 6px; }
|
|
390
402
|
.reconnect-spinner { display: inline-flex; vertical-align: -1px; margin-right: 4px; }
|
|
391
403
|
.reconnect-spinner svg { animation: reconnectSpin 1s linear infinite; }
|
|
392
404
|
@keyframes reconnectSpin { to { transform: rotate(360deg); } }
|
|
@@ -404,15 +416,17 @@ svg { display: block; }
|
|
|
404
416
|
.tab-close { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); opacity: 0.4; flex-shrink: 0; transition: opacity var(--transition-fast), background var(--transition-fast); }
|
|
405
417
|
.tab:hover .tab-close, .tab.active .tab-close { opacity: 0.6; }
|
|
406
418
|
.tab-close:hover { opacity: 1; background: var(--border); }
|
|
407
|
-
#new-tab-btn { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius); color: var(--fg2); flex-shrink: 0; }
|
|
419
|
+
#new-tab-btn { width: 28px; height: 28px; min-width: 28px; min-height: 28px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius); color: var(--fg2); flex-shrink: 0; position: sticky; right: 0; background: var(--bg2); border: 1px solid var(--border); z-index: 1; margin-left: 4px; }
|
|
408
420
|
#new-tab-btn:hover { background: var(--bg4); color: var(--fg); }
|
|
409
421
|
|
|
410
422
|
/* ═══════════════════════════════════════════════════════
|
|
411
423
|
SIDEBAR / FILE EXPLORER
|
|
412
424
|
═══════════════════════════════════════════════════════ */
|
|
413
|
-
#sidebar-header { display: flex; align-items: center;
|
|
414
|
-
#sidebar-header
|
|
415
|
-
#file-
|
|
425
|
+
#sidebar-header, #file-breadcrumb { display: flex; align-items: center; gap: 3px; padding: 3px 6px; border-bottom: 1px solid var(--border); flex-shrink: 0; min-height: 30px; background: var(--bg2); }
|
|
426
|
+
#sidebar-header { flex-wrap: wrap; height: auto; }
|
|
427
|
+
#file-breadcrumb { white-space: nowrap; overflow-x: auto; line-height: 1; scrollbar-width: none; font-size: 11px; color: var(--fg2); }
|
|
428
|
+
#sidebar-header input { flex: 1 1 60px; min-width: 40px; height: 24px; font-size: 12px; border-radius: var(--radius-sm); }
|
|
429
|
+
#file-list-wrap { flex: 1; overflow-y: auto; min-height: 0; touch-action: pan-y; -webkit-overflow-scrolling: touch; position: relative; overscroll-behavior: contain; }
|
|
416
430
|
#ptr-indicator { position: absolute; top: -40px; left: 0; right: 0; height: 36px; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 11px; color: var(--fg2); opacity: 0; pointer-events: none; }
|
|
417
431
|
#ptr-indicator .ptr-arrow { display: inline-flex; }
|
|
418
432
|
#ptr-indicator .ptr-arrow svg { transition: transform 0.2s ease; }
|
|
@@ -420,28 +434,28 @@ svg { display: block; }
|
|
|
420
434
|
#ptr-indicator.release .ptr-text { color: var(--accent); }
|
|
421
435
|
/* Loading state: dim content + indeterminate progress bar */
|
|
422
436
|
#file-list-wrap.loading #file-list { opacity: 0.45; transition: opacity var(--transition-fast); }
|
|
423
|
-
#file-list-wrap.loading::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height:
|
|
424
|
-
#file-list-wrap.loading::after { content: ''; position: absolute; top: 0; left: -30%; width: 30%; height:
|
|
437
|
+
#file-list-wrap.loading::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--bg3); z-index: 5; }
|
|
438
|
+
#file-list-wrap.loading::after { content: ''; position: absolute; top: 0; left: -30%; width: 30%; height: 3px; background: var(--accent); border-radius: 0 2px 2px 0; z-index: 6; animation: fileListProgress 0.9s ease-in-out infinite; box-shadow: 0 1px 4px var(--shadow-color); }
|
|
425
439
|
@keyframes fileListProgress { from { left: -30%; } to { left: 100%; } }
|
|
426
|
-
.file-list-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding:
|
|
440
|
+
.file-list-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px 12px; color: var(--fg2); font-size: 13px; text-align: center; gap: 6px; }
|
|
427
441
|
.file-list-empty svg { opacity: 0.4; }
|
|
428
|
-
.empty-actions { display: flex; gap:
|
|
442
|
+
.empty-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }
|
|
429
443
|
.empty-actions .btn { font-size: 12px; padding: 4px 12px; }
|
|
430
|
-
#file-breadcrumb { padding:
|
|
444
|
+
#file-breadcrumb { display: flex; align-items: center; gap: 0; padding: 2px 4px; font-size: 11px; color: var(--fg2); border-bottom: 1px solid var(--border); white-space: nowrap; overflow-x: auto; line-height: 1; flex-shrink: 0; scrollbar-width: none; }
|
|
431
445
|
#file-breadcrumb::-webkit-scrollbar { display: none; }
|
|
432
|
-
#file-breadcrumb a { transition: background var(--transition-fast); border-radius: var(--radius-sm); padding:
|
|
446
|
+
#file-breadcrumb a { transition: background var(--transition-fast); border-radius: var(--radius-sm); padding: 2px 6px !important; display: inline-flex !important; align-items: center; height: 24px; line-height: 1; }
|
|
433
447
|
#file-breadcrumb a:hover { background: var(--bg3); }
|
|
434
448
|
@media (pointer: coarse) {
|
|
435
449
|
#file-breadcrumb { padding: 10px 12px; font-size: 13px; }
|
|
436
450
|
#file-breadcrumb a { padding: 6px 8px !important; }
|
|
437
451
|
}
|
|
438
|
-
.file-item { display: flex; align-items: center; gap:
|
|
452
|
+
.file-item { display: flex; align-items: center; gap: 2px; padding: 1px 4px; cursor: pointer; border-bottom: 1px solid transparent; font-size: 12px; position: relative; user-select: none; white-space: nowrap; flex-wrap: nowrap; }
|
|
439
453
|
.file-item:hover { background: var(--bg3); }
|
|
440
454
|
.file-item.selected { background: var(--bg4); }
|
|
441
455
|
.file-item.focused { background: var(--bg3); outline: 1px solid var(--accent); outline-offset: -1px; }
|
|
442
|
-
.file-item .file-ellipsis { display: flex; align-items: center; justify-content: center; width:
|
|
456
|
+
.file-item .file-ellipsis { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; min-width: 32px; min-height: 32px; border-radius: var(--radius-sm); color: var(--fg2); opacity: 0.45; flex-shrink: 0; }
|
|
443
457
|
.file-item:hover .file-ellipsis { opacity: 1; }
|
|
444
|
-
.file-icon { font-size: 14px; width: 16px;
|
|
458
|
+
.file-icon { font-size: 14px; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
445
459
|
.file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; user-select: text; -webkit-user-select: text; }
|
|
446
460
|
.file-size { font-size: 10px; color: var(--fg2); flex-shrink: 0; user-select: text; -webkit-user-select: text; }
|
|
447
461
|
.file-dir { color: var(--accent); }
|
|
@@ -458,14 +472,14 @@ svg { display: block; }
|
|
|
458
472
|
#sidebar[data-select-mode="true"] .file-select-check { display: flex; }
|
|
459
473
|
#sidebar[data-select-mode="true"] .file-item { padding-left: 6px; }
|
|
460
474
|
.file-action-btn:hover { background: var(--border); color: var(--fg); }
|
|
461
|
-
#sidebar-footer { padding:
|
|
475
|
+
#sidebar-footer { padding: 4px 6px; border-top: 1px solid var(--border); display: flex; gap: 2px; flex-wrap: wrap; flex-shrink: 0; }
|
|
462
476
|
.sidebar-action-btn { flex: 1; min-width: 60px; height: 28px; background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius); color: var(--fg2); font-size: 11px; display: flex; align-items: center; justify-content: center; gap: 4px; transition: all var(--transition-fast); }
|
|
463
477
|
.sidebar-action-btn:hover { background: var(--bg4); color: var(--fg); border-color: var(--accent); }
|
|
464
478
|
|
|
465
479
|
/* Sidebar resize handle */
|
|
466
480
|
.sidebar-resize-handle {
|
|
467
|
-
position: absolute; right:
|
|
468
|
-
width:
|
|
481
|
+
position: absolute; right: -4px; top: 0; bottom: 0;
|
|
482
|
+
width: 12px; cursor: col-resize; z-index: var(--z-header);
|
|
469
483
|
background: transparent; transition: background var(--transition-fast); flex-shrink: 0;
|
|
470
484
|
}
|
|
471
485
|
.sidebar-resize-handle:hover,
|
|
@@ -521,11 +535,12 @@ svg { display: block; }
|
|
|
521
535
|
═══════════════════════════════════════════════════════ */
|
|
522
536
|
#search-bar { position: absolute; top: 8px; right: 12px; z-index: var(--z-popover); background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 8px; display: none; align-items: center; gap: 6px; box-shadow: 0 4px 16px var(--shadow-color); }
|
|
523
537
|
#search-bar.open { display: flex; }
|
|
538
|
+
#terminals.tiles-mode #search-bar { position: fixed; top: calc(var(--header-h) + var(--tab-h) + 8px); right: 12px; left: auto; z-index: var(--z-popover); }
|
|
524
539
|
#search-input { width: 200px; height: 26px; }
|
|
525
540
|
@media (max-width: 480px) { #search-bar { left: 8px; right: 8px; } #search-input { flex: 1; width: auto; min-width: 0; } }
|
|
526
541
|
.search-btn { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 4px; color: var(--fg2); }
|
|
527
542
|
.search-btn:hover { background: var(--bg4); color: var(--fg); }
|
|
528
|
-
.search-btn#search-case-btn { font-size: 11px; font-weight: bold; width:
|
|
543
|
+
.search-btn#search-case-btn { font-size: 11px; font-weight: bold; width: 28px; height: 28px; min-width: 28px; min-height: 28px; line-height: 28px; }
|
|
529
544
|
.search-btn#search-case-btn.active { background: var(--accent); color: var(--bg); }
|
|
530
545
|
#search-results { font-size: 11px; color: var(--fg2); white-space: nowrap; }
|
|
531
546
|
|
|
@@ -565,13 +580,15 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
565
580
|
.btn-danger:hover { filter: brightness(1.06); }
|
|
566
581
|
.btn:not(.btn-primary):not(.btn-ghost):not(.btn-danger):hover { background: var(--bg4); }
|
|
567
582
|
.btn:active { filter: brightness(0.95); }
|
|
568
|
-
.btn.loading { opacity: 0.
|
|
583
|
+
.btn.loading { opacity: 0.85; pointer-events: none; position: relative; }
|
|
569
584
|
.btn.loading::after {
|
|
570
585
|
content: ''; position: absolute; inset: 0; margin: auto;
|
|
571
586
|
width: 14px; height: 14px; border-radius: 50%;
|
|
572
587
|
border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
|
|
573
588
|
border-top-color: currentColor; animation: reconnectSpin 0.7s linear infinite;
|
|
574
589
|
}
|
|
590
|
+
.btn.loading[aria-busy="true"] { color: inherit; }
|
|
591
|
+
.btn.loading[aria-busy="true"] > * { opacity: 0.15; }
|
|
575
592
|
.field-error { display: none; color: var(--red); font-size: 11px; margin-top: 5px; line-height: 1.4; }
|
|
576
593
|
.field-error.show { display: block; }
|
|
577
594
|
|
|
@@ -718,17 +735,17 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
718
735
|
BOOKMARKS
|
|
719
736
|
═══════════════════════════════════════════════════════ */
|
|
720
737
|
#bookmarks-header {
|
|
721
|
-
display: flex; align-items: center; gap:
|
|
722
|
-
padding:
|
|
723
|
-
text-transform: uppercase; letter-spacing: 0.
|
|
738
|
+
display: flex; align-items: center; gap: 3px;
|
|
739
|
+
padding: 2px 6px; font-size: 10px; color: var(--fg2);
|
|
740
|
+
text-transform: uppercase; letter-spacing: 0.3px;
|
|
724
741
|
border-bottom: 1px solid var(--border); flex-shrink: 0; cursor: pointer;
|
|
725
742
|
}
|
|
726
743
|
#bookmarks-header:hover { color: var(--fg); }
|
|
727
744
|
#bookmarks-list { max-height: 0; overflow: hidden; transition: max-height var(--transition-normal); flex-shrink: 0; }
|
|
728
745
|
#bookmarks-list.open { max-height: 200px; overflow-y: auto; }
|
|
729
746
|
.bookmark-item {
|
|
730
|
-
display: flex; align-items: center; gap:
|
|
731
|
-
padding:
|
|
747
|
+
display: flex; align-items: center; gap: 3px;
|
|
748
|
+
padding: 1px 6px; font-size: 11px; cursor: pointer;
|
|
732
749
|
border-bottom: 1px solid transparent;
|
|
733
750
|
}
|
|
734
751
|
.bookmark-item:hover { background: var(--bg3); }
|
|
@@ -918,7 +935,7 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
918
935
|
#mobile-keys { padding: 4px 4px; gap: 2px; }
|
|
919
936
|
.mkey { min-width: 38px; padding: 0 6px; font-size: 11px; }
|
|
920
937
|
#header { padding: 0 4px; gap: 3px; }
|
|
921
|
-
.icon-btn { width:
|
|
938
|
+
.icon-btn { width: 36px; height: 36px; min-width: 36px; min-height: 36px; }
|
|
922
939
|
#logo { font-size: 13px; }
|
|
923
940
|
}
|
|
924
941
|
|
|
@@ -948,8 +965,9 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
948
965
|
|
|
949
966
|
/* Landscape orientation on small screens */
|
|
950
967
|
@media (max-width: 768px) and (orientation: landscape) {
|
|
951
|
-
#sidebar { width: 50vw; max-width: 320px; }
|
|
968
|
+
#sidebar { width: 50vw; max-width: 320px; top: 32px; }
|
|
952
969
|
#header { height: 32px; }
|
|
970
|
+
#settings-panel { top: 32px; }
|
|
953
971
|
#mobile-keys { height: 36px; min-height: calc(36px + env(safe-area-inset-bottom)); }
|
|
954
972
|
.mkey { height: 28px; min-width: 36px; font-size: 10px; }
|
|
955
973
|
.tab { height: 32px; font-size: 11px; }
|
|
@@ -966,14 +984,19 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
966
984
|
.terminal { background: #fff !important; color: #000 !important; }
|
|
967
985
|
#file-breadcrumb, #file-list-wrap { overflow: visible !important; }
|
|
968
986
|
}
|
|
987
|
+
@supports (width: env(titlebar-area-width, 100%)) {
|
|
988
|
+
#header { padding-left: env(titlebar-area-x, 0px); width: env(titlebar-area-width, 100%); }
|
|
989
|
+
}
|
|
969
990
|
#install-banner { display: none; }
|
|
970
991
|
@media (prefers-reduced-motion: reduce) {
|
|
971
|
-
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
|
992
|
+
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; animation-delay: 0ms !important; }
|
|
993
|
+
.overlay { backdrop-filter: none !important; }
|
|
994
|
+
#sidebar, #settings-panel, .modal { transition-duration: 0.01ms !important; }
|
|
972
995
|
}
|
|
973
996
|
</style>
|
|
974
997
|
</head>
|
|
975
998
|
<body data-theme="light">
|
|
976
|
-
<a class="skip-link" href="#content">Skip to content</a>
|
|
999
|
+
<a class="skip-link" href="#content" onclick="document.getElementById('content')?.setAttribute('tabindex','-1');document.getElementById('content')?.focus();">Skip to content</a>
|
|
977
1000
|
<script>
|
|
978
1001
|
// Safe localStorage wrapper — degrades gracefully when Tracking Prevention blocks storage
|
|
979
1002
|
const safeStorage = {
|
|
@@ -998,7 +1021,7 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
998
1021
|
<div id="pin-box">
|
|
999
1022
|
<h1 style="display:flex;align-items:center;justify-content:center;gap:6px"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><line x1="6" y1="8" x2="10" y2="8"/><line x1="6" y1="12" x2="10" y2="12"/><line x1="6" y1="16" x2="8" y2="16"/><line x1="12" y1="8" x2="18" y2="8"/><line x1="12" y1="12" x2="18" y2="12"/><line x1="12" y1="16" x2="16" y2="16"/><line x1="10" y1="16" x2="10" y2="16"/></svg>WebTun</h1>
|
|
1000
1023
|
<p>Enter PIN to continue</p>
|
|
1001
|
-
<input id="pin-input" type="password" placeholder="••••" autocomplete="off" maxlength="32" inputmode="
|
|
1024
|
+
<input id="pin-input" type="password" placeholder="••••" autocomplete="off" maxlength="32" inputmode="text" aria-label="PIN code">
|
|
1002
1025
|
<div id="pin-error" role="alert"></div>
|
|
1003
1026
|
<div style="margin-top:12px">
|
|
1004
1027
|
<button class="btn btn-primary" onclick="submitPin()">Unlock</button>
|
|
@@ -1048,7 +1071,7 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1048
1071
|
<button class="mm-item" role="menuitem" onclick="closeMoreMenu();toggleSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg> Search</button>
|
|
1049
1072
|
<button class="mm-item" role="menuitem" onclick="closeMoreMenu();openShortcuts()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 8h6M12 8v8"/><path d="m7 12 10-4"/><path d="m7 12 10 4"/></svg> Keyboard Shortcuts</button>
|
|
1050
1073
|
</div>
|
|
1051
|
-
<div id="upload-progress"><div id="upload-progress-bar"></div><span id="upload-progress-text"></span></div>
|
|
1074
|
+
<div id="upload-progress"><div id="upload-progress-bar"></div><span id="upload-progress-text"></span><button id="upload-cancel-btn" aria-label="Cancel upload" title="Cancel upload" onclick="cancelUpload()">Cancel</button></div>
|
|
1052
1075
|
</div>
|
|
1053
1076
|
<div id="reconnect-banner" style="display:none;padding:4px 12px;font-size:11px;background:var(--accent);color:#fff;text-align:center;flex-shrink:0"><span class="reconnect-spinner"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></span> Reconnecting…</div>
|
|
1054
1077
|
|
|
@@ -1085,14 +1108,14 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1085
1108
|
</div>
|
|
1086
1109
|
<div id="file-list"></div>
|
|
1087
1110
|
</div>
|
|
1088
|
-
<div id="select-actions" style="display:none;padding:8px
|
|
1111
|
+
<div id="select-actions" style="display:none;padding:6px 8px;border-top:1px solid var(--border);background:var(--bg2);gap:3px;flex-wrap:wrap">
|
|
1089
1112
|
<span id="select-count" style="font-size:11px;color:var(--fg2);line-height:28px;flex:1"></span>
|
|
1090
1113
|
<button class="sidebar-action-btn" id="select-all-btn" onclick="selectAllFiles()">Select All</button>
|
|
1091
1114
|
<button class="sidebar-action-btn" onclick="deleteSelected()">Delete</button>
|
|
1092
1115
|
<button class="sidebar-action-btn" onclick="downloadSelected()">Download</button>
|
|
1093
1116
|
<button class="sidebar-action-btn" onclick="clearSelection()">Cancel</button>
|
|
1094
1117
|
</div>
|
|
1095
|
-
<div id="bookmarks-header" onclick="toggleBookmarks()"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg> Bookmarks <span id="bm-count" style="margin-left:auto;font-size:10px"></span></div>
|
|
1118
|
+
<div id="bookmarks-header" role="button" tabindex="0" aria-expanded="true" aria-controls="bookmarks-list" onclick="toggleBookmarks()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleBookmarks();}"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg> Bookmarks <span id="bm-count" style="margin-left:auto;font-size:10px"></span></div>
|
|
1096
1119
|
<div id="bookmarks-list"></div>
|
|
1097
1120
|
<div id="sidebar-footer">
|
|
1098
1121
|
<button class="sidebar-action-btn" onclick="bookmarkCurrentDir()" title="Bookmark current directory">
|
|
@@ -1120,12 +1143,13 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1120
1143
|
</div>
|
|
1121
1144
|
|
|
1122
1145
|
<!-- Content -->
|
|
1123
|
-
<div id="content" role="main">
|
|
1146
|
+
<div id="content" role="main" tabindex="-1">
|
|
1124
1147
|
<div id="tab-bar">
|
|
1125
|
-
<div id="tab-scroll"
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1148
|
+
<div id="tab-scroll">
|
|
1149
|
+
<button id="new-tab-btn" onclick="newTab()" title="New terminal" aria-label="New terminal">
|
|
1150
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
1151
|
+
</button>
|
|
1152
|
+
</div>
|
|
1129
1153
|
</div>
|
|
1130
1154
|
<div id="editor-split-area">
|
|
1131
1155
|
<div id="terminals">
|
|
@@ -1138,7 +1162,7 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1138
1162
|
<button class="search-btn" title="Next (Enter)" onclick="searchNext()">
|
|
1139
1163
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
1140
1164
|
</button>
|
|
1141
|
-
<button class="search-btn" id="search-case-btn" title="Match Case" onclick="toggleSearchCase()">
|
|
1165
|
+
<button class="search-btn" id="search-case-btn" title="Match Case" aria-pressed="false" onclick="toggleSearchCase()">
|
|
1142
1166
|
Aa
|
|
1143
1167
|
</button>
|
|
1144
1168
|
<button class="search-btn" title="Close (Esc)" onclick="closeSearch()">
|
|
@@ -1158,9 +1182,9 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1158
1182
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
1159
1183
|
</button>
|
|
1160
1184
|
</div>
|
|
1161
|
-
<div class="cmd-lib-tabs">
|
|
1162
|
-
<button class="cmd-lib-tab active" id="cmd-tab-lib" onclick="switchCmdTab('library')">Library</button>
|
|
1163
|
-
<button class="cmd-lib-tab" id="cmd-tab-hist" onclick="switchCmdTab('history')">History</button>
|
|
1185
|
+
<div class="cmd-lib-tabs" role="tablist">
|
|
1186
|
+
<button class="cmd-lib-tab active" id="cmd-tab-lib" role="tab" aria-selected="true" aria-controls="cmd-lib-list" onclick="switchCmdTab('library')">Library</button>
|
|
1187
|
+
<button class="cmd-lib-tab" id="cmd-tab-hist" role="tab" aria-selected="false" aria-controls="cmd-hist-list" onclick="switchCmdTab('history')">History</button>
|
|
1164
1188
|
</div>
|
|
1165
1189
|
<input id="cmd-lib-search" type="text" placeholder="Filter commands…" oninput="filterCmdLib()">
|
|
1166
1190
|
<div id="cmd-lib-list"></div>
|
|
@@ -1187,7 +1211,7 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1187
1211
|
<button class="icon-btn" aria-label="Close editor" onclick="closeEditor()" title="Close editor">
|
|
1188
1212
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
1189
1213
|
</button>
|
|
1190
|
-
<span id="editor-filename-wrap"><span class="editor-dirty-dot" id="editor-dirty-dot" aria-label="Unsaved changes"></span><span id="editor-filename">—</span></span>
|
|
1214
|
+
<span id="editor-filename-wrap" aria-live="polite"><span class="editor-dirty-dot" id="editor-dirty-dot" aria-label="Unsaved changes" role="status"></span><span id="editor-filename">—</span></span>
|
|
1191
1215
|
<span id="editor-status"></span>
|
|
1192
1216
|
<button class="btn" id="md-preview-toggle" style="height:26px;padding:0 12px;font-size:12px;display:none" onclick="toggleMdPreview()">Preview</button>
|
|
1193
1217
|
<button class="icon-btn" id="preview-refresh-btn" aria-label="Refresh preview" title="Refresh preview (Ctrl+Shift+R)" onclick="refreshPreview()" style="display:none">
|
|
@@ -1196,11 +1220,14 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1196
1220
|
<button class="icon-btn active" id="editor-split-toggle" aria-label="Toggle split orientation" onclick="toggleEditorSplit()" title="Switch to horizontal split">
|
|
1197
1221
|
<svg id="split-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/></svg>
|
|
1198
1222
|
</button>
|
|
1223
|
+
<button class="icon-btn" id="editor-fullscreen-toggle" aria-label="Fullscreen" title="Fullscreen (F11)" onclick="toggleEditorFullscreen()">
|
|
1224
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
|
1225
|
+
</button>
|
|
1199
1226
|
<button class="btn btn-primary" id="editor-save-btn" style="height:26px;padding:0 12px;font-size:12px" onclick="saveFile()">Save</button>
|
|
1200
1227
|
</div>
|
|
1201
1228
|
<div id="editor-area">
|
|
1202
1229
|
<textarea id="editor-textarea" spellcheck="false"></textarea>
|
|
1203
|
-
<div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-
|
|
1230
|
+
<div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-scripts"></iframe></div>
|
|
1204
1231
|
</div>
|
|
1205
1232
|
</div>
|
|
1206
1233
|
</div>
|
|
@@ -1521,7 +1548,7 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1521
1548
|
<h3>Top Processes by CPU</h3>
|
|
1522
1549
|
<div class="sys-table-wrap">
|
|
1523
1550
|
<table class="sys-table" id="sys-proc-table">
|
|
1524
|
-
<thead><tr><th>PID</th><th>User</th><th>CPU%</th><th>Mem%</th><th>Command</th></tr></thead>
|
|
1551
|
+
<thead><tr><th>PID</th><th>User</th><th>CPU%</th><th>Mem%</th><th>Command</th><th style="width:48px">Kill</th></tr></thead>
|
|
1525
1552
|
<tbody id="sys-proc-body"></tbody>
|
|
1526
1553
|
</table>
|
|
1527
1554
|
</div>
|
|
@@ -1703,7 +1730,18 @@ function loadTabState() {
|
|
|
1703
1730
|
} catch(e) { console.warn('loadTabState error:', e); return []; }
|
|
1704
1731
|
}
|
|
1705
1732
|
|
|
1706
|
-
|
|
1733
|
+
function saveCurrentPath() {
|
|
1734
|
+
try { safeStorage.setItem('wt-current-path', currentPath); } catch(e) { console.warn('Failed to save path:', e); }
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function loadCurrentPath() {
|
|
1738
|
+
try {
|
|
1739
|
+
const val = safeStorage.getItem('wt-current-path');
|
|
1740
|
+
return typeof val === 'string' && val ? val : null;
|
|
1741
|
+
} catch(e) { console.warn('loadCurrentPath error:', e); return null; }
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// ════════════════════════════════════════════════════════
|
|
1707
1745
|
// INIT
|
|
1708
1746
|
// ═══════════════════════════════════════════════════════
|
|
1709
1747
|
function handlePinKeydown(e) {
|
|
@@ -1770,10 +1808,22 @@ async function unlockApp() {
|
|
|
1770
1808
|
|
|
1771
1809
|
const info = await api('/api/home');
|
|
1772
1810
|
homeDir = info.home;
|
|
1773
|
-
currentPath = homeDir;
|
|
1774
1811
|
serverPlatform = info.platform || '';
|
|
1775
1812
|
document.getElementById('hostname-badge').textContent = info.hostname || '—';
|
|
1776
1813
|
|
|
1814
|
+
// Restore last visited path
|
|
1815
|
+
const savedPath = loadCurrentPath();
|
|
1816
|
+
if (savedPath) {
|
|
1817
|
+
try {
|
|
1818
|
+
// Validate saved path is within workspace or full fs is allowed
|
|
1819
|
+
currentPath = savedPath;
|
|
1820
|
+
} catch(e) {
|
|
1821
|
+
currentPath = homeDir;
|
|
1822
|
+
}
|
|
1823
|
+
} else {
|
|
1824
|
+
currentPath = homeDir;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1777
1827
|
// Check tmux availability
|
|
1778
1828
|
const sessInfo = await api('/api/sessions');
|
|
1779
1829
|
hasTmux = sessInfo.tmux === true;
|
|
@@ -1794,6 +1844,7 @@ async function unlockApp() {
|
|
|
1794
1844
|
setupSwipeGestures();
|
|
1795
1845
|
setupFileListTouch();
|
|
1796
1846
|
setupFileListKeyboard();
|
|
1847
|
+
setupFileListContextMenu();
|
|
1797
1848
|
setupCtxMenuKeyboard();
|
|
1798
1849
|
setupMoreMenuKeyboard();
|
|
1799
1850
|
restoreSidebarWidth();
|
|
@@ -1980,30 +2031,36 @@ function setupTabInlineRename(tabTitleSpan, tab) {
|
|
|
1980
2031
|
}
|
|
1981
2032
|
|
|
1982
2033
|
function setupTabSwipeGesture(tabEl, id) {
|
|
1983
|
-
let swipeStartX = 0;
|
|
2034
|
+
let swipeStartX = 0, swipeStartY = 0;
|
|
1984
2035
|
let swipeStartScroll = 0;
|
|
1985
2036
|
tabEl.addEventListener('touchstart', e => {
|
|
1986
2037
|
swipeStartX = e.touches[0].clientX;
|
|
2038
|
+
swipeStartY = e.touches[0].clientY;
|
|
1987
2039
|
swipeStartScroll = tabEl.parentElement.scrollLeft;
|
|
1988
2040
|
}, { passive: true });
|
|
1989
2041
|
tabEl.addEventListener('touchmove', e => {
|
|
1990
2042
|
if (swipeStartX === 0) return;
|
|
1991
2043
|
if (tabEl.parentElement.scrollLeft !== swipeStartScroll) { swipeStartX = 0; return; }
|
|
1992
2044
|
const dx = e.touches[0].clientX - swipeStartX;
|
|
1993
|
-
|
|
2045
|
+
const dy = e.touches[0].clientY - swipeStartY;
|
|
2046
|
+
// Require horizontal swipe dominant (dx > dy*1.5) to avoid scroll confusion (U59)
|
|
2047
|
+
if (Math.abs(dx) < Math.abs(dy) * 1.5) return;
|
|
2048
|
+
if (Math.abs(dx) > 5) {
|
|
2049
|
+
e.preventDefault();
|
|
1994
2050
|
tabEl.style.transform = `translateX(${dx}px)`;
|
|
1995
2051
|
tabEl.style.opacity = Math.max(0.3, 1 - Math.abs(dx) / 200);
|
|
1996
2052
|
tabEl.style.background = `rgba(247,118,142,${Math.min(Math.abs(dx) / 80, 1) * 0.2})`;
|
|
1997
2053
|
}
|
|
1998
|
-
}, { passive:
|
|
2054
|
+
}, { passive: false });
|
|
1999
2055
|
tabEl.addEventListener('touchend', e => {
|
|
2000
2056
|
if (swipeStartX === 0) return;
|
|
2001
2057
|
const dx = e.changedTouches[0].clientX - swipeStartX;
|
|
2058
|
+
const dy = e.changedTouches[0].clientY - swipeStartY;
|
|
2002
2059
|
tabEl.style.transform = '';
|
|
2003
2060
|
tabEl.style.opacity = '';
|
|
2004
2061
|
tabEl.style.background = '';
|
|
2005
|
-
if (Math.abs(dx) >
|
|
2006
|
-
swipeStartX = 0;
|
|
2062
|
+
if (Math.abs(dx) > 90 && Math.abs(dx) > Math.abs(dy) * 1.5) closeTab({ stopPropagation() {} }, id);
|
|
2063
|
+
swipeStartX = 0; swipeStartY = 0;
|
|
2007
2064
|
}, { passive: true });
|
|
2008
2065
|
}
|
|
2009
2066
|
|
|
@@ -2045,7 +2102,10 @@ function createTabButton(tab) {
|
|
|
2045
2102
|
setupTabInlineRename(titleSpan, tab);
|
|
2046
2103
|
setupTabSwipeGesture(tabEl, id);
|
|
2047
2104
|
|
|
2048
|
-
document.getElementById('tab-scroll')
|
|
2105
|
+
const scroll = document.getElementById('tab-scroll');
|
|
2106
|
+
const newBtn = document.getElementById('new-tab-btn');
|
|
2107
|
+
if (newBtn && newBtn.parentElement === scroll) scroll.insertBefore(tabEl, newBtn);
|
|
2108
|
+
else scroll.appendChild(tabEl);
|
|
2049
2109
|
tab.el = tabEl;
|
|
2050
2110
|
}
|
|
2051
2111
|
|
|
@@ -2278,19 +2338,21 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
2278
2338
|
if (tab.closed) return;
|
|
2279
2339
|
updateConnStatus(false);
|
|
2280
2340
|
tab.reconnectAttempts = (tab.reconnectAttempts || 0) + 1;
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2341
|
+
const _attempt = tab.reconnectAttempts;
|
|
2342
|
+
const _maxAttempts = 10;
|
|
2343
|
+
const spinnerHtml = '<span class="reconnect-spinner"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></span>';
|
|
2344
|
+
const banner = document.getElementById('reconnect-banner');
|
|
2345
|
+
if (banner) {
|
|
2346
|
+
if (_attempt >= 3) {
|
|
2347
|
+
banner.innerHTML = `Connection lost — retry ${_attempt}/${_maxAttempts}. <button class="btn btn-primary" onclick="manualReconnect()" style="height:26px;padding:0 12px;font-size:11px;margin-left:8px">Reconnect</button>`;
|
|
2348
|
+
} else {
|
|
2349
|
+
banner.innerHTML = `${spinnerHtml} Reconnecting (${_attempt}/${_maxAttempts})…`;
|
|
2286
2350
|
}
|
|
2287
|
-
|
|
2288
|
-
const banner = document.getElementById('reconnect-banner');
|
|
2289
|
-
if (banner) banner.style.display = 'block';
|
|
2351
|
+
banner.style.display = 'block';
|
|
2290
2352
|
}
|
|
2291
2353
|
tab.reconnectDelay = Math.min((tab.reconnectDelay || 1000) * 2, 15000);
|
|
2292
2354
|
const secs = tab.reconnectDelay / 1000;
|
|
2293
|
-
tab.term.writeln(`\r\n\x1b[33m[Disconnected — reconnecting in ${secs}s…]\x1b[0m`);
|
|
2355
|
+
tab.term.writeln(`\r\n\x1b[33m[Disconnected — reconnecting in ${secs}s (attempt ${_attempt}/${_maxAttempts})…]\x1b[0m`);
|
|
2294
2356
|
tab.reconnectTimer = setTimeout(() => {
|
|
2295
2357
|
if (!tab.closed) connectWebSocket(tab, true);
|
|
2296
2358
|
}, tab.reconnectDelay);
|
|
@@ -2305,7 +2367,7 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
2305
2367
|
function manualReconnect() {
|
|
2306
2368
|
const banner = document.getElementById('reconnect-banner');
|
|
2307
2369
|
if (banner) {
|
|
2308
|
-
banner.innerHTML = '<span class="reconnect-spinner"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></span> Reconnecting…';
|
|
2370
|
+
banner.innerHTML = '<span class="reconnect-spinner"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></span> Reconnecting (1/10)…';
|
|
2309
2371
|
banner.style.display = 'block';
|
|
2310
2372
|
}
|
|
2311
2373
|
tabs.forEach(tab => {
|
|
@@ -2389,12 +2451,15 @@ function initTerminal(tab) {
|
|
|
2389
2451
|
term.unicode.activeVersion = '11';
|
|
2390
2452
|
} catch (_) {}
|
|
2391
2453
|
|
|
2392
|
-
// GPU-accelerated renderer — falls back to canvas if WebGL unavailable
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2454
|
+
// GPU-accelerated renderer — falls back to canvas if WebGL unavailable (skip if >4 tabs to guard memory)
|
|
2455
|
+
if (tabs.filter(t => t.term).length < 4) {
|
|
2456
|
+
try {
|
|
2457
|
+
const webglAddon = new WebglAddon.WebglAddon();
|
|
2458
|
+
webglAddon.onContextLoss(() => { try { webglAddon.dispose(); } catch {} try { term.element?.querySelector('canvas')?.remove(); } catch {} });
|
|
2459
|
+
term.loadAddon(webglAddon);
|
|
2460
|
+
tab._webglAddon = webglAddon;
|
|
2461
|
+
} catch (_) {}
|
|
2462
|
+
}
|
|
2398
2463
|
|
|
2399
2464
|
term.open(tab.wrapper);
|
|
2400
2465
|
|
|
@@ -2408,7 +2473,7 @@ function initTerminal(tab) {
|
|
|
2408
2473
|
tab.textarea.name = 'terminal-input';
|
|
2409
2474
|
}
|
|
2410
2475
|
|
|
2411
|
-
// Intercept paste events to use bracketed paste mode (required for TUI apps like vim, nano, mc)
|
|
2476
|
+
// Intercept paste events to use bracketed paste mode (required for TUI apps like vim, nano, mc) — chunk 60KB with loop
|
|
2412
2477
|
if (tab.textarea) {
|
|
2413
2478
|
tab.textarea.addEventListener('paste', e => {
|
|
2414
2479
|
e.preventDefault();
|
|
@@ -2417,9 +2482,13 @@ function initTerminal(tab) {
|
|
|
2417
2482
|
tab._lastPasteText = text;
|
|
2418
2483
|
const bracketed = '\x1b[200~' + text + '\x1b[201~';
|
|
2419
2484
|
const enc = new TextEncoder().encode(bracketed);
|
|
2420
|
-
const
|
|
2421
|
-
|
|
2422
|
-
|
|
2485
|
+
const CHUNK = 60 * 1024;
|
|
2486
|
+
for (let off = 0; off < enc.length; off += CHUNK) {
|
|
2487
|
+
const slice = enc.subarray(off, Math.min(off + CHUNK, enc.length));
|
|
2488
|
+
const buf = new Uint8Array(1 + slice.length);
|
|
2489
|
+
buf[0] = 0x00; buf.set(slice, 1);
|
|
2490
|
+
try { tab.ws.send(buf.buffer); } catch {}
|
|
2491
|
+
}
|
|
2423
2492
|
}, true);
|
|
2424
2493
|
}
|
|
2425
2494
|
|
|
@@ -2436,15 +2505,18 @@ function initTerminal(tab) {
|
|
|
2436
2505
|
tab.el.querySelector('.tab-title').textContent = tab.title;
|
|
2437
2506
|
});
|
|
2438
2507
|
|
|
2439
|
-
// Bell: visual flash + desktop notification for background tabs
|
|
2508
|
+
// Bell: visual flash + desktop notification for background tabs (also flash parent for WebGL)
|
|
2440
2509
|
term.onBell(() => {
|
|
2441
|
-
// Visual flash on terminal screen
|
|
2510
|
+
// Visual flash on terminal screen and parent (WebGL uses canvas, so also flash parent)
|
|
2442
2511
|
const screen = term.element?.querySelector('.xterm-screen');
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2512
|
+
const parent = term.element;
|
|
2513
|
+
[screen, parent].forEach(el => {
|
|
2514
|
+
if (!el) return;
|
|
2515
|
+
el.classList.remove('term-bell-flash');
|
|
2516
|
+
void el.offsetWidth; // reflow to restart animation
|
|
2517
|
+
el.classList.add('term-bell-flash');
|
|
2518
|
+
setTimeout(() => el.classList.remove('term-bell-flash'), 150);
|
|
2519
|
+
});
|
|
2448
2520
|
// Desktop notification if tab is in the background
|
|
2449
2521
|
if (document.hidden && settings.bell && Notification.permission === 'granted') {
|
|
2450
2522
|
try {
|
|
@@ -2543,27 +2615,42 @@ function initTerminal(tab) {
|
|
|
2543
2615
|
if (!tab.closed) connectWebSocket(tab, false);
|
|
2544
2616
|
});
|
|
2545
2617
|
|
|
2546
|
-
// Scroll terminal on wheel
|
|
2618
|
+
// Scroll terminal on wheel — with try/catch fallback for isMouseTracking
|
|
2547
2619
|
tab.wrapper.addEventListener('wheel', e => {
|
|
2548
2620
|
if (e.ctrlKey || e.metaKey) return;
|
|
2549
2621
|
e.preventDefault();
|
|
2550
2622
|
// TUI apps with mouse tracking handle scroll themselves — forward the event to xterm.js
|
|
2623
|
+
let isTracking = false;
|
|
2551
2624
|
try {
|
|
2552
2625
|
const core = term._core;
|
|
2553
|
-
if (core
|
|
2626
|
+
if (core?.coreMouseService?.isMouseTrackingActive?.()) isTracking = core.coreMouseService.isMouseTrackingActive();
|
|
2627
|
+
// fallback check via private API may throw across xterm versions
|
|
2628
|
+
} catch (_) { isTracking = false; }
|
|
2629
|
+
if (isTracking) {
|
|
2630
|
+
try {
|
|
2554
2631
|
const vp = tab.wrapper.querySelector('.xterm-viewport');
|
|
2555
|
-
if (vp) vp.dispatchEvent(new WheelEvent('wheel', e));
|
|
2632
|
+
if (vp) vp.dispatchEvent(new WheelEvent('wheel', { deltaY: e.deltaY, deltaMode: e.deltaMode, deltaX: e.deltaX, clientX: e.clientX, clientY: e.clientY, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey, bubbles: true }));
|
|
2556
2633
|
return;
|
|
2557
|
-
}
|
|
2558
|
-
}
|
|
2634
|
+
} catch (_) { /* fallback to manual scroll */ }
|
|
2635
|
+
}
|
|
2559
2636
|
const atTop = term.buffer.active.baseY === 0;
|
|
2560
2637
|
const atBottom = term.buffer.active.baseY + term.rows >= term.buffer.active.length;
|
|
2561
2638
|
if (tilesMode && ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom))) return;
|
|
2562
2639
|
let delta = e.deltaY;
|
|
2563
2640
|
if (e.deltaMode === 1) delta *= 16;
|
|
2564
2641
|
else if (e.deltaMode === 2) delta *= 16 * term.rows;
|
|
2565
|
-
|
|
2566
|
-
|
|
2642
|
+
let lineHeight = 16;
|
|
2643
|
+
try {
|
|
2644
|
+
const d = term._core?.viewportRenderer?.dimensions;
|
|
2645
|
+
if (d?.actualCellHeight) lineHeight = d.actualCellHeight;
|
|
2646
|
+
else {
|
|
2647
|
+
const rowsEl = tab.wrapper.querySelector('.xterm-rows');
|
|
2648
|
+
lineHeight = rowsEl && rowsEl.children[0] ? rowsEl.children[0].getBoundingClientRect().height || 16 : 16;
|
|
2649
|
+
}
|
|
2650
|
+
} catch (_) {
|
|
2651
|
+
const rowsEl = tab.wrapper.querySelector('.xterm-rows');
|
|
2652
|
+
lineHeight = rowsEl && rowsEl.children[0] ? rowsEl.children[0].getBoundingClientRect().height || 16 : 16;
|
|
2653
|
+
}
|
|
2567
2654
|
const lines = Math.sign(delta) * Math.max(1, Math.round(Math.abs(delta) / lineHeight));
|
|
2568
2655
|
term.scrollLines(lines);
|
|
2569
2656
|
}, { passive: false });
|
|
@@ -2720,19 +2807,32 @@ function layoutTiles() {
|
|
|
2720
2807
|
}
|
|
2721
2808
|
hdr = document.createElement('div');
|
|
2722
2809
|
hdr.className = 'term-tile-header';
|
|
2810
|
+
hdr.setAttribute('tabindex', '0');
|
|
2811
|
+
hdr.setAttribute('role', 'button');
|
|
2812
|
+
hdr.setAttribute('aria-label', 'Activate ' + tab.title);
|
|
2723
2813
|
const tthName = document.createElement('span');
|
|
2724
2814
|
tthName.className = 'tth-name';
|
|
2815
|
+
tthName.setAttribute('tabindex', '0');
|
|
2816
|
+
tthName.setAttribute('role', 'button');
|
|
2817
|
+
tthName.setAttribute('aria-label', 'Focus terminal ' + tab.title);
|
|
2725
2818
|
tthName.textContent = tab.title;
|
|
2726
2819
|
hdr.appendChild(tthName);
|
|
2727
2820
|
const tthClose = document.createElement('button');
|
|
2728
2821
|
tthClose.className = 'tth-close';
|
|
2729
2822
|
tthClose.setAttribute('aria-label', 'Close tab');
|
|
2823
|
+
tthClose.setAttribute('tabindex', '0');
|
|
2730
2824
|
tthClose.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
2731
2825
|
hdr.appendChild(tthClose);
|
|
2732
2826
|
hdr.addEventListener('click', e => {
|
|
2733
2827
|
if (e.target.closest('.tth-close')) return;
|
|
2734
2828
|
activateTab(tab.id);
|
|
2735
2829
|
});
|
|
2830
|
+
hdr.addEventListener('keydown', e => {
|
|
2831
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activateTab(tab.id); }
|
|
2832
|
+
});
|
|
2833
|
+
tthName.addEventListener('keydown', e => {
|
|
2834
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); activateTab(tab.id); tab.term?.focus(); }
|
|
2835
|
+
});
|
|
2736
2836
|
hdr.querySelector('.tth-close').addEventListener('click', e => {
|
|
2737
2837
|
e.stopPropagation();
|
|
2738
2838
|
closeTab(e, tab.id);
|
|
@@ -2792,20 +2892,10 @@ async function loadFiles(dir) {
|
|
|
2792
2892
|
if (loadFilesAbortController) loadFilesAbortController.abort();
|
|
2793
2893
|
loadFilesAbortController = new AbortController();
|
|
2794
2894
|
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
} else {
|
|
2798
|
-
if (currentPath && currentPath !== dir) {
|
|
2799
|
-
navHistory.push(currentPath);
|
|
2800
|
-
}
|
|
2801
|
-
navForwardHistory = [];
|
|
2802
|
-
}
|
|
2895
|
+
const prevPath = currentPath;
|
|
2896
|
+
const shouldPushHistory = !skipHistoryPush && currentPath && currentPath !== dir;
|
|
2803
2897
|
skipHistoryPush = false;
|
|
2804
2898
|
|
|
2805
|
-
currentPath = dir;
|
|
2806
|
-
document.getElementById('path-input').value = dir;
|
|
2807
|
-
renderBreadcrumb(dir);
|
|
2808
|
-
|
|
2809
2899
|
const list = document.getElementById('file-list');
|
|
2810
2900
|
|
|
2811
2901
|
setFileListLoading(true);
|
|
@@ -2818,19 +2908,52 @@ async function loadFiles(dir) {
|
|
|
2818
2908
|
const errorDiv = document.createElement('div');
|
|
2819
2909
|
errorDiv.style.cssText = 'padding:16px;text-align:center;';
|
|
2820
2910
|
errorDiv.innerHTML = '<div style="color:var(--red);margin-bottom:8px">Failed to load directory</div>' +
|
|
2911
|
+
'<div style="color:var(--fg2);font-size:11px;margin-bottom:8px;word-break:break-all">' + escHtml(e.message || 'Unknown error') + '</div>' +
|
|
2821
2912
|
'<button class="btn btn-primary" style="height:28px;padding:0 12px;font-size:11px">Retry</button>';
|
|
2822
2913
|
list.innerHTML = '';
|
|
2823
2914
|
list.appendChild(errorDiv);
|
|
2824
2915
|
errorDiv.querySelector('button').addEventListener('click', () => loadFiles(dir));
|
|
2916
|
+
toast(e.message || 'Failed to load directory', 'error');
|
|
2825
2917
|
return;
|
|
2826
2918
|
}
|
|
2827
2919
|
|
|
2828
2920
|
if (!data.files) {
|
|
2829
2921
|
setFileListLoading(false);
|
|
2830
|
-
|
|
2922
|
+
const msg = data.error || 'Failed to load directory';
|
|
2923
|
+
// Revert optimistic path — don't save failed dir
|
|
2924
|
+
// Show error instead of silent bounce
|
|
2925
|
+
const errorDiv = document.createElement('div');
|
|
2926
|
+
errorDiv.style.cssText = 'padding:16px;text-align:center;';
|
|
2927
|
+
errorDiv.innerHTML = '<div style="color:var(--red);margin-bottom:8px">' + escHtml(msg) + '</div>' +
|
|
2928
|
+
'<button class="btn btn-primary" style="height:28px;padding:0 12px;font-size:11px">Retry</button>';
|
|
2929
|
+
list.innerHTML = '';
|
|
2930
|
+
list.appendChild(errorDiv);
|
|
2931
|
+
errorDiv.querySelector('button').addEventListener('click', () => loadFiles(dir));
|
|
2932
|
+
toast(msg, 'error');
|
|
2933
|
+
// Only bounce to home if dir != home and home is different and we haven't already shown home
|
|
2934
|
+
if (dir !== homeDir && prevPath !== homeDir) {
|
|
2935
|
+
// keep currentPath as prevPath, don't update breadcrumb to failed dir
|
|
2936
|
+
document.getElementById('path-input').value = prevPath;
|
|
2937
|
+
renderBreadcrumb(prevPath);
|
|
2938
|
+
} else {
|
|
2939
|
+
// we are already at home or prevPath is home — update UI to reflect attempted dir for debugging
|
|
2940
|
+
document.getElementById('path-input').value = dir;
|
|
2941
|
+
renderBreadcrumb(dir);
|
|
2942
|
+
}
|
|
2831
2943
|
return;
|
|
2832
2944
|
}
|
|
2833
2945
|
|
|
2946
|
+
// Success — now update history and UI state
|
|
2947
|
+
if (shouldPushHistory) {
|
|
2948
|
+
navHistory.push(prevPath);
|
|
2949
|
+
navForwardHistory = [];
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
currentPath = data.path || dir;
|
|
2953
|
+
document.getElementById('path-input').value = currentPath;
|
|
2954
|
+
renderBreadcrumb(currentPath);
|
|
2955
|
+
saveCurrentPath();
|
|
2956
|
+
|
|
2834
2957
|
currentParent = data.parent || '';
|
|
2835
2958
|
|
|
2836
2959
|
// Map existing DOM children by dataset.path
|
|
@@ -2894,13 +3017,16 @@ async function loadFiles(dir) {
|
|
|
2894
3017
|
sizeEl.remove();
|
|
2895
3018
|
}
|
|
2896
3019
|
existing.dataset.isDir = String(f.isDir);
|
|
3020
|
+
existing.dataset.path = key;
|
|
3021
|
+
const checkEl = existing.querySelector('.file-select-check');
|
|
3022
|
+
if (checkEl) checkEl.setAttribute('data-path', key);
|
|
3023
|
+
const ellEl = existing.querySelector('.file-ellipsis');
|
|
3024
|
+
if (ellEl) ellEl.setAttribute('aria-label', 'More actions for ' + f.name);
|
|
3025
|
+
const nameEl2 = existing.querySelector('.file-name');
|
|
3026
|
+
if (nameEl2) nameEl2.title = key;
|
|
2897
3027
|
}
|
|
2898
3028
|
|
|
2899
|
-
addItem(key, () =>
|
|
2900
|
-
const item = makeFileItem(f.name, f.isDir, key, sizeStr, icon);
|
|
2901
|
-
item.addEventListener('contextmenu', e => showCtxMenu(e, f));
|
|
2902
|
-
return item;
|
|
2903
|
-
});
|
|
3029
|
+
addItem(key, () => makeFileItem(f.name, f.isDir, key, sizeStr, icon));
|
|
2904
3030
|
});
|
|
2905
3031
|
|
|
2906
3032
|
// Remove any orphaned nodes (not in new data)
|
|
@@ -2942,25 +3068,28 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2942
3068
|
<span class="file-ellipsis" role="button" tabindex="0" aria-label="More actions for ${escHtml(name)}" title="More actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg></span>
|
|
2943
3069
|
`;
|
|
2944
3070
|
div.addEventListener('click', e => {
|
|
3071
|
+
const curPath = div.dataset.path;
|
|
3072
|
+
const curIsDir = div.dataset.isDir === 'true';
|
|
3073
|
+
const curName = (div.querySelector('.file-name')?.textContent || curPath.split(/[\\/]/).pop() || curPath);
|
|
2945
3074
|
if (e.target.closest('.file-select-check')) {
|
|
2946
|
-
toggleFileSelection(
|
|
3075
|
+
toggleFileSelection(curPath, div);
|
|
2947
3076
|
if (selectedFiles.length > 0) document.getElementById('select-actions').style.display = 'flex';
|
|
2948
3077
|
return;
|
|
2949
3078
|
}
|
|
2950
3079
|
if (e.target.closest('.file-ellipsis')) {
|
|
2951
3080
|
e.stopPropagation();
|
|
2952
3081
|
const rect = div.querySelector('.file-ellipsis').getBoundingClientRect();
|
|
2953
|
-
const m =
|
|
2954
|
-
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path:
|
|
3082
|
+
const m = curPath.match(/\.([^.]+)$/);
|
|
3083
|
+
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path: curPath, name: curName, isDir: curIsDir, ext: m ? '.' + m[1].toLowerCase() : '' });
|
|
2955
3084
|
return;
|
|
2956
3085
|
}
|
|
2957
3086
|
if (selectMode) {
|
|
2958
|
-
toggleFileSelection(
|
|
3087
|
+
toggleFileSelection(curPath, div);
|
|
2959
3088
|
return;
|
|
2960
3089
|
}
|
|
2961
|
-
if (
|
|
2962
|
-
else if (isImageFile(
|
|
2963
|
-
else openFileEditor(
|
|
3090
|
+
if (curIsDir) loadFiles(curPath);
|
|
3091
|
+
else if (isImageFile(curPath)) openImageViewer(curPath);
|
|
3092
|
+
else openFileEditor(curPath);
|
|
2964
3093
|
});
|
|
2965
3094
|
const ell = div.querySelector('.file-ellipsis');
|
|
2966
3095
|
if (ell) {
|
|
@@ -2968,9 +3097,12 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
2968
3097
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
2969
3098
|
e.preventDefault();
|
|
2970
3099
|
e.stopPropagation();
|
|
3100
|
+
const curPath = div.dataset.path;
|
|
3101
|
+
const curIsDir = div.dataset.isDir === 'true';
|
|
3102
|
+
const curName = (div.querySelector('.file-name')?.textContent || curPath.split(/[\\/]/).pop() || curPath);
|
|
2971
3103
|
const rect = ell.getBoundingClientRect();
|
|
2972
|
-
const m =
|
|
2973
|
-
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path:
|
|
3104
|
+
const m = curPath.match(/\.([^.]+)$/);
|
|
3105
|
+
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path: curPath, name: curName, isDir: curIsDir, ext: m ? '.' + m[1].toLowerCase() : '' });
|
|
2974
3106
|
}
|
|
2975
3107
|
});
|
|
2976
3108
|
}
|
|
@@ -3014,10 +3146,33 @@ function setupFileListKeyboard() {
|
|
|
3014
3146
|
});
|
|
3015
3147
|
}
|
|
3016
3148
|
|
|
3149
|
+
function setupFileListContextMenu() {
|
|
3150
|
+
const list = document.getElementById('file-list');
|
|
3151
|
+
if (!list || list.dataset._ctxSetup) return;
|
|
3152
|
+
list.dataset._ctxSetup = '1';
|
|
3153
|
+
list.addEventListener('contextmenu', e => {
|
|
3154
|
+
const item = e.target.closest('.file-item');
|
|
3155
|
+
if (!item || !item.dataset.path) return;
|
|
3156
|
+
const curPath = item.dataset.path;
|
|
3157
|
+
const curIsDir = item.dataset.isDir === 'true';
|
|
3158
|
+
const curName = (item.querySelector('.file-name')?.textContent || curPath.split(/[\\/]/).pop() || curPath);
|
|
3159
|
+
const m = curPath.match(/\.([^.]+)$/);
|
|
3160
|
+
const ext = m ? '.' + m[1].toLowerCase() : '';
|
|
3161
|
+
showCtxMenu(e, { path: curPath, name: curName, isDir: curIsDir, ext });
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3017
3165
|
function refreshFiles() {
|
|
3018
3166
|
if (currentPath) loadFiles(currentPath);
|
|
3019
3167
|
}
|
|
3020
|
-
function navigateTo(p) {
|
|
3168
|
+
function navigateTo(p) {
|
|
3169
|
+
if (!p || !String(p).trim()) return loadFiles(homeDir);
|
|
3170
|
+
let target = String(p).trim();
|
|
3171
|
+
// If relative path, resolve against currentPath
|
|
3172
|
+
const isAbsolute = target.startsWith('/') || /^[A-Za-z]:[\\/]/.test(target) || target.startsWith('\\\\');
|
|
3173
|
+
if (!isAbsolute) target = joinPath(currentPath || homeDir, target);
|
|
3174
|
+
loadFiles(target);
|
|
3175
|
+
}
|
|
3021
3176
|
function navigateUp() {
|
|
3022
3177
|
if (currentParent && currentParent !== currentPath) {
|
|
3023
3178
|
loadFiles(currentParent);
|
|
@@ -3053,7 +3208,10 @@ function startFileWatcher() {
|
|
|
3053
3208
|
if (fileWatchTimer) return;
|
|
3054
3209
|
const watch = () => {
|
|
3055
3210
|
const sb = document.getElementById('sidebar');
|
|
3211
|
+
const sp = document.getElementById('settings-panel');
|
|
3056
3212
|
if (!sb || sb.classList.contains('hidden') || document.hidden) return;
|
|
3213
|
+
if (sp && sp.classList.contains('open')) return;
|
|
3214
|
+
if (document.querySelector('.overlay.open')) return;
|
|
3057
3215
|
if (currentPath) loadFiles(currentPath);
|
|
3058
3216
|
};
|
|
3059
3217
|
const onVisibility = () => {
|
|
@@ -3134,13 +3292,13 @@ function renderBreadcrumb(fullPath) {
|
|
|
3134
3292
|
}
|
|
3135
3293
|
|
|
3136
3294
|
if (!segments.length) {
|
|
3137
|
-
html = `<span style="color:var(--fg2)">\\</span><span style="color:var(--fg2);font-size:11px;margin-left:
|
|
3295
|
+
html = `<span style="color:var(--fg2)">\\</span><span style="color:var(--fg2);font-size:11px;margin-left:3px">(root)</span>`;
|
|
3138
3296
|
} else {
|
|
3139
3297
|
html = segments.map((seg, i) => {
|
|
3140
3298
|
const isLast = i === segments.length - 1;
|
|
3141
|
-
if (isLast) return `<span style="color:var(--fg);font-weight:500">${escHtml(seg.label)}</span>`;
|
|
3142
|
-
return `<a href="#" data-path="${escHtml(seg.path)}" style="color:var(--accent);text-decoration:none;padding:
|
|
3143
|
-
`<span style="color:var(--fg2);margin:0 2px">\\</span>`;
|
|
3299
|
+
if (isLast) return `<span style="color:var(--fg);font-weight:500;display:inline-flex;align-items:center;height:24px;padding:2px 4px">${escHtml(seg.label)}</span>`;
|
|
3300
|
+
return `<a href="#" data-path="${escHtml(seg.path)}" style="color:var(--accent);text-decoration:none;display:inline-flex;align-items:center;height:24px;padding:2px 6px;border-radius:var(--radius-sm)">${escHtml(seg.label)}</a>` +
|
|
3301
|
+
`<span style="color:var(--fg2);margin:0 2px;display:inline-flex;align-items:center">\\</span>`;
|
|
3144
3302
|
}).join('');
|
|
3145
3303
|
}
|
|
3146
3304
|
} else {
|
|
@@ -3150,10 +3308,10 @@ function renderBreadcrumb(fullPath) {
|
|
|
3150
3308
|
accumulated += '/' + part;
|
|
3151
3309
|
const isLast = part === parts[parts.length - 1];
|
|
3152
3310
|
if (isLast) {
|
|
3153
|
-
html += `<span style="color:var(--fg);font-weight:500">${escHtml(part)}</span>`;
|
|
3311
|
+
html += `<span style="color:var(--fg);font-weight:500;display:inline-flex;align-items:center;height:24px;padding:2px 4px">${escHtml(part)}</span>`;
|
|
3154
3312
|
} else {
|
|
3155
|
-
html += `<a href="#" data-path="${escHtml(accumulated)}" style="color:var(--accent);text-decoration:none;padding:
|
|
3156
|
-
html += `<span style="color:var(--fg2);margin:0 2px">/</span>`;
|
|
3313
|
+
html += `<a href="#" data-path="${escHtml(accumulated)}" style="color:var(--accent);text-decoration:none;display:inline-flex;align-items:center;height:24px;padding:2px 6px;border-radius:var(--radius-sm)">${escHtml(part)}</a>`;
|
|
3314
|
+
html += `<span style="color:var(--fg2);margin:0 2px;display:inline-flex;align-items:center">/</span>`;
|
|
3157
3315
|
}
|
|
3158
3316
|
}
|
|
3159
3317
|
if (!parts.length) {
|
|
@@ -3161,7 +3319,7 @@ function renderBreadcrumb(fullPath) {
|
|
|
3161
3319
|
}
|
|
3162
3320
|
}
|
|
3163
3321
|
|
|
3164
|
-
el.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--fg2);
|
|
3322
|
+
el.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--fg2);flex-shrink:0;vertical-align:middle"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>' + html;
|
|
3165
3323
|
|
|
3166
3324
|
if (!el.dataset.delegated) {
|
|
3167
3325
|
el.addEventListener('click', e => {
|
|
@@ -3558,9 +3716,8 @@ async function confirmRename() {
|
|
|
3558
3716
|
body: JSON.stringify({ oldPath: renamePath, newName })
|
|
3559
3717
|
});
|
|
3560
3718
|
setBtnBusy(document.getElementById('rename-ok-btn'), false);
|
|
3561
|
-
if (r.success) { toast('Renamed', 'success'); refreshFiles(); }
|
|
3719
|
+
if (r.success) { toast('Renamed', 'success'); refreshFiles(); closeOverlay('rename-overlay'); }
|
|
3562
3720
|
else showFieldError('rename-error', r.error || 'Rename failed');
|
|
3563
|
-
closeOverlay('rename-overlay');
|
|
3564
3721
|
}
|
|
3565
3722
|
|
|
3566
3723
|
async function deleteFile(file) {
|
|
@@ -3763,6 +3920,8 @@ async function handleUploadInput() {
|
|
|
3763
3920
|
}
|
|
3764
3921
|
|
|
3765
3922
|
let _uploadProgressTimeout = null;
|
|
3923
|
+
let _currentUploadXhr = null;
|
|
3924
|
+
let _uploadCancelled = false;
|
|
3766
3925
|
function setUploadProgress(pct, status, text) {
|
|
3767
3926
|
const bar = document.getElementById('upload-progress');
|
|
3768
3927
|
const barFill = document.getElementById('upload-progress-bar');
|
|
@@ -3783,19 +3942,33 @@ function clearUploadProgress(delay = 1500) {
|
|
|
3783
3942
|
bar.classList.remove('active', 'success', 'error');
|
|
3784
3943
|
bar.querySelector('#upload-progress-bar').style.width = '0%';
|
|
3785
3944
|
bar.querySelector('#upload-progress-text').textContent = '';
|
|
3945
|
+
_currentUploadXhr = null;
|
|
3946
|
+
_uploadCancelled = false;
|
|
3786
3947
|
}
|
|
3787
3948
|
}, delay);
|
|
3788
3949
|
}
|
|
3950
|
+
function cancelUpload() {
|
|
3951
|
+
_uploadCancelled = true;
|
|
3952
|
+
if (_currentUploadXhr) {
|
|
3953
|
+
try { _currentUploadXhr.abort(); } catch {}
|
|
3954
|
+
_currentUploadXhr = null;
|
|
3955
|
+
}
|
|
3956
|
+
setUploadProgress(100, 'error', 'Upload cancelled');
|
|
3957
|
+
clearUploadProgress(1500);
|
|
3958
|
+
toast('Upload cancelled', 'warning');
|
|
3959
|
+
}
|
|
3789
3960
|
|
|
3790
3961
|
async function uploadFileList(items) {
|
|
3791
3962
|
const path = encodeURIComponent(currentPath);
|
|
3792
3963
|
const total = items.length;
|
|
3793
3964
|
let completed = 0;
|
|
3794
3965
|
let failedItems = [];
|
|
3966
|
+
_uploadCancelled = false;
|
|
3795
3967
|
|
|
3796
3968
|
setUploadProgress(0, '', `0/${total}`);
|
|
3797
3969
|
|
|
3798
3970
|
for (const item of items) {
|
|
3971
|
+
if (_uploadCancelled) break;
|
|
3799
3972
|
const file = item.file || item;
|
|
3800
3973
|
const fileName = item.path || item.webkitRelativePath || item.name;
|
|
3801
3974
|
const formData = new FormData();
|
|
@@ -3804,6 +3977,7 @@ async function uploadFileList(items) {
|
|
|
3804
3977
|
try {
|
|
3805
3978
|
await new Promise((resolve, reject) => {
|
|
3806
3979
|
const xhr = new XMLHttpRequest();
|
|
3980
|
+
_currentUploadXhr = xhr;
|
|
3807
3981
|
xhr.open('POST', `/api/files/upload?path=${path}`);
|
|
3808
3982
|
xhr.setRequestHeader('x-pin-token', authToken);
|
|
3809
3983
|
|
|
@@ -3816,6 +3990,7 @@ async function uploadFileList(items) {
|
|
|
3816
3990
|
});
|
|
3817
3991
|
|
|
3818
3992
|
xhr.onload = () => {
|
|
3993
|
+
_currentUploadXhr = null;
|
|
3819
3994
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
3820
3995
|
try {
|
|
3821
3996
|
const resp = JSON.parse(xhr.responseText);
|
|
@@ -3839,26 +4014,44 @@ async function uploadFileList(items) {
|
|
|
3839
4014
|
};
|
|
3840
4015
|
|
|
3841
4016
|
xhr.onerror = () => {
|
|
4017
|
+
_currentUploadXhr = null;
|
|
4018
|
+
if (_uploadCancelled) { reject(new Error('Cancelled')); return; }
|
|
3842
4019
|
failedItems.push(fileName);
|
|
3843
4020
|
reject(new Error('Network error'));
|
|
3844
4021
|
};
|
|
4022
|
+
xhr.onabort = () => {
|
|
4023
|
+
_currentUploadXhr = null;
|
|
4024
|
+
reject(new Error('Cancelled'));
|
|
4025
|
+
};
|
|
3845
4026
|
xhr.send(formData);
|
|
3846
4027
|
});
|
|
3847
4028
|
} catch (e) {
|
|
4029
|
+
if (_uploadCancelled) break;
|
|
3848
4030
|
console.warn('Upload item failed:', e);
|
|
3849
4031
|
}
|
|
3850
4032
|
}
|
|
3851
4033
|
|
|
3852
|
-
|
|
4034
|
+
_currentUploadXhr = null;
|
|
4035
|
+
// Final result — keep error visible longer (no auto-clear 1500ms for errors)
|
|
4036
|
+
if (_uploadCancelled) {
|
|
4037
|
+
// already handled in cancelUpload
|
|
4038
|
+
refreshFiles();
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
3853
4041
|
if (failedItems.length === 0) {
|
|
3854
4042
|
setUploadProgress(100, 'success', `${completed}/${total} uploaded`);
|
|
4043
|
+
if (failedItems.length) toast(`${failedItems.length} failed`, 'error');
|
|
4044
|
+
clearUploadProgress(2000);
|
|
3855
4045
|
} else if (completed > 0) {
|
|
3856
4046
|
setUploadProgress(100, 'error', `${completed} ok, ${failedItems.length} failed`);
|
|
4047
|
+
toast(`${failedItems.length} failed`, 'error');
|
|
4048
|
+
clearUploadProgress(3000);
|
|
3857
4049
|
} else {
|
|
3858
4050
|
setUploadProgress(100, 'error', `All ${total} failed`);
|
|
4051
|
+
toast(`All ${total} failed`, 'error');
|
|
4052
|
+
clearUploadProgress(3000);
|
|
3859
4053
|
}
|
|
3860
4054
|
|
|
3861
|
-
clearUploadProgress(2000);
|
|
3862
4055
|
refreshFiles();
|
|
3863
4056
|
}
|
|
3864
4057
|
|
|
@@ -3890,9 +4083,27 @@ function initCodeMirror() {
|
|
|
3890
4083
|
},
|
|
3891
4084
|
}
|
|
3892
4085
|
});
|
|
3893
|
-
editor.on('change', () => updateEditorDirty());
|
|
4086
|
+
editor.on('change', () => { updateEditorDirty(); autoSaveDraft(); });
|
|
3894
4087
|
return editor;
|
|
3895
4088
|
}
|
|
4089
|
+
let _autoSaveTimer = null;
|
|
4090
|
+
function autoSaveDraft() {
|
|
4091
|
+
clearTimeout(_autoSaveTimer);
|
|
4092
|
+
_autoSaveTimer = setTimeout(() => {
|
|
4093
|
+
try {
|
|
4094
|
+
if (!editorPath) return;
|
|
4095
|
+
const content = editor ? editor.getValue() : '';
|
|
4096
|
+
if (content !== editorOriginalContent) {
|
|
4097
|
+
safeStorage.setItem('wt-draft:' + editorPath, content);
|
|
4098
|
+
} else {
|
|
4099
|
+
safeStorage.removeItem('wt-draft:' + editorPath);
|
|
4100
|
+
}
|
|
4101
|
+
} catch {}
|
|
4102
|
+
}, 2000);
|
|
4103
|
+
}
|
|
4104
|
+
function loadDraft(p) {
|
|
4105
|
+
try { return safeStorage.getItem('wt-draft:' + p); } catch { return null; }
|
|
4106
|
+
}
|
|
3896
4107
|
|
|
3897
4108
|
function updateEditorDirty() {
|
|
3898
4109
|
const el = document.getElementById('editor-view');
|
|
@@ -3905,8 +4116,11 @@ function getCMmode(fileName) {
|
|
|
3905
4116
|
const m = fileName.match(/\.([^.]+)$/);
|
|
3906
4117
|
if (!m) return null;
|
|
3907
4118
|
switch (m[1].toLowerCase()) {
|
|
3908
|
-
case 'js':
|
|
3909
|
-
case '
|
|
4119
|
+
case 'js': return 'javascript';
|
|
4120
|
+
case 'jsx': return {name: 'javascript', json: false, jsx: true};
|
|
4121
|
+
case 'mjs': case 'cjs': return 'javascript';
|
|
4122
|
+
case 'ts': return {name: 'javascript', typescript: true};
|
|
4123
|
+
case 'tsx': return {name: 'javascript', typescript: true, jsx: true};
|
|
3910
4124
|
case 'json': return {name: 'javascript', json: true};
|
|
3911
4125
|
case 'py': return 'python';
|
|
3912
4126
|
case 'html': case 'htm': return 'htmlmixed';
|
|
@@ -4051,7 +4265,12 @@ async function closeEditor() {
|
|
|
4051
4265
|
if (!ok) return;
|
|
4052
4266
|
}
|
|
4053
4267
|
clearPreviewLiveReload();
|
|
4054
|
-
document.getElementById('editor-view')
|
|
4268
|
+
const ev = document.getElementById('editor-view');
|
|
4269
|
+
ev.classList.remove('open');
|
|
4270
|
+
ev.classList.remove('fullscreen');
|
|
4271
|
+
document.removeEventListener('keydown', _fsEscHandler);
|
|
4272
|
+
const fsBtn = document.getElementById('editor-fullscreen-toggle');
|
|
4273
|
+
if (fsBtn) { fsBtn.setAttribute('aria-label','Fullscreen'); fsBtn.title='Fullscreen (F11)'; fsBtn.innerHTML='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>'; }
|
|
4055
4274
|
document.getElementById('content').classList.remove('editor-open');
|
|
4056
4275
|
document.getElementById('editor-preview').classList.remove('active');
|
|
4057
4276
|
document.getElementById('md-preview-toggle').style.display = 'none';
|
|
@@ -4144,6 +4363,8 @@ function renderHtmlPreview() {
|
|
|
4144
4363
|
const iframe = document.getElementById('editor-preview-iframe');
|
|
4145
4364
|
const raw = editor ? editor.getValue() : '';
|
|
4146
4365
|
if (!iframe) return;
|
|
4366
|
+
// Clear previous live reload to prevent leak on re-entry (U83)
|
|
4367
|
+
clearPreviewLiveReload();
|
|
4147
4368
|
const s = getComputedStyle(document.documentElement);
|
|
4148
4369
|
const bg = s.getPropertyValue('--bg').trim();
|
|
4149
4370
|
const fg = s.getPropertyValue('--fg').trim();
|
|
@@ -4192,7 +4413,7 @@ function renderHtmlPreview() {
|
|
|
4192
4413
|
input:focus, textarea:focus, select:focus { outline: 2px solid ${accent}; outline-offset: 1px; }
|
|
4193
4414
|
</style>
|
|
4194
4415
|
</head>
|
|
4195
|
-
<body>${typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw, { USE_PROFILES: { html: true },
|
|
4416
|
+
<body>${typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw, { USE_PROFILES: { html: true }, ADD_ATTR: ['target'] }) : raw}</body>
|
|
4196
4417
|
</html>`;
|
|
4197
4418
|
iframe.srcdoc = doc;
|
|
4198
4419
|
iframe.style.display = 'block';
|
|
@@ -4319,7 +4540,9 @@ function closeSearch() {
|
|
|
4319
4540
|
let searchCaseSensitive = false;
|
|
4320
4541
|
function toggleSearchCase() {
|
|
4321
4542
|
searchCaseSensitive = !searchCaseSensitive;
|
|
4322
|
-
document.getElementById('search-case-btn')
|
|
4543
|
+
const btn = document.getElementById('search-case-btn');
|
|
4544
|
+
btn.classList.toggle('active', searchCaseSensitive);
|
|
4545
|
+
btn.setAttribute('aria-pressed', String(searchCaseSensitive));
|
|
4323
4546
|
doSearch();
|
|
4324
4547
|
}
|
|
4325
4548
|
|
|
@@ -4454,12 +4677,35 @@ function toggleSetting(k) {
|
|
|
4454
4677
|
function openSettings() {
|
|
4455
4678
|
const panel = document.getElementById('settings-panel');
|
|
4456
4679
|
const isOpen = panel.classList.contains('open');
|
|
4457
|
-
panel.
|
|
4458
|
-
|
|
4459
|
-
|
|
4680
|
+
// Don't inert #main because panel is inside #main — inert would disable panel itself. Inert content only (sidebar is sibling).
|
|
4681
|
+
const content = document.getElementById('content');
|
|
4682
|
+
if (!isOpen) {
|
|
4683
|
+
panel.classList.add('open');
|
|
4684
|
+
panel.setAttribute('aria-modal', 'true');
|
|
4685
|
+
panel.setAttribute('role', 'dialog');
|
|
4686
|
+
// Defer inert + focus to next frame so panel transform can composite on GPU without reflowing sidebar/file-list
|
|
4687
|
+
requestAnimationFrame(() => {
|
|
4688
|
+
if (content) content.setAttribute('inert', '');
|
|
4689
|
+
installFocusTrap(panel);
|
|
4690
|
+
const firstFocusable = panel.querySelector('select, button, input');
|
|
4691
|
+
if (firstFocusable) firstFocusable.focus({ preventScroll: true });
|
|
4692
|
+
});
|
|
4693
|
+
setTimeout(() => document.addEventListener('click', closeSettingsOnClickOutside, true), 50);
|
|
4694
|
+
} else {
|
|
4695
|
+
panel.classList.remove('open');
|
|
4696
|
+
panel.removeAttribute('aria-modal');
|
|
4697
|
+
if (content) content.removeAttribute('inert');
|
|
4698
|
+
removeFocusTrap();
|
|
4699
|
+
document.removeEventListener('click', closeSettingsOnClickOutside, true);
|
|
4700
|
+
}
|
|
4460
4701
|
}
|
|
4461
4702
|
function closeSettings() {
|
|
4462
|
-
document.getElementById('settings-panel')
|
|
4703
|
+
const panel = document.getElementById('settings-panel');
|
|
4704
|
+
const content = document.getElementById('content');
|
|
4705
|
+
panel.classList.remove('open');
|
|
4706
|
+
panel.removeAttribute('aria-modal');
|
|
4707
|
+
if (content) content.removeAttribute('inert');
|
|
4708
|
+
removeFocusTrap();
|
|
4463
4709
|
document.removeEventListener('click', closeSettingsOnClickOutside, true);
|
|
4464
4710
|
}
|
|
4465
4711
|
function closeSettingsOnClickOutside(e) {
|
|
@@ -4912,8 +5158,37 @@ function toggleEditorSplit() {
|
|
|
4912
5158
|
try { safeStorage.setItem('wt-editor-split-orientation', isHoriz ? 'horizontal' : 'vertical'); } catch(e) { console.warn(e); }
|
|
4913
5159
|
}
|
|
4914
5160
|
|
|
4915
|
-
|
|
4916
|
-
|
|
5161
|
+
function toggleEditorFullscreen() {
|
|
5162
|
+
const view = document.getElementById('editor-view');
|
|
5163
|
+
if (!view || !view.classList.contains('open')) return;
|
|
5164
|
+
const isFS = view.classList.toggle('fullscreen');
|
|
5165
|
+
const btn = document.getElementById('editor-fullscreen-toggle');
|
|
5166
|
+
if (btn) {
|
|
5167
|
+
btn.setAttribute('aria-label', isFS ? 'Exit fullscreen' : 'Fullscreen');
|
|
5168
|
+
btn.title = isFS ? 'Exit fullscreen (Esc/F11)' : 'Fullscreen (F11)';
|
|
5169
|
+
btn.innerHTML = isFS
|
|
5170
|
+
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 14 10 14 10 4"/><polyline points="20 10 14 10 14 20"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="10" y1="14" x2="3" y2="21"/></svg>'
|
|
5171
|
+
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';
|
|
5172
|
+
}
|
|
5173
|
+
if (isFS) {
|
|
5174
|
+
document.addEventListener('keydown', _fsEscHandler);
|
|
5175
|
+
} else {
|
|
5176
|
+
document.removeEventListener('keydown', _fsEscHandler);
|
|
5177
|
+
requestAnimationFrame(() => { tabs.forEach(t=>{try{fitTerm(t);}catch(e){console.warn(e);}}); if(editor) editor.refresh(); });
|
|
5178
|
+
}
|
|
5179
|
+
if (editor) setTimeout(()=>editor.refresh(), 80);
|
|
5180
|
+
// Keep sidebar visible — editor fullscreen now only covers terminal area (absolute inside #editor-split-area)
|
|
5181
|
+
requestAnimationFrame(() => { tabs.forEach(t=>{try{fitTerm(t);}catch(e){console.warn(e);}}); });
|
|
5182
|
+
}
|
|
5183
|
+
function _fsEscHandler(e) {
|
|
5184
|
+
if (e.key === 'Escape' || e.key === 'F11') {
|
|
5185
|
+
const v = document.getElementById('editor-view');
|
|
5186
|
+
if (v && v.classList.contains('fullscreen')) { e.preventDefault(); toggleEditorFullscreen(); }
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
// Terminal fullscreen removed — per request, terminal fullscreen should not cover sidebar
|
|
5191
|
+
// toggleTerminalFullscreen and _termFsEscHandler intentionally removed
|
|
4917
5192
|
const hasWakeLock = 'wakeLock' in navigator;
|
|
4918
5193
|
|
|
4919
5194
|
async function requestWakeLock() {
|
|
@@ -4962,7 +5237,11 @@ function moveTabToEnd(fromId) {
|
|
|
4962
5237
|
const [moved] = tabs.splice(fromIdx, 1);
|
|
4963
5238
|
tabs.push(moved);
|
|
4964
5239
|
const bar = document.getElementById('tab-scroll');
|
|
4965
|
-
|
|
5240
|
+
const newBtn = document.getElementById('new-tab-btn');
|
|
5241
|
+
if (moved.el && moved.el.parentNode === bar) {
|
|
5242
|
+
if (newBtn && newBtn.parentElement === bar) bar.insertBefore(moved.el, newBtn);
|
|
5243
|
+
else bar.appendChild(moved.el);
|
|
5244
|
+
}
|
|
4966
5245
|
saveTabState();
|
|
4967
5246
|
}
|
|
4968
5247
|
|
|
@@ -5004,6 +5283,7 @@ function setupKeyboardShortcuts() {
|
|
|
5004
5283
|
if (ctrl && e.shiftKey && e.key === 'ArrowRight') { e.preventDefault(); cycleTab(1); }
|
|
5005
5284
|
if (ctrl && e.shiftKey && e.key === 'ArrowLeft') { e.preventDefault(); cycleTab(-1); }
|
|
5006
5285
|
if (ctrl && e.shiftKey && e.key === 'r') { e.preventDefault(); refreshPreview(); }
|
|
5286
|
+
if (e.key === 'F11' && document.getElementById('editor-view').classList.contains('open')) { e.preventDefault(); toggleEditorFullscreen(); }
|
|
5007
5287
|
if (e.key === 'Escape') {
|
|
5008
5288
|
const termMenu = document.getElementById('term-ctx-menu');
|
|
5009
5289
|
if (termMenu && termMenu.style.display !== 'none') {
|
|
@@ -5059,26 +5339,25 @@ function toggleTermSelect() {
|
|
|
5059
5339
|
}
|
|
5060
5340
|
}
|
|
5061
5341
|
|
|
5342
|
+
const activeTab = getActiveTab();
|
|
5062
5343
|
if (termSelectMode) {
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
const ta = t.textarea || t.term.textarea || t.term.element?.querySelector('.xterm-textarea, textarea');
|
|
5344
|
+
if (activeTab?.term) {
|
|
5345
|
+
activeTab.term.options.disableStdin = true;
|
|
5346
|
+
const ta = activeTab.textarea || activeTab.term.textarea || activeTab.term.element?.querySelector('.xterm-textarea, textarea');
|
|
5067
5347
|
if (ta) ta.disabled = true;
|
|
5068
|
-
}
|
|
5348
|
+
}
|
|
5069
5349
|
} else {
|
|
5070
5350
|
scrollMode = false;
|
|
5071
5351
|
cleanupScrollHandlers();
|
|
5072
5352
|
const scrollBtn = document.getElementById('sel-scroll-btn');
|
|
5073
5353
|
if (scrollBtn) scrollBtn.classList.remove('active-mode');
|
|
5074
5354
|
shiftLatch = false; altLatch = false; updateModifierButtons();
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
const ta = t.textarea || t.term.textarea || t.term.element?.querySelector('.xterm-textarea, textarea');
|
|
5355
|
+
if (activeTab?.term) {
|
|
5356
|
+
activeTab.term.options.disableStdin = false;
|
|
5357
|
+
const ta = activeTab.textarea || activeTab.term.textarea || activeTab.term.element?.querySelector('.xterm-textarea, textarea');
|
|
5079
5358
|
if (ta) ta.disabled = false;
|
|
5080
|
-
|
|
5081
|
-
}
|
|
5359
|
+
activeTab.term.clearSelection();
|
|
5360
|
+
}
|
|
5082
5361
|
}
|
|
5083
5362
|
}
|
|
5084
5363
|
|
|
@@ -5164,12 +5443,16 @@ function pasteToTerminal() {
|
|
|
5164
5443
|
if (!text) return;
|
|
5165
5444
|
const tab = getActiveTab();
|
|
5166
5445
|
if (tab?.ws && tab.ws.readyState === WebSocket.OPEN) {
|
|
5167
|
-
// Bracketed paste: wrap in escape sequences so the shell buffers the input
|
|
5446
|
+
// Bracketed paste: wrap in escape sequences so the shell buffers the input — chunk 60KB with loop
|
|
5168
5447
|
const bracketed = '\x1b[200~' + text + '\x1b[201~';
|
|
5169
5448
|
const enc = new TextEncoder().encode(bracketed);
|
|
5170
|
-
const
|
|
5171
|
-
|
|
5172
|
-
|
|
5449
|
+
const CHUNK = 60 * 1024;
|
|
5450
|
+
for (let off = 0; off < enc.length; off += CHUNK) {
|
|
5451
|
+
const slice = enc.subarray(off, Math.min(off + CHUNK, enc.length));
|
|
5452
|
+
const buf = new Uint8Array(1 + slice.length);
|
|
5453
|
+
buf[0] = 0x00; buf.set(slice, 1);
|
|
5454
|
+
try { tab.ws.send(buf.buffer); } catch {}
|
|
5455
|
+
}
|
|
5173
5456
|
tab.term?.focus();
|
|
5174
5457
|
toast('Pasted to terminal', 'success');
|
|
5175
5458
|
}
|
|
@@ -5191,14 +5474,24 @@ function fallbackPaste(callback) {
|
|
|
5191
5474
|
document.body.appendChild(_pasteInput);
|
|
5192
5475
|
_pasteInput.addEventListener('paste', () => {
|
|
5193
5476
|
setTimeout(() => {
|
|
5194
|
-
|
|
5477
|
+
if (_pasteInput.value) {
|
|
5478
|
+
callback(_pasteInput.value);
|
|
5479
|
+
toast('Pasted to terminal', 'success');
|
|
5480
|
+
} else {
|
|
5481
|
+
toast('Paste failed — please use Ctrl+V', 'warning');
|
|
5482
|
+
}
|
|
5195
5483
|
_pasteInput.value = '';
|
|
5196
5484
|
}, 0);
|
|
5197
5485
|
});
|
|
5198
5486
|
}
|
|
5199
5487
|
_pasteInput.value = '';
|
|
5200
5488
|
_pasteInput.focus();
|
|
5201
|
-
|
|
5489
|
+
try {
|
|
5490
|
+
const ok = document.execCommand('paste');
|
|
5491
|
+
if (!ok) toast('Paste failed — please use Ctrl+V', 'warning');
|
|
5492
|
+
} catch {
|
|
5493
|
+
toast('Paste not supported — use Ctrl+V', 'warning');
|
|
5494
|
+
}
|
|
5202
5495
|
}
|
|
5203
5496
|
|
|
5204
5497
|
function selectAllTerm() {
|
|
@@ -5244,48 +5537,66 @@ function toggleCtrlRow() {
|
|
|
5244
5537
|
}
|
|
5245
5538
|
|
|
5246
5539
|
let _viewportHandlerInstalled = false;
|
|
5540
|
+
let _viewportDebounce = null;
|
|
5247
5541
|
function setupVisualViewport() {
|
|
5248
5542
|
if (_viewportHandlerInstalled) return;
|
|
5249
5543
|
_viewportHandlerInstalled = true;
|
|
5250
5544
|
if (window.visualViewport) {
|
|
5251
5545
|
const handler = () => {
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
const
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5546
|
+
clearTimeout(_viewportDebounce);
|
|
5547
|
+
_viewportDebounce = setTimeout(() => {
|
|
5548
|
+
const vvH = window.visualViewport.height;
|
|
5549
|
+
const vvScale = window.visualViewport.scale || 1;
|
|
5550
|
+
// Samsung Internet: innerHeight resizes, not visualViewport. Use layout viewport height via window.innerHeight vs vvH
|
|
5551
|
+
// On iOS, keyboard shows as vvH < innerHeight. On Samsung, opposite. Take max diff and ignore when scaled (pinch zoom).
|
|
5552
|
+
if (vvScale !== 1) return; // ignore pinch-zoom (U61, U65)
|
|
5553
|
+
const diff = vvH - window.innerHeight;
|
|
5554
|
+
const absDiff = Math.abs(diff);
|
|
5555
|
+
const isKeyboard = absDiff > 50 && diff < 0; // iOS: vv smaller. For Samsung, also check visualViewport offsetTop
|
|
5556
|
+
// Fallback: check visualViewport offsetTop > 10 indicates keyboard on some Android
|
|
5557
|
+
const offsetTop = window.visualViewport.offsetTop || 0;
|
|
5558
|
+
const keyboardOpen = isKeyboard || offsetTop > 10;
|
|
5559
|
+
const mobileKeys = document.getElementById('mobile-keys');
|
|
5560
|
+
const selRow = document.getElementById('mkey-sel-row');
|
|
5561
|
+
const terminals = document.getElementById('terminals');
|
|
5562
|
+
if (!terminals) return;
|
|
5563
|
+
if (keyboardOpen) {
|
|
5564
|
+
terminals.style.marginBottom = absDiff + 'px';
|
|
5565
|
+
if (mobileKeys) mobileKeys.style.display = 'none';
|
|
5271
5566
|
const ctrlRow = document.getElementById('mkey-ctrl-row');
|
|
5272
|
-
if (ctrlRow && ctrlRow.dataset.wasOpen
|
|
5273
|
-
|
|
5274
|
-
|
|
5567
|
+
if (ctrlRow && ctrlRow.style.display === 'flex') ctrlRow.dataset.wasOpen = '1';
|
|
5568
|
+
if (ctrlRow) ctrlRow.style.display = 'none';
|
|
5569
|
+
if (selRow && selRow.style.display === 'flex') selRow.dataset.wasOpen = '1';
|
|
5570
|
+
if (selRow) selRow.style.display = 'none';
|
|
5571
|
+
} else {
|
|
5572
|
+
terminals.style.marginBottom = '0';
|
|
5573
|
+
if (mobileKeys && window.innerWidth <= 768 && settings.mobilekeys !== false) {
|
|
5574
|
+
mobileKeys.style.display = 'flex';
|
|
5275
5575
|
}
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5576
|
+
if (window.innerWidth <= 768 && settings.mobilekeys !== false) {
|
|
5577
|
+
const ctrlRow = document.getElementById('mkey-ctrl-row');
|
|
5578
|
+
if (ctrlRow && ctrlRow.dataset.wasOpen === '1') {
|
|
5579
|
+
ctrlRow.style.display = 'flex';
|
|
5580
|
+
delete ctrlRow.dataset.wasOpen;
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
if (selRow && window.innerWidth <= 768) {
|
|
5584
|
+
if (selRow.dataset.wasOpen === '1') {
|
|
5585
|
+
selRow.style.display = 'flex';
|
|
5586
|
+
delete selRow.dataset.wasOpen;
|
|
5587
|
+
} else {
|
|
5588
|
+
selRow.style.display = termSelectMode ? 'flex' : 'none';
|
|
5589
|
+
}
|
|
5283
5590
|
}
|
|
5284
5591
|
}
|
|
5285
|
-
}
|
|
5592
|
+
}, 100);
|
|
5286
5593
|
};
|
|
5287
5594
|
window.visualViewport.addEventListener('resize', handler);
|
|
5288
|
-
window.
|
|
5595
|
+
window.visualViewport.addEventListener('scroll', handler);
|
|
5596
|
+
window._cleanups?.push(() => {
|
|
5597
|
+
window.visualViewport.removeEventListener('resize', handler);
|
|
5598
|
+
window.visualViewport.removeEventListener('scroll', handler);
|
|
5599
|
+
});
|
|
5289
5600
|
}
|
|
5290
5601
|
}
|
|
5291
5602
|
|
|
@@ -5362,8 +5673,23 @@ function setupDragDrop() {
|
|
|
5362
5673
|
});
|
|
5363
5674
|
document.addEventListener('dragleave', e => {
|
|
5364
5675
|
if (!isFileDrag(e)) return;
|
|
5365
|
-
|
|
5366
|
-
|
|
5676
|
+
// Only hide when leaving the viewport (relatedTarget null) or overlay itself
|
|
5677
|
+
// Prevents nested dragenter/leave from child elements desyncing counter
|
|
5678
|
+
if (!e.relatedTarget || e.relatedTarget === document.documentElement || e.target === overlay) {
|
|
5679
|
+
dragCnt = 0;
|
|
5680
|
+
overlay.classList.remove('active');
|
|
5681
|
+
} else {
|
|
5682
|
+
dragCnt--;
|
|
5683
|
+
if (dragCnt <= 0) { dragCnt = 0; overlay.classList.remove('active'); }
|
|
5684
|
+
}
|
|
5685
|
+
});
|
|
5686
|
+
document.addEventListener('dragend', () => {
|
|
5687
|
+
dragCnt = 0;
|
|
5688
|
+
overlay.classList.remove('active');
|
|
5689
|
+
});
|
|
5690
|
+
window.addEventListener('blur', () => {
|
|
5691
|
+
dragCnt = 0;
|
|
5692
|
+
overlay.classList.remove('active');
|
|
5367
5693
|
});
|
|
5368
5694
|
document.addEventListener('dragover', e => {
|
|
5369
5695
|
if (!isFileDrag(e)) return;
|
|
@@ -5440,8 +5766,10 @@ function closeOverlay(id) {
|
|
|
5440
5766
|
lastFocusedElement = null;
|
|
5441
5767
|
}
|
|
5442
5768
|
}
|
|
5769
|
+
let _focusTrapContainer = null;
|
|
5443
5770
|
function installFocusTrap(container) {
|
|
5444
5771
|
removeFocusTrap();
|
|
5772
|
+
_focusTrapContainer = container;
|
|
5445
5773
|
_focusTrapHandler = e => {
|
|
5446
5774
|
if (e.key !== 'Tab') return;
|
|
5447
5775
|
const focusable = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
@@ -5457,15 +5785,25 @@ function installFocusTrap(container) {
|
|
|
5457
5785
|
container.addEventListener('keydown', _focusTrapHandler);
|
|
5458
5786
|
}
|
|
5459
5787
|
function removeFocusTrap() {
|
|
5460
|
-
if (_focusTrapHandler) {
|
|
5788
|
+
if (_focusTrapHandler && _focusTrapContainer) {
|
|
5789
|
+
_focusTrapContainer.removeEventListener('keydown', _focusTrapHandler);
|
|
5790
|
+
_focusTrapHandler = null;
|
|
5791
|
+
_focusTrapContainer = null;
|
|
5792
|
+
} else if (_focusTrapHandler) {
|
|
5461
5793
|
document.removeEventListener('keydown', _focusTrapHandler);
|
|
5462
5794
|
_focusTrapHandler = null;
|
|
5463
5795
|
}
|
|
5464
5796
|
}
|
|
5465
5797
|
|
|
5466
5798
|
let _confirmState = null;
|
|
5799
|
+
let _confirmQueue = [];
|
|
5467
5800
|
function confirmDialog({ title = 'Confirm', message = '', okText = 'OK', cancelText = 'Cancel', danger = false } = {}) {
|
|
5468
5801
|
return new Promise(resolve => {
|
|
5802
|
+
if (_confirmState) {
|
|
5803
|
+
// Queue second dialog until first settles (fixes race U51)
|
|
5804
|
+
_confirmQueue.push({ title, message, okText, cancelText, danger, resolve });
|
|
5805
|
+
return;
|
|
5806
|
+
}
|
|
5469
5807
|
const ov = document.getElementById('confirm-overlay');
|
|
5470
5808
|
if (!ov || !ov.parentNode) { resolve(false); return; }
|
|
5471
5809
|
const okBtn = document.getElementById('confirm-ok-btn');
|
|
@@ -5486,8 +5824,14 @@ function confirmDialog({ title = 'Confirm', message = '', okText = 'OK', cancelT
|
|
|
5486
5824
|
settled = true;
|
|
5487
5825
|
_confirmState = null;
|
|
5488
5826
|
ov.classList.remove('open');
|
|
5827
|
+
ov.removeEventListener('keydown', onKey);
|
|
5489
5828
|
removeFocusTrap();
|
|
5490
5829
|
if (lastFocusedElement) { setTimeout(() => lastFocusedElement?.focus(), 50); lastFocusedElement = null; }
|
|
5830
|
+
// Dequeue next pending dialog
|
|
5831
|
+
if (_confirmQueue.length) {
|
|
5832
|
+
const next = _confirmQueue.shift();
|
|
5833
|
+
setTimeout(() => confirmDialog(next).then(next.resolve), 80);
|
|
5834
|
+
}
|
|
5491
5835
|
};
|
|
5492
5836
|
const finish = val => { const cb = _confirmState && _confirmState.resolve; cleanup(); if (cb) cb(val); };
|
|
5493
5837
|
const onKey = e => {
|
|
@@ -5517,8 +5861,8 @@ document.querySelectorAll('.overlay').forEach(o => {
|
|
|
5517
5861
|
|
|
5518
5862
|
function setBtnBusy(btn, busy) {
|
|
5519
5863
|
if (!btn) return;
|
|
5520
|
-
if (busy) { btn.dataset.busy = 'true'; btn.classList.add('loading'); btn.disabled = true; }
|
|
5521
|
-
else { btn.dataset.busy = 'false'; btn.classList.remove('loading'); btn.disabled = false; }
|
|
5864
|
+
if (busy) { btn.dataset.busy = 'true'; btn.classList.add('loading'); btn.disabled = true; btn.setAttribute('aria-busy','true'); }
|
|
5865
|
+
else { btn.dataset.busy = 'false'; btn.classList.remove('loading'); btn.disabled = false; btn.removeAttribute('aria-busy'); }
|
|
5522
5866
|
}
|
|
5523
5867
|
|
|
5524
5868
|
function showFieldError(id, msg) {
|
|
@@ -5829,15 +6173,25 @@ function saveCmdLib(cmds) {
|
|
|
5829
6173
|
function toggleCmdLib() {
|
|
5830
6174
|
const panel = document.getElementById('cmd-lib-panel');
|
|
5831
6175
|
const isOpen = panel.classList.contains('open');
|
|
6176
|
+
// Only inert the terminals area (content), not sidebar (panel is on right side)
|
|
6177
|
+
const terms = document.getElementById('terminals');
|
|
5832
6178
|
panel.classList.toggle('open');
|
|
6179
|
+
panel.setAttribute('role', 'dialog');
|
|
6180
|
+
panel.setAttribute('aria-modal', String(!isOpen));
|
|
5833
6181
|
document.getElementById('cmd-lib-toggle').classList.toggle('active', !isOpen);
|
|
6182
|
+
document.getElementById('cmd-lib-toggle').setAttribute('aria-expanded', String(!isOpen));
|
|
5834
6183
|
if (!isOpen) {
|
|
5835
6184
|
loadHistMax();
|
|
5836
6185
|
switchCmdTab('library');
|
|
6186
|
+
if (terms) terms.setAttribute('inert', '');
|
|
6187
|
+
installFocusTrap(panel);
|
|
5837
6188
|
setTimeout(() => {
|
|
5838
6189
|
document.addEventListener('click', closeCmdLibOnClickOutside, true);
|
|
5839
|
-
|
|
6190
|
+
panel.querySelector('input, button')?.focus();
|
|
6191
|
+
}, 50);
|
|
5840
6192
|
} else {
|
|
6193
|
+
if (terms) terms.removeAttribute('inert');
|
|
6194
|
+
removeFocusTrap();
|
|
5841
6195
|
document.removeEventListener('click', closeCmdLibOnClickOutside, true);
|
|
5842
6196
|
}
|
|
5843
6197
|
}
|
|
@@ -5851,6 +6205,9 @@ function closeCmdLibOnClickOutside(e) {
|
|
|
5851
6205
|
}
|
|
5852
6206
|
if (panel.contains(e.target) || btn.contains(e.target)) return;
|
|
5853
6207
|
toggleCmdLib();
|
|
6208
|
+
const terms = document.getElementById('terminals');
|
|
6209
|
+
if (terms) terms.removeAttribute('inert');
|
|
6210
|
+
removeFocusTrap();
|
|
5854
6211
|
}
|
|
5855
6212
|
|
|
5856
6213
|
function renderCmdLib(filter) {
|
|
@@ -5935,12 +6292,18 @@ function filterCmdLib() {
|
|
|
5935
6292
|
renderCmdLib();
|
|
5936
6293
|
}
|
|
5937
6294
|
|
|
5938
|
-
function runCmdLib(cmd) {
|
|
6295
|
+
async function runCmdLib(cmd) {
|
|
5939
6296
|
const tab = getActiveTab();
|
|
5940
6297
|
if (!tab || !tab.ws || tab.ws.readyState !== WebSocket.OPEN) {
|
|
5941
6298
|
toast('No active terminal', 'error');
|
|
5942
6299
|
return;
|
|
5943
6300
|
}
|
|
6301
|
+
// RCE guard: curl | bash requires confirmation (U91)
|
|
6302
|
+
const needsConfirm = (typeof DEFAULT_CMDS !== 'undefined' && DEFAULT_CMDS.some(c => c.cmd === cmd && c.requiresConfirm)) || /\bcurl\b.*\|\s*bash/.test(cmd);
|
|
6303
|
+
if (needsConfirm) {
|
|
6304
|
+
const ok = await confirmDialog({ title: 'Confirm run', message: `Run sensitive command?\n\n${cmd}`, okText: 'Run', cancelText: 'Cancel', danger: true });
|
|
6305
|
+
if (!ok) return;
|
|
6306
|
+
}
|
|
5944
6307
|
const text = cmd + '\n';
|
|
5945
6308
|
const enc = new TextEncoder().encode(text);
|
|
5946
6309
|
const buf = new Uint8Array(1 + enc.length);
|
|
@@ -6055,8 +6418,8 @@ function switchCmdTab(tab) {
|
|
|
6055
6418
|
const footer = document.getElementById('cmd-lib-footer');
|
|
6056
6419
|
const search = document.getElementById('cmd-lib-search');
|
|
6057
6420
|
if (tab === 'library') {
|
|
6058
|
-
libBtn.classList.add('active');
|
|
6059
|
-
histBtn.classList.remove('active');
|
|
6421
|
+
libBtn.classList.add('active'); libBtn.setAttribute('aria-selected','true');
|
|
6422
|
+
histBtn.classList.remove('active'); histBtn.setAttribute('aria-selected','false');
|
|
6060
6423
|
libList.style.display = '';
|
|
6061
6424
|
histList.style.display = 'none';
|
|
6062
6425
|
histHeader.style.display = 'none';
|
|
@@ -6065,9 +6428,10 @@ function switchCmdTab(tab) {
|
|
|
6065
6428
|
search.oninput = () => filterCmdLib();
|
|
6066
6429
|
filterCmdLib();
|
|
6067
6430
|
} else {
|
|
6068
|
-
histBtn.classList.add('active');
|
|
6069
|
-
libBtn.classList.remove('active');
|
|
6431
|
+
histBtn.classList.add('active'); histBtn.setAttribute('aria-selected','true');
|
|
6432
|
+
libBtn.classList.remove('active'); libBtn.setAttribute('aria-selected','false');
|
|
6070
6433
|
histList.style.display = 'flex';
|
|
6434
|
+
histList.setAttribute('aria-live','polite');
|
|
6071
6435
|
libList.style.display = 'none';
|
|
6072
6436
|
histHeader.style.display = '';
|
|
6073
6437
|
footer.style.display = 'none';
|
|
@@ -6146,13 +6510,23 @@ function openSystemStats() {
|
|
|
6146
6510
|
refreshSystemStats(true);
|
|
6147
6511
|
clearInterval(sysStatsTimer);
|
|
6148
6512
|
if (!settings.datasaver) {
|
|
6149
|
-
sysStatsTimer = setInterval(() =>
|
|
6513
|
+
sysStatsTimer = setInterval(() => {
|
|
6514
|
+
if (document.hidden) return;
|
|
6515
|
+
refreshSystemStats(false);
|
|
6516
|
+
}, 5000);
|
|
6517
|
+
document.addEventListener('visibilitychange', _sysVisibilityHandler);
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
function _sysVisibilityHandler() {
|
|
6521
|
+
if (document.hidden && sysStatsTimer) {
|
|
6522
|
+
// pause polling when hidden to save tunnel bandwidth (U93)
|
|
6150
6523
|
}
|
|
6151
6524
|
}
|
|
6152
6525
|
|
|
6153
6526
|
function closeSystemStats() {
|
|
6154
6527
|
clearInterval(sysStatsTimer);
|
|
6155
6528
|
sysStatsTimer = null;
|
|
6529
|
+
document.removeEventListener('visibilitychange', _sysVisibilityHandler);
|
|
6156
6530
|
closeOverlay('sys-overlay');
|
|
6157
6531
|
}
|
|
6158
6532
|
|
|
@@ -6226,11 +6600,31 @@ async function refreshSystemStats(showLoading) {
|
|
|
6226
6600
|
const addTd = (txt, extra) => { const td = document.createElement('td'); if (extra) td.style.cssText = extra; td.textContent = txt; tr.appendChild(td); };
|
|
6227
6601
|
addTd(p.pid); addTd(p.user); addTd(p.cpu); addTd(p.mem);
|
|
6228
6602
|
addTd(p.cmd, 'max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap');
|
|
6603
|
+
const killTd = document.createElement('td');
|
|
6604
|
+
killTd.style.textAlign = 'center';
|
|
6605
|
+
const killBtn = document.createElement('button');
|
|
6606
|
+
killBtn.className = 'btn btn-danger';
|
|
6607
|
+
killBtn.style.cssText = 'height:22px;padding:0 8px;font-size:11px;min-width:36px';
|
|
6608
|
+
killBtn.textContent = 'Kill';
|
|
6609
|
+
killBtn.setAttribute('aria-label', 'Kill process ' + p.pid);
|
|
6610
|
+
killBtn.title = 'Kill PID ' + p.pid;
|
|
6611
|
+
killBtn.onclick = () => killProcess(p.pid, p.cmd);
|
|
6612
|
+
killTd.appendChild(killBtn);
|
|
6613
|
+
tr.appendChild(killTd);
|
|
6229
6614
|
tbody.appendChild(tr);
|
|
6230
6615
|
});
|
|
6231
6616
|
}
|
|
6232
6617
|
}
|
|
6233
6618
|
|
|
6619
|
+
async function killProcess(pid, cmd) {
|
|
6620
|
+
const display = cmd ? `${pid} (${cmd.split('/').pop().slice(0,30)})` : String(pid);
|
|
6621
|
+
const ok = await confirmDialog({ title: 'Kill process', message: `Kill process ${display}?\n\nThis will send SIGTERM (then SIGKILL).`, okText: 'Kill', cancelText: 'Cancel', danger: true });
|
|
6622
|
+
if (!ok) return;
|
|
6623
|
+
const r = await api('/api/system/kill', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pid: Number(pid) }) });
|
|
6624
|
+
if (r && r.success) { toast('Killed ' + pid, 'success'); refreshSystemStats(false); }
|
|
6625
|
+
else toast(r && r.error ? r.error : 'Kill failed', 'error');
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6234
6628
|
// ═══════════════════════════════════════════════════════
|
|
6235
6629
|
// PWA / SERVICE WORKER
|
|
6236
6630
|
// ═══════════════════════════════════════════════════════
|
|
@@ -6262,7 +6656,9 @@ function showInstallBanner(isIOS) {
|
|
|
6262
6656
|
}
|
|
6263
6657
|
const mk = document.getElementById('mobile-keys');
|
|
6264
6658
|
if (mk && window.getComputedStyle(mk).display !== 'none') {
|
|
6265
|
-
el.style.bottom = 'var(--mobilekey-h)';
|
|
6659
|
+
el.style.bottom = 'calc(var(--mobilekey-h) + env(safe-area-inset-bottom, 0px))';
|
|
6660
|
+
} else {
|
|
6661
|
+
el.style.bottom = 'env(safe-area-inset-bottom, 0px)';
|
|
6266
6662
|
}
|
|
6267
6663
|
el.style.display = 'flex';
|
|
6268
6664
|
document.getElementById('toast-container').classList.add('banner-visible');
|