// ==UserScript== // @name 学习视频文字提取助手 // @namespace https://github.com/local/study-video-text-extractor // @version 1.0.0 // @icon data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%232563eb' d='M6 2h9l5 5v15H6z'/%3E%3Cpath fill='%23ffffff' d='M15 2v6h5'/%3E%3Cpath fill='%23ffffff' d='M9 12h6M9 15h6M9 18h4'/%3E%3C/svg%3E // @description 在看学习类视频时自动收集字幕,提取简介和评论区里的学习链接与重点句子,一键复制,方便粘贴到笔记。 // @author Codex // @match *://*.bilibili.com/video/* // @match *://*.bilibili.com/bangumi/play/* // @match *://*.bilibili.com/list/* // @match *://www.youtube.com/watch* // @match *://m.youtube.com/watch* // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @grant GM_registerMenuCommand // @run-at document-idle // @noframes // @license MIT // ==/UserScript== (function () { 'use strict'; const APP_ID = 'study-video-text-extractor'; const STORE_KEY = APP_ID + ':notes:v1'; const MAX_SEGMENTS = 800; const MAX_HIGHLIGHTS = 300; const MAX_LINKS = 200; const MAX_STORED_VIDEOS = 30; const HIGHLIGHT_KEYWORDS = [ '重点', '考点', '必考', '注意', '牢记', '记住', '关键', '核心', '总结', '结论', '定义', '公式', '定理', '方法', '步骤', '技巧', '误区', '易错', '错误', '常见', '典型', '例题', '提示', '提醒', '一定要', '必须', '重要', '切记', '快捷键', '命令', '参数', 'note', 'important', 'tip', 'key', 'remember', 'summary', 'warning', 'error', 'faq' ]; const URL_RE = /\bhttps?:\/\/[^\s<>"'()\[\])】]+/gi; function detectSite() { const host = location.hostname.toLowerCase(); if (host === 'bilibili.com' || host.endsWith('.bilibili.com')) return 'bilibili'; if (host === 'youtube.com' || host.endsWith('.youtube.com') || host === 'youtu.be' || host.endsWith('.youtu.be')) { return 'youtube'; } return null; } const site = detectSite(); if (!site) return; const gm = { getValue: typeof GM_getValue === 'function' ? GM_getValue : null, setValue: typeof GM_setValue === 'function' ? GM_setValue : null, setClipboard: typeof GM_setClipboard === 'function' ? GM_setClipboard : null, registerMenuCommand: typeof GM_registerMenuCommand === 'function' ? GM_registerMenuCommand : null }; const state = { videoKey: '', title: '', segments: [], highlights: [], links: [], autoCapture: true, panelOpen: false, activeTab: 'subtitles', seenAnchors: new WeakSet(), tickCount: 0, saveTimer: null, extractTimers: [] }; const lastRendered = { subtitles: -1, highlights: -1, links: -1 }; let root = null; let panelEl = null; let fabEl = null; let toastEl = null; let toastTimer = null; function loadNotes() { try { if (gm.getValue) { const val = gm.getValue(STORE_KEY, {}); if (val && typeof val === 'object') return val; } } catch (e) {} try { const raw = window.localStorage.getItem(STORE_KEY); if (raw) { const val = JSON.parse(raw); if (val && typeof val === 'object') return val; } } catch (e) {} return {}; } function writeNotes(notes) { try { if (gm.setValue) { gm.setValue(STORE_KEY, notes); return; } } catch (e) {} try { window.localStorage.setItem(STORE_KEY, JSON.stringify(notes)); } catch (e) {} } function currentVideoKey() { const url = new URL(location.href); if (site === 'bilibili') { const m = url.pathname.match(/^\/(?:video|bangumi\/play|list)\/([^/?#]+)/); return 'bilibili:' + (m ? m[1] : url.pathname + url.search); } if (site === 'youtube') { const v = url.searchParams.get('v'); return 'youtube:' + (v || url.pathname + url.search); } return url.href; } function loadVideoState() { const key = currentVideoKey(); const entry = loadNotes()[key] || {}; state.videoKey = key; state.title = entry.title || ''; state.segments = Array.isArray(entry.segments) ? entry.segments : []; state.highlights = Array.isArray(entry.highlights) ? entry.highlights : []; state.links = Array.isArray(entry.links) ? entry.links : []; state.seenAnchors = new WeakSet(); state.tickCount = 0; } function saveNow() { const notes = loadNotes(); const keys = Object.keys(notes); if (keys.length >= MAX_STORED_VIDEOS) { keys.sort(function (a, b) { return (notes[a].updatedAt || 0) - (notes[b].updatedAt || 0); }); while (keys.length >= MAX_STORED_VIDEOS) { delete notes[keys.shift()]; } } notes[state.videoKey] = { title: state.title, segments: state.segments.slice(-MAX_SEGMENTS), highlights: state.highlights.slice(0, MAX_HIGHLIGHTS), links: state.links.slice(0, MAX_LINKS), updatedAt: Date.now() }; writeNotes(notes); } function scheduleSave() { if (state.saveTimer) return; state.saveTimer = setTimeout(function () { state.saveTimer = null; saveNow(); }, 400); } function normalizeText(s) { return String(s || '').replace(/\s+/g, ' ').trim(); } function splitLines(text) { return String(text || '').split(/\r?\n/).map(function (line) { return line.trim(); }).filter(Boolean); } function formatTime(seconds) { if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds < 0) return '00:00'; const total = Math.floor(seconds); const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; const mm = String(m).padStart(2, '0'); const ss = String(s).padStart(2, '0'); return h > 0 ? h + ':' + mm + ':' + ss : mm + ':' + ss; } function matchKeyword(text) { const t = String(text || '').toLowerCase(); for (let i = 0; i < HIGHLIGHT_KEYWORDS.length; i++) { const k = HIGHLIGHT_KEYWORDS[i]; if (t.indexOf(k.toLowerCase()) !== -1) return k; } return ''; } function isHighlight(text) { return matchKeyword(text) !== ''; } function isChapterLine(line) { return /^\s*\d{1,2}:\d{2}(:\d{2})?\s+/.test(line); } function uniqueLines(lines) { const seen = new Set(); const out = []; for (let i = 0; i < lines.length; i++) { const key = normalizeText(lines[i]); if (!key || seen.has(key)) continue; seen.add(key); out.push(lines[i].trim()); } return out; } function readCaptionLines() { if (site === 'bilibili') { const box = document.querySelector('.bpx-player-subtitle'); if (!box) return []; const spans = Array.prototype.slice.call(box.querySelectorAll('span')); let chosen = spans.filter(function (el) { return el.classList.contains('bpx-player-subtitle-text'); }); if (!chosen.length) { chosen = spans.filter(function (el) { return !el.querySelector('span'); }); } if (!chosen.length) chosen = [box]; return uniqueLines(chosen.map(function (el) { return el.textContent; })); } if (site === 'youtube') { const container = document.querySelector('.ytp-caption-window-container'); if (!container) return []; const segs = Array.prototype.slice.call(container.querySelectorAll('.ytp-caption-segment')); return uniqueLines(segs.map(function (el) { return el.textContent; })); } return []; } function addHighlight(h) { const key = normalizeText(h.text); if (!key) return; const dup = state.highlights.some(function (x) { return normalizeText(x.text) === key; }); if (dup) return; state.highlights.push(h); if (state.highlights.length > MAX_HIGHLIGHTS) { state.highlights.splice(0, state.highlights.length - MAX_HIGHLIGHTS); } scheduleSave(); } function captureTick() { if (!state.autoCapture) return; const video = document.querySelector('video'); if (!video) return; const lines = readCaptionLines(); if (!lines.length) return; const now = typeof video.currentTime === 'number' ? video.currentTime : 0; const recent = state.segments.slice(-8); let changed = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const key = normalizeText(line); if (!key) continue; const dup = recent.some(function (s) { return normalizeText(s.text) === key && Math.abs((s.time || 0) - now) < 5; }); if (dup) continue; state.segments.push({ time: now, text: line, source: 'subtitle' }); changed = true; if (isHighlight(line)) { addHighlight({ time: now, text: line, source: 'subtitle' }); } } if (changed) { if (state.segments.length > MAX_SEGMENTS) { state.segments.splice(0, state.segments.length - MAX_SEGMENTS); } scheduleSave(); } } function normalizeUrl(url) { return String(url || '').replace(/[.,;:!?,。;:!?、]+$/, '').trim(); } function extractUrls(text) { if (!text) return []; const out = []; const re = new RegExp(URL_RE.source, 'gi'); let m; while ((m = re.exec(String(text))) !== null) { out.push(m[0]); } return out; } function isNoiseUrl(raw) { try { const u = new URL(raw); const host = u.hostname.replace(/^www\./, '').toLowerCase(); const path = u.pathname.replace(/\/+$/, '').toLowerCase(); if (/\.(png|jpe?g|gif|webp|svg|ico|css|js)$/i.test(path)) return true; if (host === 'youtube.com' || host.endsWith('.youtube.com') || host === 'youtu.be') return true; if (host === 'bilibili.com' || host.endsWith('.bilibili.com')) { if (/^\/(?:$|login|register|passport|account|search|watchlater|history|fav|following|friends|index|v\d|readlist|blackboard)/.test(path)) { return true; } return false; } if (!path || path === '/' || path === '/index.html' || path === '/index.php') return true; } catch (e) {} return false; } function linkFromAnchor(a, source) { if (!a || !a.getAttribute) return null; const href = a.getAttribute('href'); if (!href) return null; let url; try { url = new URL(href, location.href).href; } catch (e) { return null; } if (!/^https?:/i.test(url)) return null; if (isNoiseUrl(url)) return null; const label = normalizeText(a.textContent) || url; const context = normalizeText(a.parentElement ? a.parentElement.textContent : ''); const keyword = matchKeyword(context + ' ' + label); return { url: url, label: label.slice(0, 100), source: source || '', keyword: keyword || '' }; } function addLink(link) { if (!link || !link.url) return; const clean = function (u) { return normalizeUrl(u).replace(/\/+$/, ''); }; const u = clean(link.url); const dup = state.links.some(function (l) { return clean(l.url) === u; }); if (dup) return; state.links.push({ url: u, label: link.label || u, source: link.source || '', keyword: link.keyword || '' }); if (state.links.length > MAX_LINKS) { state.links.splice(0, state.links.length - MAX_LINKS); } scheduleSave(); } function scanPageForLinks() { const containers = []; if (site === 'bilibili') { const desc = document.querySelector('#v_desc, .basic-desc-info, .desc-info-text'); if (desc) containers.push({ el: desc, source: '简介' }); const comments = document.querySelectorAll('#commentapp, .reply-wrap, .comment-container'); for (let i = 0; i < comments.length; i++) { containers.push({ el: comments[i], source: '评论区' }); } } else if (site === 'youtube') { const desc = document.querySelector('#description, #description-inline-expander, ytd-text-inline-expander#description'); if (desc) containers.push({ el: desc, source: '简介' }); const comments = document.querySelectorAll('#comments, ytd-comment-thread-renderer'); for (let i = 0; i < comments.length; i++) { containers.push({ el: comments[i], source: '评论区' }); } } for (let c = 0; c < containers.length; c++) { const item = containers[c]; const anchors = item.el.querySelectorAll('a[href]'); for (let i = 0; i < anchors.length; i++) { const a = anchors[i]; if (state.seenAnchors.has(a)) continue; state.seenAnchors.add(a); const link = linkFromAnchor(a, item.source); if (link) addLink(link); } const lines = splitLines(item.el.textContent); for (let i = 0; i < lines.length; i++) { if (isHighlight(lines[i])) { addHighlight({ time: null, text: lines[i], source: item.source }); } } } } function descriptionLines() { if (site === 'bilibili') { try { const init = window.__INITIAL_STATE__; if (init && init.videoData && init.videoData.desc) { return splitLines(init.videoData.desc); } } catch (e) {} const dom = document.querySelector('#v_desc, .basic-desc-info, .desc-info-text'); if (dom) return splitLines(dom.textContent); return []; } if (site === 'youtube') { try { const resp = window.ytInitialPlayerResponse; if (resp && resp.videoDetails && resp.videoDetails.shortDescription) { return splitLines(resp.videoDetails.shortDescription); } } catch (e) {} const dom = document.querySelector('#description, #description-inline-expander, ytd-text-inline-expander#description'); if (dom) return splitLines(dom.textContent); return []; } return []; } function readTitle() { if (site === 'bilibili') { const el = document.querySelector('#viewbox_report h1, .video-title, h1'); return normalizeText(el ? el.textContent : ''); } if (site === 'youtube') { const el = document.querySelector('h1 yt-formatted-string, h1.title'); return normalizeText(el ? el.textContent : ''); } return ''; } function extractStatic() { const t = readTitle(); if (t) state.title = t; const lines = descriptionLines(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const urls = extractUrls(line); if (urls.length) { const label = normalizeText(line.replace(URL_RE, ' ')) || line.slice(0, 60); for (let j = 0; j < urls.length; j++) { addLink({ url: normalizeUrl(urls[j]), label: label.slice(0, 100), source: '简介', keyword: matchKeyword(line) }); } } else if (isHighlight(line) || isChapterLine(line)) { addHighlight({ time: null, text: line, source: '简介' }); } } scanPageForLinks(); scheduleSave(); } function buildSummary() { const parts = []; parts.push('# ' + (state.title || '学习视频笔记')); parts.push('来源: ' + location.href); parts.push(''); if (state.segments.length) { parts.push('## 字幕 (' + state.segments.length + ')'); for (let i = 0; i < state.segments.length; i++) { const s = state.segments[i]; parts.push('[' + formatTime(s.time) + '] ' + s.text); } parts.push(''); } if (state.highlights.length) { parts.push('## 重点句子 (' + state.highlights.length + ')'); for (let i = 0; i < state.highlights.length; i++) { const h = state.highlights[i]; parts.push('- ' + (typeof h.time === 'number' ? '[' + formatTime(h.time) + '] ' : '') + h.text + (h.source ? ' (' + h.source + ')' : '')); } parts.push(''); } if (state.links.length) { parts.push('## 学习链接 (' + state.links.length + ')'); for (let i = 0; i < state.links.length; i++) { const l = state.links[i]; let line = '- '; if (l.keyword) line += l.keyword + ' | '; if (l.label && l.label !== l.url) line += l.label + ': '; line += l.url; if (l.source) line += ' (' + l.source + ')'; parts.push(line); } } return parts.join('\n'); } function contentForTab(tab) { if (tab === 'subtitles') { return state.segments.map(function (s) { return '[' + formatTime(s.time) + '] ' + s.text; }).join('\n'); } if (tab === 'highlights') { return state.highlights.map(function (h) { return (typeof h.time === 'number' ? '[' + formatTime(h.time) + '] ' : '') + h.text + (h.source ? ' (' + h.source + ')' : ''); }).join('\n'); } if (tab === 'links') { return state.links.map(function (l) { return (l.label && l.label !== l.url ? l.label + ': ' : '') + l.url; }).join('\n'); } return buildSummary(); } function textForCopySpec(spec) { const parts = String(spec || '').split(':'); const kind = parts[0]; const index = Number(parts[1]); if (kind === 'segment' && state.segments[index]) { return '[' + formatTime(state.segments[index].time) + '] ' + state.segments[index].text; } if (kind === 'highlight' && state.highlights[index]) { const h = state.highlights[index]; return (typeof h.time === 'number' ? '[' + formatTime(h.time) + '] ' : '') + h.text; } if (kind === 'link' && state.links[index]) { const l = state.links[index]; return (l.label && l.label !== l.url ? l.label + ': ' : '') + l.url; } return ''; } function emptyHint(msg) { const div = document.createElement('div'); div.className = 'lte-empty'; div.textContent = msg; return div; } function segmentItem(s, index) { const row = document.createElement('div'); row.className = 'lte-item'; row.title = '点击复制'; row.dataset.copy = 'segment:' + index; const time = document.createElement('span'); time.className = 'lte-time'; time.textContent = '[' + formatTime(s.time) + ']'; const text = document.createElement('span'); text.className = 'lte-text'; text.textContent = s.text; row.appendChild(time); row.appendChild(text); return row; } function highlightItem(h, index) { const row = document.createElement('div'); row.className = 'lte-item'; row.title = '点击复制'; row.dataset.copy = 'highlight:' + index; if (typeof h.time === 'number') { const time = document.createElement('span'); time.className = 'lte-time'; time.textContent = '[' + formatTime(h.time) + ']'; row.appendChild(time); } const text = document.createElement('span'); text.className = 'lte-text'; text.textContent = h.text; row.appendChild(text); const source = document.createElement('span'); source.className = 'lte-source'; source.textContent = h.source || ''; row.appendChild(source); return row; } function linkItem(l, index) { const row = document.createElement('div'); row.className = 'lte-link'; const label = document.createElement('div'); label.className = 'lte-link-label'; label.textContent = l.label && l.label !== l.url ? l.label : '链接'; row.appendChild(label); const url = document.createElement('div'); url.className = 'lte-link-url'; url.textContent = l.url; row.appendChild(url); const meta = document.createElement('div'); meta.className = 'lte-link-meta'; if (l.keyword) { const badge = document.createElement('span'); badge.className = 'lte-tag'; badge.textContent = l.keyword; meta.appendChild(badge); } const source = document.createElement('span'); source.className = 'lte-source'; source.textContent = l.source || ''; meta.appendChild(source); const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.className = 'lte-copy-btn'; copyBtn.textContent = '复制'; copyBtn.dataset.copy = 'link:' + index; meta.appendChild(copyBtn); row.appendChild(meta); return row; } function renderSegments(pane) { pane.textContent = ''; if (!state.segments.length) { pane.appendChild(emptyHint('打开视频字幕/CC 后会自动收集字幕')); return; } const frag = document.createDocumentFragment(); for (let i = 0; i < state.segments.length; i++) { frag.appendChild(segmentItem(state.segments[i], i)); } pane.appendChild(frag); } function renderHighlights(pane) { pane.textContent = ''; if (!state.highlights.length) { pane.appendChild(emptyHint('包含重点/注意/公式/总结等关键词的句子会自动收集')); return; } const frag = document.createDocumentFragment(); for (let i = 0; i < state.highlights.length; i++) { frag.appendChild(highlightItem(state.highlights[i], i)); } pane.appendChild(frag); } function renderLinks(pane) { pane.textContent = ''; if (!state.links.length) { pane.appendChild(emptyHint('简介和评论区中的学习链接会自动整理')); return; } const frag = document.createDocumentFragment(); for (let i = 0; i < state.links.length; i++) { frag.appendChild(linkItem(state.links[i], i)); } pane.appendChild(frag); } function renderSummary(pane) { pane.textContent = ''; const ta = document.createElement('textarea'); ta.className = 'lte-summary'; ta.readOnly = true; ta.value = buildSummary(); ta.addEventListener('focus', function () { ta.select(); }); pane.appendChild(ta); } function renderCounts() { if (!root) return; const counts = { subtitles: state.segments.length, highlights: state.highlights.length, links: state.links.length }; const tabs = root.querySelectorAll('.lte-tabs [data-tab]'); for (let i = 0; i < tabs.length; i++) { const countEl = tabs[i].querySelector('.lte-count'); if (countEl) { const n = counts[tabs[i].dataset.tab] || 0; countEl.textContent = n ? String(n) : ''; countEl.style.display = n ? '' : 'none'; } } } function syncAutoButton() { if (!root) return; const btn = root.querySelector('[data-action="toggle-auto"]'); if (!btn) return; btn.textContent = state.autoCapture ? '自动采集:开' : '自动采集:关'; btn.classList.toggle('lte-btn-on', state.autoCapture); } function renderTab(force) { if (!root) return; const panes = root.querySelectorAll('.lte-pane'); for (let i = 0; i < panes.length; i++) { panes[i].classList.toggle('lte-hidden', panes[i].dataset.pane !== state.activeTab); } const tabs = root.querySelectorAll('.lte-tabs [data-tab]'); for (let i = 0; i < tabs.length; i++) { tabs[i].classList.toggle('lte-active', tabs[i].dataset.tab === state.activeTab); } const countMap = { subtitles: state.segments.length, highlights: state.highlights.length, links: state.links.length }; const changed = force || lastRendered[state.activeTab] !== countMap[state.activeTab]; if (changed || state.activeTab === 'summary') { const pane = root.querySelector('.lte-pane[data-pane="' + state.activeTab + '"]'); if (pane) { if (state.activeTab === 'subtitles') renderSegments(pane); else if (state.activeTab === 'highlights') renderHighlights(pane); else if (state.activeTab === 'links') renderLinks(pane); else renderSummary(pane); } lastRendered[state.activeTab] = countMap[state.activeTab] || -1; } renderCounts(); syncAutoButton(); } function togglePanel() { state.panelOpen = !state.panelOpen; panelEl.classList.toggle('lte-hidden', !state.panelOpen); if (state.panelOpen) renderTab(true); } function toast(msg) { if (!toastEl) return; toastEl.textContent = msg; toastEl.classList.add('lte-show'); if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(function () { toastEl.classList.remove('lte-show'); }, 1800); } async function copyText(text) { if (!text) { toast('还没有可复制的内容'); return; } let ok = false; try { if (gm.setClipboard) { gm.setClipboard(text, 'text'); ok = true; } } catch (e) {} if (!ok) { try { await navigator.clipboard.writeText(text); ok = true; } catch (e) {} } if (!ok) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { ok = document.execCommand('copy'); } catch (e) {} ta.remove(); } toast(ok ? '已复制到剪贴板' : '复制失败,请手动选择复制'); } function handleKeydown(e) { if (e.altKey && e.shiftKey && (e.key === 'X' || e.key === 'x')) { e.preventDefault(); togglePanel(); } } function handleClick(e) { const tabBtn = e.target.closest('[data-tab]'); if (tabBtn) { state.activeTab = tabBtn.dataset.tab; renderTab(true); return; } const actionBtn = e.target.closest('[data-action]'); if (actionBtn) { const action = actionBtn.dataset.action; if (action === 'toggle-auto') { state.autoCapture = !state.autoCapture; syncAutoButton(); toast(state.autoCapture ? '自动采集已开启' : '自动采集已关闭'); } else if (action === 'copy') { copyText(contentForTab(state.activeTab)); } else if (action === 'clear') { state.segments = []; state.highlights = []; state.links = []; saveNow(); renderTab(true); toast('已清空当前视频的记录'); } else if (action === 'close') { togglePanel(); } return; } const copyEl = e.target.closest('[data-copy]'); if (copyEl) { const text = textForCopySpec(copyEl.dataset.copy); if (text) copyText(text); } } const STYLE_TEXT = [ '#lte-root, #lte-root * { box-sizing: border-box; }', '#lte-root { all: initial; --lte-panel:#ffffff; --lte-line:#e5e7eb; --lte-muted:#6b7280; --lte-text:#1f2328; --lte-btn:#f2f4f7; --lte-btn-hover:#e8ebf0; --lte-accent:#2563eb; --lte-item:#fcfcfd; --lte-tag:#f3f4f6; --lte-active:#dbeafe; position:fixed; right:16px; bottom:16px; z-index:2147483000; font-family:system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif; font-size:13px; line-height:1.5; color:var(--lte-text); }', '@media (prefers-color-scheme: dark) { #lte-root { --lte-panel:#1f242b; --lte-line:#343b45; --lte-muted:#9aa3af; --lte-text:#e6e9ed; --lte-btn:#2a313a; --lte-btn-hover:#38414c; --lte-accent:#7cb3ff; --lte-item:#232a33; --lte-tag:#2d3540; --lte-active:#1e3a5f; } }', '#lte-root button { font:inherit; color:inherit; background:var(--lte-btn); border:1px solid var(--lte-line); border-radius:6px; padding:4px 9px; cursor:pointer; }', '#lte-root button:hover { background:var(--lte-btn-hover); }', '#lte-fab { position:fixed; right:20px; bottom:20px; width:46px; height:46px; border-radius:50%; background:#2563eb; border:none; color:#fff; font-size:18px; box-shadow:0 4px 14px rgba(0,0,0,.28); display:flex; align-items:center; justify-content:center; padding:0; }', '#lte-fab:hover { background:#1d4ed8; }', '#lte-panel { position:fixed; right:20px; bottom:78px; width:min(400px, calc(100vw - 40px)); max-height:min(76vh, 620px); display:flex; flex-direction:column; background:var(--lte-panel); border:1px solid var(--lte-line); border-radius:10px; box-shadow:0 12px 36px rgba(0,0,0,.22); overflow:hidden; }', '#lte-panel.lte-hidden { display:none; }', '.lte-head { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:10px 12px; border-bottom:1px solid var(--lte-line); }', '.lte-title { font-weight:600; font-size:14px; white-space:nowrap; }', '.lte-actions { display:flex; gap:6px; }', '.lte-actions button { white-space:nowrap; }', '.lte-btn-on { background:var(--lte-active) !important; border-color:var(--lte-accent) !important; }', '.lte-tabs { display:flex; gap:2px; padding:6px 10px 0; border-bottom:1px solid var(--lte-line); }', '.lte-tabs button { flex:1; border:none; border-bottom:2px solid transparent; background:transparent; padding:6px 2px; border-radius:0; color:var(--lte-muted); }', '.lte-tabs button:hover { background:transparent; color:var(--lte-text); }', '.lte-tabs button.lte-active { color:var(--lte-accent); border-bottom-color:var(--lte-accent); font-weight:600; }', '.lte-count { display:inline-block; min-width:16px; margin-left:3px; padding:0 4px; border-radius:8px; background:var(--lte-tag); font-size:11px; line-height:16px; font-weight:400; }', '.lte-body { flex:1; overflow:auto; padding:8px 10px; min-height:140px; }', '.lte-pane { display:flex; flex-direction:column; gap:6px; }', '.lte-pane.lte-hidden { display:none; }', '.lte-empty { color:var(--lte-muted); padding:20px 8px; text-align:center; }', '.lte-item { display:flex; gap:8px; align-items:flex-start; padding:7px 8px; border:1px solid var(--lte-line); border-radius:6px; background:var(--lte-item); cursor:pointer; }', '.lte-item:hover { border-color:var(--lte-accent); }', '.lte-time { flex:none; color:var(--lte-muted); font-variant-numeric:tabular-nums; font-size:12px; white-space:nowrap; }', '.lte-text { flex:1; min-width:0; word-break:break-word; }', '.lte-source { flex:none; font-size:11px; color:var(--lte-muted); background:var(--lte-tag); border-radius:4px; padding:0 5px; line-height:18px; white-space:nowrap; }', '.lte-link { border:1px solid var(--lte-line); border-radius:6px; padding:8px; background:var(--lte-item); display:flex; flex-direction:column; gap:4px; }', '.lte-link-label { font-weight:600; word-break:break-word; }', '.lte-link-url { color:var(--lte-accent); word-break:break-all; font-size:12px; }', '.lte-link-meta { display:flex; align-items:center; gap:6px; }', '.lte-link-meta .lte-source { margin-right:auto; }', '.lte-tag { font-size:11px; color:var(--lte-accent); background:var(--lte-active); border-radius:4px; padding:0 5px; line-height:18px; white-space:nowrap; }', '.lte-copy-btn { margin-left:auto; }', '.lte-summary { width:100%; min-height:260px; resize:vertical; border:1px solid var(--lte-line); border-radius:6px; padding:8px; font:12px/1.6 ui-monospace,Consolas,"Courier New",monospace; color:var(--lte-text); background:var(--lte-item); }', '.lte-foot { padding:8px 12px; border-top:1px solid var(--lte-line); color:var(--lte-muted); font-size:12px; }', '#lte-toast { position:fixed; left:50%; bottom:90px; transform:translateX(-50%) translateY(8px); background:rgba(17,24,39,.92); color:#fff; padding:7px 14px; border-radius:6px; opacity:0; pointer-events:none; transition:opacity .18s, transform .18s; font-size:13px; }', '#lte-toast.lte-show { opacity:1; transform:translateX(-50%) translateY(0); }' ].join('\n'); function buildUI() { if (document.getElementById('lte-root')) return; const style = document.createElement('style'); style.id = 'lte-style'; style.textContent = STYLE_TEXT; document.head.appendChild(style); root = document.createElement('div'); root.id = 'lte-root'; fabEl = document.createElement('button'); fabEl.id = 'lte-fab'; fabEl.type = 'button'; fabEl.title = '学习文字提取 (Alt+Shift+X)'; fabEl.textContent = '摘'; root.appendChild(fabEl); panelEl = document.createElement('div'); panelEl.id = 'lte-panel'; panelEl.className = 'lte-hidden'; panelEl.innerHTML = `
学习文字提取
开启字幕/CC 后自动采集,简介与评论中的链接会自动整理。
`; root.appendChild(panelEl); toastEl = document.createElement('div'); toastEl.id = 'lte-toast'; root.appendChild(toastEl); document.body.appendChild(root); fabEl.addEventListener('click', togglePanel); root.addEventListener('click', handleClick); root.addEventListener('mousedown', function (e) { e.stopPropagation(); }); document.addEventListener('keydown', handleKeydown); } function scheduleExtract() { state.extractTimers.forEach(clearTimeout); state.extractTimers = []; [1200, 4000, 8000].forEach(function (delay) { const id = setTimeout(extractStatic, delay); state.extractTimers.push(id); }); } function tick() { const key = currentVideoKey(); if (key !== state.videoKey) { saveNow(); loadVideoState(); scheduleExtract(); if (state.panelOpen) renderTab(true); } const t = readTitle(); if (t && t !== state.title) { state.title = t; scheduleSave(); } captureTick(); state.tickCount += 1; if (state.tickCount % 3 === 0) { scanPageForLinks(); if (state.panelOpen && state.activeTab !== 'summary') renderTab(false); } } function registerMenuCommand() { const label = '学习文字提取: 打开/收起面板'; let registered = false; try { if (gm.registerMenuCommand) { gm.registerMenuCommand(label, togglePanel); registered = true; } } catch (e) {} if (!registered) { try { if (typeof GM !== 'undefined' && typeof GM.registerMenuCommand === 'function') { GM.registerMenuCommand(label, togglePanel); } } catch (e) {} } } function init() { loadVideoState(); buildUI(); scheduleExtract(); setInterval(tick, 1000); registerMenuCommand(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();