diff --git a/js/Bangumi Topic Share/Bangumi_Topic_Share.js b/js/Bangumi Topic Share/Bangumi_Topic_Share.js
index 942ed57..c9f5b66 100644
--- a/js/Bangumi Topic Share/Bangumi_Topic_Share.js
+++ b/js/Bangumi Topic Share/Bangumi_Topic_Share.js
@@ -1,8 +1,8 @@
// ==UserScript==
// @name Bangumi Topic Share
// @namespace https://greasyfork.org/scripts/574689
-// @version 6.3
-// @description Bangumi 分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签
+// @version 6.4
+// @description Bangumi 分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、贴贴反应展示、可选 AI 标签
// @author Stardream
// @contributor Chang ji, Mewtw0
// @match *://bgm.tv/group/topic/*
@@ -253,6 +253,12 @@
.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; }
+ .reactions-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
+ .reaction-item { display: inline-flex; align-items: center; gap: 4px; background: #f2f2f2; border: 1px solid #ebebeb; border-radius: 20px; padding: 2px 9px 2px 7px; line-height: 1; }
+ .reaction-item img { height: 18px; width: auto; display: block; }
+ .reaction-num { font-size: 12px; color: #888; font-weight: bold; }
+ .share-card.dark .reaction-item { background: #2a2a2a; border: 1px solid rgba(255,255,255,0.08); }
+ .share-card.dark .reaction-num { color: #aaa; }
`;
document.head.appendChild(style);
@@ -440,7 +446,7 @@
// 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 = '' }) {
+ replies = [], replyId = '', charImageUrl = '', badgeLabel = '', overrideTags = null, topicTitle = '', noCardTitle = false, shareTitle = '', mainReactions = [] }) {
if (document.getElementById('bgm-share-overlay')) return;
if (typeof html2canvas === 'undefined') {
alert("截图库加载失败,请刷新页面或检查网络。");
@@ -485,6 +491,22 @@
...replies.map(r => inlineImages(r.contentHtml || r.content))
]);
+ const reactionIconUrls = [...new Set([
+ ...mainReactions.map(r => r.icon),
+ ...replies.flatMap(r => (r.reactions || []).map(x => x.icon))
+ ].filter(Boolean))];
+ const reactionB64 = {};
+ await Promise.all(reactionIconUrls.map(async u => { reactionB64[u] = await fetchAsBase64(u); }));
+ const renderReactions = (arr) => {
+ if (!arr || !arr.length) return '';
+ const items = arr.map(r => {
+ const b64 = reactionB64[r.icon];
+ if (!b64) return '';
+ return `
${r.count ? `${r.count}` : ''}`;
+ }).join('');
+ return items ? `
${items}
` : '';
+ };
+
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;
@@ -506,6 +528,7 @@
${r.time}
${inlinedReplyContents[idx]}
+ ${renderReactions(r.reactions)}
${inner}
`;
};
@@ -536,6 +559,7 @@
${(base64CharImage || badgeLabel || noCardTitle) ? '' : `${pureTitle}
`}
${topicTitle ? `${topicTitle}
` : ''}
${inlinedMainContent ? `` : ''}
+ ${renderReactions(mainReactions)}
${replySection}
${tagsHtml}
@@ -1068,7 +1092,8 @@
username, userSlug, avatarUrl,
time: domTime, rating: 0, collectionStatus,
content: truncated ? rawText.substring(0, 150) + '...' : rawText,
- contentHtml
+ contentHtml,
+ reactions: extractReactions(itemEl, itemEl.ownerDocument)
};
}
@@ -1147,7 +1172,7 @@
}
const truncated = rawText.length > 150;
const contentHtml = truncated ? rawText.substring(0, 150) + '...' : rawText;
- return { username, userSlug, avatarUrl, rating, time, content: contentHtml, contentHtml };
+ return { username, userSlug, avatarUrl, rating, time, content: contentHtml, contentHtml, reactions: extractReactions(itemEl, itemEl.ownerDocument) };
}
const COLLECTION_PAGE_STATUS = { collections: '看过', wishes: '想看', doings: '在看', on_hold: '搁置', dropped: '抛弃' };
@@ -1204,7 +1229,7 @@
const subjectTopicId = getSubjectIdFromTopicPage(contentWin.location.pathname, contentDoc);
const isSubjectTopic = !!subjectTopicId;
- let username, postTime, avatarUrl, contentEl;
+ let username, postTime, avatarUrl, contentEl, mainReactions = [];
if (isEpisode || isCharacter || isPerson) {
username = ""; postTime = ""; avatarUrl = ""; contentEl = null;
} else if (isBlog) {
@@ -1225,6 +1250,7 @@
const avatarBox = firstPost?.querySelector('.avatarSize48');
avatarUrl = avatarBox ? contentWin.getComputedStyle(avatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : "";
contentEl = firstPost?.querySelector('.topic_content') || firstPost?.querySelector('.inner');
+ mainReactions = extractReactions(firstPost, contentDoc);
}
const h1Node = contentDoc.querySelector('#pageHeader h1') || contentDoc.querySelector('h1.title') || contentDoc.querySelector('h1.nameSingle a') || contentDoc.querySelector('h1');
@@ -1288,7 +1314,7 @@
}
}
}
- await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, charImageUrl, badgeLabel, topicTitle });
+ await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, charImageUrl, badgeLabel, topicTitle, mainReactions });
}
function truncateHtml(html, limit) {
@@ -1312,6 +1338,35 @@
return tmp.innerHTML;
}
+ // Extract "贴贴" reactions for a post/reply from its .likes_grid display container.
+ // Grid id mirrors the post id: post_{N} -> likes_grid_{N}. Icons are CSS background-images.
+ function extractReactions(postEl, contentDoc) {
+ if (!postEl) return [];
+ const doc = contentDoc || postEl.ownerDocument || document;
+ const win = doc.defaultView || window;
+ let grid = null;
+ if (postEl.classList?.contains('likes_grid')) {
+ grid = postEl;
+ } else {
+ const idMatch = (postEl.id || '').match(/^post_(\d+)$/);
+ if (idMatch) grid = doc.getElementById('likes_grid_' + idMatch[1]);
+ if (!grid) grid = postEl.querySelector('.likes_grid');
+ }
+ if (!grid) return [];
+ return [...grid.querySelectorAll('a.item')].map(item => {
+ const emojiSpan = item.querySelector('.emoji');
+ let icon = '';
+ if (emojiSpan) {
+ const bg = win.getComputedStyle(emojiSpan).backgroundImage || '';
+ const m = bg.match(/url\(["']?([^"')]+)["']?\)/);
+ if (m && m[1] !== 'none') icon = m[1];
+ if (!icon) { const img = emojiSpan.querySelector('img'); if (img) icon = img.src; }
+ }
+ const count = item.querySelector('.num')?.textContent.trim() || '';
+ return icon ? { icon, count } : null;
+ }).filter(Boolean);
+ }
+
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() : "未知用户";
@@ -1351,7 +1406,8 @@
const contentHtml = truncated
? savedQuoteHtml + truncateHtml(bodyClone?.innerHTML?.trim() || "", 150)
: (contentEl?.innerHTML?.trim() || "");
- return { username, time, avatarUrl, content: truncated ? rawText.substring(0, 150) + "..." : rawText, contentHtml };
+ const reactions = extractReactions(postEl, postEl.ownerDocument);
+ return { username, time, avatarUrl, content: truncated ? rawText.substring(0, 150) + "..." : rawText, contentHtml, reactions };
}
async function createReplyShareImage(replyEl, contentDoc = document) {
@@ -1365,7 +1421,7 @@
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;
+ let username, postTime, avatarUrl, contentEl, mainReactions = [];
if (isEpisode || isCharacter || isPerson) {
username = ""; postTime = ""; avatarUrl = ""; contentEl = null;
} else if (isBlog) {
@@ -1385,6 +1441,7 @@
const mainAvatarBox = firstPost?.querySelector('.avatarSize48');
avatarUrl = mainAvatarBox ? contentWin.getComputedStyle(mainAvatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : "";
contentEl = firstPost?.querySelector('.topic_content') || firstPost?.querySelector('.inner');
+ mainReactions = extractReactions(firstPost, contentDoc);
}
// Build replies chain: if sub-reply, prepend the parent reply first
@@ -1457,7 +1514,7 @@
}
}
await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark,
- replies, replyId: replyEl.id, charImageUrl, badgeLabel, topicTitle });
+ replies, replyId: replyEl.id, charImageUrl, badgeLabel, topicTitle, mainReactions });
}
const REPLY_SHARE_BTN_CLASS = 'bgm-reply-share-btn';
@@ -1873,6 +1930,7 @@
const contentWin = doc.defaultView || window;
const { username, avatarUrl, postTime } = _getStatusAuthorInfo(doc);
const replyCount = doc.querySelectorAll('.subReply .reply_item').length;
+ const statusReactions = _extractStatusMainReactions(doc);
const collectInfo = await _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime);
if (collectInfo) {
@@ -1882,7 +1940,7 @@
pureTitle: subjectName,
shareTitle: `${username}的收藏 - ${subjectName}`,
contentDoc: doc, contentWin, dark,
- replies: [{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating }],
+ replies: [{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating, reactions: statusReactions }],
charImageUrl: subjectData?.imageUrl || '',
badgeLabel: subjectData?.type || '作品',
overrideTags,
@@ -1893,11 +1951,20 @@
username, postTime, avatarUrl, contentEl,
pureTitle: username + '的吐槽', contentDoc: doc, contentWin, dark,
noCardTitle: true,
+ mainReactions: statusReactions,
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
});
}
}
+ // Status detail page (/timeline/status/N): reactions for the main status live in a
+ // .likes_grid outside the reply list; pick the first one not inside a .reply_item.
+ function _extractStatusMainReactions(doc) {
+ const scope = doc.querySelector('.statusContent') || doc;
+ const grid = [...scope.querySelectorAll('.likes_grid')].find(g => !g.closest('.reply_item'));
+ return grid ? extractReactions(grid, doc) : [];
+ }
+
async function createStatusReplyShareImage(replyLi, doc = document) {
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
const contentWin = doc.defaultView || window;
@@ -1923,6 +1990,8 @@
});
const replyCount = doc.querySelectorAll('.subReply .reply_item').length;
+ const statusReactions = _extractStatusMainReactions(doc);
+ const replyReactions = extractReactions(replyLi, doc);
const collectInfo = await _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime);
if (collectInfo) {
const { subjectData, subjectName, collectComment, cleanCommentHtml, apiTime, rating, overrideTags } = collectInfo;
@@ -1932,8 +2001,8 @@
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: '' },
+ { username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating, reactions: statusReactions },
+ { username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '', reactions: replyReactions },
],
charImageUrl: subjectData?.imageUrl || '',
badgeLabel: subjectData?.type || '作品',
@@ -1945,7 +2014,8 @@
username, postTime, avatarUrl, contentEl,
pureTitle: username + '的吐槽', contentDoc: doc, contentWin, dark,
noCardTitle: true,
- replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
+ mainReactions: statusReactions,
+ replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '', reactions: replyReactions }],
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
});
}
@@ -2058,6 +2128,7 @@
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 itemReactions = extractReactions(item, doc);
if (statusP && statusP.textContent.trim()) {
const statusUrl = item.querySelector('.tml_comment')?.href;
@@ -2069,6 +2140,7 @@
contentDoc: doc, contentWin, dark,
noCardTitle: true,
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
+ mainReactions: itemReactions,
});
} else if (collectComment && collectComment.textContent.trim()) {
// Subject link is in .info, carries data-subject-id attribute
@@ -2138,7 +2210,7 @@
? { 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 }],
+ replies: [{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating, reactions: itemReactions }],
charImageUrl: subjectData?.imageUrl || '',
badgeLabel: subjectData?.type || '作品',
overrideTags,
@@ -2151,6 +2223,7 @@
contentDoc: doc, contentWin, dark,
noCardTitle: true,
overrideTags: ['收藏'],
+ mainReactions: itemReactions,
});
}
} else if (blogLink) {
@@ -2205,6 +2278,7 @@
dark,
charImageUrl, badgeLabel, topicTitle,
overrideTags,
+ mainReactions: itemReactions,
});
}
}
@@ -2273,6 +2347,8 @@
const statusUrl = tmlItem.querySelector('.tml_comment')?.href;
const statusUrlParsed = statusUrl ? new URL(statusUrl) : null;
const replyCount = tmlItem.querySelectorAll('.subReply .reply_item').length;
+ const itemReactions = extractReactions(tmlItem, doc);
+ const replyReactions = extractReactions(replyLi, doc);
if (isCollect) {
const subjectLink = tmlItem.querySelector('a[data-subject-id]');
@@ -2328,8 +2404,8 @@
: { 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: '' },
+ { username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating, reactions: itemReactions },
+ { username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '', reactions: replyReactions },
],
charImageUrl: subjectData?.imageUrl || '',
badgeLabel: subjectData?.type || '作品',
@@ -2345,7 +2421,8 @@
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
: contentWin,
dark, noCardTitle: true,
- replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
+ mainReactions: itemReactions,
+ replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '', reactions: replyReactions }],
overrideTags: ['收藏'],
});
}
@@ -2359,7 +2436,8 @@
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
: contentWin,
dark, noCardTitle: true,
- replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
+ mainReactions: itemReactions,
+ replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '', reactions: replyReactions }],
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
});
}