mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
Merge pull request #659: feat(file-viewer): resize and expand the window, back/forward between previewed files
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
cef64dc052
11 files changed
+629
-10
No files matched your search
@@ -685,6 +685,14 @@ const DEFAULT_SHORTCUTS = [
|
||||
label: 'Close Panels',
|
||||
displayBindings: ['Escape'],
|
||||
},
|
||||
// Handled by the File Viewer's own capture-phase listener while it is open
|
||||
// (_bindFilePreviewChrome, panels-ui.js), never through the registry.
|
||||
{
|
||||
id: 'file-preview-history',
|
||||
group: 'Panels',
|
||||
label: 'File Viewer Back / Forward',
|
||||
displayBindings: ['Alt/Option+ArrowLeft', 'Alt/Option+ArrowRight'],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -1039,6 +1039,12 @@
|
||||
'Source type filter': '来源类型筛选',
|
||||
'Copy content': '复制内容',
|
||||
'Edit file': '编辑文件',
|
||||
'Back (Alt+←)': '后退 (Alt+←)',
|
||||
'Forward (Alt+→)': '前进 (Alt+→)',
|
||||
'Back to the previous file': '返回上一个文件',
|
||||
'Forward to the next file': '前进到下一个文件',
|
||||
Expand: '展开',
|
||||
'Drag to resize': '拖动以调整大小',
|
||||
'Rendered markdown': '渲染 Markdown',
|
||||
'Line numbers': '行号',
|
||||
'Wrap lines': '自动换行',
|
||||
@@ -1235,6 +1241,7 @@
|
||||
'Voice input (Ctrl+Shift+V)': '语音输入(Ctrl+Shift+V)',
|
||||
'Insert Newline': '插入换行',
|
||||
'Close Panels': '关闭面板',
|
||||
'File Viewer Back / Forward': '文件查看器后退 / 前进',
|
||||
'Previous / Next Session': '上一个 / 下一个会话',
|
||||
'Next Session': '下一个会话',
|
||||
'Previous Session': '上一个会话',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -922,6 +926,7 @@
|
||||
<div><kbd>Ctrl</kbd>+<kbd>?</kbd></div><div>Show Shortcuts</div>
|
||||
<div><kbd>Alt/Option</kbd>+<kbd>?</kbd></div><div>Show Shortcuts</div>
|
||||
<div><kbd>Escape</kbd></div><div>Close Panels</div>
|
||||
<div><kbd>Alt/Option</kbd>+<kbd>ArrowLeft</kbd> / <kbd>Alt/Option</kbd>+<kbd>ArrowRight</kbd></div><div>File Viewer Back / Forward</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -2298,6 +2298,19 @@ html.mobile-init .file-browser-panel {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Expand and the resize grip would only shrink the full-screen window (96vw x 94vh,
|
||||
or a px size left by a drag in landscape), so a phone ignores both. */
|
||||
.file-preview-window,
|
||||
.file-preview-window.expanded {
|
||||
width: 100vw !important;
|
||||
height: var(--app-height, 100vh) !important;
|
||||
}
|
||||
|
||||
#filePreviewExpandBtn,
|
||||
.file-preview-resize {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-preview-editbar {
|
||||
padding-bottom: calc(0.4rem + var(--safe-area-bottom));
|
||||
}
|
||||
|
||||
+152
-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,148 @@ 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 {
|
||||
// A folder-grid page goes through the grid so index, lastIndex and the highlighted
|
||||
// tile follow; otherwise the next ‹ › would continue from the newer file.
|
||||
const folderFile = e.options?.folderFile;
|
||||
if (folderFile && folderFile.folderId === this.folderGrid?.folderId) {
|
||||
const index = this.folderGrid.files.findIndex((f) => f.name === folderFile.name);
|
||||
if (index !== -1) return this._openFolderGridFile(index);
|
||||
}
|
||||
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);
|
||||
// The label stays "Expand" and aria-pressed carries the state, like MD, Lines and
|
||||
// Wrap, so a screen reader never announces "Restore size, pressed".
|
||||
this.$('filePreviewExpandBtn')?.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// Swallowed even at the ends of the list: otherwise the terminal behind the overlay
|
||||
// types it as word-left/right, and on a header button the browser's own Alt+← goes Back.
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const h = this.filePreviewHistory;
|
||||
const delta = ev.key === 'ArrowLeft' ? -1 : 1;
|
||||
if (!h || h.index + delta < 0 || h.index + delta >= h.entries.length) return;
|
||||
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) => {
|
||||
if (ev.button !== 0) return; // a right-click on the grip must not start a resize
|
||||
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 +4766,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,36 @@ 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. Only phones hide it (with Expand, mobile.css under 600px), since
|
||||
the window is full screen there; tablets show and use it. */
|
||||
.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