From 27797d81ea253699dcbee3d343390fb74b74d73c Mon Sep 17 00:00:00 2001 From: Stardream Date: Tue, 21 Apr 2026 18:36:58 +1000 Subject: [PATCH] feat(Bangumi_Topic_Share): add Rakuen support via outer-frame iframe monitoring, fix share button placement to main post actions bar, use inline SVG share icon, bump version to 4.11 Co-Authored-By: Claude Sonnet 4.6 --- js/Bangumi_Topic_Share.js | 125 +++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 42 deletions(-) diff --git a/js/Bangumi_Topic_Share.js b/js/Bangumi_Topic_Share.js index c857415..cea5ff0 100644 --- a/js/Bangumi_Topic_Share.js +++ b/js/Bangumi_Topic_Share.js @@ -1,13 +1,19 @@ // ==UserScript== // @name Bangumi Topic Share // @namespace http://tampermonkey.net/ -// @version 4.10 +// @version 4.11 // @description Bangumi 话题分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签 // @author Chang ji // @contributor Stardream // @match *://bgm.tv/group/topic/* // @match *://bangumi.tv/group/topic/* // @match *://chii.in/group/topic/* +// @match *://bgm.tv/subject/*/topic/* +// @match *://bangumi.tv/subject/*/topic/* +// @match *://chii.in/subject/*/topic/* +// @match *://bgm.tv/rakuen* +// @match *://bangumi.tv/rakuen* +// @match *://chii.in/rakuen* // @grant GM_xmlhttpRequest // @connect * // @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js @@ -17,14 +23,10 @@ (function() { 'use strict'; - function isDark() { - return document.documentElement.getAttribute('data-theme') === 'dark'; - } - // ================= 配置区 ================= const AI_CONFIG = { - apiUrl: "在此处填入你的_API_URL", - apiKey: "在此处填入你的_API_KEY", + apiUrl: "在此处填入你的_API_URL", + apiKey: "在此处填入你的_API_KEY", model: "gpt-3.5-turbo", }; // ========================================= @@ -104,10 +106,6 @@ `; document.head.appendChild(style); - function getElementByXpath(path) { - return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; - } - function fetchAsBase64(url) { return new Promise((resolve) => { if (!url) { resolve(""); return; } @@ -124,18 +122,23 @@ }); } - function getPageTags() { - const groupLink = document.querySelector('a.avatar[href^="/group/"]'); + function getPageTags(contentDoc) { + const groupLink = contentDoc.querySelector('a.avatar[href^="/group/"]'); let groupName = ''; if (groupLink) { groupLink.childNodes.forEach(n => { if (n.nodeType === 3) groupName += n.textContent.trim(); }); } - const replyCount = Math.max(0, document.querySelectorAll('[id^="post_"]').length - 1); + 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) { - if (!AI_CONFIG.apiKey || AI_CONFIG.apiKey.includes("填入")) return getPageTags(); + async function getAITags(title, content, contentDoc) { + if (!AI_CONFIG.apiKey || AI_CONFIG.apiKey.includes("填入")) return getPageTags(contentDoc); return new Promise((resolve) => { const prompt = `根据标题和内容生成3个短标签,只要标签名,空格隔开。内容:${title} ${content.substring(0, 150)}`; GM_xmlhttpRequest({ @@ -146,15 +149,18 @@ try { const tags = JSON.parse(res.responseText).choices[0].message.content.trim().split(/\s+/).slice(0, 3); resolve(tags); - } catch (e) { resolve(["话题", "讨论", "Bangumi"]); } + } catch (e) { resolve(getPageTags(contentDoc)); } }, - onerror: () => resolve(["话题", "讨论", "Bangumi"]) + onerror: () => resolve(getPageTags(contentDoc)) }); }); } - async function createShareImage() { - const dark = isDark(); + // 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 createShareImage(contentDoc = document) { + const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; + const contentWin = contentDoc.defaultView || window; if (typeof html2canvas === 'undefined') { alert("截图库加载失败,请刷新页面或检查网络。"); @@ -165,17 +171,18 @@ loading.innerHTML = '
AI 正在提炼标签...
'; document.body.appendChild(loading); - const idNode = getElementByXpath("/html/body/div[1]/div[2]/div[1]/div[1]/div[2]/div[2]/strong/a"); + const firstPost = contentDoc.querySelector('.postTopic') || contentDoc.querySelector('[id^="post_"]'); + const idNode = firstPost?.querySelector('strong a') || firstPost?.querySelector('.author strong a'); const username = idNode ? idNode.innerText.trim() : "未知用户"; - const timeNode = getElementByXpath("/html/body/div[1]/div[2]/div[1]/div[1]/div[2]/div[1]/div[1]/small"); + const timeNode = firstPost?.querySelector('small'); let postTime = timeNode ? (timeNode.innerText.match(/\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}/)?.[0] || "未知时间") : "未知时间"; - const h1Node = document.querySelector('#pageHeader h1') || document.querySelector('h1'); + const h1Node = contentDoc.querySelector('#pageHeader h1') || contentDoc.querySelector('h1'); let pureTitle = ""; if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) pureTitle += n.textContent; }); pureTitle = pureTitle.replace(/[»\n]/g, '').trim() || "分享话题"; - const masterPost = document.querySelector('.postTopic') || document.querySelector('[id^="post_"]'); + const masterPost = contentDoc.querySelector('.postTopic') || contentDoc.querySelector('[id^="post_"]'); const contentEl = masterPost?.querySelector('.topic_content') || masterPost?.querySelector('.inner'); let fullContent = ""; if (contentEl) { @@ -187,13 +194,13 @@ let displayContent = fullContent.length > 300 ? fullContent.substring(0, 300) + "..." : fullContent; const avatarBox = masterPost?.querySelector('.avatarSize48'); - let avatarUrl = avatarBox ? window.getComputedStyle(avatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : ""; + let avatarUrl = avatarBox ? contentWin.getComputedStyle(avatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : ""; - const currentFullUrl = window.location.origin + window.location.pathname; + const currentFullUrl = contentWin.location.origin + contentWin.location.pathname; const displayUrl = currentFullUrl.replace(/^https?:\/\//, ''); const [tags, base64Avatar, base64QR] = await Promise.all([ - getAITags(pureTitle, fullContent), + getAITags(pureTitle, fullContent, contentDoc), fetchAsBase64(avatarUrl), fetchAsBase64(`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(currentFullUrl)}${dark ? '&color=F09199&bgcolor=2a2a2a' : ''}`) ]); @@ -280,7 +287,6 @@ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 10000)); const captureEl = document.querySelector('#capture-area'); - // 将卡片注入独立 iframe,避免 html2canvas 克隆整个 Bangumi 页面 DOM 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'; @@ -350,20 +356,55 @@ }, 800); } - const insertButton = () => { - const menuInner = document.querySelector('#columnInSubjectB .menu_inner'); - if (menuInner && !document.getElementById('gen-card-btn')) { - 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); + const insertButton = (targetDoc = document) => { + if (targetDoc.getElementById('gen-card-btn')) return; + + const postActions = targetDoc.querySelector('.postTopic .post_actions:not(.re_info)') + || targetDoc.querySelector('[id^="post_"] .post_actions:not(.re_info)') + || targetDoc.querySelector('.post_actions:not(.re_info)'); + if (postActions) { + const wrap = targetDoc.createElement('span'); + wrap.className = 'action'; + wrap.innerHTML = '分享'; + postActions.appendChild(wrap); + targetDoc.getElementById('gen-card-btn').addEventListener('click', () => createShareImage(targetDoc)); + return; + } + + // 降级:插入普通页面侧栏(仅非 Rakuen 场景) + if (targetDoc === document) { + const menuInner = document.querySelector('#columnInSubjectB .menu_inner') + || document.querySelector('#columnSubjectB .menu_inner'); + if (menuInner) { + const br = document.createElement('br'); + const btn = document.createElement('a'); + btn.id = 'gen-card-btn'; + btn.href = 'javascript:void(0);'; + btn.className = 'l'; + btn.textContent = '/ 分享'; + menuInner.appendChild(br); + menuInner.appendChild(btn); + btn.addEventListener('click', () => createShareImage(document)); + } } }; - setTimeout(insertButton, 500); -})(); \ No newline at end of file + // 超展开:在外层页面监听 #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)) { + insertButton(iDoc); + } + } catch (e) {} + }, 800); + }; + rightFrame.addEventListener('load', onRightFrameLoad); + } else { + setTimeout(insertButton, 500); + } +})();