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 <noreply@anthropic.com>
This commit is contained in:
+71
-30
@@ -1,13 +1,19 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Bangumi Topic Share
|
// @name Bangumi Topic Share
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 4.10
|
// @version 4.11
|
||||||
// @description Bangumi 话题分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签
|
// @description Bangumi 话题分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签
|
||||||
// @author Chang ji
|
// @author Chang ji
|
||||||
// @contributor Stardream
|
// @contributor Stardream
|
||||||
// @match *://bgm.tv/group/topic/*
|
// @match *://bgm.tv/group/topic/*
|
||||||
// @match *://bangumi.tv/group/topic/*
|
// @match *://bangumi.tv/group/topic/*
|
||||||
// @match *://chii.in/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
|
// @grant GM_xmlhttpRequest
|
||||||
// @connect *
|
// @connect *
|
||||||
// @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js
|
// @require https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js
|
||||||
@@ -17,10 +23,6 @@
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function isDark() {
|
|
||||||
return document.documentElement.getAttribute('data-theme') === 'dark';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 配置区 =================
|
// ================= 配置区 =================
|
||||||
const AI_CONFIG = {
|
const AI_CONFIG = {
|
||||||
apiUrl: "在此处填入你的_API_URL",
|
apiUrl: "在此处填入你的_API_URL",
|
||||||
@@ -104,10 +106,6 @@
|
|||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
function getElementByXpath(path) {
|
|
||||||
return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchAsBase64(url) {
|
function fetchAsBase64(url) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!url) { resolve(""); return; }
|
if (!url) { resolve(""); return; }
|
||||||
@@ -124,18 +122,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPageTags() {
|
function getPageTags(contentDoc) {
|
||||||
const groupLink = document.querySelector('a.avatar[href^="/group/"]');
|
const groupLink = contentDoc.querySelector('a.avatar[href^="/group/"]');
|
||||||
let groupName = '';
|
let groupName = '';
|
||||||
if (groupLink) {
|
if (groupLink) {
|
||||||
groupLink.childNodes.forEach(n => { if (n.nodeType === 3) groupName += n.textContent.trim(); });
|
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} 回复`];
|
return [groupName || 'Bangumi', `${replyCount} 回复`];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAITags(title, content) {
|
async function getAITags(title, content, contentDoc) {
|
||||||
if (!AI_CONFIG.apiKey || AI_CONFIG.apiKey.includes("填入")) return getPageTags();
|
if (!AI_CONFIG.apiKey || AI_CONFIG.apiKey.includes("填入")) return getPageTags(contentDoc);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const prompt = `根据标题和内容生成3个短标签,只要标签名,空格隔开。内容:${title} ${content.substring(0, 150)}`;
|
const prompt = `根据标题和内容生成3个短标签,只要标签名,空格隔开。内容:${title} ${content.substring(0, 150)}`;
|
||||||
GM_xmlhttpRequest({
|
GM_xmlhttpRequest({
|
||||||
@@ -146,15 +149,18 @@
|
|||||||
try {
|
try {
|
||||||
const tags = JSON.parse(res.responseText).choices[0].message.content.trim().split(/\s+/).slice(0, 3);
|
const tags = JSON.parse(res.responseText).choices[0].message.content.trim().split(/\s+/).slice(0, 3);
|
||||||
resolve(tags);
|
resolve(tags);
|
||||||
} catch (e) { resolve(["话题", "讨论", "Bangumi"]); }
|
} catch (e) { resolve(getPageTags(contentDoc)); }
|
||||||
},
|
},
|
||||||
onerror: () => resolve(["话题", "讨论", "Bangumi"])
|
onerror: () => resolve(getPageTags(contentDoc))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createShareImage() {
|
// contentDoc: the document containing the topic (may be an iframe's doc on Rakuen)
|
||||||
const dark = isDark();
|
// 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') {
|
if (typeof html2canvas === 'undefined') {
|
||||||
alert("截图库加载失败,请刷新页面或检查网络。");
|
alert("截图库加载失败,请刷新页面或检查网络。");
|
||||||
@@ -165,17 +171,18 @@
|
|||||||
loading.innerHTML = '<div id="bgm-share-overlay" style="display:flex"><div id="loading-info">AI 正在提炼标签...</div></div>';
|
loading.innerHTML = '<div id="bgm-share-overlay" style="display:flex"><div id="loading-info">AI 正在提炼标签...</div></div>';
|
||||||
document.body.appendChild(loading);
|
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 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] || "未知时间") : "未知时间";
|
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 = "";
|
let pureTitle = "";
|
||||||
if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) pureTitle += n.textContent; });
|
if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) pureTitle += n.textContent; });
|
||||||
pureTitle = pureTitle.replace(/[»\n]/g, '').trim() || "分享话题";
|
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');
|
const contentEl = masterPost?.querySelector('.topic_content') || masterPost?.querySelector('.inner');
|
||||||
let fullContent = "";
|
let fullContent = "";
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
@@ -187,13 +194,13 @@
|
|||||||
let displayContent = fullContent.length > 300 ? fullContent.substring(0, 300) + "..." : fullContent;
|
let displayContent = fullContent.length > 300 ? fullContent.substring(0, 300) + "..." : fullContent;
|
||||||
|
|
||||||
const avatarBox = masterPost?.querySelector('.avatarSize48');
|
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 displayUrl = currentFullUrl.replace(/^https?:\/\//, '');
|
||||||
|
|
||||||
const [tags, base64Avatar, base64QR] = await Promise.all([
|
const [tags, base64Avatar, base64QR] = await Promise.all([
|
||||||
getAITags(pureTitle, fullContent),
|
getAITags(pureTitle, fullContent, contentDoc),
|
||||||
fetchAsBase64(avatarUrl),
|
fetchAsBase64(avatarUrl),
|
||||||
fetchAsBase64(`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(currentFullUrl)}${dark ? '&color=F09199&bgcolor=2a2a2a' : ''}`)
|
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 timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 10000));
|
||||||
const captureEl = document.querySelector('#capture-area');
|
const captureEl = document.querySelector('#capture-area');
|
||||||
|
|
||||||
// 将卡片注入独立 iframe,避免 html2canvas 克隆整个 Bangumi 页面 DOM
|
|
||||||
iframe = document.createElement('iframe');
|
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.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.width = captureEl.offsetWidth + 'px';
|
||||||
@@ -350,9 +356,26 @@
|
|||||||
}, 800);
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertButton = () => {
|
const insertButton = (targetDoc = document) => {
|
||||||
const menuInner = document.querySelector('#columnInSubjectB .menu_inner');
|
if (targetDoc.getElementById('gen-card-btn')) return;
|
||||||
if (menuInner && !document.getElementById('gen-card-btn')) {
|
|
||||||
|
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 = '<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 br = document.createElement('br');
|
||||||
const btn = document.createElement('a');
|
const btn = document.createElement('a');
|
||||||
btn.id = 'gen-card-btn';
|
btn.id = 'gen-card-btn';
|
||||||
@@ -361,9 +384,27 @@
|
|||||||
btn.textContent = '/ 分享';
|
btn.textContent = '/ 分享';
|
||||||
menuInner.appendChild(br);
|
menuInner.appendChild(br);
|
||||||
menuInner.appendChild(btn);
|
menuInner.appendChild(btn);
|
||||||
btn.addEventListener('click', createShareImage);
|
btn.addEventListener('click', () => createShareImage(document));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 超展开:在外层页面监听 #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);
|
setTimeout(insertButton, 500);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
Reference in New Issue
Block a user