feat(Bangumi_Topic_Share): add timeline blog/collection/reply share support and fix CJK rendering, bump version to 6.2
- Add share buttons and cards for timeline blog (日志) items via page fetch
- Add share buttons for timeline 吐槽 and 收藏 reply items
- Collection reply card matches main collection card style (subject cover + nested replies)
- Add via (web/mobile/app) display in timeline card postTime
- Fix blog single-subject card: title/text copy was using subject name instead of blog title
- Fix collection timeline text copy URL pointing to timeline instead of subject page
- Collection text copy: title now "{user}的收藏 - {subject}", link points to status entry page
- Status page (/timeline/status/*): detect 吐槽 vs 收藏 and render appropriate card/text copy
- Fix collection status tag trailing "了" (搁置了 → 搁置)
- Fix html2canvas CJK full-width punctuation overlap by wrapping brackets as inline-block spans
- Fix blog card content-box using <p> auto-closed by nested <div> (changed to <div>)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+666
-14
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Bangumi Topic Share
|
// @name Bangumi Topic Share
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 6.1
|
// @version 6.2
|
||||||
// @description Bangumi 话题/日志分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签
|
// @description Bangumi 话题/日志分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签
|
||||||
// @author Stardream
|
// @author Stardream
|
||||||
// @contributor Chang ji, Mewtw0
|
// @contributor Chang ji, Mewtw0
|
||||||
@@ -41,6 +41,15 @@
|
|||||||
// @match *://bgm.tv/book/list/*
|
// @match *://bgm.tv/book/list/*
|
||||||
// @match *://bangumi.tv/book/list/*
|
// @match *://bangumi.tv/book/list/*
|
||||||
// @match *://chii.in/book/list/*
|
// @match *://chii.in/book/list/*
|
||||||
|
// @match *://bgm.tv/user/*/timeline/status/*
|
||||||
|
// @match *://bangumi.tv/user/*/timeline/status/*
|
||||||
|
// @match *://chii.in/user/*/timeline/status/*
|
||||||
|
// @match *://bgm.tv/user/*/timeline
|
||||||
|
// @match *://bangumi.tv/user/*/timeline
|
||||||
|
// @match *://chii.in/user/*/timeline
|
||||||
|
// @match *://bgm.tv/timeline
|
||||||
|
// @match *://bangumi.tv/timeline
|
||||||
|
// @match *://chii.in/timeline
|
||||||
// @match *://bgm.tv/
|
// @match *://bgm.tv/
|
||||||
// @match *://bangumi.tv/
|
// @match *://bangumi.tv/
|
||||||
// @match *://chii.in/
|
// @match *://chii.in/
|
||||||
@@ -187,7 +196,7 @@
|
|||||||
.main-title { font-size: 20px; color: #111; margin: 0 0 15px 0; line-height: 1.5; font-weight: 800; }
|
.main-title { font-size: 20px; color: #111; margin: 0 0 15px 0; line-height: 1.5; font-weight: 800; }
|
||||||
.topic-sub-title { font-size: 16px; color: #333; margin: 0 0 14px; font-weight: 700; line-height: 1.4; }
|
.topic-sub-title { font-size: 16px; color: #333; margin: 0 0 14px; font-weight: 700; line-height: 1.4; }
|
||||||
.content-box { background: #fdfafb; padding: 18px; border-radius: 12px; border-left: 5px solid #F09199; }
|
.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; word-break: break-all; }
|
.content-text { font-size: 14px; color: #333; line-height: 1.8; margin: 0; word-break: break-all; font-kerning: none; }
|
||||||
.tags-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
.tags-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
.share-card .tags-container { margin-top: 15px; }
|
.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; }
|
.tag-item { background: #FEEFF0; color: #F09199; font-size: 11px; padding: 4px 12px; border-radius: 20px; font-weight: bold; border: 1px solid #F0919944; }
|
||||||
@@ -431,7 +440,7 @@
|
|||||||
// contentDoc: the document containing the topic (may be an iframe's doc on Rakuen)
|
// 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)
|
// Overlay is always rendered in the outer document (where GM functions are available)
|
||||||
async function _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark,
|
async function _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark,
|
||||||
replies = [], replyId = '', charImageUrl = '', badgeLabel = '', overrideTags = null, topicTitle = '' }) {
|
replies = [], replyId = '', charImageUrl = '', badgeLabel = '', overrideTags = null, topicTitle = '', noCardTitle = false, shareTitle = '' }) {
|
||||||
if (document.getElementById('bgm-share-overlay')) return;
|
if (document.getElementById('bgm-share-overlay')) return;
|
||||||
if (typeof html2canvas === 'undefined') {
|
if (typeof html2canvas === 'undefined') {
|
||||||
alert("截图库加载失败,请刷新页面或检查网络。");
|
alert("截图库加载失败,请刷新页面或检查网络。");
|
||||||
@@ -524,9 +533,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
${(base64CharImage || badgeLabel) ? '' : `<h1 class="main-title">${pureTitle}</h1>`}
|
${(base64CharImage || badgeLabel || noCardTitle) ? '' : `<h1 class="main-title">${pureTitle}</h1>`}
|
||||||
${topicTitle ? `<h2 class="topic-sub-title">${topicTitle}</h2>` : ''}
|
${topicTitle ? `<h2 class="topic-sub-title">${topicTitle}</h2>` : ''}
|
||||||
${inlinedMainContent ? `<div class="content-box"><p class="content-text">${inlinedMainContent}</p></div>` : ''}
|
${inlinedMainContent ? `<div class="content-box"><div class="content-text">${inlinedMainContent}</div></div>` : ''}
|
||||||
${replySection}
|
${replySection}
|
||||||
<div class="tags-container">${tagsHtml}</div>
|
<div class="tags-container">${tagsHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -553,9 +562,6 @@
|
|||||||
<button id="bgm-mask-btn" class="bgm-action-btn" data-tip="显示剧透">
|
<button id="bgm-mask-btn" class="bgm-action-btn" data-tip="显示剧透">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="bgm-close-btn" class="bgm-action-btn" data-tip="关闭">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -588,10 +594,12 @@
|
|||||||
};
|
};
|
||||||
updateMaskPreview();
|
updateMaskPreview();
|
||||||
|
|
||||||
document.getElementById('bgm-close-btn').addEventListener('click', () => {
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
maskPreviewStyle.remove();
|
maskPreviewStyle.remove();
|
||||||
overlay.remove();
|
overlay.remove();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const showToast = (msg) => {
|
const showToast = (msg) => {
|
||||||
@@ -603,7 +611,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('bgm-text-btn').addEventListener('click', async () => {
|
document.getElementById('bgm-text-btn').addEventListener('click', async () => {
|
||||||
const shareText = `【链接】${pureTitle} | Bangumi番组计划\n${shareUrl}`;
|
const shareText = `【链接】${shareTitle || topicTitle || pureTitle} | Bangumi番组计划\n${shareUrl}`;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(shareText);
|
await navigator.clipboard.writeText(shareText);
|
||||||
showToast('✓ 文案已复制');
|
showToast('✓ 文案已复制');
|
||||||
@@ -656,7 +664,7 @@
|
|||||||
const iDoc = iframe.contentDocument;
|
const iDoc = iframe.contentDocument;
|
||||||
const iStyle = iDoc.createElement('style');
|
const iStyle = iDoc.createElement('style');
|
||||||
const maskCss = maskRevealed ? '[data-bgm-mask] { color: #fff !important; }' : '';
|
const maskCss = maskRevealed ? '[data-bgm-mask] { color: #fff !important; }' : '';
|
||||||
const sampleLink = contentDoc.querySelector('.topic_content a[href], #entry_content a[href], .message a[href]')
|
const sampleLink = contentDoc.querySelector('.topic_content a[href], #entry_content a[href], .message a[href], .statusContent .text a[href], .subReply a.l[href]')
|
||||||
|| contentDoc.querySelector('a[href^="http"]')
|
|| contentDoc.querySelector('a[href^="http"]')
|
||||||
|| contentDoc.querySelector('a[href]');
|
|| contentDoc.querySelector('a[href]');
|
||||||
const rawLinkColor = sampleLink ? getComputedStyle(sampleLink).color : '';
|
const rawLinkColor = sampleLink ? getComputedStyle(sampleLink).color : '';
|
||||||
@@ -697,7 +705,7 @@
|
|||||||
if (bodyColor) quoteCss += `.content-text{color:${bodyColor} !important;}`;
|
if (bodyColor) quoteCss += `.content-text{color:${bodyColor} !important;}`;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
iStyle.textContent = style.innerHTML + maskCss + quoteCss + ` a { color: ${linkColor}; text-decoration: ${sampleLinkDecoration}; } span[style*="line-through"], span[style*="line-through"] * { text-decoration: line-through !important; text-decoration-color: white !important; }`;
|
iStyle.textContent = style.innerHTML + maskCss + quoteCss + ` a { color: ${linkColor} !important; text-decoration: ${sampleLinkDecoration}; } span[style*="line-through"], span[style*="line-through"] * { text-decoration: line-through !important; text-decoration-color: white !important; }`;
|
||||||
iDoc.head.appendChild(iStyle);
|
iDoc.head.appendChild(iStyle);
|
||||||
iDoc.body.style.cssText = 'margin:0;padding:0;background:transparent;display:inline-block;';
|
iDoc.body.style.cssText = 'margin:0;padding:0;background:transparent;display:inline-block;';
|
||||||
iDoc.body.innerHTML = captureEl.innerHTML;
|
iDoc.body.innerHTML = captureEl.innerHTML;
|
||||||
@@ -722,6 +730,38 @@
|
|||||||
el.style.backgroundImage = 'none';
|
el.style.backgroundImage = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// html2canvas undercounts advance widths for full-width CJK punctuation (《(【etc.)
|
||||||
|
// because canvas.measureText() returns only the glyph width, not the full advance.
|
||||||
|
// Wrapping each as display:inline-block with width:1em forces html2canvas to read
|
||||||
|
// the DOM layout box instead of measureText, giving the correct advance.
|
||||||
|
const CJK_PUNCT_RE = /[《》〈〉「」【】〔〕〖〗(){}]/;
|
||||||
|
const CJK_PUNCT_SPLIT_RE = /(《|》|〈|〉|「|」|【|】|〔|〕|〖|〗|(|)|{|})/u;
|
||||||
|
iDoc.querySelectorAll('.content-text').forEach(contentEl => {
|
||||||
|
const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT, null);
|
||||||
|
const textNodes = [];
|
||||||
|
let tn;
|
||||||
|
while (tn = walker.nextNode()) {
|
||||||
|
if (CJK_PUNCT_RE.test(tn.textContent)) textNodes.push(tn);
|
||||||
|
}
|
||||||
|
textNodes.forEach(tn => {
|
||||||
|
const parts = tn.textContent.split(CJK_PUNCT_SPLIT_RE);
|
||||||
|
if (parts.length <= 1) return;
|
||||||
|
const frag = iDoc.createDocumentFragment();
|
||||||
|
parts.forEach(part => {
|
||||||
|
if (!part) return;
|
||||||
|
if (CJK_PUNCT_RE.test(part) && part.length === 1) {
|
||||||
|
const sp = iDoc.createElement('span');
|
||||||
|
sp.style.cssText = 'display:inline-block;width:1em;text-align:center;';
|
||||||
|
sp.textContent = part;
|
||||||
|
frag.appendChild(sp);
|
||||||
|
} else {
|
||||||
|
frag.appendChild(iDoc.createTextNode(part));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tn.parentNode.replaceChild(frag, tn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
canvas = await Promise.race([
|
canvas = await Promise.race([
|
||||||
html2canvas(iDoc.body.firstElementChild, { scale: 2, backgroundColor: null }),
|
html2canvas(iDoc.body.firstElementChild, { scale: 2, backgroundColor: null }),
|
||||||
timeout
|
timeout
|
||||||
@@ -1444,7 +1484,7 @@
|
|||||||
const postActions = targetDoc.querySelector('.entry-actions .post_actions')
|
const postActions = targetDoc.querySelector('.entry-actions .post_actions')
|
||||||
|| targetDoc.querySelector('.postTopic .post_actions:not(.re_info)')
|
|| targetDoc.querySelector('.postTopic .post_actions:not(.re_info)')
|
||||||
|| targetDoc.querySelector('[id^="post_"] .post_actions:not(.re_info)')
|
|| targetDoc.querySelector('[id^="post_"] .post_actions:not(.re_info)')
|
||||||
|| [...targetDoc.querySelectorAll('.post_actions:not(.re_info)')].find(el => !el.closest('#sliderContainer') && !el.closest('#comment_box') && !el.closest('#timeline'));
|
|| [...targetDoc.querySelectorAll('.post_actions:not(.re_info)')].find(el => !el.closest('#sliderContainer') && !el.closest('#comment_box') && !el.closest('#timeline') && !el.closest('.statusContent'));
|
||||||
if (postActions) {
|
if (postActions) {
|
||||||
const wrap = targetDoc.createElement('span');
|
const wrap = targetDoc.createElement('span');
|
||||||
wrap.className = 'action';
|
wrap.className = 'action';
|
||||||
@@ -1766,6 +1806,618 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getStatusAuthorInfo(doc) {
|
||||||
|
const usernameEl = doc.querySelector('.statusHeader h3 a');
|
||||||
|
const username = usernameEl?.textContent.trim() || '';
|
||||||
|
const avatarSpan = doc.querySelector('.statusHeader .avatarNeue');
|
||||||
|
let avatarUrl = '';
|
||||||
|
const bgImg = avatarSpan?.style.backgroundImage;
|
||||||
|
if (bgImg) {
|
||||||
|
const m = bgImg.match(/url\(['"]?([^'"]+)['"]?\)/);
|
||||||
|
if (m) avatarUrl = m[1].replace(/^\/\//, 'https://');
|
||||||
|
}
|
||||||
|
const timeEl = doc.querySelector('.statusContent .post_actions .tip_j');
|
||||||
|
const postTime = timeEl?.textContent.trim() || '';
|
||||||
|
return { username, avatarUrl, postTime };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime) {
|
||||||
|
const statusContent = doc.querySelector('.statusContent');
|
||||||
|
const subjectLink = statusContent?.querySelector('a[data-subject-id]');
|
||||||
|
const subjectId = subjectLink?.getAttribute('data-subject-id');
|
||||||
|
const collectComment = statusContent?.querySelector('.comment');
|
||||||
|
if (!subjectId || !collectComment) return null;
|
||||||
|
|
||||||
|
const userSlug = contentWin.location.pathname.match(/\/user\/([^\/]+)\//)?.[1] || '';
|
||||||
|
let collectionStatus = '';
|
||||||
|
if (subjectLink) {
|
||||||
|
let node = subjectLink.previousSibling;
|
||||||
|
while (node) {
|
||||||
|
if (node.nodeType === 3 && node.textContent.trim()) { collectionStatus = node.textContent.trim().replace(/了$/, ''); break; }
|
||||||
|
node = node.previousSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ratingEl = collectComment.querySelector('.starstop-s .starlight');
|
||||||
|
const domRating = parseInt(ratingEl?.className.match(/stars(\d+)/)?.[1] || '0');
|
||||||
|
const cleanCommentHtml = (() => {
|
||||||
|
const tmp = document.createElement('div');
|
||||||
|
tmp.innerHTML = collectComment.innerHTML;
|
||||||
|
tmp.querySelector('.starstop-s')?.remove();
|
||||||
|
return tmp.innerHTML.trim();
|
||||||
|
})();
|
||||||
|
const subjectData = await fetchSubjectDataById(subjectId);
|
||||||
|
const subjectName = subjectData?.name || subjectLink?.textContent.trim() || '作品';
|
||||||
|
let rating = domRating, userTags = null, apiTime = postTime;
|
||||||
|
if (userSlug) {
|
||||||
|
const collection = await fetchBangumiAPI(`users/${userSlug}/collections/${subjectId}`);
|
||||||
|
if (collection) {
|
||||||
|
rating = collection.rate || rating;
|
||||||
|
if (collection.updated_at) {
|
||||||
|
const d = new Date(collection.updated_at);
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
apiTime = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
if (collection.tags?.length > 0) userTags = collection.tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const infoboxTags = (SUBJECT_INFOBOX_KEYS[subjectData?.typeNum] || [])
|
||||||
|
.map(key => getInfoboxValue(subjectData?.infobox, key)).filter(Boolean);
|
||||||
|
const overrideTags = userTags
|
||||||
|
? [collectionStatus, ...userTags].filter(Boolean)
|
||||||
|
: [collectionStatus, ...infoboxTags].filter(Boolean);
|
||||||
|
return { subjectData, subjectName, collectComment, cleanCommentHtml, username, avatarUrl, apiTime, rating, overrideTags };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createStatusShareImage(doc = document) {
|
||||||
|
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||||||
|
const contentWin = doc.defaultView || window;
|
||||||
|
const { username, avatarUrl, postTime } = _getStatusAuthorInfo(doc);
|
||||||
|
const replyCount = doc.querySelectorAll('.subReply .reply_item').length;
|
||||||
|
|
||||||
|
const collectInfo = await _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime);
|
||||||
|
if (collectInfo) {
|
||||||
|
const { subjectData, subjectName, collectComment, cleanCommentHtml, apiTime, rating, overrideTags } = collectInfo;
|
||||||
|
await _doShareCard({
|
||||||
|
username: '', postTime: '', avatarUrl: '', contentEl: null,
|
||||||
|
pureTitle: subjectName,
|
||||||
|
shareTitle: `${username}的收藏 - ${subjectName}`,
|
||||||
|
contentDoc: doc, contentWin, dark,
|
||||||
|
replies: [{ username, avatarUrl, content: collectComment.innerText?.trim() || '', contentHtml: cleanCommentHtml, time: apiTime, rating }],
|
||||||
|
charImageUrl: subjectData?.imageUrl || '',
|
||||||
|
badgeLabel: subjectData?.type || '作品',
|
||||||
|
overrideTags,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const contentEl = doc.querySelector('.statusContent .text');
|
||||||
|
await _doShareCard({
|
||||||
|
username, postTime, avatarUrl, contentEl,
|
||||||
|
pureTitle: username + '的吐槽', contentDoc: doc, contentWin, dark,
|
||||||
|
noCardTitle: true,
|
||||||
|
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createStatusReplyShareImage(replyLi, doc = document) {
|
||||||
|
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||||||
|
const contentWin = doc.defaultView || window;
|
||||||
|
const { username, avatarUrl, postTime } = _getStatusAuthorInfo(doc);
|
||||||
|
|
||||||
|
const replyUserEl = replyLi.querySelector('a.l[href*="/user/"]');
|
||||||
|
const replyUsername = replyUserEl?.textContent.trim() || '';
|
||||||
|
const replySlug = replyUserEl?.getAttribute('href')?.match(/\/user\/([^\/]+)/)?.[1] || '';
|
||||||
|
let replyAvatarUrl = '';
|
||||||
|
if (replySlug) {
|
||||||
|
const userInfo = await fetchBangumiAPI(`users/${replySlug}`);
|
||||||
|
replyAvatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||||||
|
}
|
||||||
|
let replyHtml = '';
|
||||||
|
let afterDash = false;
|
||||||
|
replyLi.childNodes.forEach(n => {
|
||||||
|
if (!afterDash) {
|
||||||
|
if (n.nodeType === 1 && n.tagName === 'SPAN' && n.classList.contains('tip_j')) afterDash = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n.nodeType === 1 && n.classList?.contains(STATUS_REPLY_SHARE_BTN_CLASS)) return;
|
||||||
|
replyHtml += n.nodeType === 3 ? n.textContent : n.outerHTML;
|
||||||
|
});
|
||||||
|
|
||||||
|
const replyCount = doc.querySelectorAll('.subReply .reply_item').length;
|
||||||
|
const collectInfo = await _buildCollectionStatusInfo(doc, contentWin, username, avatarUrl, postTime);
|
||||||
|
if (collectInfo) {
|
||||||
|
const { subjectData, subjectName, collectComment, cleanCommentHtml, apiTime, rating, overrideTags } = collectInfo;
|
||||||
|
await _doShareCard({
|
||||||
|
username: '', postTime: '', avatarUrl: '', contentEl: null,
|
||||||
|
pureTitle: subjectName,
|
||||||
|
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: '' },
|
||||||
|
],
|
||||||
|
charImageUrl: subjectData?.imageUrl || '',
|
||||||
|
badgeLabel: subjectData?.type || '作品',
|
||||||
|
overrideTags,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const contentEl = doc.querySelector('.statusContent .text');
|
||||||
|
await _doShareCard({
|
||||||
|
username, postTime, avatarUrl, contentEl,
|
||||||
|
pureTitle: username + '的吐槽', contentDoc: doc, contentWin, dark,
|
||||||
|
noCardTitle: true,
|
||||||
|
replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
|
||||||
|
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_REPLY_SHARE_BTN_CLASS = 'bgm-status-reply-share-btn';
|
||||||
|
|
||||||
|
const insertStatusReplyButtons = (doc = document) => {
|
||||||
|
if (!/\/user\/[^\/]+\/timeline\/status\/\d+/.test((doc.defaultView || window).location.pathname)) return;
|
||||||
|
doc.querySelectorAll('.subReply .reply_item').forEach(li => {
|
||||||
|
if (li.querySelector('.' + STATUS_REPLY_SHARE_BTN_CLASS)) return;
|
||||||
|
const btn = doc.createElement('a');
|
||||||
|
btn.href = 'javascript:void(0);';
|
||||||
|
btn.className = STATUS_REPLY_SHARE_BTN_CLASS;
|
||||||
|
btn.title = '分享回复';
|
||||||
|
btn.style.cssText = 'display:inline-flex;align-items:center;gap:3px;margin-left:6px;vertical-align:middle;opacity:0.6;';
|
||||||
|
btn.innerHTML = SHARE_SVG;
|
||||||
|
li.appendChild(btn);
|
||||||
|
btn.addEventListener('click', () => createStatusReplyShareImage(li, doc));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertStatusShareButton = (doc = document) => {
|
||||||
|
if (!/\/user\/[^\/]+\/timeline\/status\/\d+/.test((doc.defaultView || window).location.pathname)) return;
|
||||||
|
if (doc.getElementById('bgm-status-share-btn')) return;
|
||||||
|
const postActions = doc.querySelector('.statusContent .post_actions');
|
||||||
|
if (!postActions) return;
|
||||||
|
const btn = doc.createElement('a');
|
||||||
|
btn.id = 'bgm-status-share-btn';
|
||||||
|
btn.href = 'javascript:void(0);';
|
||||||
|
btn.title = '分享吐槽';
|
||||||
|
btn.style.cssText = 'color:inherit;display:inline-flex;align-items:center;margin-right:6px;opacity:0.75;';
|
||||||
|
btn.innerHTML = SHARE_SVG;
|
||||||
|
const dropdown = postActions.querySelector('.action.dropdown');
|
||||||
|
dropdown ? dropdown.after(btn) : postActions.appendChild(btn);
|
||||||
|
btn.addEventListener('click', () => createStatusShareImage(doc));
|
||||||
|
};
|
||||||
|
|
||||||
|
const TML_SHARE_BTN_CLASS = 'bgm-tml-share-btn';
|
||||||
|
|
||||||
|
function fetchStatusReplyCount(statusUrl) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: statusUrl,
|
||||||
|
onload: res => {
|
||||||
|
if (res.status !== 200) { resolve(0); return; }
|
||||||
|
const statusDoc = new DOMParser().parseFromString(res.responseText, 'text/html');
|
||||||
|
resolve(statusDoc.querySelectorAll('.subReply .reply_item').length);
|
||||||
|
},
|
||||||
|
onerror: () => resolve(0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTimelineVia(postActionsEl) {
|
||||||
|
if (!postActionsEl) return '';
|
||||||
|
const smallEl = postActionsEl.querySelector('small.grey');
|
||||||
|
if (smallEl) return smallEl.textContent.trim();
|
||||||
|
const titleTip = postActionsEl.querySelector('.titleTip');
|
||||||
|
let node = titleTip ? titleTip.nextSibling : null;
|
||||||
|
while (node) {
|
||||||
|
if (node.nodeType === 3) {
|
||||||
|
const text = node.textContent.replace(/[\u00B7·\s]/g, '').trim();
|
||||||
|
if (text) return text;
|
||||||
|
}
|
||||||
|
node = node.nextSibling;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTimelineShareImage(item, doc = document) {
|
||||||
|
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||||||
|
const contentWin = doc.defaultView || window;
|
||||||
|
let userSlug = item.getAttribute('data-item-user') || '';
|
||||||
|
if (!userSlug) {
|
||||||
|
// Own-user timeline has data-item-user="" — extract from tml_comment href or page URL
|
||||||
|
userSlug = item.querySelector('.tml_comment')?.href?.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||||||
|
|| (doc.defaultView || window).location.pathname.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||||||
|
|| '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLink = item.querySelector('.info a.l[href*="/user/"], .info_full a.l[href*="/user/"]');
|
||||||
|
let username = userLink?.textContent.trim() || '';
|
||||||
|
|
||||||
|
const _extractNeueAvatar = el => {
|
||||||
|
if (!el) return '';
|
||||||
|
const m = el.style.backgroundImage?.match(/url\(['"]?([^'"]+)['"]?\)/);
|
||||||
|
return m ? m[1].replace(/^\/\//, 'https://') : '';
|
||||||
|
};
|
||||||
|
let avatarUrl = _extractNeueAvatar(item.querySelector('.avatar .avatarNeue'));
|
||||||
|
if (!avatarUrl && userSlug) {
|
||||||
|
const siblingNeue = doc.querySelector(`.tml_item[data-item-user="${CSS.escape(userSlug)}"] .avatar .avatarNeue`);
|
||||||
|
avatarUrl = _extractNeueAvatar(siblingNeue);
|
||||||
|
}
|
||||||
|
// For .info_full items (timeline owner's entries), there is no username link or avatar span —
|
||||||
|
// always fetch from API so username is authoritative (not a stale DOM fallback).
|
||||||
|
const isInfoFull = !!item.querySelector('.info_full');
|
||||||
|
if ((isInfoFull || !avatarUrl || !username) && userSlug) {
|
||||||
|
const userInfo = await fetchBangumiAPI(`users/${userSlug}`);
|
||||||
|
if (!avatarUrl) avatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||||||
|
if (isInfoFull || !username) username = userInfo?.nickname || userInfo?.username || username;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeTip = item.querySelector('.post_actions .titleTip');
|
||||||
|
const postTimeRaw = timeTip?.getAttribute('data-original-title') || timeTip?.textContent.trim() || '';
|
||||||
|
const via = extractTimelineVia(item.querySelector('.post_actions'));
|
||||||
|
const postTime = postTimeRaw + (via ? ` via ${via}` : '');
|
||||||
|
|
||||||
|
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/"]');
|
||||||
|
|
||||||
|
if (statusP && statusP.textContent.trim()) {
|
||||||
|
const statusUrl = item.querySelector('.tml_comment')?.href;
|
||||||
|
const replyCount = statusUrl ? await fetchStatusReplyCount(statusUrl) : 0;
|
||||||
|
await _doShareCard({
|
||||||
|
username, postTime, avatarUrl,
|
||||||
|
contentEl: statusP,
|
||||||
|
pureTitle: username + '的吐槽',
|
||||||
|
contentDoc: doc, contentWin, dark,
|
||||||
|
noCardTitle: true,
|
||||||
|
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||||||
|
});
|
||||||
|
} else if (collectComment && collectComment.textContent.trim()) {
|
||||||
|
// Subject link is in .info, carries data-subject-id attribute
|
||||||
|
const subjectLink = item.querySelector('a[data-subject-id]');
|
||||||
|
const subjectId = subjectLink?.getAttribute('data-subject-id');
|
||||||
|
|
||||||
|
// Collection status ("看过"/"在看" etc.) is in the text node just before the subject link
|
||||||
|
let collectionStatus = '';
|
||||||
|
if (subjectLink) {
|
||||||
|
let node = subjectLink.previousSibling;
|
||||||
|
while (node) {
|
||||||
|
if (node.nodeType === 3 && node.textContent.trim()) {
|
||||||
|
collectionStatus = node.textContent.trim().replace(/了$/, '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
node = node.previousSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract user rating and clean comment HTML (remove star span)
|
||||||
|
const ratingEl = collectComment.querySelector('.starstop-s .starlight');
|
||||||
|
const domRating = parseInt(ratingEl?.className.match(/stars(\d+)/)?.[1] || '0');
|
||||||
|
const cleanCommentHtml = (() => {
|
||||||
|
const tmp = document.createElement('div');
|
||||||
|
tmp.innerHTML = collectComment.innerHTML;
|
||||||
|
tmp.querySelector('.starstop-s')?.remove();
|
||||||
|
return tmp.innerHTML.trim();
|
||||||
|
})();
|
||||||
|
|
||||||
|
const statusUrl = item.querySelector('.tml_comment')?.href;
|
||||||
|
const statusUrlParsed = statusUrl ? new URL(statusUrl) : null;
|
||||||
|
|
||||||
|
if (subjectId) {
|
||||||
|
const subjectData = await fetchSubjectDataById(subjectId);
|
||||||
|
const subjectName = subjectData?.name || subjectLink?.textContent.trim() || '作品';
|
||||||
|
|
||||||
|
let rating = domRating;
|
||||||
|
let userTags = null;
|
||||||
|
let apiTime = postTime;
|
||||||
|
|
||||||
|
if (userSlug) {
|
||||||
|
const collection = await fetchBangumiAPI(`users/${userSlug}/collections/${subjectId}`);
|
||||||
|
if (collection) {
|
||||||
|
rating = collection.rate || rating;
|
||||||
|
if (collection.updated_at) {
|
||||||
|
const d = new Date(collection.updated_at);
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
apiTime = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` + (via ? ` via ${via}` : '');
|
||||||
|
}
|
||||||
|
if (collection.tags?.length > 0) userTags = collection.tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoboxTags = (SUBJECT_INFOBOX_KEYS[subjectData?.typeNum] || [])
|
||||||
|
.map(key => getInfoboxValue(subjectData?.infobox, key))
|
||||||
|
.filter(Boolean);
|
||||||
|
const overrideTags = userTags
|
||||||
|
? [collectionStatus, ...userTags].filter(Boolean)
|
||||||
|
: [collectionStatus, ...infoboxTags].filter(Boolean);
|
||||||
|
|
||||||
|
await _doShareCard({
|
||||||
|
username: '', postTime: '', avatarUrl: '', contentEl: null,
|
||||||
|
pureTitle: subjectName,
|
||||||
|
shareTitle: `${username}的收藏 - ${subjectName}`,
|
||||||
|
contentDoc: doc,
|
||||||
|
contentWin: statusUrlParsed
|
||||||
|
? { 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 }],
|
||||||
|
charImageUrl: subjectData?.imageUrl || '',
|
||||||
|
badgeLabel: subjectData?.type || '作品',
|
||||||
|
overrideTags,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await _doShareCard({
|
||||||
|
username, postTime, avatarUrl,
|
||||||
|
contentEl: collectComment,
|
||||||
|
pureTitle: username + '的收藏',
|
||||||
|
contentDoc: doc, contentWin, dark,
|
||||||
|
noCardTitle: true,
|
||||||
|
overrideTags: ['收藏'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (blogLink) {
|
||||||
|
const blogUrl = blogLink.href;
|
||||||
|
const blogHtml = await new Promise(resolve => {
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
method: 'GET', url: blogUrl,
|
||||||
|
onload: res => resolve(res.status === 200 ? res.responseText : null),
|
||||||
|
onerror: () => resolve(null),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const blogDoc = blogHtml ? new DOMParser().parseFromString(blogHtml, 'text/html') : null;
|
||||||
|
const entryContent = blogDoc?.querySelector('#entry_content');
|
||||||
|
let contentEl = null;
|
||||||
|
if (entryContent) {
|
||||||
|
contentEl = document.createElement('div');
|
||||||
|
contentEl.innerHTML = entryContent.innerHTML;
|
||||||
|
}
|
||||||
|
let pureTitle = blogLink.textContent.trim();
|
||||||
|
let topicTitle = '';
|
||||||
|
let charImageUrl = '';
|
||||||
|
let badgeLabel = '';
|
||||||
|
const blogSubjectIds = blogDoc ? [...new Set(
|
||||||
|
[...blogDoc.querySelectorAll('a[href]')]
|
||||||
|
.map(a => (a.getAttribute('href') || '').match(/\/subject\/(\d+)$/)?.[1])
|
||||||
|
.filter(Boolean)
|
||||||
|
)] : [];
|
||||||
|
if (blogSubjectIds.length === 1) {
|
||||||
|
const subjectData = await fetchSubjectDataById(blogSubjectIds[0]);
|
||||||
|
if (subjectData) {
|
||||||
|
topicTitle = pureTitle;
|
||||||
|
pureTitle = subjectData.name || pureTitle;
|
||||||
|
charImageUrl = subjectData.imageUrl || '';
|
||||||
|
badgeLabel = subjectData.type || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const blogReplyCount = blogDoc?.querySelectorAll('[id^="post_"]').length || 0;
|
||||||
|
const subjectNamesFromBlog = blogDoc ? [...new Set(
|
||||||
|
[...blogDoc.querySelectorAll('a')]
|
||||||
|
.filter(a => /\/subject\/\d+$/.test(a.getAttribute('href') || '') && a.textContent.trim())
|
||||||
|
.map(a => a.textContent.trim())
|
||||||
|
)] : [];
|
||||||
|
const tagSubjects = subjectNamesFromBlog.length === 1 ? [] : subjectNamesFromBlog;
|
||||||
|
const overrideTags = [...tagSubjects, `${blogReplyCount} 回复`, '日志'];
|
||||||
|
const blogUrlParsed = new URL(blogUrl);
|
||||||
|
await _doShareCard({
|
||||||
|
username, postTime, avatarUrl,
|
||||||
|
contentEl,
|
||||||
|
pureTitle,
|
||||||
|
contentDoc: doc,
|
||||||
|
contentWin: { location: { origin: blogUrlParsed.origin, pathname: blogUrlParsed.pathname } },
|
||||||
|
dark,
|
||||||
|
charImageUrl, badgeLabel, topicTitle,
|
||||||
|
overrideTags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TML_REPLY_SHARE_BTN_CLASS = 'bgm-tml-reply-share-btn';
|
||||||
|
|
||||||
|
async function createTimelineReplyShareImage(replyLi, tmlItem, doc = document) {
|
||||||
|
const dark = doc.documentElement.getAttribute('data-theme') === 'dark';
|
||||||
|
const contentWin = doc.defaultView || window;
|
||||||
|
|
||||||
|
let userSlug = tmlItem.getAttribute('data-item-user') || '';
|
||||||
|
if (!userSlug) {
|
||||||
|
userSlug = tmlItem.querySelector('.tml_comment')?.href?.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||||||
|
|| (doc.defaultView || window).location.pathname.match(/\/user\/([^\/]+)\/timeline/)?.[1]
|
||||||
|
|| '';
|
||||||
|
}
|
||||||
|
const userLink = tmlItem.querySelector('.info a.l[href*="/user/"], .info_full a.l[href*="/user/"]');
|
||||||
|
let username = userLink?.textContent.trim() || '';
|
||||||
|
const _extractNeueAvatar = el => {
|
||||||
|
if (!el) return '';
|
||||||
|
const m = el.style.backgroundImage?.match(/url\(['"]?([^'"]+)['"]?\)/);
|
||||||
|
return m ? m[1].replace(/^\/\//, 'https://') : '';
|
||||||
|
};
|
||||||
|
let avatarUrl = _extractNeueAvatar(tmlItem.querySelector('.avatar .avatarNeue'));
|
||||||
|
if (!avatarUrl && userSlug) {
|
||||||
|
const siblingNeue = doc.querySelector(`.tml_item[data-item-user="${CSS.escape(userSlug)}"] .avatar .avatarNeue`);
|
||||||
|
avatarUrl = _extractNeueAvatar(siblingNeue);
|
||||||
|
}
|
||||||
|
const isInfoFull = !!tmlItem.querySelector('.info_full');
|
||||||
|
if ((isInfoFull || !avatarUrl || !username) && userSlug) {
|
||||||
|
const userInfo = await fetchBangumiAPI(`users/${userSlug}`);
|
||||||
|
if (!avatarUrl) avatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||||||
|
if (isInfoFull || !username) username = userInfo?.nickname || userInfo?.username || username;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusP = tmlItem.querySelector('.info p.status, .info_full p.status');
|
||||||
|
const collectComment = tmlItem.querySelector('.collectInfo .comment');
|
||||||
|
const contentEl = statusP || collectComment;
|
||||||
|
const isCollect = !statusP && !!collectComment;
|
||||||
|
|
||||||
|
const timeTip = tmlItem.querySelector('.post_actions .titleTip');
|
||||||
|
const postTimeRaw = timeTip?.getAttribute('data-original-title') || timeTip?.textContent.trim() || '';
|
||||||
|
const via = extractTimelineVia(tmlItem.querySelector('.post_actions'));
|
||||||
|
const postTime = postTimeRaw + (via ? ` via ${via}` : '');
|
||||||
|
|
||||||
|
const replyUserEl = replyLi.querySelector('a.l[href*="/user/"]');
|
||||||
|
const replyUsername = replyUserEl?.textContent.trim() || '';
|
||||||
|
const replySlug = replyUserEl?.getAttribute('href')?.match(/\/user\/([^\/]+)/)?.[1] || '';
|
||||||
|
let replyAvatarUrl = '';
|
||||||
|
if (replySlug) {
|
||||||
|
const userInfo = await fetchBangumiAPI(`users/${replySlug}`);
|
||||||
|
replyAvatarUrl = userInfo?.avatar?.medium || userInfo?.avatar?.large || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let replyHtml = '';
|
||||||
|
let afterDash = false;
|
||||||
|
replyLi.childNodes.forEach(n => {
|
||||||
|
if (!afterDash) {
|
||||||
|
if (n.nodeType === 1 && n.tagName === 'SPAN' && n.classList.contains('tip_j')) afterDash = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n.nodeType === 1 && n.classList?.contains(TML_REPLY_SHARE_BTN_CLASS)) return;
|
||||||
|
replyHtml += n.nodeType === 3 ? n.textContent : n.outerHTML;
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusUrl = tmlItem.querySelector('.tml_comment')?.href;
|
||||||
|
const statusUrlParsed = statusUrl ? new URL(statusUrl) : null;
|
||||||
|
const replyCount = tmlItem.querySelectorAll('.subReply .reply_item').length;
|
||||||
|
|
||||||
|
if (isCollect) {
|
||||||
|
const subjectLink = tmlItem.querySelector('a[data-subject-id]');
|
||||||
|
const subjectId = subjectLink?.getAttribute('data-subject-id');
|
||||||
|
let collectionStatus = '';
|
||||||
|
if (subjectLink) {
|
||||||
|
let node = subjectLink.previousSibling;
|
||||||
|
while (node) {
|
||||||
|
if (node.nodeType === 3 && node.textContent.trim()) { collectionStatus = node.textContent.trim().replace(/了$/, ''); break; }
|
||||||
|
node = node.previousSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ratingEl = collectComment.querySelector('.starstop-s .starlight');
|
||||||
|
const domRating = parseInt(ratingEl?.className.match(/stars(\d+)/)?.[1] || '0');
|
||||||
|
const cleanCommentHtml = (() => {
|
||||||
|
const tmp = document.createElement('div');
|
||||||
|
tmp.innerHTML = collectComment.innerHTML;
|
||||||
|
tmp.querySelector('.starstop-s')?.remove();
|
||||||
|
return tmp.innerHTML.trim();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (subjectId) {
|
||||||
|
const subjectData = await fetchSubjectDataById(subjectId);
|
||||||
|
const subjectName = subjectData?.name || subjectLink?.textContent.trim() || '作品';
|
||||||
|
let rating = domRating;
|
||||||
|
let userTags = null;
|
||||||
|
let apiTime = postTime;
|
||||||
|
if (userSlug) {
|
||||||
|
const collection = await fetchBangumiAPI(`users/${userSlug}/collections/${subjectId}`);
|
||||||
|
if (collection) {
|
||||||
|
rating = collection.rate || rating;
|
||||||
|
if (collection.updated_at) {
|
||||||
|
const d = new Date(collection.updated_at);
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
apiTime = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` + (via ? ` via ${via}` : '');
|
||||||
|
}
|
||||||
|
if (collection.tags?.length > 0) userTags = collection.tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const infoboxTags = (SUBJECT_INFOBOX_KEYS[subjectData?.typeNum] || [])
|
||||||
|
.map(key => getInfoboxValue(subjectData?.infobox, key))
|
||||||
|
.filter(Boolean);
|
||||||
|
const overrideTags = userTags
|
||||||
|
? [collectionStatus, ...userTags].filter(Boolean)
|
||||||
|
: [collectionStatus, ...infoboxTags].filter(Boolean);
|
||||||
|
await _doShareCard({
|
||||||
|
username: '', postTime: '', avatarUrl: '', contentEl: null,
|
||||||
|
pureTitle: subjectName,
|
||||||
|
shareTitle: `${username}的收藏 - ${subjectName}`,
|
||||||
|
contentDoc: doc,
|
||||||
|
contentWin: statusUrlParsed
|
||||||
|
? { 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 },
|
||||||
|
{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' },
|
||||||
|
],
|
||||||
|
charImageUrl: subjectData?.imageUrl || '',
|
||||||
|
badgeLabel: subjectData?.type || '作品',
|
||||||
|
overrideTags,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await _doShareCard({
|
||||||
|
username, postTime, avatarUrl,
|
||||||
|
contentEl: collectComment,
|
||||||
|
pureTitle: username + '的收藏',
|
||||||
|
contentDoc: doc,
|
||||||
|
contentWin: statusUrlParsed
|
||||||
|
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
|
||||||
|
: contentWin,
|
||||||
|
dark, noCardTitle: true,
|
||||||
|
replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
|
||||||
|
overrideTags: ['收藏'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await _doShareCard({
|
||||||
|
username, postTime, avatarUrl,
|
||||||
|
contentEl,
|
||||||
|
pureTitle: username + '的吐槽',
|
||||||
|
contentDoc: doc,
|
||||||
|
contentWin: statusUrlParsed
|
||||||
|
? { location: { origin: statusUrlParsed.origin, pathname: statusUrlParsed.pathname } }
|
||||||
|
: contentWin,
|
||||||
|
dark, noCardTitle: true,
|
||||||
|
replies: [{ username: replyUsername, avatarUrl: replyAvatarUrl, content: replyHtml.trim(), contentHtml: replyHtml.trim(), time: '' }],
|
||||||
|
overrideTags: replyCount ? [`${replyCount} 回复`, '吐槽'] : ['吐槽'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertTimelineShareButtons = (doc = document) => {
|
||||||
|
doc.querySelectorAll('.tml_item').forEach(item => {
|
||||||
|
if (item.querySelector('.' + TML_SHARE_BTN_CLASS)) return;
|
||||||
|
|
||||||
|
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 hasContent = (statusP?.textContent.trim()) || (collectComment?.textContent.trim()) || blogLink;
|
||||||
|
if (!hasContent) return;
|
||||||
|
|
||||||
|
const postActions = item.querySelector('.post_actions');
|
||||||
|
if (!postActions) return;
|
||||||
|
|
||||||
|
const btn = doc.createElement('a');
|
||||||
|
btn.href = 'javascript:void(0);';
|
||||||
|
btn.className = TML_SHARE_BTN_CLASS;
|
||||||
|
btn.title = '分享';
|
||||||
|
btn.style.cssText = 'color:inherit;display:inline-flex;align-items:center;margin-right:6px;opacity:0.75;cursor:pointer;';
|
||||||
|
btn.innerHTML = SHARE_SVG;
|
||||||
|
|
||||||
|
const dropdown = postActions.querySelector('.action.dropdown');
|
||||||
|
dropdown ? dropdown.after(btn) : postActions.prepend(btn);
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => createTimelineShareImage(item, doc));
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.querySelectorAll('.tml_item').forEach(item => {
|
||||||
|
if (!item.querySelector('.info p.status, .info_full p.status') && !item.querySelector('.collectInfo .comment')) return;
|
||||||
|
item.querySelectorAll('.subReply .reply_item').forEach(li => {
|
||||||
|
if (li.querySelector('.' + TML_REPLY_SHARE_BTN_CLASS)) return;
|
||||||
|
const btn = doc.createElement('a');
|
||||||
|
btn.href = 'javascript:void(0);';
|
||||||
|
btn.className = TML_REPLY_SHARE_BTN_CLASS;
|
||||||
|
btn.title = '分享回复';
|
||||||
|
btn.style.cssText = 'display:inline-flex;align-items:center;gap:3px;margin-left:6px;vertical-align:middle;opacity:0.6;cursor:pointer;';
|
||||||
|
btn.innerHTML = SHARE_SVG;
|
||||||
|
li.appendChild(btn);
|
||||||
|
btn.addEventListener('click', () => createTimelineReplyShareImage(li, item, doc));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const observeTimeline = (doc = document) => {
|
||||||
|
const root = doc.querySelector('#columnTimelineA') || doc.querySelector('#main') || doc.body;
|
||||||
|
if (!root) return;
|
||||||
|
let debounce = null;
|
||||||
|
const obs = new MutationObserver(() => {
|
||||||
|
clearTimeout(debounce);
|
||||||
|
debounce = setTimeout(() => insertTimelineShareButtons(doc), 150);
|
||||||
|
});
|
||||||
|
obs.observe(root, { childList: true, subtree: true });
|
||||||
|
};
|
||||||
|
|
||||||
const observeReplies = (targetDoc) => {
|
const observeReplies = (targetDoc) => {
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
insertReplyButtons(targetDoc);
|
insertReplyButtons(targetDoc);
|
||||||
@@ -1794,7 +2446,7 @@
|
|||||||
};
|
};
|
||||||
rightFrame.addEventListener('load', onRightFrameLoad);
|
rightFrame.addEventListener('load', onRightFrameLoad);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => { insertButton(); insertReplyButtons(); insertSubjectCommentButtons(); insertCollectionPageButtons(); insertBrowserListButtons(); insertMyCollectionShareButton(); observeReplies(document); }, 500);
|
setTimeout(() => { insertButton(); insertReplyButtons(); insertSubjectCommentButtons(); insertCollectionPageButtons(); insertBrowserListButtons(); insertMyCollectionShareButton(); insertStatusShareButton(); insertStatusReplyButtons(); insertTimelineShareButtons(); observeTimeline(); observeReplies(document); }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkOAuthCallback();
|
checkOAuthCallback();
|
||||||
|
|||||||
Reference in New Issue
Block a user