mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
feat(file-viewer): resize and expand the window, back/forward between previewed files
This commit is contained in:
1 parent
e63bb65b36
commit
08e57ed708
5 files changed
+413
-5
No files matched your search
@@ -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>←</button>
|
||||
<button class="btn-icon-sm" id="filePreviewForwardBtn" onclick="app.stepFilePreviewHistory(1)" title="Forward (Alt+→)" aria-label="Forward to the next file" hidden disabled>→</button>
|
||||
<button class="btn-icon-sm" id="filePreviewPrevBtn" onclick="app.stepFolderPreview(-1)" title="Previous file in folder (←)" aria-label="Previous file in folder" hidden>‹</button>
|
||||
<button class="btn-icon-sm" id="filePreviewNextBtn" onclick="app.stepFolderPreview(1)" title="Next file in folder (→)" aria-label="Next file in folder" hidden>›</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">⎘</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">×</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
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user