diff --git a/js/Bangumi_Topic_Share.js b/js/Bangumi_Topic_Share.js
index b70482c..205733c 100644
--- a/js/Bangumi_Topic_Share.js
+++ b/js/Bangumi_Topic_Share.js
@@ -1,19 +1,13 @@
// ==UserScript==
// @name Bangumi Topic Share
// @namespace http://tampermonkey.net/
-// @version 5.4
+// @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/subject/*/topic/*
-// @match *://bangumi.tv/subject/*/topic/*
-// @match *://chii.in/subject/*/topic/*
-// @match *://bgm.tv/subject/topic/*
-// @match *://bangumi.tv/subject/topic/*
-// @match *://chii.in/subject/topic/*
// @match *://bgm.tv/blog/*
// @match *://bangumi.tv/blog/*
// @match *://chii.in/blog/*
@@ -26,10 +20,34 @@
// @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
@@ -38,13 +56,109 @@
(function() {
'use strict';
- // ================= 配置区 =================
- const AI_CONFIG = {
- apiUrl: "在此处填入你的_API_URL",
- apiKey: "在此处填入你的_API_KEY",
- model: "gpt-3.5-turbo",
+ 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 = `
@@ -67,6 +181,7 @@
.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; }
@@ -113,6 +228,7 @@
.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; }
@@ -123,7 +239,7 @@
.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; background-clip: padding-box; padding: 0 5px; position: relative; transition: color 0.5s linear; }
+ [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);
@@ -147,14 +263,35 @@
if (!html) return html;
const div = document.createElement('div');
div.innerHTML = html;
- div.querySelectorAll('.embed-play-btn, .embed-player-wrapper, iframe').forEach(el => el.remove());
+ 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'; });
- const imgs = [...div.querySelectorAll('img')];
+ 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]; });
@@ -169,6 +306,15 @@
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;
@@ -185,7 +331,9 @@
.map(a => a.textContent.trim())
)];
const replyCount = contentDoc.querySelectorAll('[id^="post_"]').length;
- return [...subjectNames, `${replyCount} 回复`, '日志'];
+ // 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;
@@ -255,13 +403,16 @@
}
async function getAITags(title, content, contentDoc) {
- if (!AI_CONFIG.apiKey || AI_CONFIG.apiKey.includes("填入")) return getPageTags(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: AI_CONFIG.apiUrl,
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${AI_CONFIG.apiKey}` },
- data: JSON.stringify({ model: AI_CONFIG.model, messages: [{ role: "user", content: prompt }], temperature: 0.5 }),
+ 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);
@@ -276,7 +427,8 @@
// 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 = '' }) {
+ replies = [], replyId = '', charImageUrl = '', badgeLabel = '', overrideTags = null, topicTitle = '' }) {
+ if (document.getElementById('bgm-share-overlay')) return;
if (typeof html2canvas === 'undefined') {
alert("截图库加载失败,请刷新页面或检查网络。");
return;
@@ -301,17 +453,14 @@
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 ? (fullContent.substring(0, lim) + "...") : fullHtml;
+ displayContentHtml = fullContent.length > lim ? truncateHtml(fullHtml, lim) : fullHtml;
}
- const mainLimit = replies.length > 0 ? 200 : 300;
- let displayContent = fullContent.length > mainLimit ? fullContent.substring(0, mainLimit) + "..." : fullContent;
-
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([
- getAITags(pureTitle, fullContent, contentDoc),
+ 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' : ''}`),
@@ -330,18 +479,20 @@
if (idx >= replies.length) return '';
const r = replies[idx];
const b64 = base64ReplyAvatars[idx];
- const avatarSize = Math.max(22, 28 - idx * 4);
+ const avatarSize = Math.max(24, 36 - idx * 4);
+ const suppressGap = base64CharImage && !topicTitle && !inlinedMainContent && !username;
const topStyle = idx === 0
- ? `margin-top:${base64CharImage ? '6' : '14'}px;padding-top:${base64CharImage ? '6' : '14'}px;${hasMainContent ? `border-top:1px solid ${divider};` : ''}`
+ ? `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 ? `
+
+ ${notice ? `
⚠ ${notice}
` : ''}
- ${base64CharImage ? `
${pureTitle}
${badgeLabel}
` : badgeLabel ? `
${pureTitle}
${badgeLabel}
` : ''}
+ ${base64CharImage ? `
${pureTitle}
${badgeLabel}
` : badgeLabel ? `
${pureTitle}
${badgeLabel}
` : ''}
${username ? `` : ''}
- ${base64CharImage ? '' : `
${pureTitle}
`}
+ ${(base64CharImage || badgeLabel) ? '' : `
${pureTitle}
`}
+ ${topicTitle ? `
${topicTitle}
` : ''}
${inlinedMainContent ? `
` : ''}
${replySection}
${tagsHtml}
@@ -379,6 +535,7 @@
+
`;
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;
@@ -497,13 +659,65 @@
const linkColor = rawLinkColor && rawLinkColor !== 'rgba(0, 0, 0, 0)' && rawLinkColor !== 'transparent'
? rawLinkColor : (dark ? '#8ec8e8' : '#0066cc');
const sampleLinkDecoration = sampleLink ? getComputedStyle(sampleLink).textDecorationLine : 'none';
- iStyle.textContent = style.innerHTML + maskCss + ` a { color: ${linkColor}; text-decoration: ${sampleLinkDecoration}; }`;
+ 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
@@ -612,12 +826,17 @@
}
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' },
- onload: res => { try { resolve(JSON.parse(res.responseText)); } catch { resolve(null); } },
+ 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)
});
});
@@ -640,20 +859,273 @@
const _episodeDataCache = {};
async function fetchEpisodeData(episodeId) {
- if (_episodeDataCache[episodeId]) return _episodeDataCache[episodeId];
+ if (_episodeDataCache[episodeId]) {
+ if (_episodeDataCache[episodeId]._domFallback) _shareNotice = 'NSFW 条目 API 无权访问,已降级为页面数据,部分信息可能不完整';
+ return _episodeDataCache[episodeId];
+ }
const ep = await fetchBangumiAPI(`episodes/${episodeId}`);
- if (!ep) return null;
+ 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: subject?.name_cn || subject?.name || '',
- subjectImageUrl: subject?.images?.common || subject?.images?.medium || '',
- epNumber: ep.ep
+ 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;
@@ -662,6 +1134,8 @@
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) {
@@ -681,10 +1155,9 @@
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 masterPost = contentDoc.querySelector('.postTopic') || contentDoc.querySelector('[id^="post_"]');
- const avatarBox = masterPost?.querySelector('.avatarSize48');
+ const avatarBox = firstPost?.querySelector('.avatarSize48');
avatarUrl = avatarBox ? contentWin.getComputedStyle(avatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : "";
- contentEl = masterPost?.querySelector('.topic_content') || masterPost?.querySelector('.inner');
+ 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');
@@ -698,6 +1171,7 @@
let charImageUrl = (isCharacter || isPerson) ? extractCharImageUrl(contentDoc) : '';
let badgeLabel = '';
+ let topicTitle = '';
if (isCharacter) badgeLabel = '角色';
if (isPerson) {
const subtitleEl = contentDoc.querySelector('h2.subtitle');
@@ -722,7 +1196,53 @@
}
}
}
- await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, charImageUrl, badgeLabel });
+ 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) {
@@ -754,9 +1274,16 @@
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 = rawText.length > 150;
- const contentHtml = truncated ? (rawText.substring(0, 150) + "...") : (contentEl?.innerHTML?.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 };
}
@@ -769,6 +1296,8 @@
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;
@@ -810,6 +1339,7 @@
let charImageUrl = (isCharacter || isPerson) ? extractCharImageUrl(contentDoc) : '';
let badgeLabel = '';
+ let topicTitle = '';
if (isCharacter) badgeLabel = '角色';
if (isPerson) {
const subtitleEl = contentDoc.querySelector('h2.subtitle');
@@ -834,8 +1364,33 @@
}
}
}
+ 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 });
+ replies, replyId: replyEl.id, charImageUrl, badgeLabel, topicTitle });
}
const REPLY_SHARE_BTN_CLASS = 'bgm-reply-share-btn';
@@ -843,6 +1398,7 @@
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');
@@ -861,7 +1417,7 @@
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'));
+ || [...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';
@@ -889,9 +1445,307 @@
}
};
+ 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 = ``;
+ 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 抓取'}
+
+
+
+
+
`;
+ 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));
- const root = targetDoc.getElementById('comment_list') || targetDoc.getElementById('reply_list') || targetDoc.body;
+ 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 });
};
@@ -913,6 +1767,21 @@
};
rightFrame.addEventListener('load', onRightFrameLoad);
} else {
- setTimeout(() => { insertButton(); insertReplyButtons(); observeReplies(document); }, 500);
+ 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 });
}
})();