c0d4d34b41
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2466 lines
136 KiB
JavaScript
2466 lines
136 KiB
JavaScript
// ==UserScript==
|
||
// @name Bangumi Topic Share
|
||
// @namespace http://tampermonkey.net/
|
||
// @version 6.2
|
||
// @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/user/*/timeline/status/*
|
||
// @match *://bangumi.tv/user/*/timeline/status/*
|
||
// @match *://chii.in/user/*/timeline/status/*
|
||
// @match *://bgm.tv/user/*/timeline
|
||
// @match *://bangumi.tv/user/*/timeline
|
||
// @match *://chii.in/user/*/timeline
|
||
// @match *://bgm.tv/timeline
|
||
// @match *://bangumi.tv/timeline
|
||
// @match *://chii.in/timeline
|
||
// @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 = () => {
|
||
const w = 600, h = 600;
|
||
const left = Math.round(window.screenX + (window.outerWidth - w) / 2);
|
||
const top = Math.round(window.screenY + (window.outerHeight - h) / 2);
|
||
window.open(
|
||
`https://bgm.tv/oauth/authorize?client_id=${BGM_CLIENT_ID}&response_type=code&redirect_uri=${encodeURIComponent(BGM_REDIRECT_URI)}`,
|
||
'bgm_oauth',
|
||
`width=${w},height=${h},left=${left},top=${top},popup=1`
|
||
);
|
||
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; font-kerning: none; }
|
||
.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 <br> 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 = '', noCardTitle = false, shareTitle = '' }) {
|
||
if (document.getElementById('bgm-share-overlay')) return;
|
||
if (typeof html2canvas === 'undefined') {
|
||
alert("截图库加载失败,请刷新页面或检查网络。");
|
||
return;
|
||
}
|
||
|
||
const loading = document.createElement('div');
|
||
loading.innerHTML = '<div id="bgm-share-overlay" style="display:flex"><div id="loading-info">AI 正在提炼标签...</div></div>';
|
||
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('!') ? `<span class="tag-item">${tag.slice(1)}</span>` : `<span class="tag-item"># ${tag}</span>`).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 ? `<div style="margin-top:10px;">${renderLevel(idx + 1)}</div>` : '';
|
||
return `<div style="${topStyle}">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||
<img src="${b64}" style="width:${avatarSize}px;height:${avatarSize}px;border-radius:${idx === 0 ? '9' : '7'}px;object-fit:cover;border:1px solid ${dark ? '#444' : '#f0f0f0'};">
|
||
<span style="font-weight:bold;color:#F09199;font-size:${15 - idx}px;">${r.username}</span>
|
||
${r.rating ? `<span style="color:${dark ? '#f5c842' : '#e6a800'};font-size:${12 - idx}px;font-weight:bold;">★ ${r.rating}</span>` : ''}
|
||
<span style="color:#aaa;font-size:${12 - idx}px;">${r.time}</span>
|
||
</div>
|
||
<div class="content-box"><div class="content-text" style="font-size:${14 - idx}px;">${inlinedReplyContents[idx]}</div></div>
|
||
${inner}
|
||
</div>`;
|
||
};
|
||
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 = `
|
||
<div style="display:flex; flex-direction:column; align-items:center; margin: auto; padding: 30px 0 50px;">
|
||
<div style="position:relative;">
|
||
${notice ? `<div id="bgm-share-notice" style="position:absolute;bottom:calc(100% + 14px);left:50%;transform:translateX(-50%);background:rgba(30,30,30,0.9);border:1px solid #F09199;color:#F09199;padding:10px 20px;border-radius:10px;font-size:13px;white-space:nowrap;opacity:0;transition:opacity 0.4s;pointer-events:none;">⚠ ${notice}</div>` : ''}
|
||
<div id="capture-area" style="padding: 4px; background: transparent;">
|
||
<div class="share-card${dark ? ' dark' : ''}">
|
||
<div class="card-top-bar"></div>
|
||
${base64CharImage ? `<div style="display:flex;align-items:stretch;"><div style="flex:1;padding:22px 20px 14px 25px;display:flex;flex-direction:column;justify-content:center;min-height:150px;position:relative;"><div style="font-size:22px;font-weight:800;color:${dark ? '#f0f0f0' : '#111'};line-height:1.3;">${pureTitle}</div><div style="margin-top:10px;"><span style="background:${dark ? '#2a2a2a' : '#FEEFF0'};color:#F09199;font-size:11px;padding:3px 10px;border-radius:20px;font-weight:bold;border:1px solid ${dark ? '#F0919966' : '#F0919944'};">${badgeLabel}</span></div><div style="position:absolute;bottom:0;left:25px;right:0;height:1px;background:linear-gradient(to right,${dark ? 'rgba(255,255,255,0.15)' : '#ddd'} 0%,${dark ? 'rgba(255,255,255,0.15)' : '#ddd'} 60%,transparent 100%);"></div></div><div style="position:relative;width:130px;flex-shrink:0;min-height:150px;overflow:hidden;background-color:${dark ? '#2a2a2a' : '#f9f5f5'};"><div style="position:absolute;top:0;left:0;right:0;bottom:0;background-image:url('${base64CharImage}');background-size:cover;background-position:center top;background-repeat:no-repeat;"></div><div style="position:absolute;top:0;left:-1px;right:0;bottom:-1px;background:linear-gradient(to right,${dark ? '#1e1e1e' : '#fff'} 0%,${dark ? 'rgba(30,30,30,0)' : 'rgba(255,255,255,0)'} 20%),linear-gradient(to top,${dark ? '#1e1e1e' : '#fff'} 0%,${dark ? 'rgba(30,30,30,0)' : 'rgba(255,255,255,0)'} 20%);"></div></div></div>` : badgeLabel ? `<div style="padding:22px 25px 14px;position:relative;"><div style="font-size:22px;font-weight:800;color:${dark ? '#f0f0f0' : '#111'};line-height:1.3;">${pureTitle}</div><div style="margin-top:10px;"><span style="background:${dark ? '#2a2a2a' : '#FEEFF0'};color:#F09199;font-size:11px;padding:3px 10px;border-radius:20px;font-weight:bold;border:1px solid ${dark ? '#F0919966' : '#F0919944'};">${badgeLabel}</span></div><div style="position:absolute;bottom:0;left:25px;right:25px;height:1px;background:${dark ? 'rgba(255,255,255,0.1)' : '#eee'};"></div></div>` : ''}
|
||
${username ? `<div class="card-header" style="">
|
||
<img class="avatar-img" src="${base64Avatar}">
|
||
<div class="user-meta">
|
||
<span class="name">${username}</span>
|
||
<span class="time">${postTime}</span>
|
||
</div>
|
||
</div>` : ''}
|
||
<div class="card-body">
|
||
${(base64CharImage || badgeLabel || noCardTitle) ? '' : `<h1 class="main-title">${pureTitle}</h1>`}
|
||
${topicTitle ? `<h2 class="topic-sub-title">${topicTitle}</h2>` : ''}
|
||
${inlinedMainContent ? `<div class="content-box"><div class="content-text">${inlinedMainContent}</div></div>` : ''}
|
||
${replySection}
|
||
<div class="tags-container">${tagsHtml}</div>
|
||
</div>
|
||
<div class="card-footer">
|
||
<div style="text-align:left">
|
||
<div style="font-size:14px; font-weight:bold; color:#F09199">Bangumi 番组计划</div>
|
||
<div style="font-size:10px; color:${dark ? '#888' : '#aaa'}; margin-top:2px;">${displayUrl}</div>
|
||
</div>
|
||
<img class="qr-img" src="${base64QR}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="bgm-btn-row">
|
||
<button id="bgm-copy-btn" class="bgm-action-btn" disabled data-tip="复制图片">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||
</button>
|
||
<button id="bgm-download-btn" class="bgm-action-btn" disabled data-tip="下载图片">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
</button>
|
||
<button id="bgm-text-btn" class="bgm-action-btn" data-tip="复制文案">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||
</button>
|
||
<button id="bgm-mask-btn" class="bgm-action-btn" data-tip="显示剧透">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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();
|
||
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) {
|
||
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 = `【链接】${shareTitle || topicTitle || 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], .statusContent .text a[href], .subReply a.l[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} !important; 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';
|
||
});
|
||
|
||
// html2canvas undercounts advance widths for full-width CJK punctuation (《(【etc.)
|
||
// because canvas.measureText() returns only the glyph width, not the full advance.
|
||
// Wrapping each as display:inline-block with width:1em forces html2canvas to read
|
||
// the DOM layout box instead of measureText, giving the correct advance.
|
||
const CJK_PUNCT_RE = /[《》〈〉「」【】〔〕〖〗(){}]/;
|
||
const CJK_PUNCT_SPLIT_RE = /(《|》|〈|〉|「|」|【|】|〔|〕|〖|〗|(|)|{|})/u;
|
||
iDoc.querySelectorAll('.content-text').forEach(contentEl => {
|
||
const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT, null);
|
||
const textNodes = [];
|
||
let tn;
|
||
while (tn = walker.nextNode()) {
|
||
if (CJK_PUNCT_RE.test(tn.textContent)) textNodes.push(tn);
|
||
}
|
||
textNodes.forEach(tn => {
|
||
const parts = tn.textContent.split(CJK_PUNCT_SPLIT_RE);
|
||
if (parts.length <= 1) return;
|
||
const frag = iDoc.createDocumentFragment();
|
||
parts.forEach(part => {
|
||
if (!part) return;
|
||
if (CJK_PUNCT_RE.test(part) && part.length === 1) {
|
||
const sp = iDoc.createElement('span');
|
||
sp.style.cssText = 'display:inline-block;width:1em;text-align:center;';
|
||
sp.textContent = part;
|
||
frag.appendChild(sp);
|
||
} else {
|
||
frag.appendChild(iDoc.createTextNode(part));
|
||
}
|
||
});
|
||
tn.parentNode.replaceChild(frag, tn);
|
||
});
|
||
});
|
||
|
||
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 无权访问,已降级为页面数据,部分信息可能不完整';
|
||
let episodeName = '', epNumber = null;
|
||
const epH2 = document.querySelector('h2.title');
|
||
if (epH2) {
|
||
let rawText = '';
|
||
epH2.childNodes.forEach(n => { if (n.nodeType === 3) rawText += n.textContent; });
|
||
rawText = rawText.trim();
|
||
const epMatch = rawText.match(/^ep\.(\d+)\s*(.*)$/i);
|
||
if (epMatch) { epNumber = parseInt(epMatch[1]); episodeName = epMatch[2].trim() || rawText; }
|
||
else episodeName = rawText;
|
||
}
|
||
if (!episodeName) {
|
||
const h1Node = document.querySelector('#pageHeader h1') || document.querySelector('h1');
|
||
if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) episodeName += n.textContent; });
|
||
episodeName = episodeName.replace(/[»\n]/g, '').trim();
|
||
}
|
||
const subjectLink = document.querySelector('#headerSubject a[href*="/subject/"]')
|
||
|| document.querySelector('#pageHeader a[href*="/subject/"]')
|
||
|| document.querySelector('a.cover[href*="/subject/"]');
|
||
const subjectName = subjectLink?.textContent?.trim() || '';
|
||
const subjectImg = document.querySelector('img[src*="/pic/cover/l/"]') || document.querySelector('img[src*="/pic/cover/m/"]');
|
||
const subjectImageUrl = subjectImg?.src || '';
|
||
const result = { episodeName, subjectName, subjectImageUrl, epNumber, _domFallback: true };
|
||
_episodeDataCache[episodeId] = result;
|
||
return result;
|
||
}
|
||
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 = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></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 = `<a href="javascript:void(0);" class="${REPLY_SHARE_BTN_CLASS} icon" title="分享回复" style="display:inline-flex;align-items:center;gap:3px;">${SHARE_SVG}<span class="title">分享</span></a>`;
|
||
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') && !el.closest('#timeline') && !el.closest('.statusContent'));
|
||
if (postActions) {
|
||
const wrap = targetDoc.createElement('span');
|
||
wrap.className = 'action';
|
||
wrap.innerHTML = '<a href="javascript:void(0);" id="gen-card-btn" class="icon" title="分享话题" style="display:inline-flex;align-items:center;gap:3px;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg><span class="title">分享</span></a>';
|
||
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 = `<a href="javascript:void(0);" id="bgm-my-collection-share-btn" class="icon" title="分享我的吐槽" style="display:inline-flex;align-items:center;gap:3px;">${SHARE_SVG}<span class="title">分享</span></a>`;
|
||
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 = `<a href="javascript:void(0);" class="${SUBJECT_COMMENT_SHARE_BTN_CLASS} icon" title="分享吐槽" style="display:inline-flex;align-items:center;gap:3px;">${SHARE_SVG}<span class="title">分享</span></a>`;
|
||
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 + '<span class="title">分享</span>';
|
||
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 + '<span>分享</span>';
|
||
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 = '<a href="javascript:void(0);" class="tab-item" data-tab="bgm-topic-share">番组分享</a>';
|
||
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 = `
|
||
<div class="section">
|
||
<div class="title">Bangumi 授权(解锁 NSFW 数据)</div>
|
||
<div class="options-container" style="flex-direction:column;align-items:flex-start;gap:10px;padding-top:8px;">
|
||
<div id="bgm-share-auth-status" style="font-size:13px;color:${token ? '#4caf50' : '#aaa'};">
|
||
${token ? '✓ 已授权,API 请求将携带 Token' : '未授权 - NSFW 条目数据将降级为 DOM 抓取'}
|
||
</div>
|
||
<div style="display:flex;gap:8px;">
|
||
<a id="bgm-share-auth-btn" href="javascript:void(0);" class="btnPink" style="${token ? 'display:none' : ''}">立即授权</a>
|
||
<a id="bgm-share-deauth-btn" href="javascript:void(0);" class="btnGraySmall" style="${token ? '' : 'display:none'}">撤销授权</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="section">
|
||
<div class="title">AI 标签配置(可选)</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px;padding-top:8px;padding-right:10px;font-size:13px;">
|
||
<label>API URL<br><input id="bgm-share-ai-url" type="text" class="inputtext" style="width:100%;margin-top:4px;" value="${aiUrl}" placeholder="https://api.openai.com/v1/chat/completions"></label>
|
||
<label>API Key<br><input id="bgm-share-ai-key" type="password" class="inputtext" style="width:100%;margin-top:4px;" value="${aiKey}" placeholder="sk-..."></label>
|
||
<label>模型<br><input id="bgm-share-ai-model" type="text" class="inputtext" style="width:100%;margin-top:4px;" value="${aiModel}"></label>
|
||
<div style="display:flex;align-items:center;gap:10px;">
|
||
<a id="bgm-share-ai-save" href="javascript:void(0);" class="btnPink">保存</a>
|
||
<span id="bgm-share-ai-saved" style="color:#4caf50;display:none;font-size:12px;">✓ 已保存</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
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);
|
||
});
|
||
}
|
||
|
||
function _getStatusAuthorInfo(doc) {
|
||
const usernameEl = doc.querySelector('.statusHeader h3 a');
|
||
const username = usernameEl?.textContent.trim() || '';
|
||
const avatarSpan = doc.querySelector('.statusHeader .avatarNeue');
|
||
let avatarUrl = '';
|
||
const bgImg = avatarSpan?.style.backgroundImage;
|
||
if (bgImg) {
|
||
const m = bgImg.match(/url\(['"]?([^'"]+)['"]?\)/);
|
||
if (m) avatarUrl = m[1].replace(/^\/\//, 'https://');
|
||
}
|
||
const timeEl = doc.querySelector('.statusContent .post_actions .tip_j');
|
||
const postTime = timeEl?.textContent.trim() || '';
|
||
return { username, avatarUrl, postTime };
|
||
}
|
||
|
||
async function _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime) {
|
||
const statusContent = doc.querySelector('.statusContent');
|
||
const subjectLink = statusContent?.querySelector('a[data-subject-id]');
|
||
const subjectId = subjectLink?.getAttribute('data-subject-id');
|
||
const collectComment = statusContent?.querySelector('.comment');
|
||
if (!subjectId || !collectComment) return null;
|
||
|
||
const userSlug = contentWin.location.pathname.match(/\/user\/([^\/]+)\//)?.[1] || '';
|
||
let collectionStatus = '';
|
||
if (subjectLink) {
|
||
let node = subjectLink.previousSibling;
|
||
while (node) {
|
||
if (node.nodeType === 3 && node.textContent.trim()) { collectionStatus = node.textContent.trim().replace(/了$/, ''); break; }
|
||
node = node.previousSibling;
|
||
}
|
||
}
|
||
const ratingEl = collectComment.querySelector('.starstop-s .starlight');
|
||
const domRating = parseInt(ratingEl?.className.match(/stars(\d+)/)?.[1] || '0');
|
||
const cleanCommentHtml = (() => {
|
||
const tmp = document.createElement('div');
|
||
tmp.innerHTML = collectComment.innerHTML;
|
||
tmp.querySelector('.starstop-s')?.remove();
|
||
return tmp.innerHTML.trim();
|
||
})();
|
||
const subjectData = await fetchSubjectDataById(subjectId);
|
||
const subjectName = subjectData?.name || subjectLink?.textContent.trim() || '作品';
|
||
let rating = domRating, userTags = null, apiTime = postTime;
|
||
if (userSlug) {
|
||
const collection = await fetchBangumiAPI(`users/${userSlug}/collections/${subjectId}`);
|
||
if (collection) {
|
||
rating = collection.rate || rating;
|
||
if (collection.updated_at) {
|
||
const d = new Date(collection.updated_at);
|
||
const pad = n => String(n).padStart(2, '0');
|
||
apiTime = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||
}
|
||
if (collection.tags?.length > 0) userTags = collection.tags;
|
||
}
|
||
}
|
||
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);
|
||
return { subjectData, subjectName, collectComment, cleanCommentHtml, username, avatarUrl, apiTime, rating, overrideTags };
|
||
}
|
||
|
||
async function createStatusShareImage(doc = document) {
|
||
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||
const contentWin = doc.defaultView || window;
|
||
const { username, avatarUrl, postTime } = _getStatusAuthorInfo(doc);
|
||
const replyCount = doc.querySelectorAll('.subReply .reply_item').length;
|
||
|
||
const collectInfo = await _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime);
|
||
if (collectInfo) {
|
||
const { subjectData, subjectName, collectComment, cleanCommentHtml, apiTime, rating, overrideTags } = collectInfo;
|
||
await _doShareCard({
|
||
username: '', postTime: '', avatarUrl: '', contentEl: null,
|
||
pureTitle: subjectName,
|
||
shareTitle: `${username}的收藏 - ${subjectName}`,
|
||
contentDoc: doc, contentWin, dark,
|
||
replies: [{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating }],
|
||
charImageUrl: subjectData?.imageUrl || '',
|
||
badgeLabel: subjectData?.type || '作品',
|
||
overrideTags,
|
||
});
|
||
} else {
|
||
const contentEl = doc.querySelector('.statusContent .text');
|
||
await _doShareCard({
|
||
username, postTime, avatarUrl, contentEl,
|
||
pureTitle: username + '的吐槽', contentDoc: doc, contentWin, dark,
|
||
noCardTitle: true,
|
||
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||
});
|
||
}
|
||
}
|
||
|
||
async function createStatusReplyShareImage(replyLi, doc = document) {
|
||
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||
const contentWin = doc.defaultView || window;
|
||
const { username, avatarUrl, postTime } = _getStatusAuthorInfo(doc);
|
||
|
||
const replyUserEl = replyLi.querySelector('a.l[href*="/user/"]');
|
||
const replyUsername = replyUserEl?.textContent.trim() || '';
|
||
const replySlug = replyUserEl?.getAttribute('href')?.match(/\/user\/([^\/]+)/)?.[1] || '';
|
||
let replyAvatarUrl = '';
|
||
if (replySlug) {
|
||
const userInfo = await fetchBangumiAPI(`users/${replySlug}`);
|
||
replyAvatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||
}
|
||
let replyHtml = '';
|
||
let afterDash = false;
|
||
replyLi.childNodes.forEach(n => {
|
||
if (!afterDash) {
|
||
if (n.nodeType === 1 && n.tagName === 'SPAN' && n.classList.contains('tip_j')) afterDash = true;
|
||
return;
|
||
}
|
||
if (n.nodeType === 1 && n.classList?.contains(STATUS_REPLY_SHARE_BTN_CLASS)) return;
|
||
replyHtml += n.nodeType === 3 ? n.textContent : n.outerHTML;
|
||
});
|
||
|
||
const replyCount = doc.querySelectorAll('.subReply .reply_item').length;
|
||
const collectInfo = await _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime);
|
||
if (collectInfo) {
|
||
const { subjectData, subjectName, collectComment, cleanCommentHtml, apiTime, rating, overrideTags } = collectInfo;
|
||
await _doShareCard({
|
||
username: '', postTime: '', avatarUrl: '', contentEl: null,
|
||
pureTitle: subjectName,
|
||
shareTitle: `${username}的收藏 - ${subjectName}`,
|
||
contentDoc: doc, contentWin, dark,
|
||
replies: [
|
||
{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating },
|
||
{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' },
|
||
],
|
||
charImageUrl: subjectData?.imageUrl || '',
|
||
badgeLabel: subjectData?.type || '作品',
|
||
overrideTags,
|
||
});
|
||
} else {
|
||
const contentEl = doc.querySelector('.statusContent .text');
|
||
await _doShareCard({
|
||
username, postTime, avatarUrl, contentEl,
|
||
pureTitle: username + '的吐槽', contentDoc: doc, contentWin, dark,
|
||
noCardTitle: true,
|
||
replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
|
||
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||
});
|
||
}
|
||
}
|
||
|
||
const STATUS_REPLY_SHARE_BTN_CLASS = 'bgm-status-reply-share-btn';
|
||
|
||
const insertStatusReplyButtons = (doc = document) => {
|
||
if (!/\/user\/[^\/]+\/timeline\/status\/\d+/.test((doc.defaultView || window).location.pathname)) return;
|
||
doc.querySelectorAll('.subReply .reply_item').forEach(li => {
|
||
if (li.querySelector('.' + STATUS_REPLY_SHARE_BTN_CLASS)) return;
|
||
const btn = doc.createElement('a');
|
||
btn.href = 'javascript:void(0);';
|
||
btn.className = STATUS_REPLY_SHARE_BTN_CLASS;
|
||
btn.title = '分享回复';
|
||
btn.style.cssText = 'display:inline-flex;align-items:center;gap:3px;margin-left:6px;vertical-align:middle;opacity:0.6;';
|
||
btn.innerHTML = SHARE_SVG;
|
||
li.appendChild(btn);
|
||
btn.addEventListener('click', () => createStatusReplyShareImage(li, doc));
|
||
});
|
||
};
|
||
|
||
const insertStatusShareButton = (doc = document) => {
|
||
if (!/\/user\/[^\/]+\/timeline\/status\/\d+/.test((doc.defaultView || window).location.pathname)) return;
|
||
if (doc.getElementById('bgm-status-share-btn')) return;
|
||
const postActions = doc.querySelector('.statusContent .post_actions');
|
||
if (!postActions) return;
|
||
const btn = doc.createElement('a');
|
||
btn.id = 'bgm-status-share-btn';
|
||
btn.href = 'javascript:void(0);';
|
||
btn.title = '分享吐槽';
|
||
btn.style.cssText = 'color:inherit;display:inline-flex;align-items:center;margin-right:6px;opacity:0.75;';
|
||
btn.innerHTML = SHARE_SVG;
|
||
const dropdown = postActions.querySelector('.action.dropdown');
|
||
dropdown ? dropdown.after(btn) : postActions.appendChild(btn);
|
||
btn.addEventListener('click', () => createStatusShareImage(doc));
|
||
};
|
||
|
||
const TML_SHARE_BTN_CLASS = 'bgm-tml-share-btn';
|
||
|
||
function fetchStatusReplyCount(statusUrl) {
|
||
return new Promise(resolve => {
|
||
GM_xmlhttpRequest({
|
||
method: 'GET',
|
||
url: statusUrl,
|
||
onload: res => {
|
||
if (res.status !== 200) { resolve(0); return; }
|
||
const statusDoc = new DOMParser().parseFromString(res.responseText, 'text/html');
|
||
resolve(statusDoc.querySelectorAll('.subReply .reply_item').length);
|
||
},
|
||
onerror: () => resolve(0),
|
||
});
|
||
});
|
||
}
|
||
|
||
function extractTimelineVia(postActionsEl) {
|
||
if (!postActionsEl) return '';
|
||
const smallEl = postActionsEl.querySelector('small.grey');
|
||
if (smallEl) return smallEl.textContent.trim();
|
||
const titleTip = postActionsEl.querySelector('.titleTip');
|
||
let node = titleTip ? titleTip.nextSibling : null;
|
||
while (node) {
|
||
if (node.nodeType === 3) {
|
||
const text = node.textContent.replace(/[\u00B7·\s]/g, '').trim();
|
||
if (text) return text;
|
||
}
|
||
node = node.nextSibling;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
async function createTimelineShareImage(item, doc = document) {
|
||
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||
const contentWin = doc.defaultView || window;
|
||
let userSlug = item.getAttribute('data-item-user') || '';
|
||
if (!userSlug) {
|
||
// Own-user timeline has data-item-user="" — extract from tml_comment href or page URL
|
||
userSlug = item.querySelector('.tml_comment')?.href?.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||
|| (doc.defaultView || window).location.pathname.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||
|| '';
|
||
}
|
||
|
||
const userLink = item.querySelector('.info a.l[href*="/user/"], .info_full a.l[href*="/user/"]');
|
||
let username = userLink?.textContent.trim() || '';
|
||
|
||
const _extractNeueAvatar = el => {
|
||
if (!el) return '';
|
||
const m = el.style.backgroundImage?.match(/url\(['"]?([^'"]+)['"]?\)/);
|
||
return m ? m[1].replace(/^\/\//, 'https://') : '';
|
||
};
|
||
let avatarUrl = _extractNeueAvatar(item.querySelector('.avatar .avatarNeue'));
|
||
if (!avatarUrl && userSlug) {
|
||
const siblingNeue = doc.querySelector(`.tml_item[data-item-user="${CSS.escape(userSlug)}"] .avatar .avatarNeue`);
|
||
avatarUrl = _extractNeueAvatar(siblingNeue);
|
||
}
|
||
// For .info_full items (timeline owner's entries), there is no username link or avatar span —
|
||
// always fetch from API so username is authoritative (not a stale DOM fallback).
|
||
const isInfoFull = !!item.querySelector('.info_full');
|
||
if ((isInfoFull || !avatarUrl || !username) && userSlug) {
|
||
const userInfo = await fetchBangumiAPI(`users/${userSlug}`);
|
||
if (!avatarUrl) avatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||
if (isInfoFull || !username) username = userInfo?.nickname || userInfo?.username || username;
|
||
}
|
||
|
||
const timeTip = item.querySelector('.post_actions .titleTip');
|
||
const postTimeRaw = timeTip?.getAttribute('data-original-title') || timeTip?.textContent.trim() || '';
|
||
const via = extractTimelineVia(item.querySelector('.post_actions'));
|
||
const postTime = postTimeRaw + (via ? ` via ${via}` : '');
|
||
|
||
const statusP = item.querySelector('.info p.status, .info_full p.status');
|
||
const collectComment = item.querySelector('.collectInfo .comment');
|
||
const blogLink = item.querySelector('.info a[href*="/blog/"], .info_full a[href*="/blog/"]');
|
||
|
||
if (statusP && statusP.textContent.trim()) {
|
||
const statusUrl = item.querySelector('.tml_comment')?.href;
|
||
const replyCount = statusUrl ? await fetchStatusReplyCount(statusUrl) : 0;
|
||
await _doShareCard({
|
||
username, postTime, avatarUrl,
|
||
contentEl: statusP,
|
||
pureTitle: username + '的吐槽',
|
||
contentDoc: doc, contentWin, dark,
|
||
noCardTitle: true,
|
||
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||
});
|
||
} else if (collectComment && collectComment.textContent.trim()) {
|
||
// Subject link is in .info, carries data-subject-id attribute
|
||
const subjectLink = item.querySelector('a[data-subject-id]');
|
||
const subjectId = subjectLink?.getAttribute('data-subject-id');
|
||
|
||
// Collection status ("看过"/"在看" etc.) is in the text node just before the subject link
|
||
let collectionStatus = '';
|
||
if (subjectLink) {
|
||
let node = subjectLink.previousSibling;
|
||
while (node) {
|
||
if (node.nodeType === 3 && node.textContent.trim()) {
|
||
collectionStatus = node.textContent.trim().replace(/了$/, '');
|
||
break;
|
||
}
|
||
node = node.previousSibling;
|
||
}
|
||
}
|
||
|
||
// Extract user rating and clean comment HTML (remove star span)
|
||
const ratingEl = collectComment.querySelector('.starstop-s .starlight');
|
||
const domRating = parseInt(ratingEl?.className.match(/stars(\d+)/)?.[1] || '0');
|
||
const cleanCommentHtml = (() => {
|
||
const tmp = document.createElement('div');
|
||
tmp.innerHTML = collectComment.innerHTML;
|
||
tmp.querySelector('.starstop-s')?.remove();
|
||
return tmp.innerHTML.trim();
|
||
})();
|
||
|
||
const statusUrl = item.querySelector('.tml_comment')?.href;
|
||
const statusUrlParsed = statusUrl ? new URL(statusUrl) : null;
|
||
|
||
if (subjectId) {
|
||
const subjectData = await fetchSubjectDataById(subjectId);
|
||
const subjectName = subjectData?.name || subjectLink?.textContent.trim() || '作品';
|
||
|
||
let rating = domRating;
|
||
let userTags = null;
|
||
let apiTime = postTime;
|
||
|
||
if (userSlug) {
|
||
const collection = await fetchBangumiAPI(`users/${userSlug}/collections/${subjectId}`);
|
||
if (collection) {
|
||
rating = collection.rate || rating;
|
||
if (collection.updated_at) {
|
||
const d = new Date(collection.updated_at);
|
||
const pad = n => String(n).padStart(2, '0');
|
||
apiTime = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` + (via ? ` via ${via}` : '');
|
||
}
|
||
if (collection.tags?.length > 0) userTags = collection.tags;
|
||
}
|
||
}
|
||
|
||
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: subjectName,
|
||
shareTitle: `${username}的收藏 - ${subjectName}`,
|
||
contentDoc: doc,
|
||
contentWin: statusUrlParsed
|
||
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
|
||
: { location: { origin: contentWin.location.origin, pathname: `/subject/${subjectId}` } },
|
||
dark,
|
||
replies: [{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating }],
|
||
charImageUrl: subjectData?.imageUrl || '',
|
||
badgeLabel: subjectData?.type || '作品',
|
||
overrideTags,
|
||
});
|
||
} else {
|
||
await _doShareCard({
|
||
username, postTime, avatarUrl,
|
||
contentEl: collectComment,
|
||
pureTitle: username + '的收藏',
|
||
contentDoc: doc, contentWin, dark,
|
||
noCardTitle: true,
|
||
overrideTags: ['收藏'],
|
||
});
|
||
}
|
||
} else if (blogLink) {
|
||
const blogUrl = blogLink.href;
|
||
const blogHtml = await new Promise(resolve => {
|
||
GM_xmlhttpRequest({
|
||
method: 'GET', url: blogUrl,
|
||
onload: res => resolve(res.status === 200 ? res.responseText : null),
|
||
onerror: () => resolve(null),
|
||
});
|
||
});
|
||
const blogDoc = blogHtml ? new DOMParser().parseFromString(blogHtml, 'text/html') : null;
|
||
const entryContent = blogDoc?.querySelector('#entry_content');
|
||
let contentEl = null;
|
||
if (entryContent) {
|
||
contentEl = document.createElement('div');
|
||
contentEl.innerHTML = entryContent.innerHTML;
|
||
}
|
||
let pureTitle = blogLink.textContent.trim();
|
||
let topicTitle = '';
|
||
let charImageUrl = '';
|
||
let badgeLabel = '';
|
||
const blogSubjectIds = blogDoc ? [...new Set(
|
||
[...blogDoc.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 || '';
|
||
}
|
||
}
|
||
const blogReplyCount = blogDoc?.querySelectorAll('[id^="post_"]').length || 0;
|
||
const subjectNamesFromBlog = blogDoc ? [...new Set(
|
||
[...blogDoc.querySelectorAll('a')]
|
||
.filter(a => /\/subject\/\d+$/.test(a.getAttribute('href') || '') && a.textContent.trim())
|
||
.map(a => a.textContent.trim())
|
||
)] : [];
|
||
const tagSubjects = subjectNamesFromBlog.length === 1 ? [] : subjectNamesFromBlog;
|
||
const overrideTags = [...tagSubjects, `${blogReplyCount} 回复`, '日志'];
|
||
const blogUrlParsed = new URL(blogUrl);
|
||
await _doShareCard({
|
||
username, postTime, avatarUrl,
|
||
contentEl,
|
||
pureTitle,
|
||
contentDoc: doc,
|
||
contentWin: { location: { origin: blogUrlParsed.origin, pathname: blogUrlParsed.pathname } },
|
||
dark,
|
||
charImageUrl, badgeLabel, topicTitle,
|
||
overrideTags,
|
||
});
|
||
}
|
||
}
|
||
|
||
const TML_REPLY_SHARE_BTN_CLASS = 'bgm-tml-reply-share-btn';
|
||
|
||
async function createTimelineReplyShareImage(replyLi, tmlItem, doc = document) {
|
||
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||
const contentWin = doc.defaultView || window;
|
||
|
||
let userSlug = tmlItem.getAttribute('data-item-user') || '';
|
||
if (!userSlug) {
|
||
userSlug = tmlItem.querySelector('.tml_comment')?.href?.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||
|| (doc.defaultView || window).location.pathname.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||
|| '';
|
||
}
|
||
const userLink = tmlItem.querySelector('.info a.l[href*="/user/"], .info_full a.l[href*="/user/"]');
|
||
let username = userLink?.textContent.trim() || '';
|
||
const _extractNeueAvatar = el => {
|
||
if (!el) return '';
|
||
const m = el.style.backgroundImage?.match(/url\(['"]?([^'"]+)['"]?\)/);
|
||
return m ? m[1].replace(/^\/\//, 'https://') : '';
|
||
};
|
||
let avatarUrl = _extractNeueAvatar(tmlItem.querySelector('.avatar .avatarNeue'));
|
||
if (!avatarUrl && userSlug) {
|
||
const siblingNeue = doc.querySelector(`.tml_item[data-item-user="${CSS.escape(userSlug)}"] .avatar .avatarNeue`);
|
||
avatarUrl = _extractNeueAvatar(siblingNeue);
|
||
}
|
||
const isInfoFull = !!tmlItem.querySelector('.info_full');
|
||
if ((isInfoFull || !avatarUrl || !username) && userSlug) {
|
||
const userInfo = await fetchBangumiAPI(`users/${userSlug}`);
|
||
if (!avatarUrl) avatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||
if (isInfoFull || !username) username = userInfo?.nickname || userInfo?.username || username;
|
||
}
|
||
|
||
const statusP = tmlItem.querySelector('.info p.status, .info_full p.status');
|
||
const collectComment = tmlItem.querySelector('.collectInfo .comment');
|
||
const contentEl = statusP || collectComment;
|
||
const isCollect = !statusP && !!collectComment;
|
||
|
||
const timeTip = tmlItem.querySelector('.post_actions .titleTip');
|
||
const postTimeRaw = timeTip?.getAttribute('data-original-title') || timeTip?.textContent.trim() || '';
|
||
const via = extractTimelineVia(tmlItem.querySelector('.post_actions'));
|
||
const postTime = postTimeRaw + (via ? ` via ${via}` : '');
|
||
|
||
const replyUserEl = replyLi.querySelector('a.l[href*="/user/"]');
|
||
const replyUsername = replyUserEl?.textContent.trim() || '';
|
||
const replySlug = replyUserEl?.getAttribute('href')?.match(/\/user\/([^\/]+)/)?.[1] || '';
|
||
let replyAvatarUrl = '';
|
||
if (replySlug) {
|
||
const userInfo = await fetchBangumiAPI(`users/${replySlug}`);
|
||
replyAvatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||
}
|
||
|
||
let replyHtml = '';
|
||
let afterDash = false;
|
||
replyLi.childNodes.forEach(n => {
|
||
if (!afterDash) {
|
||
if (n.nodeType === 1 && n.tagName === 'SPAN' && n.classList.contains('tip_j')) afterDash = true;
|
||
return;
|
||
}
|
||
if (n.nodeType === 1 && n.classList?.contains(TML_REPLY_SHARE_BTN_CLASS)) return;
|
||
replyHtml += n.nodeType === 3 ? n.textContent : n.outerHTML;
|
||
});
|
||
|
||
const statusUrl = tmlItem.querySelector('.tml_comment')?.href;
|
||
const statusUrlParsed = statusUrl ? new URL(statusUrl) : null;
|
||
const replyCount = tmlItem.querySelectorAll('.subReply .reply_item').length;
|
||
|
||
if (isCollect) {
|
||
const subjectLink = tmlItem.querySelector('a[data-subject-id]');
|
||
const subjectId = subjectLink?.getAttribute('data-subject-id');
|
||
let collectionStatus = '';
|
||
if (subjectLink) {
|
||
let node = subjectLink.previousSibling;
|
||
while (node) {
|
||
if (node.nodeType === 3 && node.textContent.trim()) { collectionStatus = node.textContent.trim().replace(/了$/, ''); break; }
|
||
node = node.previousSibling;
|
||
}
|
||
}
|
||
const ratingEl = collectComment.querySelector('.starstop-s .starlight');
|
||
const domRating = parseInt(ratingEl?.className.match(/stars(\d+)/)?.[1] || '0');
|
||
const cleanCommentHtml = (() => {
|
||
const tmp = document.createElement('div');
|
||
tmp.innerHTML = collectComment.innerHTML;
|
||
tmp.querySelector('.starstop-s')?.remove();
|
||
return tmp.innerHTML.trim();
|
||
})();
|
||
|
||
if (subjectId) {
|
||
const subjectData = await fetchSubjectDataById(subjectId);
|
||
const subjectName = subjectData?.name || subjectLink?.textContent.trim() || '作品';
|
||
let rating = domRating;
|
||
let userTags = null;
|
||
let apiTime = postTime;
|
||
if (userSlug) {
|
||
const collection = await fetchBangumiAPI(`users/${userSlug}/collections/${subjectId}`);
|
||
if (collection) {
|
||
rating = collection.rate || rating;
|
||
if (collection.updated_at) {
|
||
const d = new Date(collection.updated_at);
|
||
const pad = n => String(n).padStart(2, '0');
|
||
apiTime = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` + (via ? ` via ${via}` : '');
|
||
}
|
||
if (collection.tags?.length > 0) userTags = collection.tags;
|
||
}
|
||
}
|
||
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: subjectName,
|
||
shareTitle: `${username}的收藏 - ${subjectName}`,
|
||
contentDoc: doc,
|
||
contentWin: statusUrlParsed
|
||
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
|
||
: { location: { origin: contentWin.location.origin, pathname: `/subject/${subjectId}` } },
|
||
dark,
|
||
replies: [
|
||
{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating },
|
||
{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' },
|
||
],
|
||
charImageUrl: subjectData?.imageUrl || '',
|
||
badgeLabel: subjectData?.type || '作品',
|
||
overrideTags,
|
||
});
|
||
} else {
|
||
await _doShareCard({
|
||
username, postTime, avatarUrl,
|
||
contentEl: collectComment,
|
||
pureTitle: username + '的收藏',
|
||
contentDoc: doc,
|
||
contentWin: statusUrlParsed
|
||
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
|
||
: contentWin,
|
||
dark, noCardTitle: true,
|
||
replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
|
||
overrideTags: ['收藏'],
|
||
});
|
||
}
|
||
} else {
|
||
await _doShareCard({
|
||
username, postTime, avatarUrl,
|
||
contentEl,
|
||
pureTitle: username + '的吐槽',
|
||
contentDoc: doc,
|
||
contentWin: statusUrlParsed
|
||
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
|
||
: contentWin,
|
||
dark, noCardTitle: true,
|
||
replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
|
||
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||
});
|
||
}
|
||
}
|
||
|
||
const insertTimelineShareButtons = (doc = document) => {
|
||
doc.querySelectorAll('.tml_item').forEach(item => {
|
||
if (item.querySelector('.' + TML_SHARE_BTN_CLASS)) return;
|
||
|
||
const statusP = item.querySelector('.info p.status, .info_full p.status');
|
||
const collectComment = item.querySelector('.collectInfo .comment');
|
||
const blogLink = item.querySelector('.info a[href*="/blog/"], .info_full a[href*="/blog/"]');
|
||
const hasContent = (statusP?.textContent.trim()) || (collectComment?.textContent.trim()) || blogLink;
|
||
if (!hasContent) return;
|
||
|
||
const postActions = item.querySelector('.post_actions');
|
||
if (!postActions) return;
|
||
|
||
const btn = doc.createElement('a');
|
||
btn.href = 'javascript:void(0);';
|
||
btn.className = TML_SHARE_BTN_CLASS;
|
||
btn.title = '分享';
|
||
btn.style.cssText = 'color:inherit;display:inline-flex;align-items:center;margin-right:6px;opacity:0.75;cursor:pointer;';
|
||
btn.innerHTML = SHARE_SVG;
|
||
|
||
const dropdown = postActions.querySelector('.action.dropdown');
|
||
dropdown ? dropdown.after(btn) : postActions.prepend(btn);
|
||
|
||
btn.addEventListener('click', () => createTimelineShareImage(item, doc));
|
||
});
|
||
|
||
doc.querySelectorAll('.tml_item').forEach(item => {
|
||
if (!item.querySelector('.info p.status, .info_full p.status') && !item.querySelector('.collectInfo .comment')) return;
|
||
item.querySelectorAll('.subReply .reply_item').forEach(li => {
|
||
if (li.querySelector('.' + TML_REPLY_SHARE_BTN_CLASS)) return;
|
||
const btn = doc.createElement('a');
|
||
btn.href = 'javascript:void(0);';
|
||
btn.className = TML_REPLY_SHARE_BTN_CLASS;
|
||
btn.title = '分享回复';
|
||
btn.style.cssText = 'display:inline-flex;align-items:center;gap:3px;margin-left:6px;vertical-align:middle;opacity:0.6;cursor:pointer;';
|
||
btn.innerHTML = SHARE_SVG;
|
||
li.appendChild(btn);
|
||
btn.addEventListener('click', () => createTimelineReplyShareImage(li, item, doc));
|
||
});
|
||
});
|
||
};
|
||
|
||
const observeTimeline = (doc = document) => {
|
||
const root = doc.querySelector('#columnTimelineA') || doc.querySelector('#main') || doc.body;
|
||
if (!root) return;
|
||
let debounce = null;
|
||
const obs = new MutationObserver(() => {
|
||
clearTimeout(debounce);
|
||
debounce = setTimeout(() => insertTimelineShareButtons(doc), 150);
|
||
});
|
||
obs.observe(root, { childList: true, subtree: true });
|
||
};
|
||
|
||
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(); insertStatusShareButton(); insertStatusReplyButtons(); insertTimelineShareButtons(); observeTimeline(); 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 });
|
||
}
|
||
})(); |