diff --git a/js/Bangumi_Topic_Share.js b/js/Bangumi_Topic_Share.js index 3c2bd83..701a422 100644 --- a/js/Bangumi_Topic_Share.js +++ b/js/Bangumi_Topic_Share.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Bangumi Topic Share // @namespace http://tampermonkey.net/ -// @version 4.13 +// @version 5.0 // @description Bangumi 话题/日志分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签 // @author Chang ji // @contributor Stardream @@ -17,6 +17,15 @@ // @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/rakuen* // @match *://bangumi.tv/rakuen* // @match *://chii.in/rakuen* @@ -42,7 +51,8 @@ #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; + 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; @@ -58,7 +68,7 @@ .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; } + .content-text { font-size: 14px; color: #333; line-height: 1.8; margin: 0; word-break: break-all; } .tags-container { display: flex; flex-wrap: wrap; gap: 8px; } .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; } @@ -109,6 +119,11 @@ .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: 1px solid #555; border-radius: 2px; padding: 0 4px; transition: color 0.3s ease; } `; document.head.appendChild(style); @@ -128,9 +143,31 @@ }); } - function getPageTags(contentDoc) { + 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').forEach(el => el.remove()); + div.querySelectorAll('.text_mask').forEach(el => { el.removeAttribute('style'); el.classList.remove('text_mask'); el.dataset.bgmMask = '1'; }); + const imgs = [...div.querySelectorAll('img')]; + imgs.forEach(img => { + if (img.hasAttribute('smileid') || /\/smiles\//.test(img.src)) { + img.setAttribute('data-bgm-emoji', '1'); + } + }); + 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 isBlog = /\/blog\/\d+/.test(contentWin.location.pathname); + const pathname = contentWin.location.pathname; + const isBlog = /\/blog\/\d+/.test(pathname); + const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(pathname); + const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(pathname); if (isBlog) { const subjectNames = [...new Set( [...contentDoc.querySelectorAll('a')] @@ -140,6 +177,59 @@ const replyCount = contentDoc.querySelectorAll('[id^="post_"]').length; return [...subjectNames, `${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) { @@ -175,10 +265,8 @@ // contentDoc: the document containing the topic (may be an iframe's doc on Rakuen) // Overlay is always rendered in the outer document (where GM functions are available) - async function createShareImage(contentDoc = document) { - const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; - const contentWin = contentDoc.defaultView || window; - + async function _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, + replies = [], replyId = '', charImageUrl = '', badgeLabel = '' }) { if (typeof html2canvas === 'undefined') { alert("截图库加载失败,请刷新页面或检查网络。"); return; @@ -188,74 +276,82 @@ loading.innerHTML = '
AI 正在提炼标签...
'; 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 = ""; + let displayContentHtml = ""; if (contentEl) { - const toHide = contentEl.querySelectorAll('.forum_category, #catfish_likes_grid'); + const toHide = contentEl.querySelectorAll('.forum_category, #catfish_likes_grid, .embed-play-btn'); toHide.forEach(el => el.style.display = 'none'); fullContent = contentEl.innerText?.trim() || ""; + const fullHtml = contentEl.innerHTML?.trim() || ""; toHide.forEach(el => el.style.display = ''); + const lim = replies.length > 0 ? 200 : 300; + displayContentHtml = fullContent.length > lim ? (fullContent.substring(0, lim) + "...") : fullHtml; } - let displayContent = fullContent.length > 300 ? fullContent.substring(0, 300) + "..." : fullContent; + const mainLimit = replies.length > 0 ? 200 : 300; + let displayContent = fullContent.length > mainLimit ? fullContent.substring(0, mainLimit) + "..." : fullContent; const currentFullUrl = contentWin.location.origin + contentWin.location.pathname; + const shareUrl = replyId ? currentFullUrl + '#' + replyId : currentFullUrl; const displayUrl = currentFullUrl.replace(/^https?:\/\//, ''); - const [tags, base64Avatar, base64QR] = await Promise.all([ + const [tags, base64Avatar, base64CharImage, base64QR, ...base64ReplyAvatars] = 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' : ''}`) + 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 tagsHtml = tags.map(tag => `# ${tag}`).join(''); + const [inlinedMainContent, ...inlinedReplyContents] = await Promise.all([ + inlineImages(displayContentHtml), + ...replies.map(r => inlineImages(r.contentHtml || r.content)) + ]); + + const tagsHtml = tags.map(tag => tag.startsWith('!') ? `${tag.slice(1)}` : `# ${tag}`).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(22, 28 - idx * 4); + const topStyle = idx === 0 + ? `margin-top:${base64CharImage ? '6' : '14'}px;padding-top:${base64CharImage ? '6' : '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 ? `
${renderLevel(idx + 1)}
` : ''; + return `
+
+ + ${r.username} + ${r.time} +
+

${inlinedReplyContents[idx]}

+ ${inner} +
`; + }; + const replySection = replies.length > 0 ? renderLevel(0) : ''; loading.remove(); const overlay = document.createElement('div'); overlay.id = 'bgm-share-overlay'; overlay.style.display = 'flex'; overlay.innerHTML = ` -
+