// ==UserScript== // @name Bangumi Topic Share // @namespace http://tampermonkey.net/ // @version 6.0 // @description Bangumi 话题/日志分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签 // @author Stardream // @contributor Chang ji, Mewtw0 // @match *://bgm.tv/group/topic/* // @match *://bangumi.tv/group/topic/* // @match *://chii.in/group/topic/* // @match *://bgm.tv/blog/* // @match *://bangumi.tv/blog/* // @match *://chii.in/blog/* // @match *://bgm.tv/ep/* // @match *://bangumi.tv/ep/* // @match *://chii.in/ep/* // @match *://bgm.tv/character/* // @match *://bangumi.tv/character/* // @match *://chii.in/character/* // @match *://bgm.tv/person/* // @match *://bangumi.tv/person/* // @match *://chii.in/person/* // @match *://bgm.tv/subject/* // @match *://bangumi.tv/subject/* // @match *://chii.in/subject/* // @match *://bgm.tv/rakuen* // @match *://bangumi.tv/rakuen* // @match *://chii.in/rakuen* // @match *://bgm.tv/anime/list/* // @match *://bangumi.tv/anime/list/* // @match *://chii.in/anime/list/* // @match *://bgm.tv/music/list/* // @match *://bangumi.tv/music/list/* // @match *://chii.in/music/list/* // @match *://bgm.tv/game/list/* // @match *://bangumi.tv/game/list/* // @match *://chii.in/game/list/* // @match *://bgm.tv/real/list/* // @match *://bangumi.tv/real/list/* // @match *://chii.in/real/list/* // @match *://bgm.tv/book/list/* // @match *://bangumi.tv/book/list/* // @match *://chii.in/book/list/* // @match *://bgm.tv/ // @match *://bangumi.tv/ // @match *://chii.in/ // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @connect * // @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js // @license MIT // ==/UserScript== (function() { 'use strict'; const CARD_CONTENT_IMAGE_LIMIT = 3; // ================= Bangumi OAuth ================= const BGM_CLIENT_ID = 'bgm600069e7caaa1d2ba'; const BGM_REDIRECT_URI = 'https://bgm.tv/rakuen'; const BGM_TOKEN_PROXY = 'https://bgm-topic-share.stdm.workers.dev'; const getBgmToken = () => GM_getValue('bgm_share_token', ''); const setBgmToken = t => GM_setValue('bgm_share_token', t); const clearBgmToken = () => { GM_setValue('bgm_share_token', ''); GM_setValue('bgm_share_refresh_token', ''); GM_setValue('bgm_share_token_expiry', 0); }; function _refreshAuthStatusUI() { const statusEl = document.getElementById('bgm-share-auth-status'); const authBtn = document.getElementById('bgm-share-auth-btn'); const deauthBtn = document.getElementById('bgm-share-deauth-btn'); if (!statusEl) return; const hasToken = !!getBgmToken(); statusEl.textContent = hasToken ? '✓ 已授权,API 请求将携带 Token' : '未授权 - NSFW 条目数据将降级为 DOM 抓取'; statusEl.style.color = hasToken ? '#4caf50' : '#aaa'; if (authBtn) authBtn.style.display = hasToken ? 'none' : ''; if (deauthBtn) deauthBtn.style.display = hasToken ? '' : 'none'; } const startBgmOAuth = () => { window.open( `https://bgm.tv/oauth/authorize?client_id=${BGM_CLIENT_ID}&response_type=code&redirect_uri=${encodeURIComponent(BGM_REDIRECT_URI)}`, '_blank' ); const onFocus = () => { window.removeEventListener('focus', onFocus); _refreshAuthStatusUI(); }; window.addEventListener('focus', onFocus); }; GM_registerMenuCommand('Bangumi Topic Share 授权', startBgmOAuth); GM_registerMenuCommand('Bangumi Topic Share 清除授权', () => { clearBgmToken(); alert('已清除授权'); }); function _saveBgmTokenResponse(data) { if (!data?.access_token) return false; setBgmToken(data.access_token); if (data.refresh_token) GM_setValue('bgm_share_refresh_token', data.refresh_token); if (data.expires_in) GM_setValue('bgm_share_token_expiry', Date.now() + data.expires_in * 1000); return true; } function _showOAuthToast(msg, ok = true) { const toast = document.createElement('div'); toast.textContent = msg; toast.style.cssText = `position:fixed;top:20px;right:20px;background:${ok ? '#F09199' : '#888'};color:#fff;padding:10px 20px;border-radius:8px;font-size:14px;z-index:100002;box-shadow:0 4px 12px rgba(0,0,0,0.3);`; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function checkOAuthCallback() { if (window.location.pathname !== '/rakuen') return; const params = new URLSearchParams(window.location.search); const code = params.get('code'); if (!code) return; history.replaceState(null, '', '/rakuen'); GM_xmlhttpRequest({ method: 'POST', url: BGM_TOKEN_PROXY, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: new URLSearchParams({ grant_type: 'authorization_code', code, }).toString(), onload: res => { try { const data = JSON.parse(res.responseText); if (_saveBgmTokenResponse(data)) { window.close(); } else { _showOAuthToast('✗ 授权失败:' + (data.error_description || data.error || '未知错误'), false); } } catch { _showOAuthToast('✗ 授权响应解析失败', false); } }, onerror: () => _showOAuthToast('✗ 授权请求失败', false) }); } function refreshBgmTokenIfNeeded() { const expiry = GM_getValue('bgm_share_token_expiry', 0); const refreshToken = GM_getValue('bgm_share_refresh_token', ''); if (!refreshToken || !expiry) return; if (Date.now() < expiry - 86400000) return; // 超过剩余 1 天才刷新 GM_xmlhttpRequest({ method: 'POST', url: BGM_TOKEN_PROXY, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, }).toString(), onload: res => { try { _saveBgmTokenResponse(JSON.parse(res.responseText)); } catch {} } }); } // ================================================= const style = document.createElement('style'); style.innerHTML = ` #bgm-share-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; justify-content: center; align-items: flex-start; overflow-y: auto; z-index: 100000; box-sizing: border-box; } .share-card { width: 420px; background: #fff; border-radius: 20px; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; box-shadow: 0 25px 60px rgba(0,0,0,0.5); } .card-top-bar { height: 6px; background: #F09199; } .card-header { padding: 25px 25px 15px; display: flex; align-items: center; gap: 15px; text-align: left; border-bottom: 1px solid #eee; } .avatar-img { width: 54px; height: 54px; border-radius: 12px; background: #eee; background-size: cover; background-position: center; border: 1px solid #f0f0f0; flex-shrink: 0; } .user-meta { text-align: left; } .user-meta .name { display: block; font-weight: bold; color: #F09199; font-size: 17px; line-height: 1.2; } .user-meta .time { font-size: 12px; color: #aaa; margin-top: 4px; display: block; } .card-body { padding: 15px 25px 25px; text-align: left; } .main-title { font-size: 20px; color: #111; margin: 0 0 15px 0; line-height: 1.5; font-weight: 800; } .topic-sub-title { font-size: 16px; color: #333; margin: 0 0 14px; font-weight: 700; line-height: 1.4; } .content-box { background: #fdfafb; padding: 18px; border-radius: 12px; border-left: 5px solid #F09199; } .content-text { font-size: 14px; color: #333; line-height: 1.8; margin: 0; word-break: break-all; } .tags-container { display: flex; flex-wrap: wrap; gap: 8px; } .share-card .tags-container { margin-top: 15px; } .tag-item { background: #FEEFF0; color: #F09199; font-size: 11px; padding: 4px 12px; border-radius: 20px; font-weight: bold; border: 1px solid #F0919944; } .card-footer { background: #f9f9f9; padding: 20px 25px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #eee; } .qr-img { width: 55px; height: 55px; background: #fff; } #loading-info { position: fixed; top: 55%; left: 50%; transform: translateX(-50%); color: #fff; font-size: 14px; z-index: 100001; } .bgm-btn-row { display: flex; gap: 16px; margin: 20px auto 0; justify-content: center; } .bgm-action-btn { width: 48px; height: 48px; padding: 0; background: rgba(255,255,255,0.15); color: #fff; border: 1.5px solid rgba(255,255,255,0.35); border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.15s, opacity 0.2s; position: relative; } .bgm-action-btn:hover:not(:disabled) { background: rgba(255,255,255,0.3); transform: scale(1.1); } .bgm-action-btn:disabled { opacity: 0.3; cursor: default; } .bgm-action-btn svg { display: block; } .bgm-action-btn::after { content: attr(data-tip); position: absolute; top: calc(100% + 10px); left: 50%; transform: translateX(-50%) translateY(-6px); background: rgba(20,20,20,0.82); color: #fff; font-size: 12px; padding: 5px 12px; border-radius: 8px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.18s ease, transform 0.18s ease; } .bgm-action-btn:hover:not(:disabled)::after { opacity: 1; transform: translateX(-50%) translateY(0); } /* ===== 暗色主题 ===== */ .share-card.dark { background: #1e1e1e; } .share-card.dark .card-header { border-bottom: 1px solid rgba(255,255,255,0.1); } .share-card.dark .card-body { background: #1e1e1e; padding-top: 15px; } .share-card.dark .main-title { color: #f0f0f0; } .share-card.dark .topic-sub-title { color: #e0e0e0; } .share-card.dark .content-box { background: #2a2a2a; border-left: 5px solid #F09199; } .share-card.dark .content-text { color: #ddd; } .share-card.dark .tags-container { margin-top: 15px; } .share-card.dark .tag-item { background: #2a2a2a; border: 1px solid #F0919966; } .share-card.dark .card-footer { background: #181818; border-top: 1px solid rgba(255,255,255,0.1); } .share-card.dark .qr-img { background: #2a2a2a; } .content-text img[data-bgm-emoji]:not(.smile-dynamic) { height: 1.5em; width: auto; vertical-align: baseline; } .content-text img.bmoji-image { display: inline; vertical-align: baseline; } .content-text img.smile-dynamic { display: inline !important; height: 3em !important; width: auto !important; vertical-align: baseline; } .content-text img:not([data-bgm-emoji]):not(.bmoji-image):not(.smile-dynamic) { max-width: 100%; height: auto; border-radius: 4px; margin: 4px 0; display: block; } [data-bgm-mask] { display: inline; background-color: #555; color: #555; border-radius: 2px; padding: 0 5px; position: relative; transition: color 0.5s linear; } `; document.head.appendChild(style); function fetchAsBase64(url) { return new Promise((resolve) => { if (!url) { resolve(""); return; } const finalUrl = url.startsWith('//') ? 'https:' + url : url; GM_xmlhttpRequest({ method: "GET", url: finalUrl, responseType: "blob", onload: (res) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(res.response); }, onerror: () => resolve("") }); }); } async function inlineImages(html) { if (!html) return html; const div = document.createElement('div'); div.innerHTML = html; div.querySelectorAll('.embed-play-btn, .embed-player-wrapper, iframe, #catfish_likes_grid').forEach(el => el.remove()); div.querySelectorAll('p').forEach(el => { if (!el.textContent.trim() && !el.querySelector('img')) el.remove(); }); // Remove
immediately after non-emoji images (avoids blank line below image) div.querySelectorAll('img:not([smileid]):not([data-bgm-emoji])').forEach(img => { let next = img.nextSibling; while (next && next.nodeType === 3 && !next.textContent.trim()) next = next.nextSibling; if (next && next.nodeName === 'BR') next.remove(); }); div.querySelectorAll('.text_mask').forEach(el => { el.removeAttribute('style'); el.classList.remove('text_mask'); el.dataset.bgmMask = '1'; }); let imgs = [...div.querySelectorAll('img')]; imgs.forEach(img => { if (img.hasAttribute('smileid') || /\/smiles\//.test(img.src)) { img.setAttribute('data-bgm-emoji', '1'); } }); const contentImgs = imgs.filter(img => !img.hasAttribute('data-bgm-emoji') && !img.classList.contains('bmoji-image') && !img.classList.contains('smile-dynamic') ); if (CARD_CONTENT_IMAGE_LIMIT >= 0 && contentImgs.length > CARD_CONTENT_IMAGE_LIMIT) { const omitted = contentImgs.length - CARD_CONTENT_IMAGE_LIMIT; contentImgs.slice(CARD_CONTENT_IMAGE_LIMIT).forEach(img => img.remove()); const omittedTip = document.createElement('div'); omittedTip.style.cssText = 'font-size:12px;color:#999;margin-top:6px;'; omittedTip.textContent = `还有 ${omitted} 张图片已省略`; div.appendChild(omittedTip); imgs = [...div.querySelectorAll('img')]; } if (imgs.length > 0) { const base64s = await Promise.all(imgs.map(img => fetchAsBase64(img.src))); imgs.forEach((img, i) => { if (base64s[i]) img.src = base64s[i]; }); } return div.innerHTML; } async function getPageTags(contentDoc) { const contentWin = contentDoc.defaultView || window; const pathname = contentWin.location.pathname; const isBlog = /\/blog\/\d+/.test(pathname); const isEpisode = /\/ep\/\d+/.test(pathname); const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(pathname); const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(pathname); const isSubject = /^\/subject\/\d+$/.test(pathname); if (isSubject) { const subjectIdMatch = pathname.match(/\/subject\/(\d+)/); if (subjectIdMatch) { const subjectData = await fetchSubjectDataById(subjectIdMatch[1]); if (subjectData?.type) return [subjectData.type]; } return ['作品']; } if (isEpisode) { const epIdMatch = pathname.match(/\/ep\/(\d+)/); const replyCount = contentDoc.querySelectorAll('[id^="post_"]').length; if (epIdMatch) { const epData = await fetchEpisodeData(epIdMatch[1]); if (epData?.subjectName) return [epData.subjectName, `${replyCount} 回复`]; } return [`${replyCount} 回复`]; } if (isBlog) { const subjectNames = [...new Set( [...contentDoc.querySelectorAll('a')] .filter(a => /\/subject\/\d+$/.test(a.href) && a.textContent.trim()) .map(a => a.textContent.trim()) )]; const replyCount = contentDoc.querySelectorAll('[id^="post_"]').length; // Single subject: name moves to main title, skip it from tags const tagSubjects = subjectNames.length === 1 ? [] : subjectNames; return [...tagSubjects, `${replyCount} 回复`, '日志']; } if (isCharacter) { const replyCount = contentDoc.querySelectorAll('[id^="post_"]').length; const crtMatch = pathname.match(/\/rakuen\/topic\/crt\/(\d+)/); if (crtMatch) { const [subjects, persons] = await Promise.all([ fetchBangumiAPI(`characters/${crtMatch[1]}/subjects`), fetchBangumiAPI(`characters/${crtMatch[1]}/persons`) ]); const staffPriority = { '主角': 0, '配角': 1, '客串': 2 }; const typePriorityApi = { 2: 0, 4: 1 }; const scored = (subjects || []).map(s => ({ name: s.name_cn || s.name, rp: staffPriority[s.staff] ?? 99, tp: typePriorityApi[s.type] ?? 99 })).sort((a, b) => a.rp !== b.rp ? a.rp - b.rp : a.tp - b.tp); const subjectNames = [...new Set(scored.map(s => s.name))].slice(0, 2); const cvNames = [...new Set((persons || []).filter(p => p.type === 1).map(p => p.name))]; const tags = []; if (cvNames.length) tags.push('!CV: ' + cvNames.join(' / ')); tags.push(...subjectNames); tags.push(`${replyCount} 回复`); return tags; } const cvNames = [...new Set( [...contentDoc.querySelectorAll('.browserList .badge_actor h3 a')] .map(a => a.textContent.trim()).filter(Boolean) )]; const rolePriority = { '1': 0, '2': 1, '3': 2 }; const typePriority = { '2': 0, '3': 1 }; const scoredItems = [...contentDoc.querySelectorAll('.browserList .item')].map(item => { const nameEl = item.querySelector('.innerLeftItem h3 a.l'); if (!nameEl) return null; const roleAttr = item.querySelector('.badge_job[attr-crt-type]')?.getAttribute('attr-crt-type') || '99'; const typeMatch = item.querySelector('.ico_subject_type')?.className.match(/subject_type_(\d+)/); const typeNum = typeMatch ? typeMatch[1] : '99'; return { name: nameEl.textContent.trim(), rp: rolePriority[roleAttr] ?? 99, tp: typePriority[typeNum] ?? 99 }; }).filter(Boolean); scoredItems.sort((a, b) => a.rp !== b.rp ? a.rp - b.rp : a.tp - b.tp); const subjectNames = [...new Set(scoredItems.map(s => s.name))].slice(0, 2); const tags = []; if (cvNames.length) tags.push('!CV: ' + cvNames.join(' / ')); tags.push(...subjectNames); tags.push(`${replyCount} 回复`); return tags; } if (isPerson) { const replyCount = contentDoc.querySelectorAll('[id^="post_"]').length; const personIdMatch = pathname.match(/\/rakuen\/topic\/prsn\/(\d+)/) || pathname.match(/\/person\/(\d+)/); if (personIdMatch) { const workTags = await fetchPersonWorkTags(personIdMatch[1]); return [...workTags, `${replyCount} 回复`]; } } const groupLink = contentDoc.querySelector('a.avatar[href^="/group/"]'); let groupName = ''; if (groupLink) { groupLink.childNodes.forEach(n => { if (n.nodeType === 3) groupName += n.textContent.trim(); }); } if (!groupName) { const subjectLink = contentDoc.querySelector('#pageHeader a[href^="/subject/"]') || contentDoc.querySelector('a[href^="/subject/"]'); if (subjectLink) groupName = subjectLink.textContent.trim(); } const replyCount = Math.max(0, contentDoc.querySelectorAll('[id^="post_"]').length - 1); return [groupName || 'Bangumi', `${replyCount} 回复`]; } async function getAITags(title, content, contentDoc) { const apiKey = GM_getValue('bgm_share_ai_key', ''); const apiUrl = GM_getValue('bgm_share_ai_url', ''); const model = GM_getValue('bgm_share_ai_model', '') || 'gpt-3.5-turbo'; if (!apiKey || !apiUrl) return getPageTags(contentDoc); return new Promise((resolve) => { const prompt = `根据标题和内容生成3个短标签,只要标签名,空格隔开。内容:${title} ${content.substring(0, 150)}`; GM_xmlhttpRequest({ method: "POST", url: apiUrl, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, data: JSON.stringify({ model, messages: [{ role: "user", content: prompt }], temperature: 0.5 }), onload: (res) => { try { const tags = JSON.parse(res.responseText).choices[0].message.content.trim().split(/\s+/).slice(0, 3); resolve(tags); } catch (e) { resolve(getPageTags(contentDoc)); } }, onerror: () => resolve(getPageTags(contentDoc)) }); }); } // contentDoc: the document containing the topic (may be an iframe's doc on Rakuen) // Overlay is always rendered in the outer document (where GM functions are available) async function _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, replies = [], replyId = '', charImageUrl = '', badgeLabel = '', overrideTags = null, topicTitle = '' }) { if (document.getElementById('bgm-share-overlay')) return; if (typeof html2canvas === 'undefined') { alert("截图库加载失败,请刷新页面或检查网络。"); return; } const loading = document.createElement('div'); loading.innerHTML = '
AI 正在提炼标签...
'; document.body.appendChild(loading); let fullContent = ""; let displayContentHtml = ""; if (contentEl) { const toHide = contentEl.querySelectorAll('.forum_category, #catfish_likes_grid, .embed-play-btn'); toHide.forEach(el => el.style.display = 'none'); const computedHidden = [...contentEl.querySelectorAll('*')].filter(el => { const cs = getComputedStyle(el); return cs.display === 'none' || cs.visibility === 'hidden'; }); computedHidden.forEach(el => { el.dataset.hiddenSnapshot = '1'; el.style.display = 'none'; }); fullContent = contentEl.innerText?.trim() || ""; const fullHtml = contentEl.innerHTML?.trim() || ""; computedHidden.forEach(el => { delete el.dataset.hiddenSnapshot; el.style.display = ''; }); toHide.forEach(el => el.style.display = ''); const lim = replies.length > 0 ? 200 : 300; displayContentHtml = fullContent.length > lim ? truncateHtml(fullHtml, lim) : fullHtml; } const currentFullUrl = contentWin.location.origin + contentWin.location.pathname; const shareUrl = replyId ? currentFullUrl + '#' + replyId : currentFullUrl; const displayUrl = currentFullUrl.replace(/^https?:\/\//, ''); const [tags, base64Avatar, base64CharImage, base64QR, ...base64ReplyAvatars] = await Promise.all([ overrideTags ? Promise.resolve(overrideTags) : getAITags(pureTitle, fullContent, contentDoc), fetchAsBase64(avatarUrl), fetchAsBase64(charImageUrl), fetchAsBase64(`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(shareUrl)}${dark ? '&color=F09199&bgcolor=2a2a2a' : ''}`), ...replies.map(r => fetchAsBase64(r.avatarUrl)) ]); const [inlinedMainContent, ...inlinedReplyContents] = await Promise.all([ inlineImages(displayContentHtml), ...replies.map(r => inlineImages(r.contentHtml || r.content)) ]); const tagsHtml = tags.map(tag => tag.startsWith('!') ? `${tag.slice(1)}` : `# ${tag}`).join(''); const divider = dark ? 'rgba(255,255,255,0.1)' : '#eee'; const hasMainContent = !!inlinedMainContent || !!username; const renderLevel = (idx) => { if (idx >= replies.length) return ''; const r = replies[idx]; const b64 = base64ReplyAvatars[idx]; const avatarSize = Math.max(24, 36 - idx * 4); const suppressGap = base64CharImage && !topicTitle && !inlinedMainContent && !username; const topStyle = idx === 0 ? `margin-top:${suppressGap ? '0' : '14'}px;padding-top:${suppressGap ? '0' : '14'}px;${hasMainContent ? `border-top:1px solid ${divider};` : ''}` : `margin-top:10px;padding-left:14px;border-left:3px solid ${dark ? '#F0919955' : '#F0919933'};`; const inner = idx + 1 < replies.length ? `
${renderLevel(idx + 1)}
` : ''; return `
${r.username} ${r.rating ? `★ ${r.rating}` : ''} ${r.time}
${inlinedReplyContents[idx]}
${inner}
`; }; const replySection = replies.length > 0 ? renderLevel(0) : ''; loading.remove(); const overlay = document.createElement('div'); overlay.id = 'bgm-share-overlay'; overlay.style.display = 'flex'; const notice = _shareNotice; _shareNotice = ''; overlay.innerHTML = `
${notice ? `
⚠ ${notice}
` : ''}
`; document.body.appendChild(overlay); const noticeEl = document.getElementById('bgm-share-notice'); if (noticeEl) { requestAnimationFrame(() => requestAnimationFrame(() => { noticeEl.style.opacity = '1'; })); setTimeout(() => { noticeEl.style.opacity = '0'; setTimeout(() => noticeEl.remove(), 400); }, 4000); } let cancelled = false; let maskRevealed = false; let currentCanvas = null; const maskPreviewStyle = document.createElement('style'); document.head.appendChild(maskPreviewStyle); const copyBtn = document.getElementById('bgm-copy-btn'); const downloadBtn = document.getElementById('bgm-download-btn'); const hasMask = !!document.querySelector('#capture-area [data-bgm-mask]'); if (!hasMask) document.getElementById('bgm-mask-btn').style.display = 'none'; const updateMaskPreview = () => { maskPreviewStyle.textContent = maskRevealed ? '#capture-area [data-bgm-mask] { color: #fff !important; }' : ''; const btn = document.getElementById('bgm-mask-btn'); if (btn) btn.setAttribute('data-tip', maskRevealed ? '遮住剧透' : '显示剧透'); }; updateMaskPreview(); document.getElementById('bgm-close-btn').addEventListener('click', () => { cancelled = true; maskPreviewStyle.remove(); overlay.remove(); }); const showToast = (msg) => { const toast = document.createElement('div'); toast.textContent = msg; toast.style.cssText = 'position:fixed;bottom:40px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:8px 20px;border-radius:20px;font-size:14px;z-index:100002;opacity:1;transition:opacity 0.5s'; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); }, 1800); }; document.getElementById('bgm-text-btn').addEventListener('click', async () => { const shareText = `【链接】${pureTitle} | Bangumi番组计划\n${shareUrl}`; try { await navigator.clipboard.writeText(shareText); showToast('✓ 文案已复制'); } catch (e) { showToast('✗ 复制失败'); } }); document.getElementById('bgm-mask-btn').addEventListener('click', () => { maskRevealed = !maskRevealed; updateMaskPreview(); doCapture(); }); copyBtn.addEventListener('click', async () => { if (!currentCanvas) return; try { const blob = await new Promise(resolve => currentCanvas.toBlob(resolve, 'image/png')); await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]); showToast('✓ 已复制到剪贴板'); } catch (e) { showToast('✗ 复制失败,请改用下载'); } }); downloadBtn.addEventListener('click', () => { if (!currentCanvas) return; const link = document.createElement('a'); link.download = `BGM_Share_${username}.png`; link.href = currentCanvas.toDataURL('image/png'); link.click(); }); const doCapture = async () => { if (cancelled) return; copyBtn.disabled = true; downloadBtn.disabled = true; let canvas; let iframe = null; try { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 10000)); const captureEl = document.querySelector('#capture-area'); iframe = document.createElement('iframe'); iframe.style.cssText = 'position:fixed;top:0;left:0;border:0;opacity:0;pointer-events:none;z-index:99999;'; iframe.style.width = captureEl.offsetWidth + 'px'; iframe.style.height = captureEl.offsetHeight + 'px'; document.body.appendChild(iframe); const iDoc = iframe.contentDocument; const iStyle = iDoc.createElement('style'); const maskCss = maskRevealed ? '[data-bgm-mask] { color: #fff !important; }' : ''; const sampleLink = contentDoc.querySelector('.topic_content a[href], #entry_content a[href], .message a[href]') || contentDoc.querySelector('a[href^="http"]') || contentDoc.querySelector('a[href]'); const rawLinkColor = sampleLink ? getComputedStyle(sampleLink).color : ''; const linkColor = rawLinkColor && rawLinkColor !== 'rgba(0, 0, 0, 0)' && rawLinkColor !== 'transparent' ? rawLinkColor : (dark ? '#8ec8e8' : '#0066cc'); const sampleLinkDecoration = sampleLink ? getComputedStyle(sampleLink).textDecorationLine : 'none'; let quoteCss = ''; try { for (const sheet of document.styleSheets) { try { for (const rule of sheet.cssRules) { if (rule.selectorText && /\.quote/.test(rule.selectorText)) { quoteCss += rule.cssText + '\n'; // Also add ancestor-stripped version to ensure match inside card const sel = rule.selectorText.split(',').map(s => { const i = s.indexOf('.quote'); return i >= 0 ? s.slice(i).trim() : s.trim(); }).join(','); if (sel !== rule.selectorText.trim()) { quoteCss += `${sel}{${rule.style.cssText}}\n`; } } } } catch (e) { /* cross-origin sheet */ } } } catch (e) {} // Sync quote and body text colors from actual page computed styles try { const cw = contentDoc.defaultView || window; const sampleQuoteQ = contentDoc.querySelector('.quote q'); const sampleBody = contentDoc.querySelector('.cmt_sub_content') || contentDoc.querySelector('.reply_content .message') || contentDoc.querySelector('.topic_content'); if (sampleQuoteQ) { const quoteColor = cw.getComputedStyle(sampleQuoteQ).color; if (quoteColor) quoteCss += `.quote,.quote q{color:${quoteColor} !important;}`; } if (sampleBody) { const bodyColor = cw.getComputedStyle(sampleBody).color; if (bodyColor) quoteCss += `.content-text{color:${bodyColor} !important;}`; } } catch (e) {} iStyle.textContent = style.innerHTML + maskCss + quoteCss + ` a { color: ${linkColor}; text-decoration: ${sampleLinkDecoration}; } span[style*="line-through"], span[style*="line-through"] * { text-decoration: line-through !important; text-decoration-color: white !important; }`; iDoc.head.appendChild(iStyle); iDoc.body.style.cssText = 'margin:0;padding:0;background:transparent;display:inline-block;'; iDoc.body.innerHTML = captureEl.innerHTML; await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); // html2canvas renders inline backgrounds as full-width blocks; replace each // [data-bgm-mask] background with precisely-positioned absolute overlays // matching the actual per-line text rects from getClientRects(). const captureRoot = iDoc.body.firstElementChild; captureRoot.style.position = 'relative'; const rootRect = captureRoot.getBoundingClientRect(); iDoc.querySelectorAll('[data-bgm-mask]').forEach(el => { Array.from(el.getClientRects()).forEach(rect => { const ov = iDoc.createElement('div'); ov.style.cssText = `position:absolute;left:${rect.left - rootRect.left}px;top:${rect.top - rootRect.top}px;width:${rect.width}px;height:${rect.height}px;background-color:#555;border-radius:2px;pointer-events:none;`; // Revealed: overlay behind text (prepend); Hidden: overlay on top (append) if (maskRevealed) captureRoot.insertBefore(ov, captureRoot.firstChild); else captureRoot.appendChild(ov); }); el.style.backgroundColor = 'transparent'; el.style.backgroundImage = 'none'; }); canvas = await Promise.race([ html2canvas(iDoc.body.firstElementChild, { scale: 2, backgroundColor: null }), timeout ]); // 裁掉底部全透明行(inline-block baseline gap 产生的透明条) const imgData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data; let trimH = canvas.height; for (let y = canvas.height - 1; y >= 0; y--) { let opaque = false; for (let x = 0; x < canvas.width; x++) { if (imgData[(y * canvas.width + x) * 4 + 3] > 0) { opaque = true; break; } } if (opaque) { trimH = y + 1; break; } } if (trimH < canvas.height) { const trimmed = document.createElement('canvas'); trimmed.width = canvas.width; trimmed.height = trimH; trimmed.getContext('2d').drawImage(canvas, 0, 0); canvas = trimmed; } } catch (e) { iframe?.remove(); showToast('✗ 截图失败,请刷新后重试'); return; } iframe?.remove(); if (cancelled) return; currentCanvas = canvas; copyBtn.disabled = false; downloadBtn.disabled = false; }; setTimeout(() => doCapture(), 800); } function extractCharImageUrl(contentDoc) { const link = contentDoc.querySelector('#columnCrtA a.thickbox') || contentDoc.querySelector('a.thickbox[href*="/pic/crt/"]') || contentDoc.querySelector('a.thickbox[href*="/pic/user/"]'); if (link?.href) return link.href; const img = contentDoc.querySelector('#columnCrtA img') || contentDoc.querySelector('#crt_cover img') || contentDoc.querySelector('img[src*="/pic/crt/l/"]') || contentDoc.querySelector('img[src*="/pic/user/l/"]') || contentDoc.querySelector('img[src*="/pic/crt/m/"]'); return img?.src || ''; } async function fetchPersonWorkTags(personId) { const staffPriority = { '主角': 0, '配角': 1, '客串': 2 }; const typePriority = { 2: 0, 4: 1 }; const chars = await fetchBangumiAPI(`persons/${personId}/characters`); if (chars && chars.length) { const charMap = new Map(); for (const c of chars) { const rp = staffPriority[c.staff] ?? 99; const tp = typePriority[c.subject_type] ?? 99; const existing = charMap.get(c.id); if (!existing || rp < existing.rp || (rp === existing.rp && tp < existing.tp)) { charMap.set(c.id, { charName: c.name, workName: c.subject_name_cn || c.subject_name, rp, tp }); } } return [...charMap.values()] .sort((a, b) => a.rp !== b.rp ? a.rp - b.rp : a.tp - b.tp) .slice(0, 2) .map(c => `${c.charName} - ${c.workName}`); } const subjects = await fetchBangumiAPI(`persons/${personId}/subjects`); if (subjects && subjects.length) { const staffOrder = [ // 核心创作 '监督', '原案', '原作', '系列构成', '脚本', '剧本', '编剧', '企画', // 人设/美术 '人物设计', '总作画监督', '作画监督', '作画监督助理', '监修', '设定协力', // 动画制作 '演出', '分镜', '原画', '主动画师', '作画', '第二原画', '补间动画', '动画', // 音乐 '音乐', '作曲', '编曲', '作词', '主题歌演出', '主题歌作曲', '主题歌编曲', '母带制作', '艺术家', // 技术/制作 '音响监督', '摄影监督', '色彩设计', '美术监督', '制作人', '监制', '执行制片人', '协力', // 出演 '主演', '配角', '出演', '客串' ]; const staffRank = (staff) => { const i = staffOrder.indexOf(staff); return i === -1 ? staffOrder.length : i; }; const seen = new Set(); const seenStaff = new Set(); const result = []; for (const s of [...subjects].sort((a, b) => { const sr = staffRank(a.staff) - staffRank(b.staff); if (sr !== 0) return sr; return (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99); })) { const name = s.name_cn || s.name; if (name && !seen.has(s.id) && !seenStaff.has(s.staff)) { seen.add(s.id); seenStaff.add(s.staff); result.push(s.staff ? `${s.staff} - ${name}` : name); } if (result.length >= 2) break; } return result; } return []; } function fetchBangumiAPI(path) { const token = getBgmToken(); return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: 'https://api.bgm.tv/v0/' + path, headers: { 'Accept': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, onload: res => { if (res.status === 401) clearBgmToken(); if (res.status !== 200) { resolve(null); return; } try { resolve(JSON.parse(res.responseText)); } catch { resolve(null); } }, onerror: () => resolve(null) }); }); } async function getRakuenCharPersonData(pathname) { const crtMatch = pathname.match(/\/rakuen\/topic\/crt\/(\d+)/); const prsnMatch = pathname.match(/\/rakuen\/topic\/prsn\/(\d+)/); if (!crtMatch && !prsnMatch) return null; const id = (crtMatch || prsnMatch)[1]; const type = crtMatch ? 'characters' : 'persons'; const data = await fetchBangumiAPI(`${type}/${id}`); if (!data) return null; const imageUrl = data.images?.large || data.images?.medium || ''; const name = data.name || ''; const careerMap = { producer: '制作人', mangaka: '漫画家', artist: '画师', seiyu: '声优', writer: '作者', illustrator: '插画师', actor: '演员' }; const badgeLabel = crtMatch ? '角色' : ((data.career || []).map(c => careerMap[c]).filter(Boolean).join(' ') || '人物'); return { imageUrl, name, badgeLabel, id, type }; } const _episodeDataCache = {}; async function fetchEpisodeData(episodeId) { if (_episodeDataCache[episodeId]) { if (_episodeDataCache[episodeId]._domFallback) _shareNotice = 'NSFW 条目 API 无权访问,已降级为页面数据,部分信息可能不完整'; return _episodeDataCache[episodeId]; } const ep = await fetchBangumiAPI(`episodes/${episodeId}`); if (!ep) { _shareNotice = 'NSFW 条目 API 无权访问,已降级为页面数据,部分信息可能不完整'; return null; } const subject = await fetchBangumiAPI(`subjects/${ep.subject_id}`); let subjectName = subject?.name_cn || subject?.name || ''; let subjectImageUrl = subject?.images?.medium || subject?.images?.common || ''; if (!subject) { _shareNotice = 'NSFW 条目 API 无权访问,已降级为页面数据,部分信息可能不完整'; const subjectLink = document.querySelector('#headerSubject a[href*="/subject/"]') || document.querySelector('a.cover[href*="/subject/"]'); if (subjectLink) subjectName = subjectLink.textContent?.trim() || ''; const subjectImg = document.querySelector('img[src*="/pic/cover/l/"]') || document.querySelector('img[src*="/pic/cover/m/"]'); if (subjectImg) subjectImageUrl = subjectImg.src; } const result = { episodeName: ep.name_cn || ep.name || '', subjectName, subjectImageUrl, epNumber: ep.ep, _domFallback: !subject }; _episodeDataCache[episodeId] = result; return result; } function extractSubjectDataFromDOM(subjectId, doc) { const h1 = doc.querySelector('#pageHeader h1'); let name = ''; if (h1) h1.childNodes.forEach(n => { if (n.nodeType === 3) name += n.textContent; }); name = name.trim() || doc.title?.split(/\s*[|//]\s*/)[0].trim() || ''; const coverA = doc.querySelector('a.thickbox[href*="/pic/cover/"]'); const coverImg = doc.querySelector('img[src*="/pic/cover/l/"]') || doc.querySelector('img[src*="/pic/cover/m/"]') || doc.querySelector('#bangumiInfo img'); const imageUrl = coverA?.href || coverImg?.src || ''; const navTypeMap = { '/anime': ['动画', 2], '/book': ['书籍', 1], '/music': ['音乐', 3], '/game': ['游戏', 4], '/real': ['三次元', 6] }; let type = '作品', typeNum = 0; for (const [href, [t, n]] of Object.entries(navTypeMap)) { if (doc.querySelector(`#navMenuNeue a[href="${href}"].focus, #navMenuNeue a[href="${href}"][class*="selected"]`)) { type = t; typeNum = n; break; } } return { name, imageUrl, type, typeNum, infobox: [] }; } function getSubjectIdFromTopicPage(pathname, contentDoc) { const direct = pathname.match(/\/subject\/(\d+)\/topic\/\d+/); if (direct) return direct[1]; if (/\/subject\/topic\/\d+/.test(pathname) || /\/rakuen\/topic\/subject\/\d+/.test(pathname)) { const link = contentDoc.querySelector('#pageHeader a[href^="/subject/"]') || contentDoc.querySelector('a.cover[href^="/subject/"]') || contentDoc.querySelector('a[href^="/subject/"]'); const m = link?.getAttribute('href')?.match(/\/subject\/(\d+)/); if (m) return m[1]; } return null; } let _shareNotice = ''; const _subjectDataCache = {}; async function fetchSubjectDataById(subjectId) { if (_subjectDataCache[subjectId]) { if (_subjectDataCache[subjectId]._domFallback) _shareNotice = 'NSFW 条目 API 无权访问,已降级为页面数据,部分信息可能不完整'; return _subjectDataCache[subjectId]; } const data = await fetchBangumiAPI(`subjects/${subjectId}`); if (!data) { if (new RegExp(`/subject/${subjectId}(/|$)`).test(window.location.pathname)) { _shareNotice = 'NSFW 条目 API 无权访问,已降级为页面数据,部分信息可能不完整'; const result = extractSubjectDataFromDOM(subjectId, document); result._domFallback = true; _subjectDataCache[subjectId] = result; return result; } return null; } const typeMap = { 1: '书籍', 2: '动画', 3: '音乐', 4: '游戏', 6: '三次元' }; const result = { name: data.name_cn || data.name || '', imageUrl: data.images?.medium || data.images?.common || '', type: typeMap[data.type] || '作品', typeNum: data.type, infobox: data.infobox || [] }; _subjectDataCache[subjectId] = result; return result; } function getInfoboxValue(infobox, key) { const entry = (infobox || []).find(e => e.key === key); if (!entry) return null; if (typeof entry.value === 'string') { const parts = entry.value.split(/[、\/]/).map(s => s.trim()).filter(Boolean); if (parts.length > 3) return parts.slice(0, 3).join(' / ') + ' 等'; return entry.value; } if (Array.isArray(entry.value)) { const vals = entry.value.map(v => v.v || v).filter(Boolean); const limited = vals.slice(0, 3); return limited.join(' / ') + (vals.length > 3 ? ' 等' : ''); } return null; } const SUBJECT_INFOBOX_KEYS = { 1: ['作者', '出版社'], 2: ['导演', '原作'], 3: ['艺术家', '厂牌'], 4: ['游戏类型', '开发'], 6: ['导演', '主演'] }; function extractCommentItemInfo(itemEl, contentWin) { const userLink = itemEl.querySelector('a[href*="/user/"]'); const username = userLink ? userLink.textContent.trim() : '未知用户'; const userSlug = userLink ? (userLink.getAttribute('href') || '').replace('/user/', '').replace(/^\//, '') : ''; const avatarEl = itemEl.querySelector('.avatarNeue') || itemEl.querySelector('[class*="avatar"]'); let avatarUrl = ''; if (avatarEl) { const bg = contentWin.getComputedStyle(avatarEl).backgroundImage; if (bg && bg !== 'none') avatarUrl = bg.replace(/url\(["']?([^"']+)["']?\)/, '$1'); if (!avatarUrl) { const m = (avatarEl.getAttribute('style') || '').match(/background-image:\s*url\(["']?([^"']+)["']?\)/); if (m) avatarUrl = m[1]; } } const commentEl = itemEl.querySelector('p.comment'); const rawText = commentEl?.innerText?.trim() || ''; const truncated = rawText.length > 150; const contentHtml = truncated ? rawText.substring(0, 150) + '...' : (commentEl?.innerHTML?.trim() || ''); const greyEls = [...itemEl.querySelectorAll('small.grey')]; const timeEl = greyEls.find(el => el.textContent.trim().startsWith('@')); const domTime = timeEl ? timeEl.textContent.trim().replace(/^@\s*/, '') : ''; const statusEl = greyEls.find(el => !el.textContent.trim().startsWith('@')); const collectionStatus = statusEl ? statusEl.textContent.trim() : ''; return { username, userSlug, avatarUrl, time: domTime, rating: 0, collectionStatus, content: truncated ? rawText.substring(0, 150) + '...' : rawText, contentHtml }; } async function createSubjectCommentShareImage(itemEl, contentDoc = document) { const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = contentDoc.defaultView || window; const subjectIdMatch = contentWin.location.pathname.match(/\/subject\/(\d+)/); if (!subjectIdMatch) return; const subjectId = subjectIdMatch[1]; const subjectData = await fetchSubjectDataById(subjectId); const pureTitle = subjectData?.name || contentDoc.title?.split(/\s*[|//]\s*/)[0].trim() || '作品'; const charImageUrl = subjectData?.imageUrl || ''; const badgeLabel = subjectData?.type || '作品'; const commentInfo = extractCommentItemInfo(itemEl, contentWin); let userTags = null; if (commentInfo.userSlug) { const collection = await fetchBangumiAPI(`users/${commentInfo.userSlug}/collections/${subjectId}`); if (collection) { commentInfo.rating = collection.rate || 0; if (collection.updated_at) { const d = new Date(collection.updated_at); const pad = n => String(n).padStart(2, '0'); commentInfo.time = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } if (collection.tags?.length > 0) userTags = collection.tags; } } if (!commentInfo.time) commentInfo.time = '未知时间'; const infoboxTags = (SUBJECT_INFOBOX_KEYS[subjectData?.typeNum] || []) .map(key => getInfoboxValue(subjectData?.infobox, key)) .filter(Boolean); const fallbackTags = [commentInfo.collectionStatus, ...infoboxTags].filter(Boolean); const overrideTags = userTags ? [commentInfo.collectionStatus, ...userTags].filter(Boolean) : fallbackTags; await _doShareCard({ username: '', postTime: '', avatarUrl: '', contentEl: null, pureTitle, contentDoc, contentWin, dark, replies: [commentInfo], replyId: itemEl.id || '', charImageUrl, badgeLabel, overrideTags }); } function extractCollectionPageItemInfo(itemEl, contentWin) { const userLink = itemEl.querySelector('a.avatar[href*="/user/"]'); const userSlug = userLink?.getAttribute('href')?.match(/\/user\/([^\/\?]+)/)?.[1] || ''; let username = ''; if (userLink) { userLink.childNodes.forEach(n => { if (n.nodeType === 3) username += n.textContent; }); username = username.trim(); } if (!username) username = '未知用户'; const avatarEl = itemEl.querySelector('.avatarNeue'); let avatarUrl = ''; if (avatarEl) { const bg = contentWin.getComputedStyle(avatarEl).backgroundImage; if (bg && bg !== 'none') avatarUrl = bg.replace(/url\(["']?([^"']+)["']?\)/, '$1'); if (!avatarUrl) { const m = (avatarEl.getAttribute('style') || '').match(/background-image:\s*url\(["']?([^"']+)["']?\)/); if (m) avatarUrl = m[1]; } } const ratingMatch = itemEl.querySelector('.starlight')?.className.match(/stars(\d+)/); const rating = ratingMatch ? parseInt(ratingMatch[1]) : 0; const timeEl = itemEl.querySelector('p.info'); const time = timeEl ? (timeEl.textContent.trim().match(/\d{4}-\d+-\d+\s+\d+:\d+/)?.[0] || timeEl.textContent.trim()) : ''; const container = itemEl.querySelector('.userContainer'); let rawText = ''; if (container) { let afterInfo = false; container.childNodes.forEach(n => { if (n.nodeType === 1 && n.tagName === 'P' && n.classList.contains('info')) { afterInfo = true; return; } if (afterInfo) rawText += n.nodeType === 3 ? n.textContent : (n.innerText || n.textContent || ''); }); rawText = rawText.trim(); } const truncated = rawText.length > 150; const contentHtml = truncated ? rawText.substring(0, 150) + '...' : rawText; return { username, userSlug, avatarUrl, rating, time, content: contentHtml, contentHtml }; } const COLLECTION_PAGE_STATUS = { collections: '看过', wishes: '想看', doings: '在看', on_hold: '搁置', dropped: '抛弃' }; async function createCollectionPageShareImage(itemEl, contentDoc = document) { const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = contentDoc.defaultView || window; const subjectIdMatch = contentWin.location.pathname.match(/\/subject\/(\d+)/); if (!subjectIdMatch) return; const subjectId = subjectIdMatch[1]; const subjectData = await fetchSubjectDataById(subjectId); const pureTitle = subjectData?.name || contentDoc.title?.split(/\s*[|//]\s*/)[0].trim() || '作品'; const charImageUrl = subjectData?.imageUrl || ''; const badgeLabel = subjectData?.type || '作品'; const commentInfo = extractCollectionPageItemInfo(itemEl, contentWin); let userTags = null; if (commentInfo.userSlug) { const collection = await fetchBangumiAPI(`users/${commentInfo.userSlug}/collections/${subjectId}`); if (collection) { if (!commentInfo.rating) commentInfo.rating = collection.rate || 0; if (!commentInfo.time && collection.updated_at) { const d = new Date(collection.updated_at); const pad = n => String(n).padStart(2, '0'); commentInfo.time = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } if (collection.tags?.length > 0) userTags = collection.tags; } } if (!commentInfo.time) commentInfo.time = '未知时间'; const pageType = contentWin.location.pathname.match(/\/subject\/\d+\/(\w+)/)?.[1] || ''; const collectionStatus = COLLECTION_PAGE_STATUS[pageType] || ''; const infoboxTags = (SUBJECT_INFOBOX_KEYS[subjectData?.typeNum] || []) .map(key => getInfoboxValue(subjectData?.infobox, key)) .filter(Boolean); const overrideTags = userTags ? [collectionStatus, ...userTags].filter(Boolean) : [collectionStatus, ...infoboxTags].filter(Boolean); await _doShareCard({ username: '', postTime: '', avatarUrl: '', contentEl: null, pureTitle, contentDoc, contentWin, dark, replies: [commentInfo], replyId: '', charImageUrl, badgeLabel, overrideTags }); } async function createShareImage(contentDoc = document) { const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = contentDoc.defaultView || window; const isBlog = /\/blog\/\d+/.test(contentWin.location.pathname); const isEpisode = /\/ep\/\d+/.test(contentWin.location.pathname); const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(contentWin.location.pathname); const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(contentWin.location.pathname); const subjectTopicId = getSubjectIdFromTopicPage(contentWin.location.pathname, contentDoc); const isSubjectTopic = !!subjectTopicId; let username, postTime, avatarUrl, contentEl; if (isEpisode || isCharacter || isPerson) { username = ""; postTime = ""; avatarUrl = ""; contentEl = null; } else if (isBlog) { const authorLink = contentDoc.querySelector('.author.user-card .title p a') || contentDoc.querySelector('.author.user-card a.avatar'); username = authorLink ? authorLink.textContent.trim() : "未知用户"; const timeEl = contentDoc.querySelector('.header .tools .time'); postTime = timeEl ? (timeEl.innerText.match(/\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}/)?.[0] || "未知时间") : "未知时间"; const avatarImg = contentDoc.querySelector('.author.user-card a.avatar img'); avatarUrl = avatarImg ? avatarImg.src : ""; contentEl = contentDoc.querySelector('#entry_content'); } else { const firstPost = contentDoc.querySelector('.postTopic') || contentDoc.querySelector('[id^="post_"]'); const idNode = firstPost?.querySelector('strong a') || firstPost?.querySelector('.author strong a'); username = idNode ? idNode.innerText.trim() : "未知用户"; const timeNode = firstPost?.querySelector('small'); postTime = timeNode ? (timeNode.innerText.match(/\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}/)?.[0] || "未知时间") : "未知时间"; const avatarBox = firstPost?.querySelector('.avatarSize48'); avatarUrl = avatarBox ? contentWin.getComputedStyle(avatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : ""; contentEl = firstPost?.querySelector('.topic_content') || firstPost?.querySelector('.inner'); } const h1Node = contentDoc.querySelector('#pageHeader h1') || contentDoc.querySelector('h1.title') || contentDoc.querySelector('h1.nameSingle a') || contentDoc.querySelector('h1'); let pureTitle = ""; if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) pureTitle += n.textContent; }); pureTitle = pureTitle.replace(/[»\n]/g, '').trim(); if (!pureTitle) { const rawTitle = (contentDoc.title || '').split(/\s*[|//]\s*/)[0].trim(); pureTitle = rawTitle || "分享话题"; } let charImageUrl = (isCharacter || isPerson) ? extractCharImageUrl(contentDoc) : ''; let badgeLabel = ''; let topicTitle = ''; if (isCharacter) badgeLabel = '角色'; if (isPerson) { const subtitleEl = contentDoc.querySelector('h2.subtitle'); const subtitleText = subtitleEl ? subtitleEl.textContent.trim() : ''; badgeLabel = subtitleText.replace(/^[^::]*[::]\s*/, '').trim() || '人物'; } if ((isCharacter || isPerson) && (!charImageUrl || /\/rakuen\/topic\/(crt|prsn)\//.test(contentWin.location.pathname))) { const apiData = await getRakuenCharPersonData(contentWin.location.pathname); if (apiData) { if (!charImageUrl) charImageUrl = apiData.imageUrl; if (!badgeLabel || badgeLabel === '人物') badgeLabel = apiData.badgeLabel; } } if (isEpisode) { const epIdMatch = contentWin.location.pathname.match(/\/ep\/(\d+)/); if (epIdMatch) { const epData = await fetchEpisodeData(epIdMatch[1]); if (epData) { if (epData.episodeName) pureTitle = epData.episodeName; if (epData.subjectImageUrl) charImageUrl = epData.subjectImageUrl; badgeLabel = epData.epNumber ? `第${epData.epNumber}话` : '章节'; } } } if (isSubjectTopic) { const subjectData = await fetchSubjectDataById(subjectTopicId); if (subjectData) { topicTitle = pureTitle; pureTitle = subjectData.name || pureTitle; charImageUrl = subjectData.imageUrl || ''; badgeLabel = subjectData.type || ''; } } if (isBlog) { const blogSubjectIds = [...new Set( [...contentDoc.querySelectorAll('a[href]')] .map(a => (a.getAttribute('href') || '').match(/\/subject\/(\d+)$/)?.[1]) .filter(Boolean) )]; if (blogSubjectIds.length === 1) { const subjectData = await fetchSubjectDataById(blogSubjectIds[0]); if (subjectData) { topicTitle = pureTitle; pureTitle = subjectData.name || pureTitle; charImageUrl = subjectData.imageUrl || ''; badgeLabel = subjectData.type || ''; } } } await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, charImageUrl, badgeLabel, topicTitle }); } function truncateHtml(html, limit) { const tmp = document.createElement('div'); tmp.innerHTML = html; let count = 0, done = false; const walk = (node) => { if (done) { node.remove(); return; } if (node.nodeType === 3) { const rem = limit - count; if (node.textContent.length > rem) { node.textContent = node.textContent.substring(0, rem) + '...'; done = true; } else { count += node.textContent.length; } return; } if (node.nodeType !== 1) return; [...node.childNodes].forEach(c => walk(c)); }; [...tmp.childNodes].forEach(c => walk(c)); return tmp.innerHTML; } function extractPostInfo(postEl, contentWin) { const idNode = postEl.querySelector('.userInfo strong a') || postEl.querySelector('.userName a') || postEl.querySelector('strong a'); const username = idNode ? idNode.innerText.trim() : "未知用户"; const timeNode = postEl.querySelector('.post_actions.re_info small'); const time = timeNode ? (timeNode.innerText.match(/\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}/)?.[0] || "未知时间") : "未知时间"; const avatarBox = postEl.querySelector('.avatarReSize40') || postEl.querySelector('.avatarReSize32') || postEl.querySelector('.avatarSize48'); let avatarUrl = ""; if (avatarBox) { const bg = contentWin.getComputedStyle(avatarBox).backgroundImage; if (bg && bg !== 'none') avatarUrl = bg.replace(/url\(["']?([^"']+)["']?\)/, '$1'); if (!avatarUrl) { const m = (avatarBox.getAttribute('style') || '').match(/background-image:\s*url\(["']?([^"']+)["']?\)/); if (m) avatarUrl = m[1]; } } if (!avatarUrl) { const anyBg = postEl.querySelector('[style*="background-image"]'); if (anyBg) { const m = (anyBg.getAttribute('style') || '').match(/background-image:\s*url\(["']?([^"']+)["']?\)/); if (m) avatarUrl = m[1]; } } if (!avatarUrl) { const avatarImg = postEl.querySelector('a.avatar img') || postEl.querySelector('[href*="/user/"] img') || postEl.querySelector('img[src*="/pic/user"]'); if (avatarImg) avatarUrl = avatarImg.src; } const contentEl = postEl.querySelector('.reply_content .message') || postEl.querySelector('.cmt_sub_content') || postEl.querySelector('.topic_content'); const contentClone = contentEl ? contentEl.cloneNode(true) : null; contentClone?.querySelectorAll('.embed-play-btn, .embed-player-wrapper, iframe').forEach(el => el.remove()); const quoteEl = contentClone?.querySelector('.quote'); const savedQuoteHtml = quoteEl ? quoteEl.outerHTML : ''; const bodyClone = contentClone?.cloneNode(true); bodyClone?.querySelector('.quote')?.remove(); const bodyText = bodyClone?.innerText?.trim() || ""; const rawText = contentClone?.innerText?.trim() || ""; const truncated = bodyText.length > 150; const contentHtml = truncated ? savedQuoteHtml + truncateHtml(bodyClone?.innerHTML?.trim() || "", 150) : (contentEl?.innerHTML?.trim() || ""); return { username, time, avatarUrl, content: truncated ? rawText.substring(0, 150) + "..." : rawText, contentHtml }; } async function createReplyShareImage(replyEl, contentDoc = document) { const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = contentDoc.defaultView || window; // Main post (楼主) data const isBlog = /\/blog\/\d+/.test(contentWin.location.pathname); const isEpisode = /\/ep\/\d+/.test(contentWin.location.pathname); const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(contentWin.location.pathname); const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(contentWin.location.pathname); const subjectTopicId = getSubjectIdFromTopicPage(contentWin.location.pathname, contentDoc); const isSubjectTopic = !!subjectTopicId; let username, postTime, avatarUrl, contentEl; if (isEpisode || isCharacter || isPerson) { username = ""; postTime = ""; avatarUrl = ""; contentEl = null; } else if (isBlog) { const authorLink = contentDoc.querySelector('.author.user-card .title p a') || contentDoc.querySelector('.author.user-card a.avatar'); username = authorLink ? authorLink.textContent.trim() : "未知用户"; const timeEl = contentDoc.querySelector('.header .tools .time'); postTime = timeEl ? (timeEl.innerText.match(/\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}/)?.[0] || "未知时间") : "未知时间"; const avatarImg = contentDoc.querySelector('.author.user-card a.avatar img'); avatarUrl = avatarImg ? avatarImg.src : ""; contentEl = contentDoc.querySelector('#entry_content'); } else { const firstPost = contentDoc.querySelector('.postTopic') || contentDoc.querySelector('[id^="post_"]'); const mainIdNode = firstPost?.querySelector('strong a') || firstPost?.querySelector('.author strong a'); username = mainIdNode ? mainIdNode.innerText.trim() : "未知用户"; const mainTimeNode = firstPost?.querySelector('small'); postTime = mainTimeNode ? (mainTimeNode.innerText.match(/\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}/)?.[0] || "未知时间") : "未知时间"; const mainAvatarBox = firstPost?.querySelector('.avatarSize48'); avatarUrl = mainAvatarBox ? contentWin.getComputedStyle(mainAvatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : ""; contentEl = firstPost?.querySelector('.topic_content') || firstPost?.querySelector('.inner'); } // Build replies chain: if sub-reply, prepend the parent reply first const replies = []; const parentPost = replyEl.closest('.topic_sub_reply')?.closest('[id^="post_"]'); if (parentPost) { replies.push(extractPostInfo(parentPost, contentWin)); } replies.push(extractPostInfo(replyEl, contentWin)); const h1Node = contentDoc.querySelector('#pageHeader h1') || contentDoc.querySelector('h1.title') || contentDoc.querySelector('h1.nameSingle a') || contentDoc.querySelector('h1'); let pureTitle = ""; if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) pureTitle += n.textContent; }); pureTitle = pureTitle.replace(/[»\n]/g, '').trim(); if (!pureTitle) { const rawTitle = (contentDoc.title || '').split(/\s*[|//]\s*/)[0].trim(); pureTitle = rawTitle || "分享话题"; } let charImageUrl = (isCharacter || isPerson) ? extractCharImageUrl(contentDoc) : ''; let badgeLabel = ''; let topicTitle = ''; if (isCharacter) badgeLabel = '角色'; if (isPerson) { const subtitleEl = contentDoc.querySelector('h2.subtitle'); const subtitleText = subtitleEl ? subtitleEl.textContent.trim() : ''; badgeLabel = subtitleText.replace(/^[^::]*[::]\s*/, '').trim() || '人物'; } if ((isCharacter || isPerson) && (!charImageUrl || /\/rakuen\/topic\/(crt|prsn)\//.test(contentWin.location.pathname))) { const apiData = await getRakuenCharPersonData(contentWin.location.pathname); if (apiData) { if (!charImageUrl) charImageUrl = apiData.imageUrl; if (!badgeLabel || badgeLabel === '人物') badgeLabel = apiData.badgeLabel; } } if (isEpisode) { const epIdMatch = contentWin.location.pathname.match(/\/ep\/(\d+)/); if (epIdMatch) { const epData = await fetchEpisodeData(epIdMatch[1]); if (epData) { if (epData.episodeName) pureTitle = epData.episodeName; if (epData.subjectImageUrl) charImageUrl = epData.subjectImageUrl; badgeLabel = epData.epNumber ? `第${epData.epNumber}话` : '章节'; } } } if (isSubjectTopic) { const subjectData = await fetchSubjectDataById(subjectTopicId); if (subjectData) { topicTitle = pureTitle; pureTitle = subjectData.name || pureTitle; charImageUrl = subjectData.imageUrl || ''; badgeLabel = subjectData.type || ''; } } if (isBlog) { const blogSubjectIds = [...new Set( [...contentDoc.querySelectorAll('a[href]')] .map(a => (a.getAttribute('href') || '').match(/\/subject\/(\d+)$/)?.[1]) .filter(Boolean) )]; if (blogSubjectIds.length === 1) { const subjectData = await fetchSubjectDataById(blogSubjectIds[0]); if (subjectData) { topicTitle = pureTitle; pureTitle = subjectData.name || pureTitle; charImageUrl = subjectData.imageUrl || ''; badgeLabel = subjectData.type || ''; } } } await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, replies, replyId: replyEl.id, charImageUrl, badgeLabel, topicTitle }); } const REPLY_SHARE_BTN_CLASS = 'bgm-reply-share-btn'; const SHARE_SVG = ''; const insertReplyButtons = (targetDoc = document) => { targetDoc.querySelectorAll('[id^="post_"]').forEach(post => { if (post.classList.contains('postTopic')) return; const reInfo = post.querySelector('.post_actions.re_info'); if (!reInfo || post.querySelector('.' + REPLY_SHARE_BTN_CLASS)) return; const wrap = targetDoc.createElement('span'); wrap.className = 'action'; wrap.innerHTML = `${SHARE_SVG}分享`; const dropdowns = reInfo.querySelectorAll('.action.dropdown'); const moreMenu = dropdowns[dropdowns.length - 1]; moreMenu ? reInfo.insertBefore(wrap, moreMenu) : reInfo.appendChild(wrap); wrap.querySelector('a').addEventListener('click', () => createReplyShareImage(post, targetDoc)); }); }; const insertButton = (targetDoc = document) => { if (targetDoc.getElementById('gen-card-btn')) return; const postActions = targetDoc.querySelector('.entry-actions .post_actions') || targetDoc.querySelector('.postTopic .post_actions:not(.re_info)') || targetDoc.querySelector('[id^="post_"] .post_actions:not(.re_info)') || [...targetDoc.querySelectorAll('.post_actions:not(.re_info)')].find(el => !el.closest('#sliderContainer') && !el.closest('#comment_box')); if (postActions) { const wrap = targetDoc.createElement('span'); wrap.className = 'action'; wrap.innerHTML = '分享'; postActions.appendChild(wrap); targetDoc.getElementById('gen-card-btn').addEventListener('click', () => createShareImage(targetDoc)); return; } // 降级:插入普通页面侧栏(仅非 Rakuen 场景) if (targetDoc === document) { const menuInner = document.querySelector('#columnInSubjectB .menu_inner') || document.querySelector('#columnSubjectB .menu_inner'); if (menuInner) { const br = document.createElement('br'); const btn = document.createElement('a'); btn.id = 'gen-card-btn'; btn.href = 'javascript:void(0);'; btn.className = 'l'; btn.textContent = '/ 分享'; menuInner.appendChild(br); menuInner.appendChild(btn); btn.addEventListener('click', () => createShareImage(document)); } } }; async function createMyCollectionShareImage(targetDoc) { const dark = targetDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = targetDoc.defaultView || window; const subjectIdMatch = contentWin.location.pathname.match(/\/subject\/(\d+)/); if (!subjectIdMatch) return; const subjectId = subjectIdMatch[1]; const commentText = (targetDoc.querySelector('textarea#comment')?.value || '').trim(); if (!commentText) return; const collectLink = targetDoc.querySelector('.shareBtn a[href*="/user/"]'); const userSlug = collectLink?.getAttribute('href').match(/\/user\/([^\/\?]+)/)?.[1] || ''; let avatarUrl = ''; let username = userSlug; if (userSlug) { const userInfo = await fetchBangumiAPI(`users/${userSlug}`); if (userInfo) { avatarUrl = userInfo.avatar?.medium || userInfo.avatar?.large || userInfo.avatar?.small || ''; username = userInfo.nickname || userInfo.username || userSlug; } } const ratedInput = targetDoc.querySelector('#panelInterestWrapper input[name="rate"]:checked'); const rating = ratedInput ? parseInt(ratedInput.value) : 0; const timeEl = targetDoc.querySelector('#panelInterestWrapper p.tip'); const timeText = timeEl ? (timeEl.textContent.match(/\d{4}-\d+-\d+\s+\d+:\d+/)?.[0] || '') : ''; const interestNow = targetDoc.querySelector('.interest_now')?.textContent.trim() || ''; const statusKeys = ['看过', '在看', '想看', '玩过', '在玩', '想玩', '读过', '在读', '想读', '听过', '在听', '想听', '搁置', '抛弃']; const collectionStatus = statusKeys.find(k => interestNow.includes(k)) || ''; const subjectData = await fetchSubjectDataById(subjectId); const pureTitle = subjectData?.name || targetDoc.title?.split(/\s*[|//]\s*/)[0].trim() || '作品'; const charImageUrl = subjectData?.imageUrl || ''; const badgeLabel = subjectData?.type || '作品'; const domTagsRaw = (targetDoc.querySelector('#collectBoxForm input[name="tags"]')?.value || '').trim(); const domTags = domTagsRaw ? domTagsRaw.split(/[\s,,]+/).map(t => t.trim()).filter(Boolean) : []; const infoboxTags = (SUBJECT_INFOBOX_KEYS[subjectData?.typeNum] || []) .map(key => getInfoboxValue(subjectData?.infobox, key)) .filter(Boolean); const overrideTags = domTags.length > 0 ? [collectionStatus, ...domTags].filter(Boolean) : [collectionStatus, ...infoboxTags].filter(Boolean); const truncated = commentText.length > 150; const contentHtml = truncated ? commentText.substring(0, 150) + '...' : commentText; await _doShareCard({ username: '', postTime: '', avatarUrl: '', contentEl: null, pureTitle, contentDoc: targetDoc, contentWin, dark, replies: [{ username, time: timeText, avatarUrl, rating, content: contentHtml, contentHtml }], charImageUrl, badgeLabel, overrideTags }); } const insertMyCollectionShareButton = (targetDoc = document) => { if (targetDoc.getElementById('bgm-my-collection-share-btn')) return; const shareBtn = targetDoc.querySelector('.shareBtn'); if (!shareBtn) return; const commentText = (targetDoc.querySelector('textarea#comment')?.value || '').trim(); if (!commentText) return; shareBtn.querySelector('.share_pasteboard')?.closest('.action')?.remove(); shareBtn.querySelector('.shareText')?.remove(); shareBtn.querySelectorAll('a.share').forEach(el => el.remove()); const wrap = targetDoc.createElement('span'); wrap.className = 'action'; wrap.innerHTML = `${SHARE_SVG}分享`; shareBtn.appendChild(wrap); wrap.querySelector('a').addEventListener('click', () => createMyCollectionShareImage(targetDoc)); }; const SUBJECT_COMMENT_SHARE_BTN_CLASS = 'bgm-subject-comment-share-btn'; const insertSubjectCommentButtons = (targetDoc = document) => { targetDoc.querySelectorAll('#comment_box .item.clearit').forEach(item => { if (item.querySelector('.' + SUBJECT_COMMENT_SHARE_BTN_CLASS)) return; const postActions = item.querySelector('.post_actions'); if (!postActions) return; const wrap = targetDoc.createElement('span'); wrap.className = 'action'; wrap.innerHTML = `${SHARE_SVG}分享`; postActions.appendChild(wrap); wrap.querySelector('a').addEventListener('click', () => createSubjectCommentShareImage(item, targetDoc)); }); }; const COLLECTION_PAGE_SHARE_BTN_CLASS = 'bgm-collection-page-share-btn'; const insertCollectionPageButtons = (targetDoc = document) => { if (!Object.keys(COLLECTION_PAGE_STATUS).some(k => new RegExp(`/subject/\\d+/${k}`).test((targetDoc.defaultView || window).location.pathname) )) return; targetDoc.querySelectorAll('#memberUserList li.user').forEach(item => { if (item.querySelector('.' + COLLECTION_PAGE_SHARE_BTN_CLASS)) return; const container = item.querySelector('.userContainer'); if (!container) return; let hasComment = false; let afterInfo = false; container.childNodes.forEach(n => { if (n.nodeType === 1 && n.tagName === 'P' && n.classList.contains('info')) { afterInfo = true; return; } if (afterInfo && n.nodeType === 3 && n.textContent.trim()) hasComment = true; }); if (!hasComment) return; const infoEl = item.querySelector('p.info'); if (!infoEl) return; const btn = targetDoc.createElement('a'); btn.href = 'javascript:void(0);'; btn.className = COLLECTION_PAGE_SHARE_BTN_CLASS + ' icon'; btn.title = '分享吐槽'; btn.style.cssText = 'display:inline-flex;align-items:center;gap:4px;margin-left:8px;font-size:12px;'; btn.innerHTML = SHARE_SVG + '分享'; infoEl.appendChild(btn); btn.addEventListener('click', () => createCollectionPageShareImage(item, targetDoc)); }); }; const BROWSER_LIST_CATEGORY_STATUS = { anime: { do: '在看', collect: '看过', wish: '想看', on_hold: '搁置', dropped: '抛弃' }, music: { do: '在听', collect: '听过', wish: '想听', on_hold: '搁置', dropped: '抛弃' }, game: { do: '在玩', collect: '玩过', wish: '想玩', on_hold: '搁置', dropped: '抛弃' }, real: { do: '在看', collect: '看过', wish: '想看', on_hold: '搁置', dropped: '抛弃' }, book: { do: '在读', collect: '读过', wish: '想读', on_hold: '搁置', dropped: '抛弃' } }; function extractBrowserListItemInfo(itemEl, contentWin) { const subjectId = itemEl.id?.replace('item_', '') || ''; const ratingMatch = itemEl.querySelector('.starlight')?.className.match(/stars(\d+)/); const rating = ratingMatch ? parseInt(ratingMatch[1]) : 0; const time = itemEl.querySelector('.tip_j')?.textContent.trim() || ''; const tagsRaw = itemEl.querySelector('p.collectInfo .tip')?.textContent.replace(/^[^::]*[::]\s*/, '').trim() || ''; const domTags = tagsRaw ? tagsRaw.split(/\s+/).filter(Boolean) : []; const commentEl = itemEl.querySelector('#comment_box .text'); const rawText = commentEl?.textContent.trim() || ''; const truncated = rawText.length > 150; const contentHtml = truncated ? rawText.substring(0, 150) + '...' : rawText; return { subjectId, rating, time, domTags, content: contentHtml, contentHtml }; } async function createBrowserListShareImage(itemEl, contentDoc = document) { const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = contentDoc.defaultView || window; const m = contentWin.location.pathname.match(/\/(anime|music|game|real|book)\/list\/([^\/]+)\/(do|collect|wish|on_hold|dropped)/); if (!m) return; const [, category, userSlug, statusKey] = m; const collectionStatus = BROWSER_LIST_CATEGORY_STATUS[category]?.[statusKey] || ''; const itemInfo = extractBrowserListItemInfo(itemEl, contentWin); if (!itemInfo.subjectId) return; const [subjectData, userInfo] = await Promise.all([ fetchSubjectDataById(itemInfo.subjectId), fetchBangumiAPI(`users/${userSlug}`) ]); const categoryTypeMap = { anime: '动画', music: '音乐', game: '游戏', real: '三次元', book: '书籍' }; let pureTitle = subjectData?.name || ''; let charImageUrl = subjectData?.imageUrl || ''; let badgeLabel = subjectData?.type || ''; if (!subjectData) { _shareNotice = 'NSFW 条目 API 无权访问,已降级为页面数据,部分信息可能不完整'; pureTitle = itemEl.querySelector('h3 a.l')?.textContent.trim() || ''; const coverImg = itemEl.querySelector('.subjectCover img'); charImageUrl = coverImg?.src || coverImg?.dataset.src || ''; badgeLabel = categoryTypeMap[category] || '作品'; } if (!badgeLabel) badgeLabel = categoryTypeMap[category] || '作品'; let username = userInfo?.nickname || userInfo?.username || userSlug; let avatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || ''; let userTags = null; if (userSlug) { const collection = await fetchBangumiAPI(`users/${userSlug}/collections/${itemInfo.subjectId}`); if (collection) { if (!itemInfo.rating) itemInfo.rating = collection.rate || 0; if (!itemInfo.time && collection.updated_at) { const d = new Date(collection.updated_at); const pad = n => String(n).padStart(2, '0'); itemInfo.time = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } if (collection.tags?.length > 0) userTags = collection.tags; } } if (!itemInfo.time) itemInfo.time = '未知时间'; const infoboxTags = (SUBJECT_INFOBOX_KEYS[subjectData?.typeNum] || []) .map(key => getInfoboxValue(subjectData?.infobox, key)) .filter(Boolean); const effectiveTags = userTags || (itemInfo.domTags.length > 0 ? itemInfo.domTags : null); const overrideTags = effectiveTags ? [collectionStatus, ...effectiveTags].filter(Boolean) : [collectionStatus, ...infoboxTags].filter(Boolean); await _doShareCard({ username: '', postTime: '', avatarUrl: '', contentEl: null, pureTitle, contentDoc, contentWin, dark, replies: [{ username, time: itemInfo.time, avatarUrl, rating: itemInfo.rating, content: itemInfo.content, contentHtml: itemInfo.contentHtml }], charImageUrl, badgeLabel, overrideTags }); } const BROWSER_LIST_SHARE_BTN_CLASS = 'bgm-browser-list-share-btn'; const insertBrowserListButtons = (targetDoc = document) => { const pathname = (targetDoc.defaultView || window).location.pathname; if (!/\/(anime|music|game|real|book)\/list\/[^\/]+\/(do|collect|wish|on_hold|dropped)/.test(pathname)) return; targetDoc.querySelectorAll('#browserItemList li.item').forEach(item => { if (item.querySelector('.' + BROWSER_LIST_SHARE_BTN_CLASS)) return; const commentEl = item.querySelector('#comment_box .text'); if (!commentEl?.textContent.trim()) return; const modifyP = item.querySelector('p.collectModify'); if (!modifyP) return; modifyP.appendChild(targetDoc.createTextNode(' | ')); const btn = targetDoc.createElement('a'); btn.href = 'javascript:void(0);'; btn.className = BROWSER_LIST_SHARE_BTN_CLASS + ' l'; btn.style.cssText = 'display:inline-flex;align-items:center;gap:3px;vertical-align:middle;'; btn.innerHTML = SHARE_SVG + '分享'; modifyP.appendChild(btn); btn.addEventListener('click', () => createBrowserListShareImage(item, targetDoc)); }); }; function injectShareSettingsTab() { const panel = document.getElementById('customize-panel'); if (!panel || panel.querySelector('[data-tab="bgm-topic-share"]')) return; const tabList = panel.querySelector('.panel-tabs .scrollable'); const contentArea = panel.querySelector('.content'); if (!tabList || !contentArea) return; tabList.style.overflowX = 'auto'; tabList.style.scrollbarWidth = 'thin'; tabList.style.flexWrap = 'nowrap'; const tabLi = document.createElement('li'); tabLi.innerHTML = '番组分享'; tabList.appendChild(tabLi); const token = getBgmToken(); const aiUrl = GM_getValue('bgm_share_ai_url', ''); const aiKey = GM_getValue('bgm_share_ai_key', ''); const aiModel = GM_getValue('bgm_share_ai_model', '') || 'gpt-3.5-turbo'; const tabDiv = document.createElement('div'); tabDiv.className = 'tab-content'; tabDiv.id = 'bgm-topic-share-tab'; tabDiv.innerHTML = `
Bangumi 授权(解锁 NSFW 数据)
${token ? '✓ 已授权,API 请求将携带 Token' : '未授权 - NSFW 条目数据将降级为 DOM 抓取'}
立即授权 撤销授权
AI 标签配置(可选)
保存
`; contentArea.appendChild(tabDiv); const tabLink = tabLi.querySelector('a'); tabLink.addEventListener('click', () => { panel.querySelectorAll('.tab-item').forEach(t => t.classList.remove('focus')); tabLink.classList.add('focus'); panel.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); tabDiv.classList.add('active'); }); tabDiv.querySelector('#bgm-share-auth-btn').addEventListener('click', startBgmOAuth); tabDiv.querySelector('#bgm-share-deauth-btn').addEventListener('click', () => { clearBgmToken(); tabDiv.querySelector('#bgm-share-auth-status').textContent = '未授权 - NSFW 条目数据将降级为 DOM 抓取'; tabDiv.querySelector('#bgm-share-auth-status').style.color = '#aaa'; tabDiv.querySelector('#bgm-share-auth-btn').style.display = ''; tabDiv.querySelector('#bgm-share-deauth-btn').style.display = 'none'; }); tabDiv.querySelector('#bgm-share-ai-save').addEventListener('click', () => { GM_setValue('bgm_share_ai_url', tabDiv.querySelector('#bgm-share-ai-url').value); GM_setValue('bgm_share_ai_key', tabDiv.querySelector('#bgm-share-ai-key').value); GM_setValue('bgm_share_ai_model', tabDiv.querySelector('#bgm-share-ai-model').value); const s = tabDiv.querySelector('#bgm-share-ai-saved'); s.style.display = ''; setTimeout(() => s.style.display = 'none', 2000); }); } const observeReplies = (targetDoc) => { const observer = new MutationObserver(() => { insertReplyButtons(targetDoc); insertSubjectCommentButtons(targetDoc); }); const root = targetDoc.getElementById('comment_list') || targetDoc.getElementById('reply_list') || targetDoc.getElementById('comment_box') || targetDoc.body; if (root) observer.observe(root, { childList: true, subtree: true }); }; // 超展开:在外层页面监听 #right iframe 导航,注入按钮 const rightFrame = document.getElementById('right'); if (rightFrame && rightFrame.tagName === 'IFRAME') { const onRightFrameLoad = () => { setTimeout(() => { try { const iDoc = rightFrame.contentDocument; const iUrl = rightFrame.contentWindow.location.href; if (/\/(group\/topic|subject(?:\/\d+)?\/topic)\//.test(iUrl) || /\/blog\/\d+/.test(iUrl) || /\/ep\/\d+/.test(iUrl) || /\/character\/\d+/.test(iUrl) || /\/person\/\d+/.test(iUrl)) { insertButton(iDoc); insertReplyButtons(iDoc); observeReplies(iDoc); } } catch (e) {} }, 800); }; rightFrame.addEventListener('load', onRightFrameLoad); } else { setTimeout(() => { insertButton(); insertReplyButtons(); insertSubjectCommentButtons(); insertCollectionPageButtons(); insertBrowserListButtons(); insertMyCollectionShareButton(); observeReplies(document); }, 500); } checkOAuthCallback(); refreshBgmTokenIfNeeded(); if (document.getElementById('customize-panel')) { injectShareSettingsTab(); } else { const _panelObs = new MutationObserver(() => { if (document.getElementById('customize-panel')) { _panelObs.disconnect(); injectShareSettingsTab(); } }); _panelObs.observe(document.body, { childList: true, subtree: true }); } })();