webtun 1.5.4 → 1.5.6
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 +10 -4
- package/package.json +1 -1
- package/public/index.html +740 -122
- package/server.js +27 -9
package/public/index.html
CHANGED
|
@@ -208,10 +208,10 @@ svg { display: block; }
|
|
|
208
208
|
#upload-progress.active #upload-progress-text { opacity: 1; }
|
|
209
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
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; }
|
|
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: var(--transition-normal); }
|
|
213
|
-
#sidebar.dragging { transition: none; }
|
|
214
|
-
#sidebar.hidden { width: 0 !important; }
|
|
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; }
|
|
215
215
|
#content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; min-height: 0; position: relative; }
|
|
216
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; }
|
|
217
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; }
|
|
@@ -267,17 +267,11 @@ svg { display: block; }
|
|
|
267
267
|
}
|
|
268
268
|
#editor-view { display: none; flex-direction: column; background: var(--bg); overflow: hidden; }
|
|
269
269
|
#editor-view.open { display: flex; }
|
|
270
|
-
#editor-view.fullscreen { position:
|
|
270
|
+
#editor-view.fullscreen { position: absolute; inset: 0; z-index: 30; width: 100%; height: 100%; background: var(--bg); border: none; }
|
|
271
271
|
#editor-view.fullscreen #editor-header { border-bottom: 1px solid var(--border); background: var(--bg2); }
|
|
272
272
|
#editor-view.fullscreen #editor-area { flex: 1; min-height: 0; }
|
|
273
|
-
#editor-view.fullscreen #editor-area .CodeMirror { height:
|
|
274
|
-
#
|
|
275
|
-
#terminals.fullscreen .term-wrapper { flex: 1; min-height: 0; }
|
|
276
|
-
#terminals.fullscreen #search-bar { top: 8px; }
|
|
277
|
-
#content.terminal-fullscreen { position: fixed; inset: 0; z-index: var(--z-overlay); background: var(--bg); display: flex; flex-direction: column; }
|
|
278
|
-
#content.terminal-fullscreen #tab-bar { flex-shrink: 0; }
|
|
279
|
-
#content.terminal-fullscreen #editor-split-area { flex: 1; min-height: 0; }
|
|
280
|
-
#content.terminal-fullscreen #sidebar { display: none !important; }
|
|
273
|
+
#editor-view.fullscreen #editor-area .CodeMirror { height: 100%; }
|
|
274
|
+
#editor-split-area { position: relative; }
|
|
281
275
|
|
|
282
276
|
/* Desktop: split layout */
|
|
283
277
|
@media (min-width: 769px) {
|
|
@@ -350,8 +344,11 @@ svg { display: block; }
|
|
|
350
344
|
background: var(--bg2); border-left: 1px solid var(--border);
|
|
351
345
|
display: flex; flex-direction: column;
|
|
352
346
|
transform: translateX(100%);
|
|
353
|
-
transition: var(--transition-normal);
|
|
347
|
+
transition: transform var(--transition-normal) ease;
|
|
348
|
+
will-change: transform;
|
|
349
|
+
backface-visibility: hidden;
|
|
354
350
|
z-index: var(--z-drawer);
|
|
351
|
+
contain: layout style;
|
|
355
352
|
}
|
|
356
353
|
#settings-panel.open {
|
|
357
354
|
transform: translateX(0);
|
|
@@ -401,7 +398,7 @@ svg { display: block; }
|
|
|
401
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; }
|
|
402
399
|
.tunnel-btn { width: 28px; height: 28px; min-width: 28px; min-height: 28px; flex-shrink: 0; }
|
|
403
400
|
.tunnel-btn.stop { color: var(--red); }
|
|
404
|
-
#conn-status { transition: background var(--transition-slow); }
|
|
401
|
+
#conn-status { transition: background var(--transition-slow); margin-left: 6px; }
|
|
405
402
|
.reconnect-spinner { display: inline-flex; vertical-align: -1px; margin-right: 4px; }
|
|
406
403
|
.reconnect-spinner svg { animation: reconnectSpin 1s linear infinite; }
|
|
407
404
|
@keyframes reconnectSpin { to { transform: rotate(360deg); } }
|
|
@@ -429,7 +426,7 @@ svg { display: block; }
|
|
|
429
426
|
#sidebar-header { flex-wrap: wrap; height: auto; }
|
|
430
427
|
#file-breadcrumb { white-space: nowrap; overflow-x: auto; line-height: 1; scrollbar-width: none; font-size: 11px; color: var(--fg2); }
|
|
431
428
|
#sidebar-header input { flex: 1 1 60px; min-width: 40px; height: 24px; font-size: 12px; border-radius: var(--radius-sm); }
|
|
432
|
-
#file-list-wrap { flex: 1; overflow-y: auto; min-height: 0; touch-action: pan-y; -webkit-overflow-scrolling: touch; position: relative; overscroll-behavior: contain;
|
|
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; }
|
|
433
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; }
|
|
434
431
|
#ptr-indicator .ptr-arrow { display: inline-flex; }
|
|
435
432
|
#ptr-indicator .ptr-arrow svg { transition: transform 0.2s ease; }
|
|
@@ -622,6 +619,19 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
622
619
|
#editor-preview.active { display: block; }
|
|
623
620
|
#editor-preview-iframe { width: 100%; height: 100%; border: none; display: none; background: var(--bg); border-radius: var(--radius); }
|
|
624
621
|
|
|
622
|
+
/* PDF / EPUB doc preview */
|
|
623
|
+
#pdf-viewer, #epub-viewer { display: none; flex-direction: column; width: 100%; height: 100%; overflow: hidden; background: var(--bg); }
|
|
624
|
+
#pdf-viewer.active, #epub-viewer.active { display: flex; }
|
|
625
|
+
.doc-toolbar { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--bg2); border-bottom: 1px solid var(--border); flex-shrink: 0; font-size: 12px; }
|
|
626
|
+
.doc-toolbar .doc-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fg); font-family: var(--font); font-size: 12px; }
|
|
627
|
+
.doc-toolbar .doc-page-info { color: var(--fg2); font-size: 11px; white-space: nowrap; }
|
|
628
|
+
.doc-toolbar .icon-btn { width: 26px; height: 26px; min-width: 26px; min-height: 26px; }
|
|
629
|
+
#pdf-canvas-wrap { flex: 1; overflow: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 12px; background: var(--bg3); }
|
|
630
|
+
#pdf-canvas-wrap canvas { background: white; box-shadow: 0 2px 12px rgba(0,0,0,0.3); border-radius: 2px; max-width: 100%; }
|
|
631
|
+
#epub-wrap { flex: 1; overflow: auto; display: flex; flex-direction: column; background: white; -webkit-overflow-scrolling: touch; }
|
|
632
|
+
#epub-view { flex: 1; overflow: visible; position: relative; min-height: 100%; }
|
|
633
|
+
#epub-view iframe { width: 100%; height: 100%; border: none; }
|
|
634
|
+
|
|
625
635
|
/* ═══════════════════════════════════════════════════════
|
|
626
636
|
CODEMIRROR
|
|
627
637
|
═══════════════════════════════════════════════════════ */
|
|
@@ -1061,9 +1071,6 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1061
1071
|
<button class="icon-btn" id="tilesBtn" aria-label="Tiles Mode" title="Tiles Mode" onclick="toggleTiles()">
|
|
1062
1072
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
|
1063
1073
|
</button>
|
|
1064
|
-
<button class="icon-btn" id="term-fullscreen-btn" aria-label="Terminal fullscreen" title="Terminal fullscreen (Shift+F11)" onclick="toggleTerminalFullscreen()">
|
|
1065
|
-
<svg width="16" height="16" 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>
|
|
1066
|
-
</button>
|
|
1067
1074
|
<button class="icon-btn" id="more-btn" aria-label="More options" title="More options" onclick="toggleMoreMenu()">
|
|
1068
1075
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="12" cy="5" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="12" cy="19" r="1.7"/></svg>
|
|
1069
1076
|
</button>
|
|
@@ -1233,7 +1240,29 @@ kbd { display: inline-block; padding: 1px 6px; font-family: var(--font); font-si
|
|
|
1233
1240
|
</div>
|
|
1234
1241
|
<div id="editor-area">
|
|
1235
1242
|
<textarea id="editor-textarea" spellcheck="false"></textarea>
|
|
1236
|
-
<div id="editor-preview"><iframe id="editor-preview-iframe" sandbox="allow-scripts"></iframe></div>
|
|
1243
|
+
<div id="editor-preview"><div id="md-preview-content" style="display:none"></div><iframe id="editor-preview-iframe" sandbox="allow-same-origin allow-scripts"></iframe></div>
|
|
1244
|
+
<div id="pdf-viewer">
|
|
1245
|
+
<div class="doc-toolbar">
|
|
1246
|
+
<span class="doc-title" id="pdf-title"></span>
|
|
1247
|
+
<span class="doc-page-info" id="pdf-page-info"></span>
|
|
1248
|
+
<button class="icon-btn" title="Zoom out" onclick="pdfZoom(-0.2)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="8" y1="11" x2="14" y2="11"/></svg></button>
|
|
1249
|
+
<button class="icon-btn" title="Zoom in" onclick="pdfZoom(0.2)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg></button>
|
|
1250
|
+
<button class="icon-btn" title="Previous page" onclick="pdfNav(-1)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></button>
|
|
1251
|
+
<button class="icon-btn" title="Next page" onclick="pdfNav(1)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></button>
|
|
1252
|
+
<button class="icon-btn" title="Download" onclick="downloadFile(editorPath)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></button>
|
|
1253
|
+
</div>
|
|
1254
|
+
<div id="pdf-canvas-wrap"></div>
|
|
1255
|
+
</div>
|
|
1256
|
+
<div id="epub-viewer">
|
|
1257
|
+
<div class="doc-toolbar">
|
|
1258
|
+
<span class="doc-title" id="epub-title"></span>
|
|
1259
|
+
<span class="doc-page-info" id="epub-loc"></span>
|
|
1260
|
+
<button class="icon-btn" title="Previous" onclick="epubNav(-1)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></button>
|
|
1261
|
+
<button class="icon-btn" title="Next" onclick="epubNav(1)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></button>
|
|
1262
|
+
<button class="icon-btn" title="Download" onclick="downloadFile(editorPath)"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></button>
|
|
1263
|
+
</div>
|
|
1264
|
+
<div id="epub-wrap"><div id="epub-view"></div></div>
|
|
1265
|
+
</div>
|
|
1237
1266
|
</div>
|
|
1238
1267
|
</div>
|
|
1239
1268
|
</div>
|
|
@@ -1850,6 +1879,7 @@ async function unlockApp() {
|
|
|
1850
1879
|
setupSwipeGestures();
|
|
1851
1880
|
setupFileListTouch();
|
|
1852
1881
|
setupFileListKeyboard();
|
|
1882
|
+
setupFileListContextMenu();
|
|
1853
1883
|
setupCtxMenuKeyboard();
|
|
1854
1884
|
setupMoreMenuKeyboard();
|
|
1855
1885
|
restoreSidebarWidth();
|
|
@@ -2897,21 +2927,10 @@ async function loadFiles(dir) {
|
|
|
2897
2927
|
if (loadFilesAbortController) loadFilesAbortController.abort();
|
|
2898
2928
|
loadFilesAbortController = new AbortController();
|
|
2899
2929
|
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
} else {
|
|
2903
|
-
if (currentPath && currentPath !== dir) {
|
|
2904
|
-
navHistory.push(currentPath);
|
|
2905
|
-
}
|
|
2906
|
-
navForwardHistory = [];
|
|
2907
|
-
}
|
|
2930
|
+
const prevPath = currentPath;
|
|
2931
|
+
const shouldPushHistory = !skipHistoryPush && currentPath && currentPath !== dir;
|
|
2908
2932
|
skipHistoryPush = false;
|
|
2909
2933
|
|
|
2910
|
-
currentPath = dir;
|
|
2911
|
-
document.getElementById('path-input').value = dir;
|
|
2912
|
-
renderBreadcrumb(dir);
|
|
2913
|
-
saveCurrentPath();
|
|
2914
|
-
|
|
2915
2934
|
const list = document.getElementById('file-list');
|
|
2916
2935
|
|
|
2917
2936
|
setFileListLoading(true);
|
|
@@ -2924,19 +2943,52 @@ async function loadFiles(dir) {
|
|
|
2924
2943
|
const errorDiv = document.createElement('div');
|
|
2925
2944
|
errorDiv.style.cssText = 'padding:16px;text-align:center;';
|
|
2926
2945
|
errorDiv.innerHTML = '<div style="color:var(--red);margin-bottom:8px">Failed to load directory</div>' +
|
|
2946
|
+
'<div style="color:var(--fg2);font-size:11px;margin-bottom:8px;word-break:break-all">' + escHtml(e.message || 'Unknown error') + '</div>' +
|
|
2927
2947
|
'<button class="btn btn-primary" style="height:28px;padding:0 12px;font-size:11px">Retry</button>';
|
|
2928
2948
|
list.innerHTML = '';
|
|
2929
2949
|
list.appendChild(errorDiv);
|
|
2930
2950
|
errorDiv.querySelector('button').addEventListener('click', () => loadFiles(dir));
|
|
2951
|
+
toast(e.message || 'Failed to load directory', 'error');
|
|
2931
2952
|
return;
|
|
2932
2953
|
}
|
|
2933
2954
|
|
|
2934
2955
|
if (!data.files) {
|
|
2935
2956
|
setFileListLoading(false);
|
|
2936
|
-
|
|
2957
|
+
const msg = data.error || 'Failed to load directory';
|
|
2958
|
+
// Revert optimistic path — don't save failed dir
|
|
2959
|
+
// Show error instead of silent bounce
|
|
2960
|
+
const errorDiv = document.createElement('div');
|
|
2961
|
+
errorDiv.style.cssText = 'padding:16px;text-align:center;';
|
|
2962
|
+
errorDiv.innerHTML = '<div style="color:var(--red);margin-bottom:8px">' + escHtml(msg) + '</div>' +
|
|
2963
|
+
'<button class="btn btn-primary" style="height:28px;padding:0 12px;font-size:11px">Retry</button>';
|
|
2964
|
+
list.innerHTML = '';
|
|
2965
|
+
list.appendChild(errorDiv);
|
|
2966
|
+
errorDiv.querySelector('button').addEventListener('click', () => loadFiles(dir));
|
|
2967
|
+
toast(msg, 'error');
|
|
2968
|
+
// Only bounce to home if dir != home and home is different and we haven't already shown home
|
|
2969
|
+
if (dir !== homeDir && prevPath !== homeDir) {
|
|
2970
|
+
// keep currentPath as prevPath, don't update breadcrumb to failed dir
|
|
2971
|
+
document.getElementById('path-input').value = prevPath;
|
|
2972
|
+
renderBreadcrumb(prevPath);
|
|
2973
|
+
} else {
|
|
2974
|
+
// we are already at home or prevPath is home — update UI to reflect attempted dir for debugging
|
|
2975
|
+
document.getElementById('path-input').value = dir;
|
|
2976
|
+
renderBreadcrumb(dir);
|
|
2977
|
+
}
|
|
2937
2978
|
return;
|
|
2938
2979
|
}
|
|
2939
2980
|
|
|
2981
|
+
// Success — now update history and UI state
|
|
2982
|
+
if (shouldPushHistory) {
|
|
2983
|
+
navHistory.push(prevPath);
|
|
2984
|
+
navForwardHistory = [];
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
currentPath = data.path || dir;
|
|
2988
|
+
document.getElementById('path-input').value = currentPath;
|
|
2989
|
+
renderBreadcrumb(currentPath);
|
|
2990
|
+
saveCurrentPath();
|
|
2991
|
+
|
|
2940
2992
|
currentParent = data.parent || '';
|
|
2941
2993
|
|
|
2942
2994
|
// Map existing DOM children by dataset.path
|
|
@@ -3000,13 +3052,16 @@ async function loadFiles(dir) {
|
|
|
3000
3052
|
sizeEl.remove();
|
|
3001
3053
|
}
|
|
3002
3054
|
existing.dataset.isDir = String(f.isDir);
|
|
3055
|
+
existing.dataset.path = key;
|
|
3056
|
+
const checkEl = existing.querySelector('.file-select-check');
|
|
3057
|
+
if (checkEl) checkEl.setAttribute('data-path', key);
|
|
3058
|
+
const ellEl = existing.querySelector('.file-ellipsis');
|
|
3059
|
+
if (ellEl) ellEl.setAttribute('aria-label', 'More actions for ' + f.name);
|
|
3060
|
+
const nameEl2 = existing.querySelector('.file-name');
|
|
3061
|
+
if (nameEl2) nameEl2.title = key;
|
|
3003
3062
|
}
|
|
3004
3063
|
|
|
3005
|
-
addItem(key, () =>
|
|
3006
|
-
const item = makeFileItem(f.name, f.isDir, key, sizeStr, icon);
|
|
3007
|
-
item.addEventListener('contextmenu', e => showCtxMenu(e, f));
|
|
3008
|
-
return item;
|
|
3009
|
-
});
|
|
3064
|
+
addItem(key, () => makeFileItem(f.name, f.isDir, key, sizeStr, icon));
|
|
3010
3065
|
});
|
|
3011
3066
|
|
|
3012
3067
|
// Remove any orphaned nodes (not in new data)
|
|
@@ -3048,25 +3103,27 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
3048
3103
|
<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>
|
|
3049
3104
|
`;
|
|
3050
3105
|
div.addEventListener('click', e => {
|
|
3106
|
+
const curPath = div.dataset.path;
|
|
3107
|
+
const curIsDir = div.dataset.isDir === 'true';
|
|
3108
|
+
const curName = (div.querySelector('.file-name')?.textContent || curPath.split(/[\\/]/).pop() || curPath);
|
|
3051
3109
|
if (e.target.closest('.file-select-check')) {
|
|
3052
|
-
toggleFileSelection(
|
|
3110
|
+
toggleFileSelection(curPath, div);
|
|
3053
3111
|
if (selectedFiles.length > 0) document.getElementById('select-actions').style.display = 'flex';
|
|
3054
3112
|
return;
|
|
3055
3113
|
}
|
|
3056
3114
|
if (e.target.closest('.file-ellipsis')) {
|
|
3057
3115
|
e.stopPropagation();
|
|
3058
3116
|
const rect = div.querySelector('.file-ellipsis').getBoundingClientRect();
|
|
3059
|
-
const m =
|
|
3060
|
-
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path:
|
|
3117
|
+
const m = curPath.match(/\.([^.]+)$/);
|
|
3118
|
+
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path: curPath, name: curName, isDir: curIsDir, ext: m ? '.' + m[1].toLowerCase() : '' });
|
|
3061
3119
|
return;
|
|
3062
3120
|
}
|
|
3063
3121
|
if (selectMode) {
|
|
3064
|
-
toggleFileSelection(
|
|
3122
|
+
toggleFileSelection(curPath, div);
|
|
3065
3123
|
return;
|
|
3066
3124
|
}
|
|
3067
|
-
if (
|
|
3068
|
-
else
|
|
3069
|
-
else openFileEditor(fullPath);
|
|
3125
|
+
if (curIsDir) loadFiles(curPath);
|
|
3126
|
+
else openFileEditor(curPath);
|
|
3070
3127
|
});
|
|
3071
3128
|
const ell = div.querySelector('.file-ellipsis');
|
|
3072
3129
|
if (ell) {
|
|
@@ -3074,9 +3131,12 @@ function makeFileItem(name, isDir, fullPath, size, icon) {
|
|
|
3074
3131
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
3075
3132
|
e.preventDefault();
|
|
3076
3133
|
e.stopPropagation();
|
|
3134
|
+
const curPath = div.dataset.path;
|
|
3135
|
+
const curIsDir = div.dataset.isDir === 'true';
|
|
3136
|
+
const curName = (div.querySelector('.file-name')?.textContent || curPath.split(/[\\/]/).pop() || curPath);
|
|
3077
3137
|
const rect = ell.getBoundingClientRect();
|
|
3078
|
-
const m =
|
|
3079
|
-
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path:
|
|
3138
|
+
const m = curPath.match(/\.([^.]+)$/);
|
|
3139
|
+
showCtxMenu({ clientX: rect.left + rect.width / 2, clientY: rect.bottom + 4, preventDefault() {}, stopPropagation() {} }, { path: curPath, name: curName, isDir: curIsDir, ext: m ? '.' + m[1].toLowerCase() : '' });
|
|
3080
3140
|
}
|
|
3081
3141
|
});
|
|
3082
3142
|
}
|
|
@@ -3120,10 +3180,33 @@ function setupFileListKeyboard() {
|
|
|
3120
3180
|
});
|
|
3121
3181
|
}
|
|
3122
3182
|
|
|
3183
|
+
function setupFileListContextMenu() {
|
|
3184
|
+
const list = document.getElementById('file-list');
|
|
3185
|
+
if (!list || list.dataset._ctxSetup) return;
|
|
3186
|
+
list.dataset._ctxSetup = '1';
|
|
3187
|
+
list.addEventListener('contextmenu', e => {
|
|
3188
|
+
const item = e.target.closest('.file-item');
|
|
3189
|
+
if (!item || !item.dataset.path) return;
|
|
3190
|
+
const curPath = item.dataset.path;
|
|
3191
|
+
const curIsDir = item.dataset.isDir === 'true';
|
|
3192
|
+
const curName = (item.querySelector('.file-name')?.textContent || curPath.split(/[\\/]/).pop() || curPath);
|
|
3193
|
+
const m = curPath.match(/\.([^.]+)$/);
|
|
3194
|
+
const ext = m ? '.' + m[1].toLowerCase() : '';
|
|
3195
|
+
showCtxMenu(e, { path: curPath, name: curName, isDir: curIsDir, ext });
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3123
3199
|
function refreshFiles() {
|
|
3124
3200
|
if (currentPath) loadFiles(currentPath);
|
|
3125
3201
|
}
|
|
3126
|
-
function navigateTo(p) {
|
|
3202
|
+
function navigateTo(p) {
|
|
3203
|
+
if (!p || !String(p).trim()) return loadFiles(homeDir);
|
|
3204
|
+
let target = String(p).trim();
|
|
3205
|
+
// If relative path, resolve against currentPath
|
|
3206
|
+
const isAbsolute = target.startsWith('/') || /^[A-Za-z]:[\\/]/.test(target) || target.startsWith('\\\\');
|
|
3207
|
+
if (!isAbsolute) target = joinPath(currentPath || homeDir, target);
|
|
3208
|
+
loadFiles(target);
|
|
3209
|
+
}
|
|
3127
3210
|
function navigateUp() {
|
|
3128
3211
|
if (currentParent && currentParent !== currentPath) {
|
|
3129
3212
|
loadFiles(currentParent);
|
|
@@ -3159,7 +3242,10 @@ function startFileWatcher() {
|
|
|
3159
3242
|
if (fileWatchTimer) return;
|
|
3160
3243
|
const watch = () => {
|
|
3161
3244
|
const sb = document.getElementById('sidebar');
|
|
3245
|
+
const sp = document.getElementById('settings-panel');
|
|
3162
3246
|
if (!sb || sb.classList.contains('hidden') || document.hidden) return;
|
|
3247
|
+
if (sp && sp.classList.contains('open')) return;
|
|
3248
|
+
if (document.querySelector('.overlay.open')) return;
|
|
3163
3249
|
if (currentPath) loadFiles(currentPath);
|
|
3164
3250
|
};
|
|
3165
3251
|
const onVisibility = () => {
|
|
@@ -3183,8 +3269,10 @@ function getFileIcon(ext) {
|
|
|
3183
3269
|
const codeExts = { '.js':1,'.ts':1,'.jsx':1,'.tsx':1,'.py':1,'.rs':1,'.go':1,'.java':1,'.c':1,'.cpp':1,'.html':1,'.css':1,'.sh':1,'.yml':1,'.yaml':1,'.toml':1,'.json':1,'.sql':1 };
|
|
3184
3270
|
const imgExts = { '.png':1,'.jpg':1,'.jpeg':1,'.gif':1,'.svg':1,'.webp':1,'.ico':1 };
|
|
3185
3271
|
const archiveExts = { '.zip':1,'.tar':1,'.gz':1,'.bz2':1,'.xz':1,'.7z':1,'.rar':1 };
|
|
3272
|
+
const docExts = { '.pdf':1,'.epub':1 };
|
|
3186
3273
|
if (codeExts[ext]) return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>';
|
|
3187
3274
|
if (imgExts[ext]) return '<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"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
|
|
3275
|
+
if (docExts[ext]) return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>';
|
|
3188
3276
|
if (archiveExts[ext]) return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/></svg>';
|
|
3189
3277
|
return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
|
|
3190
3278
|
}
|
|
@@ -3469,8 +3557,8 @@ function showCtxMenu(e, file) {
|
|
|
3469
3557
|
ctxTarget = file;
|
|
3470
3558
|
const menu = document.getElementById('ctx-menu');
|
|
3471
3559
|
menu.style.display = 'block';
|
|
3472
|
-
document.getElementById('ctx-open').style.display = file.isDir || isImageFile(file.path) ? '' : 'none';
|
|
3473
|
-
document.getElementById('ctx-edit').style.display = file.isDir ? 'none' : (
|
|
3560
|
+
document.getElementById('ctx-open').style.display = file.isDir || isImageFile(file.path) || isDocFile(file.path) ? '' : 'none';
|
|
3561
|
+
document.getElementById('ctx-edit').style.display = file.isDir ? 'none' : (!canOpenInEditor(file.path) ? 'none' : '');
|
|
3474
3562
|
document.getElementById('ctx-open-term').style.display = file.isDir ? '' : 'none';
|
|
3475
3563
|
document.getElementById('ctx-paste').style.display = fsClipboard ? '' : 'none';
|
|
3476
3564
|
document.getElementById('ctx-zip').style.display = '';
|
|
@@ -3501,7 +3589,12 @@ document.addEventListener('click', () => {
|
|
|
3501
3589
|
if (termMenu) termMenu.style.display = 'none';
|
|
3502
3590
|
});
|
|
3503
3591
|
|
|
3504
|
-
document.getElementById('ctx-open').onclick = () => {
|
|
3592
|
+
document.getElementById('ctx-open').onclick = () => {
|
|
3593
|
+
if (!ctxTarget) return;
|
|
3594
|
+
if (ctxTarget.isDir) loadFiles(ctxTarget.path);
|
|
3595
|
+
else if (isImageFile(ctxTarget.path) || isDocFile(ctxTarget.path)) openFileEditor(ctxTarget.path);
|
|
3596
|
+
else loadFiles(ctxTarget.path);
|
|
3597
|
+
};
|
|
3505
3598
|
document.getElementById('ctx-edit').onclick = () => ctxTarget && openFileEditor(ctxTarget.path);
|
|
3506
3599
|
document.getElementById('ctx-download').onclick = () => {
|
|
3507
3600
|
if (selectedFiles.length > 0) { downloadSelected(); return; }
|
|
@@ -3664,9 +3757,8 @@ async function confirmRename() {
|
|
|
3664
3757
|
body: JSON.stringify({ oldPath: renamePath, newName })
|
|
3665
3758
|
});
|
|
3666
3759
|
setBtnBusy(document.getElementById('rename-ok-btn'), false);
|
|
3667
|
-
if (r.success) { toast('Renamed', 'success'); refreshFiles(); }
|
|
3760
|
+
if (r.success) { toast('Renamed', 'success'); refreshFiles(); closeOverlay('rename-overlay'); }
|
|
3668
3761
|
else showFieldError('rename-error', r.error || 'Rename failed');
|
|
3669
|
-
closeOverlay('rename-overlay');
|
|
3670
3762
|
}
|
|
3671
3763
|
|
|
3672
3764
|
async function deleteFile(file) {
|
|
@@ -4101,8 +4193,15 @@ function getCMmode(fileName) {
|
|
|
4101
4193
|
}
|
|
4102
4194
|
|
|
4103
4195
|
async function openFileEditor(path) {
|
|
4196
|
+
if (isImageFile(path)) { openImageViewer(path); return; }
|
|
4197
|
+
if (isPdfFile(path)) { openPdfViewer(path); return; }
|
|
4198
|
+
if (isEpubFile(path)) { openEpubViewer(path); return; }
|
|
4199
|
+
if (!canOpenInEditor(path)) { toast('Preview not supported for this file type — use Download', 'warning'); return; }
|
|
4104
4200
|
const r = await api(`/api/files/read?path=${encodeURIComponent(path)}`);
|
|
4105
|
-
if (r.error) {
|
|
4201
|
+
if (r.error) {
|
|
4202
|
+
if (r.isBinary) { toast(r.error || 'Preview not supported for binary files — use Download', 'warning'); return; }
|
|
4203
|
+
toast(r.error, 'error'); return;
|
|
4204
|
+
}
|
|
4106
4205
|
if (r.length > MAX_EDITOR_SIZE) {
|
|
4107
4206
|
const ok = await confirmDialog({ title: 'Large file', message: `File is ${(r.length / 1024 / 1024).toFixed(1)}MB. Open anyway?`, okText: 'Open', danger: true });
|
|
4108
4207
|
if (!ok) return;
|
|
@@ -4113,6 +4212,9 @@ async function openFileEditor(path) {
|
|
|
4113
4212
|
const fileName = path.split(sep).pop() || path;
|
|
4114
4213
|
const isMd = /\.md$/i.test(fileName);
|
|
4115
4214
|
const isHtml = /\.html?$/i.test(fileName);
|
|
4215
|
+
// hide doc viewers when opening text file
|
|
4216
|
+
cleanupDocViewers();
|
|
4217
|
+
document.getElementById('editor-save-btn').style.display = '';
|
|
4116
4218
|
|
|
4117
4219
|
const cm = initCodeMirror();
|
|
4118
4220
|
cm.setValue(r.content);
|
|
@@ -4127,8 +4229,12 @@ async function openFileEditor(path) {
|
|
|
4127
4229
|
const toggleBtn = document.getElementById('md-preview-toggle');
|
|
4128
4230
|
const refreshBtn = document.getElementById('preview-refresh-btn');
|
|
4129
4231
|
const previewIframe = document.getElementById('editor-preview-iframe');
|
|
4232
|
+
const mdContent = document.getElementById('md-preview-content');
|
|
4130
4233
|
preview.classList.remove('active');
|
|
4131
4234
|
if (previewIframe) { previewIframe.style.display = 'none'; previewIframe.srcdoc = ''; }
|
|
4235
|
+
if (mdContent) { mdContent.style.display = 'none'; mdContent.innerHTML = ''; }
|
|
4236
|
+
mdPreviewActive = false;
|
|
4237
|
+
clearPreviewLiveReload();
|
|
4132
4238
|
if ((isMd && typeof marked !== 'undefined') || isHtml) {
|
|
4133
4239
|
toggleBtn.style.display = '';
|
|
4134
4240
|
toggleBtn.textContent = 'Preview';
|
|
@@ -4208,17 +4314,20 @@ async function saveFile() {
|
|
|
4208
4314
|
}
|
|
4209
4315
|
|
|
4210
4316
|
async function closeEditor() {
|
|
4211
|
-
const
|
|
4212
|
-
if (
|
|
4213
|
-
const
|
|
4214
|
-
if (
|
|
4317
|
+
const isDocPreview = !!(_pdfDoc || _epubBook);
|
|
4318
|
+
if (!isDocPreview) {
|
|
4319
|
+
const currentContent = editor ? editor.getValue() : document.getElementById('editor-textarea').value;
|
|
4320
|
+
if (currentContent !== editorOriginalContent) {
|
|
4321
|
+
const ok = await confirmDialog({ title: 'Unsaved changes', message: 'You have unsaved changes. Close anyway?', okText: 'Discard', cancelText: 'Keep Editing', danger: true });
|
|
4322
|
+
if (!ok) return;
|
|
4323
|
+
}
|
|
4215
4324
|
}
|
|
4216
4325
|
clearPreviewLiveReload();
|
|
4326
|
+
cleanupDocViewers();
|
|
4217
4327
|
const ev = document.getElementById('editor-view');
|
|
4218
4328
|
ev.classList.remove('open');
|
|
4219
4329
|
ev.classList.remove('fullscreen');
|
|
4220
4330
|
document.removeEventListener('keydown', _fsEscHandler);
|
|
4221
|
-
try { if (document.fullscreenElement) document.exitFullscreen(); } catch {}
|
|
4222
4331
|
const fsBtn = document.getElementById('editor-fullscreen-toggle');
|
|
4223
4332
|
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>'; }
|
|
4224
4333
|
document.getElementById('content').classList.remove('editor-open');
|
|
@@ -4227,6 +4336,8 @@ async function closeEditor() {
|
|
|
4227
4336
|
document.getElementById('preview-refresh-btn').style.display = 'none';
|
|
4228
4337
|
const previewIframe = document.getElementById('editor-preview-iframe');
|
|
4229
4338
|
if (previewIframe) { previewIframe.style.display = 'none'; previewIframe.srcdoc = ''; }
|
|
4339
|
+
const mdContent = document.getElementById('md-preview-content');
|
|
4340
|
+
if (mdContent) { mdContent.style.display = 'none'; mdContent.innerHTML = ''; }
|
|
4230
4341
|
const cm = document.querySelector('.CodeMirror');
|
|
4231
4342
|
if (cm) cm.style.display = '';
|
|
4232
4343
|
mdPreviewActive = false;
|
|
@@ -4239,11 +4350,445 @@ async function closeEditor() {
|
|
|
4239
4350
|
|
|
4240
4351
|
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp', '.ico']);
|
|
4241
4352
|
|
|
4353
|
+
// Binary extensions from https://github.com/sindresorhus/binary-extensions (v2.3.0)
|
|
4354
|
+
// Used to block editor preview for non-text files. Images are previewable via image viewer, so they are checked first.
|
|
4355
|
+
const BINARY_EXTS = new Set([
|
|
4356
|
+
'3dm','3ds','3g2','3gp','7z','a','aac','adp','afdesign','afphoto','afpub','ai','aif','aiff','alz','ape','apk','appimage','ar','arj','asf','au','avi','bak','baml','bh','bin','bk','bmp','btif','bz2','bzip2','cab','caf','cgm','class','cmx','cpio','cr2','cr3','cur','dat','dcm','deb','dex','djvu','dll','dmg','dng','doc','docm','docx','dot','dotm','dra','ds_store','dsk','dts','dtshd','dvb','dwg','dxf','ecelp4800','ecelp7470','ecelp9600','egg','eol','eot','epub','exe','f4v','fbs','fh','fla','flac','flatpak','fli','flv','fpx','fst','fvt','g3','gh','gif','graffle','gz','gzip','h261','h263','h264','icns','ico','ief','img','ipa','iso','jar','jpeg','jpg','jpgv','jpm','jxr','key','ktx','lha','lib','lvp','lz','lzh','lzma','lzo','m3u','m4a','m4v','mar','mdi','mht','mid','midi','mj2','mka','mkv','mmr','mng','mobi','mov','movie','mp3','mp4','mp4a','mpeg','mpg','mpga','mxu','nef','npx','numbers','nupkg','o','odp','ods','odt','oga','ogg','ogv','otf','ott','pages','pbm','pcx','pdb','pdf','pea','pgm','pic','png','pnm','pot','potm','potx','ppa','ppam','ppm','pps','ppsm','ppsx','ppt','pptm','pptx','psd','pya','pyc','pyo','pyv','qt','rar','ras','raw','resources','rgb','rip','rlc','rmf','rmvb','rpm','rtf','rz','s3m','s7z','scpt','sgi','shar','snap','sil','sketch','slk','smv','snk','so','stl','suo','sub','swf','tar','tbz','tbz2','tga','tgz','thmx','tif','tiff','tlz','ttc','ttf','txz','udf','uvh','uvi','uvm','uvp','uvs','uvu','viv','vob','war','wav','wax','wbmp','wdp','weba','webm','webp','whl','wim','wm','wma','wmv','wmx','woff','woff2','wrm','wvx','xbm','xif','xla','xlam','xls','xlsb','xlsm','xlsx','xlt','xltm','xltx','xm','xmind','xpi','xpm','xwd','xz','z','zip','zipx'
|
|
4357
|
+
]);
|
|
4358
|
+
|
|
4242
4359
|
function isImageFile(path) {
|
|
4243
4360
|
const m = path.match(/\.([^.]+)$/);
|
|
4244
4361
|
return m ? IMAGE_EXTS.has('.' + m[1].toLowerCase()) : false;
|
|
4245
4362
|
}
|
|
4246
4363
|
|
|
4364
|
+
function getFileExt(path) {
|
|
4365
|
+
const m = path.match(/\.([^.\\/]+)$/);
|
|
4366
|
+
return m ? '.' + m[1].toLowerCase() : '';
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
function isBinaryFile(path) {
|
|
4370
|
+
const ext = getFileExt(path);
|
|
4371
|
+
return ext ? BINARY_EXTS.has(ext.slice(1).toLowerCase()) : false;
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
function canOpenInEditor(path) {
|
|
4375
|
+
if (isImageFile(path)) return false;
|
|
4376
|
+
if (isPdfFile(path) || isEpubFile(path)) return false;
|
|
4377
|
+
if (isBinaryFile(path)) return false;
|
|
4378
|
+
return true;
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
function isPdfFile(path) { return /\.pdf$/i.test(path); }
|
|
4382
|
+
function isEpubFile(path) { return /\.epub$/i.test(path); }
|
|
4383
|
+
function isDocFile(path) { return isPdfFile(path) || isEpubFile(path); }
|
|
4384
|
+
|
|
4385
|
+
// ── Doc preview dynamic loader ──
|
|
4386
|
+
function loadScript(src) {
|
|
4387
|
+
return new Promise((resolve, reject) => {
|
|
4388
|
+
if (document.querySelector(`script[src="${src}"]`)) return resolve();
|
|
4389
|
+
const s = document.createElement('script');
|
|
4390
|
+
s.src = src;
|
|
4391
|
+
s.async = true;
|
|
4392
|
+
s.onload = () => resolve();
|
|
4393
|
+
s.onerror = () => reject(new Error('Failed to load ' + src));
|
|
4394
|
+
document.head.appendChild(s);
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
async function loadPdfJs() {
|
|
4398
|
+
if (window.pdfjsLib) return;
|
|
4399
|
+
// pdf.js v2 UMD global is pdfjsLib
|
|
4400
|
+
await loadScript('https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/build/pdf.min.js');
|
|
4401
|
+
const lib = window.pdfjsLib || window['pdfjs-dist/build/pdf'];
|
|
4402
|
+
if (!lib) throw new Error('PDF.js not found');
|
|
4403
|
+
window.pdfjsLib = lib;
|
|
4404
|
+
if (lib.GlobalWorkerOptions) {
|
|
4405
|
+
lib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/build/pdf.worker.min.js';
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
async function loadEpubJs() {
|
|
4409
|
+
if (window.ePub) return;
|
|
4410
|
+
await loadScript('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js');
|
|
4411
|
+
await loadScript('https://cdn.jsdelivr.net/npm/epubjs@0.3.93/dist/epub.min.js');
|
|
4412
|
+
if (!window.ePub && !window.EPUBJS) throw new Error('EPUB.js not found');
|
|
4413
|
+
if (!window.ePub && window.EPUBJS) window.ePub = window.EPUBJS;
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
let _pdfDoc = null;
|
|
4417
|
+
let _pdfPath = '';
|
|
4418
|
+
let _pdfScale = 1.4;
|
|
4419
|
+
let _pdfCurrentPage = 1;
|
|
4420
|
+
let _pdfPageOrder = null;
|
|
4421
|
+
|
|
4422
|
+
async function openPdfViewer(path) {
|
|
4423
|
+
// Show viewer shell immediately for perceived performance
|
|
4424
|
+
editorPath = path;
|
|
4425
|
+
editorOriginalContent = '';
|
|
4426
|
+
const fileName = path.split(/[\\/]/).pop() || path;
|
|
4427
|
+
document.getElementById('editor-filename').textContent = fileName + ' / ' + path;
|
|
4428
|
+
document.getElementById('editor-status').textContent = '';
|
|
4429
|
+
const cm = document.querySelector('.CodeMirror');
|
|
4430
|
+
if (cm) cm.style.display = 'none';
|
|
4431
|
+
document.getElementById('editor-preview').classList.remove('active');
|
|
4432
|
+
const iframe = document.getElementById('editor-preview-iframe');
|
|
4433
|
+
if (iframe) { iframe.style.display = 'none'; iframe.srcdoc = ''; }
|
|
4434
|
+
const mdContent = document.getElementById('md-preview-content');
|
|
4435
|
+
if (mdContent) { mdContent.style.display = 'none'; }
|
|
4436
|
+
mdPreviewActive = false;
|
|
4437
|
+
clearPreviewLiveReload();
|
|
4438
|
+
document.getElementById('epub-viewer').classList.remove('active');
|
|
4439
|
+
const pdfViewer = document.getElementById('pdf-viewer');
|
|
4440
|
+
pdfViewer.classList.add('active');
|
|
4441
|
+
document.getElementById('pdf-title').textContent = fileName;
|
|
4442
|
+
document.getElementById('pdf-page-info').textContent = 'Loading…';
|
|
4443
|
+
document.getElementById('pdf-canvas-wrap').innerHTML = '<div style="color:var(--fg2);padding:20px">Loading PDF…</div>';
|
|
4444
|
+
// open editor split immediately
|
|
4445
|
+
if (window.innerWidth > 768) {
|
|
4446
|
+
document.getElementById('content').classList.add('editor-open');
|
|
4447
|
+
const splitArea = document.getElementById('editor-split-area');
|
|
4448
|
+
try {
|
|
4449
|
+
const orient = safeStorage.getItem('wt-editor-split-orientation');
|
|
4450
|
+
const isHoriz = orient === 'horizontal';
|
|
4451
|
+
if (splitArea) splitArea.classList.toggle('horizontal', isHoriz);
|
|
4452
|
+
} catch {}
|
|
4453
|
+
}
|
|
4454
|
+
document.getElementById('editor-view').classList.add('open');
|
|
4455
|
+
document.getElementById('editor-save-btn').style.display = 'none';
|
|
4456
|
+
document.getElementById('md-preview-toggle').style.display = 'none';
|
|
4457
|
+
document.getElementById('preview-refresh-btn').style.display = 'none';
|
|
4458
|
+
requestAnimationFrame(() => { tabs.forEach(tab => { try { fitTerm(tab); } catch (e) {} }); });
|
|
4459
|
+
// Load PDF.js immediately without confirmation (cached after first load)
|
|
4460
|
+
if (!window.pdfjsLib) {
|
|
4461
|
+
const t = toast('Loading PDF viewer…', 'info');
|
|
4462
|
+
try { await loadPdfJs(); t.remove(); } catch (e) { t.remove(); toast('Failed to load PDF viewer: ' + e.message, 'error'); return; }
|
|
4463
|
+
}
|
|
4464
|
+
try {
|
|
4465
|
+
const resp = await fetch(`/api/files/image?path=${encodeURIComponent(path)}&_t=${Date.now()}`, { headers: { 'x-pin-token': authToken } });
|
|
4466
|
+
if (!resp.ok) throw new Error('Failed to fetch PDF');
|
|
4467
|
+
const buf = await resp.arrayBuffer();
|
|
4468
|
+
const lib = window.pdfjsLib;
|
|
4469
|
+
const loadingTask = lib.getDocument({ data: buf });
|
|
4470
|
+
_pdfDoc = await loadingTask.promise;
|
|
4471
|
+
_pdfPath = path;
|
|
4472
|
+
_pdfCurrentPage = 1;
|
|
4473
|
+
_pdfScale = 1.4;
|
|
4474
|
+
await renderPdfDoc();
|
|
4475
|
+
} catch (e) {
|
|
4476
|
+
console.warn('PDF load failed', e);
|
|
4477
|
+
document.getElementById('pdf-canvas-wrap').innerHTML = '<div style="color:var(--red);padding:20px">Failed to load PDF: ' + escHtml(e.message) + '</div>';
|
|
4478
|
+
document.getElementById('pdf-page-info').textContent = 'Error';
|
|
4479
|
+
toast('PDF load failed: ' + e.message, 'error');
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
async function renderPdfDoc() {
|
|
4483
|
+
if (!_pdfDoc) return;
|
|
4484
|
+
const wrap = document.getElementById('pdf-canvas-wrap');
|
|
4485
|
+
wrap.innerHTML = '';
|
|
4486
|
+
const total = _pdfDoc.numPages;
|
|
4487
|
+
let pageLabels = null;
|
|
4488
|
+
try { pageLabels = await _pdfDoc.getPageLabels(); } catch {}
|
|
4489
|
+
const getLabel = (idx) => pageLabels && pageLabels[idx-1] ? pageLabels[idx-1] : String(idx);
|
|
4490
|
+
// Build ordered list of pages sorted by actual page number (label if numeric, else index) to ensure preview matches printed page numbers
|
|
4491
|
+
let pageOrder = Array.from({length: total}, (_, k) => k+1);
|
|
4492
|
+
if (pageLabels) {
|
|
4493
|
+
try {
|
|
4494
|
+
pageOrder.sort((a,b) => {
|
|
4495
|
+
const la = getLabel(a), lb = getLabel(b);
|
|
4496
|
+
const na = parseInt(la, 10), nb = parseInt(lb, 10);
|
|
4497
|
+
const aIsNum = !isNaN(na) && String(na) === la.trim();
|
|
4498
|
+
const bIsNum = !isNaN(nb) && String(nb) === lb.trim();
|
|
4499
|
+
if (aIsNum && bIsNum) return na - nb;
|
|
4500
|
+
if (aIsNum && !bIsNum) return -1;
|
|
4501
|
+
if (!aIsNum && bIsNum) return 1;
|
|
4502
|
+
return la.localeCompare(lb);
|
|
4503
|
+
});
|
|
4504
|
+
} catch {}
|
|
4505
|
+
}
|
|
4506
|
+
_pdfPageOrder = pageOrder;
|
|
4507
|
+
_pdfCurrentPage = pageOrder[0];
|
|
4508
|
+
document.getElementById('pdf-page-info').textContent = getLabel(pageOrder[0]) + ' / ' + getLabel(pageOrder[pageOrder.length-1]) + ' • ' + total + ' pages';
|
|
4509
|
+
// Render all pages vertically for scrollable preview in sorted order
|
|
4510
|
+
try {
|
|
4511
|
+
for (const i of pageOrder) {
|
|
4512
|
+
const page = await _pdfDoc.getPage(i);
|
|
4513
|
+
const viewport = page.getViewport({ scale: _pdfScale });
|
|
4514
|
+
const canvas = document.createElement('canvas');
|
|
4515
|
+
canvas.id = 'pdf-page-' + i;
|
|
4516
|
+
canvas.dataset.page = i;
|
|
4517
|
+
canvas.dataset.label = getLabel(i);
|
|
4518
|
+
const ctx = canvas.getContext('2d');
|
|
4519
|
+
canvas.width = viewport.width;
|
|
4520
|
+
canvas.height = viewport.height;
|
|
4521
|
+
canvas.style.width = viewport.width + 'px';
|
|
4522
|
+
canvas.style.height = viewport.height + 'px';
|
|
4523
|
+
// label
|
|
4524
|
+
const pageWrap = document.createElement('div');
|
|
4525
|
+
pageWrap.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:4px';
|
|
4526
|
+
pageWrap.appendChild(canvas);
|
|
4527
|
+
const label = document.createElement('div');
|
|
4528
|
+
label.style.cssText = 'color:var(--fg2);font-size:10px';
|
|
4529
|
+
label.textContent = 'Page ' + getLabel(i) + ' • ' + i + '/' + total;
|
|
4530
|
+
pageWrap.appendChild(label);
|
|
4531
|
+
wrap.appendChild(pageWrap);
|
|
4532
|
+
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
4533
|
+
// allow UI to breathe for large docs
|
|
4534
|
+
if (i % 10 === 0) await new Promise(r => setTimeout(r, 0));
|
|
4535
|
+
}
|
|
4536
|
+
setupPdfScrollTracking();
|
|
4537
|
+
} catch (e) {
|
|
4538
|
+
wrap.innerHTML = '<div style="color:var(--red);padding:20px">Render failed: ' + escHtml(e.message) + '</div>';
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
let _pdfObserver = null;
|
|
4542
|
+
function setupPdfScrollTracking() {
|
|
4543
|
+
const wrap = document.getElementById('pdf-canvas-wrap');
|
|
4544
|
+
if (!wrap || !_pdfDoc) return;
|
|
4545
|
+
if (_pdfObserver) try { _pdfObserver.disconnect(); } catch {}
|
|
4546
|
+
const canvases = wrap.querySelectorAll('canvas[id^="pdf-page-"]');
|
|
4547
|
+
if (!canvases.length) return;
|
|
4548
|
+
let ticking = false;
|
|
4549
|
+
const update = () => {
|
|
4550
|
+
ticking = false;
|
|
4551
|
+
let best = null;
|
|
4552
|
+
let bestTop = Infinity;
|
|
4553
|
+
const wrapRect = wrap.getBoundingClientRect();
|
|
4554
|
+
for (const c of canvases) {
|
|
4555
|
+
const r = c.getBoundingClientRect();
|
|
4556
|
+
// visible if within wrap viewport
|
|
4557
|
+
if (r.bottom > wrapRect.top && r.top < wrapRect.bottom) {
|
|
4558
|
+
const dist = Math.abs(r.top - wrapRect.top);
|
|
4559
|
+
if (dist < bestTop) { bestTop = dist; best = c; }
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
if (best) {
|
|
4563
|
+
const pageNum = parseInt(best.dataset.page, 10);
|
|
4564
|
+
const label = best.dataset.label || String(pageNum);
|
|
4565
|
+
if (pageNum && pageNum !== _pdfCurrentPage) {
|
|
4566
|
+
_pdfCurrentPage = pageNum;
|
|
4567
|
+
let pageLabels2 = null;
|
|
4568
|
+
try { pageLabels2 = _pdfDoc.getPageLabels && _pdfDoc._pageLabels ? _pdfDoc._pageLabels : null; } catch {}
|
|
4569
|
+
// use stored label
|
|
4570
|
+
document.getElementById('pdf-page-info').textContent = label + ' • ' + pageNum + '/' + _pdfDoc.numPages + ' • scroll vertically';
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
};
|
|
4574
|
+
wrap.addEventListener('scroll', () => {
|
|
4575
|
+
if (!ticking) { ticking = true; requestAnimationFrame(update); }
|
|
4576
|
+
}, { passive: true });
|
|
4577
|
+
// also use IntersectionObserver for more precise
|
|
4578
|
+
try {
|
|
4579
|
+
_pdfObserver = new IntersectionObserver((entries) => {
|
|
4580
|
+
let best = null; let bestRatio = 0;
|
|
4581
|
+
for (const e of entries) {
|
|
4582
|
+
if (e.isIntersecting && e.intersectionRatio > bestRatio) {
|
|
4583
|
+
bestRatio = e.intersectionRatio; best = e.target;
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
if (best) {
|
|
4587
|
+
const pageNum = parseInt(best.dataset.page, 10);
|
|
4588
|
+
const label = best.dataset.label || String(pageNum);
|
|
4589
|
+
if (pageNum) {
|
|
4590
|
+
_pdfCurrentPage = pageNum;
|
|
4591
|
+
document.getElementById('pdf-page-info').textContent = label + ' • ' + pageNum + '/' + _pdfDoc.numPages + ' • scroll vertically';
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
}, { root: wrap, threshold: [0.5, 0.75] });
|
|
4595
|
+
canvases.forEach(c => _pdfObserver.observe(c));
|
|
4596
|
+
} catch { _pdfObserver = null; }
|
|
4597
|
+
// initial update
|
|
4598
|
+
setTimeout(update, 100);
|
|
4599
|
+
}
|
|
4600
|
+
function pdfNav(dir) {
|
|
4601
|
+
if (!_pdfDoc) return;
|
|
4602
|
+
const wrap = document.getElementById('pdf-canvas-wrap');
|
|
4603
|
+
if (!wrap) return;
|
|
4604
|
+
// Scroll to next/prev page in sorted order
|
|
4605
|
+
const total = _pdfDoc.numPages;
|
|
4606
|
+
const order = _pdfPageOrder || Array.from({length: total}, (_,k)=>k+1);
|
|
4607
|
+
const idx = order.indexOf(_pdfCurrentPage);
|
|
4608
|
+
let nextIdx = idx + dir;
|
|
4609
|
+
if (nextIdx < 0) nextIdx = 0;
|
|
4610
|
+
if (nextIdx >= order.length) nextIdx = order.length - 1;
|
|
4611
|
+
const target = order[nextIdx];
|
|
4612
|
+
_pdfCurrentPage = target;
|
|
4613
|
+
const el = document.getElementById('pdf-page-' + target);
|
|
4614
|
+
const label = el ? el.dataset.label : String(target);
|
|
4615
|
+
document.getElementById('pdf-page-info').textContent = label + ' • ' + target + '/' + total + ' • scroll vertically';
|
|
4616
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
4617
|
+
else if (dir < 0) wrap.scrollBy({ top: -wrap.clientHeight * 0.9, behavior: 'smooth' });
|
|
4618
|
+
else wrap.scrollBy({ top: wrap.clientHeight * 0.9, behavior: 'smooth' });
|
|
4619
|
+
}
|
|
4620
|
+
function pdfZoom(delta) {
|
|
4621
|
+
if (!_pdfDoc) return;
|
|
4622
|
+
_pdfScale = Math.max(0.6, Math.min(3, _pdfScale + delta));
|
|
4623
|
+
// Re-render all pages at new scale (keeps scroll position near current page)
|
|
4624
|
+
const wrap = document.getElementById('pdf-canvas-wrap');
|
|
4625
|
+
const scrollTop = wrap ? wrap.scrollTop : 0;
|
|
4626
|
+
if (_pdfObserver) try { _pdfObserver.disconnect(); _pdfObserver = null; } catch {}
|
|
4627
|
+
renderPdfDoc().then(() => {
|
|
4628
|
+
if (wrap) {
|
|
4629
|
+
const el = document.getElementById('pdf-page-' + _pdfCurrentPage);
|
|
4630
|
+
if (el) el.scrollIntoView({ block: 'start' });
|
|
4631
|
+
else wrap.scrollTop = scrollTop;
|
|
4632
|
+
}
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4636
|
+
let _epubBook = null;
|
|
4637
|
+
let _epubRendition = null;
|
|
4638
|
+
let _epubPath = '';
|
|
4639
|
+
let _epubBlobUrl = null;
|
|
4640
|
+
|
|
4641
|
+
async function openEpubViewer(path) {
|
|
4642
|
+
editorPath = path;
|
|
4643
|
+
editorOriginalContent = '';
|
|
4644
|
+
const fileName = path.split(/[\\/]/).pop() || path;
|
|
4645
|
+
document.getElementById('editor-filename').textContent = fileName + ' / ' + path;
|
|
4646
|
+
document.getElementById('editor-status').textContent = '';
|
|
4647
|
+
const cm = document.querySelector('.CodeMirror');
|
|
4648
|
+
if (cm) cm.style.display = 'none';
|
|
4649
|
+
document.getElementById('editor-preview').classList.remove('active');
|
|
4650
|
+
const iframe = document.getElementById('editor-preview-iframe');
|
|
4651
|
+
if (iframe) { iframe.style.display = 'none'; iframe.srcdoc = ''; }
|
|
4652
|
+
const mdContent2 = document.getElementById('md-preview-content');
|
|
4653
|
+
if (mdContent2) { mdContent2.style.display = 'none'; }
|
|
4654
|
+
mdPreviewActive = false;
|
|
4655
|
+
clearPreviewLiveReload();
|
|
4656
|
+
document.getElementById('pdf-viewer').classList.remove('active');
|
|
4657
|
+
const epubViewer = document.getElementById('epub-viewer');
|
|
4658
|
+
epubViewer.classList.add('active');
|
|
4659
|
+
document.getElementById('epub-title').textContent = fileName;
|
|
4660
|
+
document.getElementById('epub-loc').textContent = 'Loading…';
|
|
4661
|
+
if (!window.ePub) {
|
|
4662
|
+
const t = toast('Loading EPUB viewer…', 'info');
|
|
4663
|
+
try { await loadEpubJs(); t.remove(); } catch (e) { t.remove(); toast('Failed to load EPUB viewer: ' + e.message, 'error'); return; }
|
|
4664
|
+
}
|
|
4665
|
+
if (window.innerWidth > 768) {
|
|
4666
|
+
document.getElementById('content').classList.add('editor-open');
|
|
4667
|
+
const splitArea = document.getElementById('editor-split-area');
|
|
4668
|
+
try {
|
|
4669
|
+
const orient = safeStorage.getItem('wt-editor-split-orientation');
|
|
4670
|
+
const isHoriz = orient === 'horizontal';
|
|
4671
|
+
if (splitArea) splitArea.classList.toggle('horizontal', isHoriz);
|
|
4672
|
+
} catch {}
|
|
4673
|
+
}
|
|
4674
|
+
document.getElementById('editor-view').classList.add('open');
|
|
4675
|
+
document.getElementById('editor-save-btn').style.display = 'none';
|
|
4676
|
+
document.getElementById('md-preview-toggle').style.display = 'none';
|
|
4677
|
+
document.getElementById('preview-refresh-btn').style.display = 'none';
|
|
4678
|
+
requestAnimationFrame(() => { tabs.forEach(tab => { try { fitTerm(tab); } catch (e) {} }); });
|
|
4679
|
+
// cleanup previous
|
|
4680
|
+
try { if (_epubRendition) { _epubRendition.destroy(); _epubRendition = null; } } catch {}
|
|
4681
|
+
try { if (_epubBook) { _epubBook.destroy(); _epubBook = null; } } catch {}
|
|
4682
|
+
if (_epubBlobUrl) { URL.revokeObjectURL(_epubBlobUrl); _epubBlobUrl = null; }
|
|
4683
|
+
document.getElementById('epub-view').innerHTML = '';
|
|
4684
|
+
// Show loading immediately
|
|
4685
|
+
document.getElementById('epub-loc').textContent = 'Loading EPUB…';
|
|
4686
|
+
try {
|
|
4687
|
+
const resp = await fetch(`/api/files/image?path=${encodeURIComponent(path)}&_t=${Date.now()}`, { headers: { 'x-pin-token': authToken } });
|
|
4688
|
+
if (!resp.ok) throw new Error('Failed to fetch EPUB (' + resp.status + ')');
|
|
4689
|
+
const arrayBuffer = await resp.arrayBuffer();
|
|
4690
|
+
if (!arrayBuffer || arrayBuffer.byteLength < 100) throw new Error('Empty or invalid EPUB file (size ' + (arrayBuffer ? arrayBuffer.byteLength : 0) + ')');
|
|
4691
|
+
const header = new Uint8Array(arrayBuffer.slice(0, 4));
|
|
4692
|
+
const isZip = header[0] === 0x50 && header[1] === 0x4B;
|
|
4693
|
+
console.log('EPUB fetch OK', path, 'bytes', arrayBuffer.byteLength, 'isZip', isZip, 'header', Array.from(header));
|
|
4694
|
+
if (!isZip) throw new Error('Not a valid EPUB (missing PK header, got ' + Array.from(header).join(',') + ')');
|
|
4695
|
+
_epubPath = path;
|
|
4696
|
+
const book = window.ePub(arrayBuffer, { openAs: 'binary', encoding: 'binary', store: false });
|
|
4697
|
+
_epubBook = book;
|
|
4698
|
+
// Listen for openFailed
|
|
4699
|
+
book.on('openFailed', (e) => { console.warn('EPUB openFailed', e); throw e; });
|
|
4700
|
+
// Ensure container is visible and has layout before renderTo
|
|
4701
|
+
await new Promise(r => requestAnimationFrame(r));
|
|
4702
|
+
// Try scrolled-doc first (vertical scroll), fallback to paginated if fails
|
|
4703
|
+
let rendition;
|
|
4704
|
+
try {
|
|
4705
|
+
rendition = book.renderTo('epub-view', { flow: 'scrolled-doc', width: '100%', height: '100%', allowScriptedContent: false, store: false });
|
|
4706
|
+
} catch (e) {
|
|
4707
|
+
console.warn('scrolled-doc failed, fallback to paginated', e);
|
|
4708
|
+
try { if (rendition) rendition.destroy(); } catch {}
|
|
4709
|
+
document.getElementById('epub-view').innerHTML = '';
|
|
4710
|
+
rendition = book.renderTo('epub-view', { flow: 'paginated', width: '100%', height: '100%', manager: 'default' });
|
|
4711
|
+
}
|
|
4712
|
+
_epubRendition = rendition;
|
|
4713
|
+
const s = getComputedStyle(document.documentElement);
|
|
4714
|
+
const bg = s.getPropertyValue('--bg').trim();
|
|
4715
|
+
const fg = s.getPropertyValue('--fg').trim();
|
|
4716
|
+
try {
|
|
4717
|
+
rendition.themes.default({ body: { background: bg + ' !important', color: fg + ' !important', 'font-size': '16px', 'line-height': '1.6', 'padding': '0 12px' } });
|
|
4718
|
+
} catch {}
|
|
4719
|
+
const displayTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error('EPUB display timeout - try downloading the file')), 15000));
|
|
4720
|
+
await Promise.race([rendition.display(), displayTimeout]);
|
|
4721
|
+
document.getElementById('epub-loc').textContent = 'Scroll vertically • ' + (rendition.location ? 'ready' : '');
|
|
4722
|
+
// keyboard nav still works for scrolled-doc (prev/next section)
|
|
4723
|
+
rendition.on('keyup', (e) => {
|
|
4724
|
+
if (e.key === 'ArrowLeft') rendition.prev();
|
|
4725
|
+
if (e.key === 'ArrowRight') rendition.next();
|
|
4726
|
+
});
|
|
4727
|
+
// also allow continuous wheel scroll naturally; update location on relocate
|
|
4728
|
+
try {
|
|
4729
|
+
rendition.on('relocated', (loc) => {
|
|
4730
|
+
if (loc && loc.start && loc.start.percentage !== undefined) {
|
|
4731
|
+
const pct = Math.round(loc.start.percentage * 100);
|
|
4732
|
+
document.getElementById('epub-loc').textContent = pct + '% • scroll';
|
|
4733
|
+
}
|
|
4734
|
+
});
|
|
4735
|
+
} catch {}
|
|
4736
|
+
} catch (e) {
|
|
4737
|
+
console.warn('EPUB load failed', e);
|
|
4738
|
+
document.getElementById('epub-loc').textContent = 'Failed';
|
|
4739
|
+
document.getElementById('epub-view').innerHTML = '<div style="color:var(--red);padding:20px">Failed to load EPUB: ' + escHtml(e.message) + '</div>';
|
|
4740
|
+
toast('EPUB load failed: ' + e.message, 'error');
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
function epubNav(dir) {
|
|
4744
|
+
if (!_epubRendition) return;
|
|
4745
|
+
if (dir < 0) _epubRendition.prev();
|
|
4746
|
+
else _epubRendition.next();
|
|
4747
|
+
}
|
|
4748
|
+
function cleanupDocViewers() {
|
|
4749
|
+
// PDF
|
|
4750
|
+
if (_pdfObserver) { try { _pdfObserver.disconnect(); } catch {} _pdfObserver = null; }
|
|
4751
|
+
if (_pdfDoc) { try { _pdfDoc.destroy(); } catch {} _pdfDoc = null; }
|
|
4752
|
+
_pdfPath = '';
|
|
4753
|
+
_pdfPageOrder = null;
|
|
4754
|
+
_pdfCurrentPage = 1;
|
|
4755
|
+
const pdfWrap = document.getElementById('pdf-canvas-wrap');
|
|
4756
|
+
if (pdfWrap) {
|
|
4757
|
+
// remove scroll listener added in setupPdfScrollTracking
|
|
4758
|
+
try { pdfWrap.onscroll = null; } catch {}
|
|
4759
|
+
pdfWrap.innerHTML = '';
|
|
4760
|
+
}
|
|
4761
|
+
const pdfViewer = document.getElementById('pdf-viewer');
|
|
4762
|
+
if (pdfViewer) pdfViewer.classList.remove('active');
|
|
4763
|
+
// EPUB
|
|
4764
|
+
try { if (_epubRendition) { _epubRendition.destroy(); _epubRendition = null; } } catch {}
|
|
4765
|
+
try { if (_epubBook) { _epubBook.destroy(); _epubBook = null; } } catch {}
|
|
4766
|
+
if (_epubBlobUrl) { URL.revokeObjectURL(_epubBlobUrl); _epubBlobUrl = null; }
|
|
4767
|
+
_epubPath = '';
|
|
4768
|
+
const epubViewer = document.getElementById('epub-viewer');
|
|
4769
|
+
if (epubViewer) epubViewer.classList.remove('active');
|
|
4770
|
+
const epubView = document.getElementById('epub-view');
|
|
4771
|
+
if (epubView) epubView.innerHTML = '';
|
|
4772
|
+
document.getElementById('editor-save-btn').style.display = '';
|
|
4773
|
+
}
|
|
4774
|
+
// Preload doc viewers in background for instant open (respect datasaver)
|
|
4775
|
+
function preloadDocViewers() {
|
|
4776
|
+
try {
|
|
4777
|
+
if (settings && settings.datasaver) return;
|
|
4778
|
+
if (!window.pdfjsLib) loadPdfJs().catch(()=>{});
|
|
4779
|
+
if (!window.ePub) loadEpubJs().catch(()=>{});
|
|
4780
|
+
} catch {}
|
|
4781
|
+
}
|
|
4782
|
+
setTimeout(() => {
|
|
4783
|
+
if (document.hidden) {
|
|
4784
|
+
const h = () => { if (!document.hidden) { document.removeEventListener('visibilitychange', h); preloadDocViewers(); } };
|
|
4785
|
+
document.addEventListener('visibilitychange', h);
|
|
4786
|
+
} else {
|
|
4787
|
+
if ('requestIdleCallback' in window) requestIdleCallback(preloadDocViewers, { timeout: 3000 });
|
|
4788
|
+
else setTimeout(preloadDocViewers, 1500);
|
|
4789
|
+
}
|
|
4790
|
+
}, 1500);
|
|
4791
|
+
|
|
4247
4792
|
let _ivImageUrl = null;
|
|
4248
4793
|
|
|
4249
4794
|
function openImageViewer(path) {
|
|
@@ -4299,6 +4844,9 @@ function toggleHtmlPreview() {
|
|
|
4299
4844
|
editor.focus();
|
|
4300
4845
|
clearPreviewLiveReload();
|
|
4301
4846
|
} else {
|
|
4847
|
+
// hide doc viewers when entering html preview
|
|
4848
|
+
document.getElementById('pdf-viewer').classList.remove('active');
|
|
4849
|
+
document.getElementById('epub-viewer').classList.remove('active');
|
|
4302
4850
|
renderHtmlPreview();
|
|
4303
4851
|
if (cmWrapper) cmWrapper.style.display = 'none';
|
|
4304
4852
|
preview.classList.add('active');
|
|
@@ -4309,12 +4857,74 @@ function toggleHtmlPreview() {
|
|
|
4309
4857
|
}
|
|
4310
4858
|
}
|
|
4311
4859
|
|
|
4860
|
+
function getHtmlBaseDir() {
|
|
4861
|
+
const p = editorPath || currentPath || '';
|
|
4862
|
+
if (!p) return '';
|
|
4863
|
+
const sep = p.includes('\\') ? '\\' : '/';
|
|
4864
|
+
const idx = p.lastIndexOf(sep);
|
|
4865
|
+
return idx > 0 ? p.slice(0, idx) : p;
|
|
4866
|
+
}
|
|
4867
|
+
function isAbsoluteUrlForHtml(url) {
|
|
4868
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/|data:|blob:|#)/i.test(url.trim()) || url.trim().startsWith('/');
|
|
4869
|
+
}
|
|
4870
|
+
function toPreviewApiUrl(rel, baseDir) {
|
|
4871
|
+
if (!rel || isAbsoluteUrlForHtml(rel)) return rel;
|
|
4872
|
+
if (/^[A-Za-z]:[\\/]/.test(rel)) return rel;
|
|
4873
|
+
// keep query/hash part
|
|
4874
|
+
let hash = '';
|
|
4875
|
+
let query = '';
|
|
4876
|
+
let pathPart = rel;
|
|
4877
|
+
const hIdx = pathPart.indexOf('#');
|
|
4878
|
+
if (hIdx !== -1) { hash = pathPart.slice(hIdx); pathPart = pathPart.slice(0, hIdx); }
|
|
4879
|
+
const qIdx = pathPart.indexOf('?');
|
|
4880
|
+
if (qIdx !== -1) { query = pathPart.slice(qIdx); pathPart = pathPart.slice(0, qIdx); }
|
|
4881
|
+
pathPart = pathPart.trim();
|
|
4882
|
+
if (!pathPart) return rel;
|
|
4883
|
+
const abs = joinPath(baseDir || '', pathPart);
|
|
4884
|
+
let api = `/api/files/image?path=${encodeURIComponent(abs)}`;
|
|
4885
|
+
if (authToken) api += `&token=${encodeURIComponent(authToken)}`;
|
|
4886
|
+
if (query) api += (query.startsWith('?') ? `&${query.slice(1)}` : query);
|
|
4887
|
+
if (hash) api += hash;
|
|
4888
|
+
return api;
|
|
4889
|
+
}
|
|
4890
|
+
function rewriteHtmlRelativeUrls(html, baseDir) {
|
|
4891
|
+
if (!baseDir) return html;
|
|
4892
|
+
// src/href/srcset/poster/data/action/cite/background
|
|
4893
|
+
html = html.replace(/\b(src|href|srcset|poster|data|cite|action|background)\s*=\s*(["'])([^"']+)\2/gi, (m, attr, q, val) => {
|
|
4894
|
+
if (attr.toLowerCase() === 'srcset') {
|
|
4895
|
+
const parts = val.split(',').map(p => {
|
|
4896
|
+
const seg = p.trim();
|
|
4897
|
+
if (!seg) return seg;
|
|
4898
|
+
const sp = seg.split(/\s+/);
|
|
4899
|
+
const url = sp[0];
|
|
4900
|
+
const desc = sp.slice(1).join(' ');
|
|
4901
|
+
const newUrl = toPreviewApiUrl(url, baseDir);
|
|
4902
|
+
return desc ? `${newUrl} ${desc}` : newUrl;
|
|
4903
|
+
});
|
|
4904
|
+
return `${attr}=${q}${parts.join(', ')}${q}`;
|
|
4905
|
+
}
|
|
4906
|
+
const newVal = toPreviewApiUrl(val, baseDir);
|
|
4907
|
+
if (newVal === val) return m;
|
|
4908
|
+
return `${attr}=${q}${newVal}${q}`;
|
|
4909
|
+
});
|
|
4910
|
+
// url(...) in style attributes and <style> tags
|
|
4911
|
+
html = html.replace(/url\(\s*(["']?)([^"'\)]+)\1\s*\)/gi, (m, q, url) => {
|
|
4912
|
+
const trimmed = url.trim();
|
|
4913
|
+
if (!trimmed || isAbsoluteUrlForHtml(trimmed) || trimmed.startsWith('/')) return m;
|
|
4914
|
+
if (/^[A-Za-z]:[\\/]/.test(trimmed)) return m;
|
|
4915
|
+
const newUrl = toPreviewApiUrl(trimmed, baseDir);
|
|
4916
|
+
return `url(${q}${newUrl}${q})`;
|
|
4917
|
+
});
|
|
4918
|
+
return html;
|
|
4919
|
+
}
|
|
4312
4920
|
function renderHtmlPreview() {
|
|
4313
4921
|
const iframe = document.getElementById('editor-preview-iframe');
|
|
4922
|
+
const mdContent = document.getElementById('md-preview-content');
|
|
4314
4923
|
const raw = editor ? editor.getValue() : '';
|
|
4315
4924
|
if (!iframe) return;
|
|
4316
|
-
|
|
4317
|
-
|
|
4925
|
+
if (mdContent) mdContent.style.display = 'none';
|
|
4926
|
+
if (iframe) iframe.style.display = 'block';
|
|
4927
|
+
const baseDir = getHtmlBaseDir();
|
|
4318
4928
|
const s = getComputedStyle(document.documentElement);
|
|
4319
4929
|
const bg = s.getPropertyValue('--bg').trim();
|
|
4320
4930
|
const fg = s.getPropertyValue('--fg').trim();
|
|
@@ -4323,11 +4933,31 @@ function renderHtmlPreview() {
|
|
|
4323
4933
|
const border = s.getPropertyValue('--border').trim();
|
|
4324
4934
|
const font = s.getPropertyValue('--font').trim();
|
|
4325
4935
|
const isDark = !['#f9f9fb', '#ffffff', 'rgb(249, 249, 251)', 'rgb(255, 255, 255)'].includes(bg);
|
|
4326
|
-
const
|
|
4936
|
+
const isFullDoc = /<!DOCTYPE|<html[\s>]/i.test(raw);
|
|
4937
|
+
let doc;
|
|
4938
|
+
if (isFullDoc) {
|
|
4939
|
+
let sanitized = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw, { WHOLE_DOCUMENT: true, USE_PROFILES: { html: true }, ADD_TAGS: ['base','style'], ADD_ATTR: ['target'] }) : raw;
|
|
4940
|
+
sanitized = rewriteHtmlRelativeUrls(sanitized, baseDir);
|
|
4941
|
+
// Inject base tag if missing for any remaining relative URLs
|
|
4942
|
+
if (!/<base\b/i.test(sanitized)) {
|
|
4943
|
+
const baseHref = `/api/files/image?path=${encodeURIComponent(baseDir + '/')}` + (authToken ? `&token=${encodeURIComponent(authToken)}` : '');
|
|
4944
|
+
sanitized = sanitized.replace(/<head[^>]*>/i, m => m + `<base href="${escHtml(baseHref)}">`);
|
|
4945
|
+
}
|
|
4946
|
+
// Ensure color-scheme meta
|
|
4947
|
+
if (!/<meta[^>]*color-scheme/i.test(sanitized)) {
|
|
4948
|
+
sanitized = sanitized.replace(/<head[^>]*>/i, m => m + `<meta name="color-scheme" content="${isDark ? 'dark' : 'light'}">`);
|
|
4949
|
+
}
|
|
4950
|
+
doc = sanitized;
|
|
4951
|
+
} else {
|
|
4952
|
+
let fragment = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw, { USE_PROFILES: { html: true }, ADD_ATTR: ['target'] }) : raw;
|
|
4953
|
+
fragment = rewriteHtmlRelativeUrls(fragment, baseDir);
|
|
4954
|
+
const baseHrefFrag = `/api/files/image?path=${encodeURIComponent(baseDir + '/')}` + (authToken ? `&token=${encodeURIComponent(authToken)}` : '');
|
|
4955
|
+
doc = `<!DOCTYPE html>
|
|
4327
4956
|
<html>
|
|
4328
4957
|
<head>
|
|
4329
4958
|
<meta charset="UTF-8">
|
|
4330
4959
|
<meta name="color-scheme" content="${isDark ? 'dark' : 'light'}">
|
|
4960
|
+
<base href="${escHtml(baseHrefFrag)}">
|
|
4331
4961
|
<style>
|
|
4332
4962
|
:root { color-scheme: ${isDark ? 'dark' : 'light'}; }
|
|
4333
4963
|
* { box-sizing: border-box; }
|
|
@@ -4363,16 +4993,21 @@ function renderHtmlPreview() {
|
|
|
4363
4993
|
input:focus, textarea:focus, select:focus { outline: 2px solid ${accent}; outline-offset: 1px; }
|
|
4364
4994
|
</style>
|
|
4365
4995
|
</head>
|
|
4366
|
-
<body>${
|
|
4996
|
+
<body>${fragment}</body>
|
|
4367
4997
|
</html>`;
|
|
4998
|
+
}
|
|
4368
4999
|
iframe.srcdoc = doc;
|
|
4369
5000
|
iframe.style.display = 'block';
|
|
4370
5001
|
}
|
|
4371
5002
|
|
|
4372
5003
|
function renderMdPreview() {
|
|
4373
5004
|
const preview = document.getElementById('editor-preview');
|
|
5005
|
+
const mdContent = document.getElementById('md-preview-content');
|
|
5006
|
+
const iframe = document.getElementById('editor-preview-iframe');
|
|
4374
5007
|
const raw = editor ? editor.getValue() : '';
|
|
4375
|
-
if (!preview) return;
|
|
5008
|
+
if (!preview || !mdContent) return;
|
|
5009
|
+
if (iframe) iframe.style.display = 'none';
|
|
5010
|
+
mdContent.style.display = 'block';
|
|
4376
5011
|
try {
|
|
4377
5012
|
const html = marked.parse(raw, { breaks: true, gfm: true, langPrefix: 'language-' });
|
|
4378
5013
|
const sanitized = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : html;
|
|
@@ -4385,7 +5020,7 @@ function renderMdPreview() {
|
|
|
4385
5020
|
const border = s.getPropertyValue('--border').trim();
|
|
4386
5021
|
const fg2 = s.getPropertyValue('--fg2').trim();
|
|
4387
5022
|
const font = s.getPropertyValue('--font').trim();
|
|
4388
|
-
|
|
5023
|
+
mdContent.innerHTML = `
|
|
4389
5024
|
<style>
|
|
4390
5025
|
.md-rendered { max-width: 800px; margin: 0 auto; }
|
|
4391
5026
|
.md-rendered h1, .md-rendered h2, .md-rendered h3, .md-rendered h4, .md-rendered h5, .md-rendered h6 { color: ${fg}; margin: 1.2em 0 0.5em; font-weight: 700; }
|
|
@@ -4411,7 +5046,7 @@ function renderMdPreview() {
|
|
|
4411
5046
|
<div class="md-rendered">${sanitized}</div>`;
|
|
4412
5047
|
} catch (e) {
|
|
4413
5048
|
console.warn('Markdown preview error:', e);
|
|
4414
|
-
|
|
5049
|
+
mdContent.textContent = 'Error rendering markdown preview';
|
|
4415
5050
|
}
|
|
4416
5051
|
}
|
|
4417
5052
|
|
|
@@ -4452,6 +5087,8 @@ function toggleMdPreview() {
|
|
|
4452
5087
|
const refreshBtn = document.getElementById('preview-refresh-btn');
|
|
4453
5088
|
mdPreviewActive = !mdPreviewActive;
|
|
4454
5089
|
if (mdPreviewActive) {
|
|
5090
|
+
document.getElementById('pdf-viewer').classList.remove('active');
|
|
5091
|
+
document.getElementById('epub-viewer').classList.remove('active');
|
|
4455
5092
|
renderMdPreview();
|
|
4456
5093
|
if (cmWrapper) cmWrapper.style.display = 'none';
|
|
4457
5094
|
preview.classList.add('active');
|
|
@@ -4629,16 +5266,20 @@ function openSettings() {
|
|
|
4629
5266
|
const isOpen = panel.classList.contains('open');
|
|
4630
5267
|
// Don't inert #main because panel is inside #main — inert would disable panel itself. Inert content only (sidebar is sibling).
|
|
4631
5268
|
const content = document.getElementById('content');
|
|
4632
|
-
panel.classList.toggle('open');
|
|
4633
5269
|
if (!isOpen) {
|
|
5270
|
+
panel.classList.add('open');
|
|
4634
5271
|
panel.setAttribute('aria-modal', 'true');
|
|
4635
5272
|
panel.setAttribute('role', 'dialog');
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
5273
|
+
// Defer inert + focus to next frame so panel transform can composite on GPU without reflowing sidebar/file-list
|
|
5274
|
+
requestAnimationFrame(() => {
|
|
5275
|
+
if (content) content.setAttribute('inert', '');
|
|
5276
|
+
installFocusTrap(panel);
|
|
5277
|
+
const firstFocusable = panel.querySelector('select, button, input');
|
|
5278
|
+
if (firstFocusable) firstFocusable.focus({ preventScroll: true });
|
|
5279
|
+
});
|
|
4640
5280
|
setTimeout(() => document.addEventListener('click', closeSettingsOnClickOutside, true), 50);
|
|
4641
5281
|
} else {
|
|
5282
|
+
panel.classList.remove('open');
|
|
4642
5283
|
panel.removeAttribute('aria-modal');
|
|
4643
5284
|
if (content) content.removeAttribute('inert');
|
|
4644
5285
|
removeFocusTrap();
|
|
@@ -5118,13 +5759,13 @@ function toggleEditorFullscreen() {
|
|
|
5118
5759
|
}
|
|
5119
5760
|
if (isFS) {
|
|
5120
5761
|
document.addEventListener('keydown', _fsEscHandler);
|
|
5121
|
-
try { view.requestFullscreen?.(); } catch {}
|
|
5122
5762
|
} else {
|
|
5123
5763
|
document.removeEventListener('keydown', _fsEscHandler);
|
|
5124
|
-
try { if (document.fullscreenElement) document.exitFullscreen(); } catch {}
|
|
5125
5764
|
requestAnimationFrame(() => { tabs.forEach(t=>{try{fitTerm(t);}catch(e){console.warn(e);}}); if(editor) editor.refresh(); });
|
|
5126
5765
|
}
|
|
5127
5766
|
if (editor) setTimeout(()=>editor.refresh(), 80);
|
|
5767
|
+
// Keep sidebar visible — editor fullscreen now only covers terminal area (absolute inside #editor-split-area)
|
|
5768
|
+
requestAnimationFrame(() => { tabs.forEach(t=>{try{fitTerm(t);}catch(e){console.warn(e);}}); });
|
|
5128
5769
|
}
|
|
5129
5770
|
function _fsEscHandler(e) {
|
|
5130
5771
|
if (e.key === 'Escape' || e.key === 'F11') {
|
|
@@ -5133,49 +5774,8 @@ function _fsEscHandler(e) {
|
|
|
5133
5774
|
}
|
|
5134
5775
|
}
|
|
5135
5776
|
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
const term = document.getElementById('terminals');
|
|
5139
|
-
const sidebar = document.getElementById('sidebar');
|
|
5140
|
-
if (!content || !term) return;
|
|
5141
|
-
const isFS = content.classList.toggle('terminal-fullscreen');
|
|
5142
|
-
term.classList.toggle('fullscreen', isFS);
|
|
5143
|
-
const btn = document.getElementById('term-fullscreen-btn');
|
|
5144
|
-
if (btn) {
|
|
5145
|
-
btn.classList.toggle('active', isFS);
|
|
5146
|
-
btn.setAttribute('aria-label', isFS ? 'Exit terminal fullscreen' : 'Terminal fullscreen');
|
|
5147
|
-
btn.title = isFS ? 'Exit terminal fullscreen (Esc/Shift+F11)' : 'Terminal fullscreen (Shift+F11)';
|
|
5148
|
-
btn.innerHTML = isFS
|
|
5149
|
-
? '<svg width="16" height="16" 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>'
|
|
5150
|
-
: '<svg width="16" height="16" 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>';
|
|
5151
|
-
}
|
|
5152
|
-
if (isFS) {
|
|
5153
|
-
document.addEventListener('keydown', _termFsEscHandler);
|
|
5154
|
-
try { content.requestFullscreen?.(); } catch {}
|
|
5155
|
-
} else {
|
|
5156
|
-
document.removeEventListener('keydown', _termFsEscHandler);
|
|
5157
|
-
try { if (document.fullscreenElement) document.exitFullscreen(); } catch {}
|
|
5158
|
-
// Ensure sidebar is visible when exiting fullscreen
|
|
5159
|
-
if (sidebar) {
|
|
5160
|
-
sidebar.style.display = '';
|
|
5161
|
-
sidebar.classList.remove('hidden');
|
|
5162
|
-
if (window.innerWidth <= 768) {
|
|
5163
|
-
sidebar.classList.add('mobile-open');
|
|
5164
|
-
}
|
|
5165
|
-
}
|
|
5166
|
-
}
|
|
5167
|
-
requestAnimationFrame(() => { tabs.forEach(t=>{try{fitTerm(t);}catch(e){console.warn(e);}}); const at=getActiveTab(); try{at?.term?.focus();}catch{} });
|
|
5168
|
-
}
|
|
5169
|
-
function _termFsEscHandler(e) {
|
|
5170
|
-
const isTermFS = document.getElementById('content')?.classList.contains('terminal-fullscreen');
|
|
5171
|
-
if (!isTermFS) return;
|
|
5172
|
-
if (e.key === 'Escape' || (e.key === 'F11' && e.shiftKey)) { e.preventDefault(); toggleTerminalFullscreen(); }
|
|
5173
|
-
else if (e.key === 'F11' && !e.shiftKey) {
|
|
5174
|
-
// Let editor handle F11 when editor fullscreen, but prevent browser default
|
|
5175
|
-
const ev = document.getElementById('editor-view');
|
|
5176
|
-
if (!ev || !ev.classList.contains('fullscreen')) e.preventDefault();
|
|
5177
|
-
}
|
|
5178
|
-
}
|
|
5777
|
+
// Terminal fullscreen removed — per request, terminal fullscreen should not cover sidebar
|
|
5778
|
+
// toggleTerminalFullscreen and _termFsEscHandler intentionally removed
|
|
5179
5779
|
const hasWakeLock = 'wakeLock' in navigator;
|
|
5180
5780
|
|
|
5181
5781
|
async function requestWakeLock() {
|
|
@@ -5270,8 +5870,7 @@ function setupKeyboardShortcuts() {
|
|
|
5270
5870
|
if (ctrl && e.shiftKey && e.key === 'ArrowRight') { e.preventDefault(); cycleTab(1); }
|
|
5271
5871
|
if (ctrl && e.shiftKey && e.key === 'ArrowLeft') { e.preventDefault(); cycleTab(-1); }
|
|
5272
5872
|
if (ctrl && e.shiftKey && e.key === 'r') { e.preventDefault(); refreshPreview(); }
|
|
5273
|
-
if (e.key === 'F11' &&
|
|
5274
|
-
else if (e.key === 'F11' && document.getElementById('editor-view').classList.contains('open')) { e.preventDefault(); toggleEditorFullscreen(); }
|
|
5873
|
+
if (e.key === 'F11' && document.getElementById('editor-view').classList.contains('open')) { e.preventDefault(); toggleEditorFullscreen(); }
|
|
5275
5874
|
if (e.key === 'Escape') {
|
|
5276
5875
|
const termMenu = document.getElementById('term-ctx-menu');
|
|
5277
5876
|
if (termMenu && termMenu.style.display !== 'none') {
|
|
@@ -5661,8 +6260,23 @@ function setupDragDrop() {
|
|
|
5661
6260
|
});
|
|
5662
6261
|
document.addEventListener('dragleave', e => {
|
|
5663
6262
|
if (!isFileDrag(e)) return;
|
|
5664
|
-
|
|
5665
|
-
|
|
6263
|
+
// Only hide when leaving the viewport (relatedTarget null) or overlay itself
|
|
6264
|
+
// Prevents nested dragenter/leave from child elements desyncing counter
|
|
6265
|
+
if (!e.relatedTarget || e.relatedTarget === document.documentElement || e.target === overlay) {
|
|
6266
|
+
dragCnt = 0;
|
|
6267
|
+
overlay.classList.remove('active');
|
|
6268
|
+
} else {
|
|
6269
|
+
dragCnt--;
|
|
6270
|
+
if (dragCnt <= 0) { dragCnt = 0; overlay.classList.remove('active'); }
|
|
6271
|
+
}
|
|
6272
|
+
});
|
|
6273
|
+
document.addEventListener('dragend', () => {
|
|
6274
|
+
dragCnt = 0;
|
|
6275
|
+
overlay.classList.remove('active');
|
|
6276
|
+
});
|
|
6277
|
+
window.addEventListener('blur', () => {
|
|
6278
|
+
dragCnt = 0;
|
|
6279
|
+
overlay.classList.remove('active');
|
|
5666
6280
|
});
|
|
5667
6281
|
document.addEventListener('dragover', e => {
|
|
5668
6282
|
if (!isFileDrag(e)) return;
|
|
@@ -6116,7 +6730,11 @@ function doFinderSearch() {
|
|
|
6116
6730
|
fiDir.className = 'fi-dir';
|
|
6117
6731
|
fiDir.textContent = r.dir;
|
|
6118
6732
|
div.appendChild(fiDir);
|
|
6119
|
-
div.addEventListener('click', () => {
|
|
6733
|
+
div.addEventListener('click', () => {
|
|
6734
|
+
closeOverlay('finder-overlay');
|
|
6735
|
+
if (r.isDir) loadFiles(r.path);
|
|
6736
|
+
else openFileEditor(r.path);
|
|
6737
|
+
});
|
|
6120
6738
|
div.addEventListener('mouseenter', () => { document.querySelectorAll('.finder-item.selected').forEach(el => el.classList.remove('selected')); div.classList.add('selected'); finderIdx = i; });
|
|
6121
6739
|
results.appendChild(div);
|
|
6122
6740
|
});
|