// ==UserScript== // @name Bangumi Topic Share // @namespace http://tampermonkey.net/ // @version 4.13 // @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/subject/topic/* // @match *://bangumi.tv/subject/topic/* // @match *://chii.in/subject/topic/* // @match *://bgm.tv/blog/* // @match *://bangumi.tv/blog/* // @match *://chii.in/blog/* // @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 // @license MIT // ==/UserScript== (function() { 'use strict'; // ================= 配置区 ================= const AI_CONFIG = { apiUrl: "在此处填入你的_API_URL", apiKey: "在此处填入你的_API_KEY", model: "gpt-3.5-turbo", }; // ========================================= 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: center; z-index: 100000; } .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; } .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; white-space: pre-wrap; word-break: break-all; } .tags-container { display: flex; flex-wrap: wrap; gap: 8px; } .share-card .tags-container { margin-top: 15px; } .tag-item { background: #FEEFF0; color: #F09199; font-size: 11px; padding: 4px 12px; border-radius: 20px; font-weight: bold; border: 1px solid #F0919944; } .card-footer { background: #f9f9f9; padding: 20px 25px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #eee; } .qr-img { width: 55px; height: 55px; background: #fff; } #loading-info { position: fixed; top: 55%; left: 50%; transform: translateX(-50%); color: #fff; font-size: 14px; z-index: 100001; } .bgm-btn-row { display: flex; gap: 16px; margin: 20px auto 0; justify-content: center; } .bgm-action-btn { width: 48px; height: 48px; padding: 0; background: rgba(255,255,255,0.15); color: #fff; border: 1.5px solid rgba(255,255,255,0.35); border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.15s, opacity 0.2s; position: relative; } .bgm-action-btn:hover:not(:disabled) { background: rgba(255,255,255,0.3); transform: scale(1.1); } .bgm-action-btn:disabled { opacity: 0.3; cursor: default; } .bgm-action-btn svg { display: block; } .bgm-action-btn::after { content: attr(data-tip); position: absolute; top: calc(100% + 10px); left: 50%; transform: translateX(-50%) translateY(-6px); background: rgba(20,20,20,0.82); color: #fff; font-size: 12px; padding: 5px 12px; border-radius: 8px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.18s ease, transform 0.18s ease; } .bgm-action-btn:hover:not(:disabled)::after { opacity: 1; transform: translateX(-50%) translateY(0); } /* ===== 暗色主题 ===== */ .share-card.dark { background: #1e1e1e; } .share-card.dark .card-header { border-bottom: 1px solid rgba(255,255,255,0.1); } .share-card.dark .card-body { background: #1e1e1e; padding-top: 15px; } .share-card.dark .main-title { color: #f0f0f0; } .share-card.dark .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; } `; 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("") }); }); } function getPageTags(contentDoc) { const contentWin = contentDoc.defaultView || window; const isBlog = /\/blog\/\d+/.test(contentWin.location.pathname); 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; return [...subjectNames, `${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) { 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({ method: "POST", url: AI_CONFIG.apiUrl, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${AI_CONFIG.apiKey}` }, data: JSON.stringify({ model: AI_CONFIG.model, messages: [{ role: "user", content: prompt }], temperature: 0.5 }), 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 createShareImage(contentDoc = document) { const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = contentDoc.defaultView || window; if (typeof html2canvas === 'undefined') { alert("截图库加载失败,请刷新页面或检查网络。"); return; } const loading = document.createElement('div'); loading.innerHTML = '
'; document.body.appendChild(loading); const isBlog = /\/blog\/\d+/.test(contentWin.location.pathname); let username, postTime, avatarUrl, contentEl; 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 masterPost = contentDoc.querySelector('.postTopic') || contentDoc.querySelector('[id^="post_"]'); const avatarBox = masterPost?.querySelector('.avatarSize48'); avatarUrl = avatarBox ? contentWin.getComputedStyle(avatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : ""; contentEl = masterPost?.querySelector('.topic_content') || masterPost?.querySelector('.inner'); } const h1Node = contentDoc.querySelector('#pageHeader h1') || contentDoc.querySelector('h1.title') || contentDoc.querySelector('h1'); let pureTitle = ""; if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) pureTitle += n.textContent; }); pureTitle = pureTitle.replace(/[»\n]/g, '').trim() || "分享话题"; let fullContent = ""; if (contentEl) { const toHide = contentEl.querySelectorAll('.forum_category, #catfish_likes_grid'); toHide.forEach(el => el.style.display = 'none'); fullContent = contentEl.innerText?.trim() || ""; toHide.forEach(el => el.style.display = ''); } let displayContent = fullContent.length > 300 ? fullContent.substring(0, 300) + "..." : fullContent; const currentFullUrl = contentWin.location.origin + contentWin.location.pathname; const displayUrl = currentFullUrl.replace(/^https?:\/\//, ''); const [tags, base64Avatar, base64QR] = await Promise.all([ 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' : ''}`) ]); const tagsHtml = tags.map(tag => `# ${tag}`).join(''); loading.remove(); const overlay = document.createElement('div'); overlay.id = 'bgm-share-overlay'; overlay.style.display = 'flex'; overlay.innerHTML = `