From 37025276132d3d5cc105da4cc4323438fa28709b Mon Sep 17 00:00:00 2001 From: yhy Date: Sun, 30 Aug 2026 08:28:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E5=AD=97=E5=A0=86?= =?UTF-8?q?=E7=A0=8C=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addon/content/chatpapers.css | 131 ++++++++++----- addon/locale/en-US/addon.ftl | 3 + addon/locale/zh-CN/addon.ftl | 3 + package.json | 2 + src/modules/lecture/ui/lecturePane.ts | 226 +++++++++++++++++++------- src/modules/ui/readerPane.ts | 19 ++- typings/i10n.d.ts | 3 + 7 files changed, 284 insertions(+), 103 deletions(-) diff --git a/addon/content/chatpapers.css b/addon/content/chatpapers.css index 9acdb92..f2afd34 100644 --- a/addon/content/chatpapers.css +++ b/addon/content/chatpapers.css @@ -849,9 +849,43 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { overflow-y: auto; scrollbar-width: thin; scrollbar-color: color-mix(in srgb, currentColor 32%, transparent) transparent; +} + +.chatpapers-lecture-beat-list.chatpapers-lecture-scroll-y { scrollbar-gutter: stable; } +/* 段落列表:单行省略;详情区:换行(二者作用域分离) */ +.chatpapers-lecture-beat-list .chatpapers-lecture-beat-line { + display: flex; + align-items: center; + gap: 8px; + flex: 1 1 auto; + min-width: 0; + max-width: 100%; + overflow: hidden; +} + +.chatpapers-lecture-beat-list .chatpapers-lecture-beat-label { + flex: 0 0 auto; + font-size: 11px; + font-weight: 600; + line-height: 1.3; + opacity: 0.72; + white-space: nowrap; +} + +.chatpapers-lecture-beat-list .chatpapers-lecture-beat-preview { + flex: 1 1 0; + width: 0; + min-width: 0; + font-size: 12px; + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .chatpapers-lecture-beat-list.chatpapers-lecture-scroll-y::-webkit-scrollbar, .chatpapers-lecture-para-detail.chatpapers-lecture-scroll-y::-webkit-scrollbar { width: 5px; @@ -1062,19 +1096,32 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { font-size: 11px; } -.chatpapers-lecture-para-original.is-active-sentence { +.chatpapers-lecture-beat-list .chatpapers-lecture-sentence-item { + max-height: 42px; +} + +.chatpapers-lecture-beat-list .chatpapers-lecture-sentence-item .chatpapers-lecture-beat-main { + max-height: 40px; +} + +.chatpapers-lecture-para-detail .chatpapers-lecture-para-original.is-active-sentence { + display: block; + margin-top: 2px; + margin-bottom: 8px; border-left: 3px solid #1f7a8c; - padding-left: 8px; + padding: 6px 8px; background: color-mix(in srgb, #1f7a8c 8%, transparent); } -.chatpapers-lecture-sentence-list { +.chatpapers-lecture-para-detail .chatpapers-lecture-sentence-list { display: flex; flex-direction: column; gap: 4px; + margin-top: 8px; } -.chatpapers-lecture-sentence-chip { +.chatpapers-lecture-para-detail .chatpapers-lecture-sentence-chip { + display: block; appearance: none; width: 100%; box-sizing: border-box; @@ -1088,15 +1135,24 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { font-size: 11px; line-height: 1.35; color: inherit; + white-space: normal; word-break: break-word; - overflow-wrap: anywhere; + overflow-wrap: break-word; } -.chatpapers-lecture-sentence-chip.is-active { +.chatpapers-lecture-para-detail .chatpapers-lecture-sentence-chip.is-active { background: color-mix(in srgb, #1f7a8c 12%, transparent); border-color: color-mix(in srgb, #1f7a8c 40%, transparent); } +.chatpapers-lecture-selection-explain .chatpapers-lecture-para-original.is-active-sentence { + display: block; + margin-bottom: 6px; + border-left: 3px solid #1f7a8c; + padding: 6px 8px; + background: color-mix(in srgb, #1f7a8c 8%, transparent); +} + .chatpapers-lecture-pdf-sync-hint { margin-top: 4px; font-size: 10px; @@ -1105,9 +1161,7 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { } .chatpapers-lecture-para-detail { - display: flex; - flex-direction: column; - gap: 6px; + display: block; padding: 10px; border-radius: 8px; background: color-mix(in srgb, #1f7a8c 5%, transparent); @@ -1139,15 +1193,25 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; - overflow-x: hidden; max-width: 100%; font-size: 12px; line-height: 1.55; } +.chatpapers-lecture-beat-detail .chatpapers-lecture-beat-script { + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; + max-width: 100%; + font-size: 12px; + line-height: 1.55; + font-weight: 500; +} + .chatpapers-lecture-para-detail .chatpapers-lecture-para-original-label, .chatpapers-lecture-para-detail .chatpapers-lecture-para-translation-label, .chatpapers-lecture-para-detail .chatpapers-lecture-para-context-label { + display: block; white-space: normal; } @@ -1160,8 +1224,15 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { } .chatpapers-lecture-para-play-btn { - align-self: flex-start; - margin-bottom: 2px; + display: inline-flex; + margin-bottom: 6px; +} + +.chatpapers-lecture-beat-detail-meta { + font-size: 11px; + font-weight: 600; + opacity: 0.78; + line-height: 1.35; } .chatpapers-lecture-selection-explain { @@ -1208,20 +1279,26 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { .chatpapers-lecture-para-original-label, .chatpapers-lecture-para-translation-label, .chatpapers-lecture-para-context-label { + display: block; font-size: 10px; font-weight: 600; opacity: 0.6; text-transform: uppercase; letter-spacing: 0.04em; - margin-top: 4px; + margin-top: 8px; + margin-bottom: 2px; } .chatpapers-lecture-para-original { + display: block; opacity: 0.9; + margin-bottom: 4px; } .chatpapers-lecture-para-translation { + display: block; font-weight: 500; + margin-bottom: 4px; } .chatpapers-lecture-para-context { @@ -1337,33 +1414,7 @@ collapsible-section[open] > :not(.head) .chatpapers-lecture-root { opacity: 0.55; } -.chatpapers-lecture-beat-line { - display: flex; - align-items: center; - gap: 8px; - flex: 1 1 auto; - min-width: 0; - overflow: hidden; -} - -.chatpapers-lecture-beat-label { - flex-shrink: 0; - font-size: 11px; - font-weight: 600; - line-height: 1.3; - opacity: 0.72; - white-space: nowrap; -} - -.chatpapers-lecture-beat-preview { - flex: 1 1 auto; - min-width: 0; - font-size: 12px; - line-height: 1.4; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} +/* beat-line / beat-preview 规则见上方 .chatpapers-lecture-beat-list 作用域 */ .chatpapers-lecture-beat-status { flex-shrink: 0; diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl index 4320cef..d3f3d7c 100644 --- a/addon/locale/en-US/addon.ftl +++ b/addon/locale/en-US/addon.ftl @@ -82,6 +82,9 @@ lecture-status-ready-phase1 = Preparation done (Phase 1: summary ready) lecture-status-ready-step1 = Ready: { $count } segments — tap a segment or press Play lecture-status-failed = Preparation incomplete lecture-beat-list-title = Overview lecture +lecture-beat-list-hint = Click a segment for details; click ▶ to play +lecture-beat-detail-empty = Select a segment above to view the full script +lecture-beat-label = Segment { $n } lecture-play-start = Play lecture-play-pause = Pause lecture-play-next = Next diff --git a/addon/locale/zh-CN/addon.ftl b/addon/locale/zh-CN/addon.ftl index 08c5b9b..d7ebf13 100644 --- a/addon/locale/zh-CN/addon.ftl +++ b/addon/locale/zh-CN/addon.ftl @@ -82,6 +82,9 @@ lecture-status-ready-phase1 = 备课完成(Phase 1:摘要已生成) lecture-status-ready-step1 = 备课完成:{ $count } 段讲解可播放,点击下方段落或播放按钮开始听课 lecture-status-failed = 备课未完成,请重试或检查 LLM 设置 lecture-beat-list-title = 整体讲解 +lecture-beat-list-hint = 点击讲解段查看详情,点击 ▶ 开始朗读 +lecture-beat-detail-empty = 点击上方讲解段查看完整讲稿 +lecture-beat-label = 讲解 { $n } lecture-play-start = 播放 lecture-play-pause = 暂停 lecture-play-next = 下一段 diff --git a/package.json b/package.json index ed50f79..58cf429 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "scripts": { "start": "zotero-plugin serve", "build": "zotero-plugin build && tsc --noEmit", + "clean": "rm -rf .scaffold/build", + "rebuild": "npm run clean && npm run build", "lint:check": "prettier --check . && eslint .", "lint:fix": "prettier --write . && eslint . --fix", "release": "zotero-plugin release", diff --git a/src/modules/lecture/ui/lecturePane.ts b/src/modules/lecture/ui/lecturePane.ts index 3b70b73..2bf9cf4 100644 --- a/src/modules/lecture/ui/lecturePane.ts +++ b/src/modules/lecture/ui/lecturePane.ts @@ -72,6 +72,8 @@ export class LecturePaneView { private playMode: "overview" | "paragraph" = "paragraph"; private readGranularity: ReadGranularity = "paragraph"; private activeBeatId?: string; + private selectedBeatId?: string; + private cachedBeats: StoredBeat[] = []; private activeParagraphId?: string; private activeSentenceId?: string; private selectedParagraphId?: string; @@ -209,10 +211,7 @@ export class LecturePaneView { ); this.paraListHintEl.hidden = true; this.beatListEl = this.el("div", "chatpapers-lecture-beat-list chatpapers-lecture-scroll-y"); - this.paragraphDetailEl = this.el( - "div", - "chatpapers-lecture-para-detail chatpapers-lecture-scroll-y", - ); + this.paragraphDetailEl = this.el("div", "chatpapers-lecture-para-detail"); this.selectionExplainEl = this.el("div", "chatpapers-lecture-selection-explain"); this.selectionExplainEl.hidden = true; @@ -551,6 +550,7 @@ export class LecturePaneView { ? getString("lecture-sentence-list-title") : getString("lecture-paragraph-list-title"); this.paraListHintEl.hidden = false; + this.paraListHintEl.textContent = getString("lecture-para-list-hint"); this.renderParagraphList(paragraphs); this.renderParagraphDetail( paragraphs, @@ -598,11 +598,13 @@ export class LecturePaneView { this.granularityTabsEl.classList.add("is-visible"); } else if (hasBeats) { this.granularityTabsEl.classList.remove("is-visible"); - this.paraListHintEl.hidden = true; + this.paraListHintEl.hidden = false; + this.paraListHintEl.textContent = getString("lecture-beat-list-hint"); this.playerTitleEl.textContent = getString("lecture-beat-list-title"); this.beatListEl.hidden = false; - this.paragraphDetailEl.hidden = true; + this.cachedBeats = beats; this.renderBeatList(beats); + this.renderBeatDetail(beats, this.selectedBeatId ?? this.activeBeatId); if (!this.player) { this.player = createPlayLecture({ @@ -610,7 +612,9 @@ export class LecturePaneView { beats, onBeatChange: (id) => { this.activeBeatId = id; - this.highlightBeat(id); + if (id) this.selectedBeatId = id; + this.renderBeatList(this.cachedBeats); + this.renderBeatDetail(this.cachedBeats, id); }, onPlayingChange: (playing) => { this.playBtn.hidden = playing; @@ -843,7 +847,10 @@ export class LecturePaneView { if (!para) { this.paragraphDetailEl.hidden = false; this.paragraphDetailEl.classList.add("is-empty"); - this.paragraphDetailEl.classList.remove("chatpapers-lecture-scroll-y"); + this.paragraphDetailEl.classList.remove( + "chatpapers-lecture-scroll-y", + "chatpapers-lecture-beat-detail", + ); this.paragraphDetailEl.append( this.el( "div", @@ -854,7 +861,7 @@ export class LecturePaneView { return; } this.paragraphDetailEl.hidden = false; - this.paragraphDetailEl.classList.remove("is-empty"); + this.paragraphDetailEl.classList.remove("is-empty", "chatpapers-lecture-beat-detail"); this.paragraphDetailEl.classList.add("chatpapers-lecture-scroll-y"); const sentence = sentenceId @@ -896,7 +903,11 @@ export class LecturePaneView { } const originalText = sentence?.text ?? para.originalText; - const translationText = sentence?.translation ?? para.translation; + const translationText = ( + sentence?.translation || + (!sentence ? para.translation : "") || + "" + ).trim(); this.paragraphDetailEl.append( this.el("div", "chatpapers-lecture-para-original-label", getString("lecture-para-original")), @@ -905,11 +916,20 @@ export class LecturePaneView { `chatpapers-lecture-para-original${sentence ? " is-active-sentence" : ""}`, originalText, ), - this.el("div", "chatpapers-lecture-para-translation-label", getString("lecture-para-translation")), - this.el("div", "chatpapers-lecture-para-translation", translationText), ); - if (sentence && (para.sentences?.length ?? 0) > 1) { + if (translationText) { + this.paragraphDetailEl.append( + this.el("div", "chatpapers-lecture-para-translation-label", getString("lecture-para-translation")), + this.el("div", "chatpapers-lecture-para-translation", translationText), + ); + } + + if ( + sentence && + this.readGranularity !== "sentence" && + (para.sentences?.length ?? 0) > 1 + ) { const sentenceList = this.el("div", "chatpapers-lecture-sentence-list"); for (const s of para.sentences ?? []) { const item = this.doc.createElement("button"); @@ -1097,57 +1117,143 @@ export class LecturePaneView { private renderBeatList(beats: StoredBeat[]): void { this.beatListEl.replaceChildren(); for (const beat of beats) { - const row = this.doc.createElement("button") as HTMLButtonElement; - row.type = "button"; - row.className = "chatpapers-lecture-beat-item"; - row.dataset.beatId = beat.id; - if (beat.id === this.activeBeatId) { - row.classList.add("is-active"); - } - if (beat.ttsStatus !== "ready") { - row.classList.add("is-pending"); - row.disabled = true; - } - - const meta = this.el("div", "chatpapers-lecture-beat-meta"); - meta.append( - this.el( - "span", - "chatpapers-lecture-beat-type", - BEAT_TYPE_LABELS[beat.type] || beat.type, - ), - this.el( - "span", - "chatpapers-lecture-beat-title", - beat.title || beat.id, - ), - ); - - const status = this.el("span", "chatpapers-lecture-beat-status"); - if (beat.ttsStatus === "ready") { - status.textContent = "✓"; - } else if (beat.ttsStatus === "failed") { - status.textContent = getString("lecture-beat-tts-failed"); - } else { - status.textContent = getString("lecture-beat-tts-pending"); - } - - row.append(meta, status); - row.addEventListener("click", () => { - void this.player?.jumpToBeatId(beat.id); - }); - this.beatListEl.append(row); + this.beatListEl.append(this.makeBeatRow(beats, beat)); } } - private highlightBeat(beatId?: string): void { - this.activeBeatId = beatId; - for (const node of this.beatListEl.querySelectorAll( - ".chatpapers-lecture-beat-item", - )) { - const el = node as HTMLElement; - el.classList.toggle("is-active", el.dataset.beatId === beatId); + private isBeatSelected(beatId: string): boolean { + return this.selectedBeatId === beatId; + } + + private isBeatPlaying(beatId: string): boolean { + return this.activeBeatId === beatId; + } + + private selectBeat(beats: StoredBeat[], beatId: string): void { + this.selectedBeatId = beatId; + this.renderBeatList(beats); + this.renderBeatDetail(beats, beatId); + } + + private playBeat(beat: StoredBeat): void { + void this.player?.jumpToBeatId(beat.id); + } + + private makeBeatPlayButton(beat: StoredBeat): HTMLButtonElement { + const playBtn = this.doc.createElement("button"); + playBtn.type = "button"; + playBtn.className = "chatpapers-lecture-beat-play"; + playBtn.title = getString("lecture-para-play"); + playBtn.setAttribute("aria-label", getString("lecture-para-play")); + playBtn.append( + createLucideIcon(this.doc, Play, { + size: 14, + className: "chatpapers-lecture-btn-icon", + }), + ); + playBtn.disabled = beat.ttsStatus !== "ready"; + playBtn.addEventListener("click", (event) => { + event.stopPropagation(); + this.playBeat(beat); + }); + return playBtn; + } + + private makeBeatRow(beats: StoredBeat[], beat: StoredBeat): HTMLElement { + const row = this.el("div", "chatpapers-lecture-beat-item"); + row.dataset.beatId = beat.id; + if (this.isBeatSelected(beat.id)) row.classList.add("is-selected"); + if (this.isBeatPlaying(beat.id)) row.classList.add("is-active"); + if (beat.ttsStatus !== "ready") row.classList.add("is-pending"); + + const main = this.doc.createElement("button"); + main.type = "button"; + main.className = "chatpapers-lecture-beat-main"; + + const preview = beat.title?.trim() || beat.script; + const line = this.el("div", "chatpapers-lecture-beat-line"); + line.append( + this.el( + "span", + "chatpapers-lecture-beat-label", + getString("lecture-beat-label", { args: { n: beat.orderIndex + 1 } }), + ), + this.el("span", "chatpapers-lecture-beat-preview", preview), + ); + + const status = this.el("span", "chatpapers-lecture-beat-status"); + if (beat.ttsStatus === "ready") { + status.textContent = "✓"; + } else if (beat.ttsStatus === "failed") { + status.textContent = getString("lecture-beat-tts-failed"); + } else { + status.textContent = getString("lecture-beat-tts-pending"); } + + main.append(line, status); + main.title = preview; + main.addEventListener("click", () => { + this.selectBeat(beats, beat.id); + }); + + row.append(main, this.makeBeatPlayButton(beat)); + return row; + } + + private renderBeatDetail(beats: StoredBeat[], beatId?: string): void { + const beat = beatId ? beats.find((b) => b.id === beatId) : undefined; + this.paragraphDetailEl.replaceChildren(); + if (!beat) { + this.paragraphDetailEl.hidden = false; + this.paragraphDetailEl.classList.add("is-empty"); + this.paragraphDetailEl.classList.remove( + "chatpapers-lecture-scroll-y", + "chatpapers-lecture-beat-detail", + ); + this.paragraphDetailEl.append( + this.el( + "div", + "chatpapers-lecture-para-detail-empty", + getString("lecture-beat-detail-empty"), + ), + ); + return; + } + + this.paragraphDetailEl.hidden = false; + this.paragraphDetailEl.classList.remove("is-empty"); + this.paragraphDetailEl.classList.add( + "chatpapers-lecture-scroll-y", + "chatpapers-lecture-beat-detail", + ); + + const playBtn = this.doc.createElement("button"); + playBtn.type = "button"; + playBtn.className = + "chatpapers-lecture-secondary chatpapers-lecture-para-play-btn"; + playBtn.append( + createLucideIcon(this.doc, Play, { + size: 14, + className: "chatpapers-lecture-btn-icon", + }), + this.doc.createTextNode(getString("lecture-para-play")), + ); + playBtn.disabled = beat.ttsStatus !== "ready"; + playBtn.addEventListener("click", () => { + this.playBeat(beat); + }); + this.paragraphDetailEl.append(playBtn); + + const typeLabel = BEAT_TYPE_LABELS[beat.type] || beat.type; + const title = beat.title?.trim(); + this.paragraphDetailEl.append( + this.el( + "div", + "chatpapers-lecture-beat-detail-meta", + title ? `${typeLabel} · ${title}` : typeLabel, + ), + this.el("div", "chatpapers-lecture-beat-script", beat.script), + ); } private async statusMessage( diff --git a/src/modules/ui/readerPane.ts b/src/modules/ui/readerPane.ts index d7a0f79..faa555d 100644 --- a/src/modules/ui/readerPane.ts +++ b/src/modules/ui/readerPane.ts @@ -88,14 +88,27 @@ export function registerPrefs() { }); } -export function ensureChatPapersStyles(doc: Document) { +export function ensureChatPapersStyles(doc: Document): void { const id = "chatpapers-styles"; - if (doc.getElementById(id)) return; + const base = `chrome://${config.addonRef}/content/chatpapers.css`; + const href = + typeof __env__ !== "undefined" && __env__ === "development" + ? `${base}?v=${Date.now()}` + : base; + + const existing = doc.getElementById(id) as HTMLLinkElement | null; + if (existing) { + if (typeof __env__ !== "undefined" && __env__ === "development") { + existing.href = href; + } + return; + } + const link = doc.createElement("link"); link.id = id; link.rel = "stylesheet"; link.type = "text/css"; - link.href = `chrome://${config.addonRef}/content/chatpapers.css`; + link.href = href; doc.documentElement?.appendChild(link); } diff --git a/typings/i10n.d.ts b/typings/i10n.d.ts index b94753d..8612785 100644 --- a/typings/i10n.d.ts +++ b/typings/i10n.d.ts @@ -44,6 +44,9 @@ export type FluentMessageId = | 'itemmenu-compare' | 'itemmenu-label' | 'lecture-attach-error' + | 'lecture-beat-detail-empty' + | 'lecture-beat-label' + | 'lecture-beat-list-hint' | 'lecture-beat-list-title' | 'lecture-beat-tts-failed' | 'lecture-beat-tts-pending'