feat(file-viewer): resize and expand the window, back/forward between previewed files

This commit is contained in:
Devvyn committed 2026-10-18 17:45:31 +08:00
1 parent e63bb65b36
commit 08e57ed708
5 files changed
+413 -5

No files matched your search

+4
View File
@@ -614,6 +614,8 @@
<div class="file-preview-header">
<span class="file-preview-title" id="filePreviewTitle">file.ts</span>
<div class="file-preview-actions">
<button class="btn-icon-sm" id="filePreviewBackBtn" onclick="app.stepFilePreviewHistory(-1)" title="Back (Alt+←)" aria-label="Back to the previous file" hidden disabled>&larr;</button>
<button class="btn-icon-sm" id="filePreviewForwardBtn" onclick="app.stepFilePreviewHistory(1)" title="Forward (Alt+→)" aria-label="Forward to the next file" hidden disabled>&rarr;</button>
<button class="btn-icon-sm" id="filePreviewPrevBtn" onclick="app.stepFolderPreview(-1)" title="Previous file in folder (←)" aria-label="Previous file in folder" hidden>&lsaquo;</button>
<button class="btn-icon-sm" id="filePreviewNextBtn" onclick="app.stepFolderPreview(1)" title="Next file in folder (→)" aria-label="Next file in folder" hidden>&rsaquo;</button>
<button class="btn-icon-sm" id="filePreviewFolderBtn" onclick="app.openFolderOfPreview()" title="Show folder" aria-label="Show folder" hidden><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></button>
@@ -624,6 +626,7 @@
<button class="btn-icon-sm file-preview-edit-btn" id="filePreviewEditBtn" onclick="app.enterFilePreviewEdit()" title="Edit file" aria-label="Edit file" hidden><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z"/></svg></button>
<button class="btn-icon-sm" onclick="app.copyFilePreviewContent()" title="Copy content">&#x2398;</button>
<button class="btn-icon-sm" id="filePreviewDetachBtn" onclick="app.detachFilePreview()" title="Open in new tab" aria-label="Open in new tab" hidden><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></button>
<button class="btn-icon-sm" id="filePreviewExpandBtn" onclick="app.toggleFilePreviewExpanded()" title="Expand" aria-label="Expand" aria-pressed="false"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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></button>
<button class="btn-icon-sm" onclick="app.closeFilePreview()" title="Close">&times;</button>
</div>
</div>
@@ -635,6 +638,7 @@
<button class="file-preview-editbar-btn file-preview-editbar-btn--save" id="filePreviewSaveBtn" onclick="app.saveFilePreviewEdit()" disabled>Save</button>
</div>
<div class="file-preview-footer" id="filePreviewFooter"></div>
<div class="file-preview-resize" id="filePreviewResize" title="Drag to resize" aria-hidden="true"></div>
</div>
</div>
+146 -1
View File
@@ -33,8 +33,11 @@ const FILE_PREVIEW_PREF_KEYS = {
htmlRendered: 'codeman:filePreviewHtmlRendered',
lineNumbers: 'codeman:filePreviewLineNumbers',
wrap: 'codeman:filePreviewWrap',
expanded: 'codeman:filePreviewExpanded',
};
const FILE_PREVIEW_PREF_DEFAULTS = { mdRendered: true, htmlRendered: true, lineNumbers: false, wrap: true };
const FILE_PREVIEW_PREF_DEFAULTS = { mdRendered: true, htmlRendered: true, lineNumbers: false, wrap: true, expanded: false };
// Back/forward list of previews opened from one overlay session (links between documents).
const FILE_PREVIEW_HISTORY_MAX = 50;
const AWAY_DIGEST_SECTIONS = [
['needsAttention', 'Needs Attention'],
['completed', 'Completed'],
@@ -4108,6 +4111,9 @@ Object.assign(CodemanApp.prototype, {
if (!overlay || !bodyEl) return;
this._recordFilePreviewHistory(filePath, sessionId, attachmentId, options);
this._bindFilePreviewChrome();
// Only the newest load may write: paging ←/→ through a folder starts the next
// load while the previous one still awaits its fetch, and its late answer would
// put the previous file's text under the next file's title.
@@ -4133,6 +4139,7 @@ Object.assign(CodemanApp.prototype, {
// Show overlay with loading state
overlay.classList.add('visible');
this._applyFilePreviewExpanded();
titleEl.textContent = filePath;
bodyEl.innerHTML = '<div class="binary-message">Loading...</div>';
footerEl.textContent = '';
@@ -4537,6 +4544,142 @@ Object.assign(CodemanApp.prototype, {
this._openFolderGridFile(next);
},
// ═══════════════════════════════════════════════════════════════
// File Viewer window: back/forward between previews, expand, resize
// ═══════════════════════════════════════════════════════════════
/**
* Remember a preview in the back/forward list. A navigation that came FROM the
* list (stepFilePreviewHistory) is not recorded again, and neither is a reload
* of the file already showing (the MD/Page toggles reopen the same file). Opening
* something new while standing mid-list drops the forward entries, as a browser does.
*/
_recordFilePreviewHistory(filePath, sessionId, attachmentId, options) {
const h = (this.filePreviewHistory = this.filePreviewHistory || { entries: [], index: -1 });
if (!this._filePreviewHistoryStepping) {
const cur = h.entries[h.index];
const same =
cur && cur.filePath === filePath && cur.sessionId === sessionId && (cur.attachmentId || null) === (attachmentId || null);
if (!same) {
h.entries.length = h.index + 1;
h.entries.push({ filePath, sessionId, attachmentId: attachmentId || null, options });
if (h.entries.length > FILE_PREVIEW_HISTORY_MAX) h.entries.shift();
h.index = h.entries.length - 1;
}
}
this._updateFilePreviewHistoryNav();
},
/** Back (-1) / forward (+1) through the previews opened in this overlay session. */
stepFilePreviewHistory(delta) {
const h = this.filePreviewHistory;
if (!h) return;
const target = h.index + delta;
if (target < 0 || target >= h.entries.length) return;
if (this.filePreviewEdit?.dirty && !confirm('Discard unsaved changes?')) return;
h.index = target;
const e = h.entries[target];
// openFilePreview records synchronously, before its first await, so the flag
// only needs to cover the call itself.
this._filePreviewHistoryStepping = true;
try {
return this.openFilePreview(e.filePath, e.sessionId, e.attachmentId, e.options || {});
} finally {
this._filePreviewHistoryStepping = false;
}
},
/** Buttons show once there is somewhere to go; each is disabled at its end of the list. */
_updateFilePreviewHistoryNav() {
const h = this.filePreviewHistory;
const multi = !!h && h.entries.length > 1;
const back = this.$('filePreviewBackBtn');
const fwd = this.$('filePreviewForwardBtn');
if (back) {
back.hidden = !multi;
back.disabled = !multi || h.index <= 0;
}
if (fwd) {
fwd.hidden = !multi;
fwd.disabled = !multi || h.index >= h.entries.length - 1;
}
},
/** Expand button: fill the screen, remembered across opens. */
toggleFilePreviewExpanded() {
this._setFilePreviewPref('expanded', !this._filePreviewPref('expanded'));
this._applyFilePreviewExpanded();
},
_applyFilePreviewExpanded() {
const on = this._filePreviewPref('expanded');
const win = this.$('filePreviewOverlay')?.querySelector?.('.file-preview-window');
win?.classList?.toggle('expanded', on);
const btn = this.$('filePreviewExpandBtn');
if (btn) {
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
const label = on ? 'Restore size' : 'Expand';
btn.title = label;
btn.setAttribute('aria-label', label);
}
},
/**
* One-time wiring for the window chrome: Alt+←/→ for back/forward (capture phase
* so the terminal behind the overlay never types the arrow) and the corner grip.
*/
_bindFilePreviewChrome() {
if (this._filePreviewChromeBound) return;
this._filePreviewChromeBound = true;
document.addEventListener(
'keydown',
(ev) => {
if (!ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) return;
if (ev.key !== 'ArrowLeft' && ev.key !== 'ArrowRight') return;
if (!this.$('filePreviewOverlay')?.classList.contains('visible')) return;
const t = ev.target;
if (t && (t.isContentEditable || (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) && !t.classList?.contains('xterm-helper-textarea')))) return;
const h = this.filePreviewHistory;
const delta = ev.key === 'ArrowLeft' ? -1 : 1;
if (!h || h.index + delta < 0 || h.index + delta >= h.entries.length) return;
ev.preventDefault();
ev.stopPropagation();
this.stepFilePreviewHistory(delta);
},
true
);
this._bindFilePreviewResize();
},
/** Drag the corner grip to size the window; the window is centred, so the delta counts twice. */
_bindFilePreviewResize() {
const grip = this.$('filePreviewResize');
const win = grip?.parentElement;
if (!grip || !win) return;
grip.addEventListener('pointerdown', (ev) => {
ev.preventDefault();
const startX = ev.clientX;
const startY = ev.clientY;
const startW = win.offsetWidth;
const startH = win.offsetHeight;
grip.setPointerCapture?.(ev.pointerId);
const move = (e) => {
const w = Math.max(320, Math.min(window.innerWidth * 0.96, startW + 2 * (e.clientX - startX)));
const h = Math.max(240, Math.min(window.innerHeight * 0.94, startH + 2 * (e.clientY - startY)));
win.style.width = `${Math.round(w)}px`;
win.style.height = `${Math.round(h)}px`;
};
const up = () => {
grip.removeEventListener('pointermove', move);
grip.removeEventListener('pointerup', up);
grip.removeEventListener('pointercancel', up);
};
grip.addEventListener('pointermove', move);
grip.addEventListener('pointerup', up);
grip.addEventListener('pointercancel', up);
});
},
/** "Show folder" in the preview: the grid of the previewed file's folder. */
openFolderOfPreview() {
const target = this.filePreviewFolderTarget;
@@ -4617,6 +4760,8 @@ Object.assign(CodemanApp.prototype, {
// Back on the grid (if one is open underneath): its current tile stays marked.
if (this.folderGrid) this.folderGrid.index = null;
this._updateFolderPreviewNav(null);
this.filePreviewHistory = null;
this._updateFilePreviewHistoryNav();
const overlay = this.$('filePreviewOverlay');
if (overlay) {
overlay.classList.remove('visible');
+38 -4
View File
@@ -12223,10 +12223,15 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
}
.file-preview-window {
width: 80vw;
max-width: 900px;
height: 80vh;
max-height: 700px;
position: relative;
width: min(80vw, 900px);
height: min(80vh, 700px);
/* A dragged size (inline width/height) and the expanded state may exceed the
default 900x700, but never the screen. */
max-width: 96vw;
max-height: 94vh;
min-width: min(320px, 96vw);
min-height: min(240px, 94vh);
background: var(--floating-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
@@ -12238,6 +12243,35 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
overflow: hidden;
}
/* Expanded: fills the screen (a dragged size is ignored while it is on). */
.file-preview-window.expanded {
width: 96vw !important;
height: 94vh !important;
}
/* Corner grip (panels-ui.js _bindFilePreviewResize). The window is centred, so a
drag moves both edges: the handler doubles the pointer delta to keep the corner
under the pointer. Touch devices use the Expand button instead. */
.file-preview-resize {
position: absolute;
right: 0;
bottom: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
touch-action: none;
background: linear-gradient(135deg, transparent 50%, var(--text-muted, #888) 50%, var(--text-muted, #888) 58%, transparent 58%, transparent 72%, var(--text-muted, #888) 72%, var(--text-muted, #888) 80%, transparent 80%);
opacity: 0.6;
}
.file-preview-resize:hover {
opacity: 1;
}
.file-preview-window.expanded .file-preview-resize {
display: none;
}
.file-preview-header {
display: flex;
align-items: center;