// ==UserScript==
// @name Bangumi 小组评论分享图片生成器
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 生成 Bangumi 小组评论分享图片
// @license MIT
// @match https://bgm.tv/group/topic/*
// @match https://bangumi.tv/group/topic/*
// @match https://chii.in/group/topic/*
// @match http://bgm.tv/group/topic/*
// @match http://bangumi.tv/group/topic/*
// @match https://chii.in/group/topic/*
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect bgm.tv
// @connect bangumi.tv
// @connect chii.in
// ==/UserScript==
(function() {
'use strict';
let bangumiLogoDataUrl = null;
// --- v1.1 CSS Styles ---
GM_addStyle(`
/* --- Modal and general UI styles (v1.1 Refined) --- */
.share-script-modal-backdrop {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.65); z-index: 10000;
display: flex; justify-content: center; align-items: center;
}
.share-script-modal-content {
background-color: #f7f7f7; color: #333; padding: 25px;
border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.2);
max-height: 85vh; overflow-y: auto; font-size: 14px;
position: relative; line-height: 1.6;
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
#shareOptionsModalContent { width: 480px; max-width: 90vw; }
#shareOptionsModalContent h3 { margin-top: 0; margin-bottom: 24px; color: #111; border-bottom: 2px solid #e0e0e0; padding-bottom: 12px; font-size: 20px; font-weight: 700; text-align: center; }
/* Option groups for better organization */
.option-group { background: #f8f9fa; border-radius: 10px; padding: 18px; margin-bottom: 16px; border: 1px solid #e9ecef; }
.option-group-title { font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
.option-group-title::before { content: "●"; color: #007bff; font-size: 12px; }
#shareOptionsModalContent label { display: flex; align-items: center; margin-bottom: 10px; cursor: pointer; padding: 4px 0; }
#shareOptionsModalContent input[type="checkbox"] { width: 18px; height: 18px; margin-right: 12px; cursor: pointer; }
#shareOptionsModalContent select, #shareOptionsModalContent button { margin-right: 10px; height: 36px; padding: 0 12px; border: 1px solid #ced4da; border-radius: 6px; }
#shareOptionsModalContent select { background-color: white; color: #111; }
#shareOptionsModalContent .sub-reply-selection { margin-top: 8px; max-height: 200px; overflow-y: auto; border: 1px solid #e0e0e0; padding: 14px; border-radius: 8px; background-color: #fff; }
#shareOptionsModalContent .sub-reply-selection h4 { margin-top: 0; margin-bottom: 12px; font-size: 15px; font-weight: bold; color: #495057; }
#shareOptionsModalContent details { margin-top: 20px; border-top: 1px solid #e0e0e0; padding-top: 18px; }
#shareOptionsModalContent summary { cursor: pointer; font-weight: bold; color: #555; padding: 8px 0; font-size: 15px; }
#shareOptionsModalContent details > div { padding-top: 12px; }
#customCssArea { width: 100%; min-height: 100px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; padding: 12px; border: 1px solid #e0e0e0; border-radius: 8px; display: none; background-color: #f8f9fa; }
/* Improved button layout */
.modal-buttons {
display: flex; justify-content: space-between; align-items: center;
margin-top: 28px; padding-top: 20px; border-top: 2px solid #e9ecef;
}
.modal-buttons-left { display: flex; gap: 10px; }
.modal-buttons-right { display: flex; gap: 10px; }
.modal-buttons button {
padding: 12px 24px; cursor: pointer; border: none; border-radius: 8px;
font-size: 14px; font-weight: 600; transition: all 0.2s ease;
min-width: 80px; height: auto;
}
.btn-primary { background-color: #28a745; color: white; }
.btn-primary:hover { background-color: #218838; transform: translateY(-1px); }
.btn-secondary { background-color: #6c757d; color: white; }
.btn-secondary:hover { background-color: #5a6268; transform: translateY(-1px); }
.btn-outline { background-color: transparent; color: #6c757d; border: 1px solid #6c757d !important; }
.btn-outline:hover { background-color: #6c757d; color: white; }
#generateShareImageBtn { background-color: #28a745; color: white; }
#generateShareImageBtn:hover { background-color: #218838; transform: translateY(-1px); }
#imagePreviewModalContent { text-align: center; max-width: 90vw; }
#imagePreviewModalContent img { max-width: 100%; max-height: calc(80vh - 120px); border: 1px solid #ddd; margin: 20px auto; border-radius: 8px; display: block; box-shadow: 0 4px 15px rgba(0,0,0,0.15); }
/* --- Themed Card Layout --- */
.image-host-container {
/* Theme variables (Air / default) */
--bg: #f8f9fa;
--card: #ffffff;
--text: #212529;
--muted: #868e96;
--border: #e9ecef;
--accent: #0078D7;
--op: rgba(240,145,153,0.06);
--quote-bg: #f1f3f5;
--quote-border: #ced4da;
--shadow: 0 10px 30px rgba(0,0,0,0.05);
--radius: 12px;
width: 750px; padding: 32px; background-color: var(--bg) !important; position: relative;
border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--text) !important; line-height: 1.6;
}
.image-host-container.theme-ink {
--bg: #16181c;
--card: #1e2126;
--text: #e6e8eb;
--muted: #9aa0a6;
--border: #2a2f36;
--accent: #5cabff;
--op: rgba(92,171,255,0.07);
--quote-bg: #20252b;
--quote-border: #2f353d;
--shadow: 0 10px 30px rgba(0,0,0,0.35);
}
.image-host-container.theme-sakura {
--bg: #fff6f8;
--card: #ffffff;
--text: #2b2b2b;
--muted: #8c8c8c;
--border: #ffd9e1;
--accent: #ff6b9e;
--op: rgba(255, 107, 158, 0.08);
--quote-bg: #ffeaf0;
--quote-border: #ffc4d1;
--shadow: 0 10px 30px rgba(255, 107, 158, 0.08);
}
.image-host-container * { color: inherit !important; background-color: transparent !important; border: none; box-sizing: border-box; }
.image-header { padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid var(--border); }
.group-info { display: flex; align-items: center; margin-bottom: 10px; }
.group-avatar { width: 24px; height: 24px; border-radius: 4px; margin-right: 8px; }
.image-header-group { font-size: 14px; color: var(--muted) !important; }
.image-header-title { font-size: 20px; font-weight: 600; color: var(--text) !important; }
.image-corner-logo { position: absolute; top: 12px; right: 12px; height: 22px; opacity: 0.45; }
/* Main topic post - primary but clean (no colored border or badge) */
.topic_post {
display: flex; align-items: flex-start; margin-bottom: 16px; padding: 18px;
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05); position: relative;
}
/* simple separator to distinguish from comments */
.topic_post + .comment-item { border-top: 1px solid var(--border); }
/* Comments - lighter than topic */
.comment-item {
display: flex; align-items: flex-start; padding: 14px; margin-bottom: 10px;
background: var(--quote-bg); border: 1px solid var(--border);
border-radius: 8px; position: relative;
}
.topic_post + .comment-item { margin-top: 0; }
.comment-item:first-child:not(.topic_post) { margin-top: 0; }
.topic_post .comment-avatar-wrapper { width: 52px; height: 52px; margin-right: 16px; flex-shrink: 0; border-radius: 50%; overflow: hidden; background-color: #e9ecef; }
.comment-item .comment-avatar-wrapper { width: 46px; height: 46px; margin-right: 14px; flex-shrink: 0; border-radius: 50%; overflow: hidden; background-color: #e9ecef; }
.comment-item .comment-avatar, .topic_post .comment-avatar { width: 100%; height: 100%; background-size: cover; background-position: center; }
.comment-item .comment-details, .topic_post .comment-details { flex-grow: 1; min-width: 0; }
.topic_post .comment-author { font-weight: 700; font-size: 15px; color: var(--text) !important; display: flex; align-items: center; margin-bottom: 4px;}
.comment-item .comment-author { font-weight: 600; font-size: 14px; color: var(--text) !important; display: flex; align-items: center; margin-bottom: 4px;}
.comment-author-op { color: #f09199 !important; }
.op-badge { font-size: 10px; font-weight: bold; color: white !important; background-color: #f09199 !important; padding: 2px 5px; margin-left: 8px; border-radius: 4px; }
.comment-author-sign { font-size: 12px; color: var(--muted) !important; margin-left: 8px; font-weight: normal; }
.topic_post .comment-text { font-size: 15px; color: var(--text) !important; word-wrap: break-word; white-space: normal; line-height: 1.65; }
.comment-item .comment-text { font-size: 14px; color: var(--text) !important; word-wrap: break-word; white-space: normal; }
.comment-text p, .comment-text div, .comment-text blockquote, .comment-text ul, .comment-text ol { margin: 0.4em 0; }
.comment-text p:first-child, .comment-text div:first-child { margin-top: 0; }
.comment-text p:last-child, .comment-text div:last-child { margin-bottom: 0; }
.comment-text.collapsed { max-height: 150px; overflow: hidden; -webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%); mask-image: linear-gradient(to bottom, black 50%, transparent 100%); }
.comment-text img { max-height: 1.6em; vertical-align: middle; display: inline; border-radius: 4px; }
.comment-text a { color: var(--accent) !important; text-decoration: underline !important; }
.comment-text .quote { padding: 12px; margin: 10px 0; background: var(--quote-bg) !important; border-left: 3px solid var(--quote-border); border-radius: 6px; }
.sub-replies-wrapper { margin-top: 15px; padding-left: 20px; border-left: 2px solid var(--border); }
.sub-comment-item { display: flex; align-items: flex-start; padding: 12px 0; border-top: 1px dashed var(--border); }
.sub-comment-item:first-child { border-top: none; }
.sub-comment-item .comment-avatar-wrapper { width: 36px; height: 36px; margin-right: 12px; }
.image-footer { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; align-items: center; }
.image-footer-logo { height: 20px; opacity: 0.6; }
.image-footer-text { font-size: 11px; color: var(--muted) !important; }
.image-footer-left { display: none; }
.image-footer-right { display: flex; align-items: center; gap: 12px; }
.image-footer-qr { width: 72px; height: 72px; background: var(--card); padding: 4px; border: 1px solid var(--border); border-radius: 6px; display: flex; align-items: center; justify-content: center; }
.image-footer-texts { display: flex; flex-direction: column; gap: 4px; }
.image-footer-url { font-size: 11px; color: var(--muted) !important; word-break: break-all; }
`);
// Settings storage helpers
const SETTINGS_KEY = 'bgm_share_card_settings_v1';
function getStoredSettings() {
try {
const raw = (typeof GM_getValue === 'function') ? GM_getValue(SETTINGS_KEY) : localStorage.getItem(SETTINGS_KEY);
return raw ? JSON.parse(raw) : {};
} catch (_) { return {}; }
}
function setStoredSettings(obj) {
try {
const raw = JSON.stringify(obj || {});
if (typeof GM_setValue === 'function') GM_setValue(SETTINGS_KEY, raw);
else localStorage.setItem(SETTINGS_KEY, raw);
} catch (_) { /* noop */ }
}
// --- Core Functions (fetchImage, initButtons, etc. are unchanged unless necessary) ---
async function fetchImageAsDataURL(imageUrl) {
if (!imageUrl || typeof imageUrl !== 'string') return null;
const absoluteUrl = imageUrl.startsWith('//') ? 'https:' + imageUrl : imageUrl;
// Cache Bangumi logo to avoid repeated network
if (absoluteUrl === 'https://bgm.tv/img/rc3/logo_2x.png' && bangumiLogoDataUrl) return bangumiLogoDataUrl;
return new Promise((resolve) => {
if (!absoluteUrl.startsWith('http')) { resolve(null); return; }
GM_xmlhttpRequest({
method: 'GET', url: absoluteUrl, responseType: 'blob', timeout: 15000,
onload: (response) => {
if (response.status === 200 && response.response) {
const reader = new FileReader();
reader.onloadend = () => {
const dataUrl = reader.result;
if (absoluteUrl === 'https://bgm.tv/img/rc3/logo_2x.png') bangumiLogoDataUrl = dataUrl;
resolve(dataUrl);
};
reader.onerror = () => resolve(null);
reader.readAsDataURL(response.response);
} else { resolve(null); }
},
onerror: () => resolve(null), ontimeout: () => resolve(null)
});
});
}
function addShareButtonToMenu(menuUl, mainCommentOrPostElement) {
if (!menuUl || menuUl.querySelector('.share-comment-menu-item')) return;
const listItem = document.createElement('li');
const anchor = document.createElement('a');
anchor.href = 'javascript:void(0);';
anchor.textContent = '分享图片';
anchor.className = 'share-comment-menu-item';
anchor.style.cursor = 'pointer';
anchor.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
showOptionsModal(mainCommentOrPostElement);
});
listItem.appendChild(anchor);
const ignoreUserLi = Array.from(menuUl.children).find(li => li.textContent.includes('绝交'));
if (ignoreUserLi) menuUl.insertBefore(listItem, ignoreUserLi); else menuUl.appendChild(listItem);
}
function initShareButtons() {
const processElement = (element) => {
const actionDivs = element.querySelectorAll('.post_actions.re_info > .action.dropdown');
actionDivs.forEach(actionDiv => {
// Some dropdowns are for likes; skip those
if (!actionDiv.querySelector('a.like_dropdown')) {
const menuUl = actionDiv.querySelector('ul');
if (menuUl) addShareButtonToMenu(menuUl, element);
}
});
};
// Replies only (do not inject on OP)
const comments = document.querySelectorAll('div.row.row_reply[id^="post_"]');
comments.forEach(processElement);
}
function setupModalCloseEvents(modalContentDiv, modalBackdropDiv, closeXButton) {
const cleanup = () => {
if (modalBackdropDiv && modalBackdropDiv.parentNode) modalBackdropDiv.remove();
else if (modalContentDiv && modalContentDiv.parentNode) modalContentDiv.remove();
document.removeEventListener('keydown', escHandler);
if (modalBackdropDiv) modalBackdropDiv.removeEventListener('click', backdropHandler);
};
const escHandler = (event) => { if (event.key === 'Escape') cleanup(); };
const backdropHandler = (event) => { if (event.target === modalBackdropDiv) cleanup(); };
document.addEventListener('keydown', escHandler);
if (modalBackdropDiv) modalBackdropDiv.addEventListener('click', backdropHandler);
if (closeXButton) closeXButton.addEventListener('click', cleanup);
return cleanup;
}
function formatDisplayUrl(rawUrl) {
try {
const u = new URL(rawUrl);
let s = u.host + u.pathname;
if (s.length > 60) s = s.slice(0, 28) + '…' + s.slice(-26);
return s;
} catch (_) {
const noProto = String(rawUrl || '').replace(/^https?:\/\//, '');
return noProto.length > 60 ? (noProto.slice(0, 28) + '…' + noProto.slice(-26)) : noProto;
}
}
async function inlineAllExternalImages(rootElement) {
if (!rootElement) return;
const imgNodes = Array.from(rootElement.querySelectorAll('img'));
await Promise.all(imgNodes.map(async (img) => {
const src = img.getAttribute('src');
if (!src || src.startsWith('data:')) return;
let absoluteUrl = src;
if (src.startsWith('//')) absoluteUrl = 'https:' + src;
else if (src.startsWith('/')) absoluteUrl = location.origin + src;
const dataUrl = await fetchImageAsDataURL(absoluteUrl);
if (dataUrl) {
img.removeAttribute('srcset');
img.src = dataUrl;
}
}));
}
// ====================================================================
// MODIFIED: Reworked Options Modal UI (v1.1)
// ====================================================================
function showOptionsModal(mainCommentElement) {
document.querySelectorAll('.share-script-modal-backdrop').forEach(el => el.remove());
const modalBackdrop = document.createElement('div');
modalBackdrop.className = 'share-script-modal-backdrop';
const modalContent = document.createElement('div');
modalContent.id = 'shareOptionsModalContent';
modalContent.classList.add('share-script-modal-content');
let subRepliesHTML = '';
const isOpPost = mainCommentElement.classList.contains('postTopic');
if (!isOpPost) {
const subRepliesContainer = mainCommentElement.querySelector('.topic_sub_reply');
if (subRepliesContainer) {
const subReplies = subRepliesContainer.querySelectorAll('.sub_reply_bg[id^="post_"]');
if (subReplies.length > 0) {
subRepliesHTML = '<h4>包含子评论</h4><div class="sub-reply-selection">';
subReplies.forEach((subReply) => {
const author = subReply.querySelector('.userName a')?.textContent.trim() || '未知用户';
let contentPreview = subReply.querySelector('.cmt_sub_content')?.textContent.trim() || '';
contentPreview = contentPreview.substring(0, 30) + (contentPreview.length > 30 ? '...' : '');
subRepliesHTML += `<label><input type="checkbox" name="subReply" value="${subReply.id}" checked> <span>${author}: ${contentPreview}</span></label>`;
});
subRepliesHTML += '</div>';
}
}
}
// Redesigned modal with better organization
const optionsHtml = `
<h3>🖼️ 生成分享图片</h3>
${subRepliesHTML ? `<div class="option-group">${subRepliesHTML}</div>` : ''}
<details>
<summary>主题与显示</summary>
<div class="option-group" style="margin-top:12px;">
<div class="option-group-title">主题模式</div>
<label style="margin-bottom:0;">
<select id="shareOption_theme" style="min-width: 220px; color: #111;">
<option value="air" selected>Air(浅色)</option>
<option value="ink">Ink(深色)</option>
<option value="sakura">Sakura(点缀)</option>
</select>
</label>
</div>
<div class="option-group">
<div class="option-group-title">显示选项</div>
<label><input type="checkbox" id="shareOption_showLogo" checked> <span>显示 Bangumi Logo</span></label>
<label><input type="checkbox" id="shareOption_showDate"> <span>显示生成时间</span></label>
<label><input type="checkbox" id="shareOption_showUrl"> <span>显示源主题 URL</span></label>
<label><input type="checkbox" id="shareOption_showQr"> <span>显示分享二维码</span></label>
</div>
<div class="option-group">
<div class="option-group-title">高级设置</div>
<label><input type="checkbox" id="shareOption_hideSignatures" checked> <span>隐藏用户签名</span></label>
<label><input type="checkbox" id="shareOption_collapseContent"> <span>折叠主题内容</span></label>
<div style="margin: 8px 0; display:flex; align-items:center; flex-wrap:wrap; gap:8px;">
<button id="btnExportTheme" type="button" class="btn-outline">导出设置</button>
<button id="btnImportTheme" type="button" class="btn-outline">导入设置</button>
<label style="margin:0 0 0 12px;">
<input type="checkbox" id="shareOption_enableCustomCss">
<span>使用自定义 CSS</span>
</label>
</div>
<textarea id="customCssArea" placeholder="仅作用于图片容器 .image-host-container 作用域下的样式,例如: .image-host-container .comment-text { font-size: 16px; } 注意:请确保CSS语法正确,否则可能影响图片生成"></textarea>
</div>
</details>
<div class="modal-buttons">
<div class="modal-buttons-left">
<button type="button" class="btn-secondary" id="resetSettingsBtn">重置默认</button>
</div>
<div class="modal-buttons-right">
<button type="button" class="btn-outline" id="cancelBtn">取消</button>
<button id="generateShareImageBtn" class="btn-primary">生成图片</button>
</div>
</div>`;
modalContent.insertAdjacentHTML('afterbegin', optionsHtml);
modalBackdrop.appendChild(modalContent);
document.body.appendChild(modalBackdrop);
const cleanupModalListeners = setupModalCloseEvents(modalContent, modalBackdrop, null); // Pass null for closeX, we handle it inside
// Auto-select theme based on system dark mode
try {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const themeSelect = modalContent.querySelector('#shareOption_theme');
const saved = getStoredSettings();
if (themeSelect) themeSelect.value = saved.theme || (prefersDark ? 'ink' : 'air');
if (saved.customCss) {
const ta = modalContent.querySelector('#customCssArea');
const enable = modalContent.querySelector('#shareOption_enableCustomCss');
if (ta && enable) { ta.value = saved.customCss; enable.checked = !!saved.enableCustomCss; ta.style.display = enable.checked ? 'block' : 'none'; }
}
} catch (_) { /* noop */ }
// Toggle custom CSS area
const enableCssCb = modalContent.querySelector('#shareOption_enableCustomCss');
const cssArea = modalContent.querySelector('#customCssArea');
if (enableCssCb && cssArea) enableCssCb.addEventListener('change', () => { cssArea.style.display = enableCssCb.checked ? 'block' : 'none'; });
// Export / Import
const exportBtn = modalContent.querySelector('#btnExportTheme');
const importBtn = modalContent.querySelector('#btnImportTheme');
if (exportBtn) exportBtn.addEventListener('click', () => {
const theme = modalContent.querySelector('#shareOption_theme')?.value || 'air';
const enable = enableCssCb?.checked || false;
const css = cssArea?.value || '';
const payload = JSON.stringify({ theme, enableCustomCss: enable, customCss: css });
window.prompt('复制以下 JSON 以保存设置:', payload);
});
if (importBtn) importBtn.addEventListener('click', () => {
const text = window.prompt('粘贴之前导出的 JSON:');
if (!text) return;
try {
const obj = JSON.parse(text);
const themeSelect = modalContent.querySelector('#shareOption_theme');
if (themeSelect && obj.theme) themeSelect.value = obj.theme;
if (enableCssCb && typeof obj.enableCustomCss === 'boolean') enableCssCb.checked = obj.enableCustomCss;
if (cssArea && typeof obj.customCss === 'string') cssArea.value = obj.customCss;
if (cssArea && enableCssCb) cssArea.style.display = enableCssCb.checked ? 'block' : 'none';
} catch (e) { alert('导入失败:JSON 无效'); }
});
// Add event handlers for new buttons
const resetBtn = modalContent.querySelector('#resetSettingsBtn');
const cancelBtn = modalContent.querySelector('#cancelBtn');
if (resetBtn) resetBtn.addEventListener('click', () => {
// Reset to defaults
const checkboxes = modalContent.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (cb.id === 'shareOption_hideSignatures' || cb.id === 'shareOption_showLogo') {
cb.checked = true;
} else {
cb.checked = false;
}
});
const themeSelect = modalContent.querySelector('#shareOption_theme');
if (themeSelect) themeSelect.value = 'air';
if (cssArea) cssArea.value = '';
if (cssArea && enableCssCb) {
enableCssCb.checked = false;
cssArea.style.display = 'none';
}
});
if (cancelBtn) cancelBtn.addEventListener('click', () => {
cleanupModalListeners();
});
modalContent.querySelector('#generateShareImageBtn').onclick = async () => {
const options = {
hideSignatures: modalContent.querySelector('#shareOption_hideSignatures').checked,
collapseContent: modalContent.querySelector('#shareOption_collapseContent').checked,
showLogo: modalContent.querySelector('#shareOption_showLogo').checked,
showDate: modalContent.querySelector('#shareOption_showDate').checked,
showUrl: modalContent.querySelector('#shareOption_showUrl').checked,
showQr: modalContent.querySelector('#shareOption_showQr').checked,
theme: modalContent.querySelector('#shareOption_theme')?.value || 'air',
enableCustomCss: modalContent.querySelector('#shareOption_enableCustomCss')?.checked || false,
customCss: modalContent.querySelector('#customCssArea')?.value || '',
selectedSubReplyIds: isOpPost ? [] : Array.from(modalContent.querySelectorAll('input[name="subReply"]:checked')).map(cb => cb.value)
};
// Persist selection
setStoredSettings({ theme: options.theme, enableCustomCss: options.enableCustomCss, customCss: options.customCss });
cleanupModalListeners();
await generateAndShowPreview(mainCommentElement, options);
};
}
// ====================================================================
// MODIFIED: Reworked Image Generation Logic (v1.1)
// ====================================================================
async function generateAndShowPreview(mainElement, options) {
const imageHost = document.createElement('div');
imageHost.className = 'image-host-container';
if (options.theme === 'ink') imageHost.classList.add('theme-ink');
if (options.theme === 'sakura') imageHost.classList.add('theme-sakura');
imageHost.style.position = 'absolute'; imageHost.style.left = '-9999px'; imageHost.style.top = '0px';
document.body.appendChild(imageHost);
if (options.enableCustomCss && options.customCss) {
const scoped = document.createElement('style');
scoped.textContent = options.customCss;
imageHost.appendChild(scoped);
}
const headerDiv = document.createElement('div'); headerDiv.className = 'image-header';
// Group info with avatar
const groupAnchor = document.querySelector('#pageHeader h1 a.avatar');
if (groupAnchor) {
const groupInfo = document.createElement('div');
groupInfo.className = 'group-info';
const avatarImgEl = groupAnchor.querySelector('img');
const groupNameText = groupAnchor.textContent.trim();
if (avatarImgEl && avatarImgEl.getAttribute('src')) {
const avatarData = await fetchImageAsDataURL(avatarImgEl.getAttribute('src'));
if (avatarData) {
const groupAvatar = document.createElement('img');
groupAvatar.className = 'group-avatar';
groupAvatar.src = avatarData;
groupInfo.appendChild(groupAvatar);
}
}
const groupNameSpan = document.createElement('span');
groupNameSpan.className = 'image-header-group';
groupNameSpan.textContent = groupNameText || '小组';
groupInfo.appendChild(groupNameSpan);
headerDiv.appendChild(groupInfo);
}
const titleEl = document.querySelector('#pageHeader h1');
if (titleEl) { let title = titleEl.innerText.trim(); const groupText = titleEl.querySelector('span a[href^="/group/"]')?.parentElement?.innerText.trim(); if (groupText && title.startsWith(groupText)) title = title.substring(groupText.length).replace(/^»\s*/, '').trim(); else title = title.split('\n').pop().trim(); const div = document.createElement('div'); div.className = 'image-header-title'; div.textContent = title; headerDiv.appendChild(div); }
if (headerDiv.hasChildNodes()) imageHost.appendChild(headerDiv);
// Corner logo (top-right)
if (options.showLogo) {
const logoUrl = await fetchImageAsDataURL('https://bgm.tv/img/rc3/logo_2x.png');
if (logoUrl) {
const cornerLogo = document.createElement('img');
cornerLogo.className = 'image-corner-logo';
cornerLogo.src = logoUrl;
imageHost.appendChild(cornerLogo);
}
}
const opElement = document.querySelector('div.postTopic[data-item-user]');
const opUserId = opElement ? opElement.getAttribute('data-item-user') : null;
const isMainElementOp = mainElement.classList.contains('postTopic');
const elementsToRender = [];
if (opElement) { elementsToRender.push({ type: 'opPost', el: opElement, userId: opUserId }); }
if (!isMainElementOp) { elementsToRender.push({ type: 'mainReply', el: mainElement, userId: mainElement.getAttribute('data-item-user') }); }
options.selectedSubReplyIds.forEach(id => { const subEl = document.getElementById(id); if (subEl) elementsToRender.push({ type: 'subReply', el: subEl, userId: subEl.getAttribute('data-item-user') }); });
const uniqueElements = [...new Map(elementsToRender.map(item => [item.el.id, item])).values()];
const avatarPromises = uniqueElements.map(item => {
const avatarSpan = item.el.querySelector('a.avatar span.avatarNeue');
let rawUrl = null;
if (avatarSpan && avatarSpan.style.backgroundImage) { const match = avatarSpan.style.backgroundImage.match(/url\s*\(\s*['"]?(.+?)['"]?\s*\)/i); if (match && match[1]) rawUrl = match[1]; }
return fetchImageAsDataURL(rawUrl).then(dataUrl => ({ ...item, avatarDataUrl: dataUrl }));
});
const processedItems = await Promise.all(avatarPromises);
let mainReplyDomRef = null;
processedItems.forEach(item => {
let domItem;
if (item.type === 'opPost') { domItem = createCommentItemDOM(item.el, true, false, options, item.avatarDataUrl, opUserId); }
else if (item.type === 'mainReply') { domItem = createCommentItemDOM(item.el, false, false, options, item.avatarDataUrl, opUserId); mainReplyDomRef = domItem; }
if (domItem) imageHost.appendChild(domItem);
});
if (mainReplyDomRef) {
const subRepliesWrapper = document.createElement('div'); subRepliesWrapper.className = 'sub-replies-wrapper';
processedItems.filter(item => item.type === 'subReply').forEach(subItem => {
const subDomItem = createCommentItemDOM(subItem.el, false, true, options, subItem.avatarDataUrl, opUserId);
if (subDomItem) subRepliesWrapper.appendChild(subDomItem);
});
if (subRepliesWrapper.hasChildNodes) { mainReplyDomRef.querySelector('.comment-details').appendChild(subRepliesWrapper); }
}
if (options.showDate || options.showUrl || options.showQr) {
const footerDiv = document.createElement('div');
footerDiv.className = 'image-footer';
// Right: QR + URL/Date texts
const footerRight = document.createElement('div');
footerRight.className = 'image-footer-right';
const currentUrl = window.location.href;
if (options.showQr && typeof QRCode !== 'undefined') {
const qrBox = document.createElement('div');
qrBox.className = 'image-footer-qr';
const qrInner = document.createElement('div');
qrBox.appendChild(qrInner);
try {
new QRCode(qrInner, { text: currentUrl, width: 64, height: 64, correctLevel: QRCode.CorrectLevel.M });
} catch (e) { console.error('QR generation failed', e); }
footerRight.appendChild(qrBox);
}
const textCol = document.createElement('div');
textCol.className = 'image-footer-texts';
if (options.showUrl) {
const urlSpan = document.createElement('span');
urlSpan.className = 'image-footer-url';
urlSpan.textContent = formatDisplayUrl(currentUrl);
textCol.appendChild(urlSpan);
}
if (options.showDate) {
const dateSpan = document.createElement('span');
dateSpan.className = 'image-footer-text';
dateSpan.textContent = `Generated @ ${new Date().toLocaleDateString()}`;
textCol.appendChild(dateSpan);
}
if (textCol.childNodes.length > 0) footerRight.appendChild(textCol);
footerDiv.appendChild(footerRight);
imageHost.appendChild(footerDiv);
}
// Ensure QR is rendered and external images are inlined before capture
await new Promise(r => setTimeout(r, 60));
await inlineAllExternalImages(imageHost);
try {
const canvas = await html2canvas(imageHost, { backgroundColor: '#f8f9fa', useCORS: true, scale: window.devicePixelRatio || 2, logging: false });
const dataUrl = canvas.toDataURL('image/png');
if (dataUrl.length < 1024) throw new Error("Generated image is too small, assuming it's blank.");
document.querySelectorAll('.share-script-modal-backdrop').forEach(el => el.remove());
const previewBackdrop = document.createElement('div'); previewBackdrop.className = 'share-script-modal-backdrop';
const previewContentDiv = document.createElement('div'); previewContentDiv.id = 'imagePreviewModalContent'; previewContentDiv.classList.add('share-script-modal-content');
const closeXButton = document.createElement('a'); closeXButton.href = 'javascript:void(0);'; closeXButton.innerHTML = '×'; closeXButton.className = 'modal-close-x'; previewContentDiv.appendChild(closeXButton);
const imgElement = new Image(); imgElement.src = dataUrl;
const instructions = document.createElement('p'); instructions.textContent = '✅ 图片已生成!请右键复制或拖拽保存。'; instructions.style.margin = '0 0 16px 0'; instructions.style.fontSize = '15px'; instructions.style.color = '#28a745';
const buttonDiv = document.createElement('div'); buttonDiv.className = 'modal-buttons';
const buttonDivRight = document.createElement('div'); buttonDivRight.className = 'modal-buttons-right';
const closeMainButton = document.createElement('button'); closeMainButton.textContent = '关闭预览'; closeMainButton.className = 'btn-secondary';
buttonDivRight.appendChild(closeMainButton);
buttonDiv.appendChild(document.createElement('div')); // Empty left side
buttonDiv.appendChild(buttonDivRight);
previewContentDiv.appendChild(instructions); previewContentDiv.appendChild(imgElement); previewContentDiv.appendChild(buttonDiv);
previewBackdrop.appendChild(previewContentDiv); document.body.appendChild(previewBackdrop);
const cleanupPreviewListeners = setupModalCloseEvents(previewContentDiv, previewBackdrop, closeXButton);
closeMainButton.addEventListener('click', cleanupPreviewListeners);
} catch (error) { console.error('Image generation failed (html2canvas error):', error); alert('图片生成失败,详情请查看控制台。\n错误: ' + error.message);
} finally { imageHost.remove(); }
}
// ====================================================================
// MODIFIED: createCommentItemDOM to fix parentheses (v1.1)
// ====================================================================
function createCommentItemDOM(element, isOp = false, isSubComment = false, options, preFetchedAvatarDataURL, opUserId) {
// This is the container for the whole block (OP or a single comment)
const itemContainer = document.createElement('div');
// FIX: Add appropriate class for layout
itemContainer.className = isOp ? 'topic_post' : (isSubComment ? 'sub-comment-item' : 'comment-item');
const currentUserId = element.getAttribute('data-item-user');
// Avatar part
const avatarWrapper = document.createElement('div');
avatarWrapper.className = 'comment-avatar-wrapper';
const avatarDiv = document.createElement('div');
avatarDiv.className = 'comment-avatar';
if (preFetchedAvatarDataURL) {
avatarDiv.style.backgroundImage = `url("${preFetchedAvatarDataURL}")`;
}
avatarWrapper.appendChild(avatarDiv);
// Details part (username, content, etc.)
const detailsDiv = document.createElement('div');
detailsDiv.className = 'comment-details';
const authorDiv = document.createElement('div');
authorDiv.className = 'comment-author';
if (currentUserId === opUserId) authorDiv.classList.add('comment-author-op');
const contentDiv = document.createElement('div');
contentDiv.className = 'comment-text';
let authorName = '', authorSign = '', commentHTML = '';
try {
if (isOp) {
authorName = element.querySelector('.inner strong a.l')?.textContent.trim() || '楼主';
authorSign = element.querySelector('.inner span.sign')?.textContent.trim() || '';
commentHTML = element.querySelector('.topic_content')?.innerHTML || '';
if (options.collapseContent) contentDiv.classList.add('collapsed');
} else if (isSubComment) {
authorName = element.querySelector('.inner strong.userName a.l')?.textContent.trim() || '用户';
authorSign = ''; // Sub-replies on BGM do not have signatures
commentHTML = element.querySelector('.inner .cmt_sub_content')?.innerHTML || '';
} else { // Main Reply
authorName = element.querySelector('.inner span.userInfo strong a.l')?.textContent.trim() || '用户';
authorSign = element.querySelector('.inner span.userInfo span.sign')?.textContent.trim() || '';
commentHTML = element.querySelector('.inner .reply_content .message')?.innerHTML || '';
}
} catch (e) { console.error("Error parsing comment DOM:", { e, element }); }
const authorNameSpan = document.createElement('span');
authorNameSpan.textContent = authorName;
authorDiv.appendChild(authorNameSpan);
if (currentUserId === opUserId) {
const opBadge = document.createElement('span');
opBadge.className = 'op-badge';
opBadge.textContent = 'OP';
authorDiv.appendChild(opBadge);
}
if (authorSign && !options.hideSignatures) {
const normalized = authorSign
.replace(/^\s*[((]/, '')
.replace(/[))]\s*$/, '');
const signSpan = document.createElement('span');
signSpan.className = 'comment-author-sign';
signSpan.textContent = `(${normalized})`;
authorDiv.appendChild(signSpan);
}
detailsDiv.appendChild(authorDiv);
contentDiv.innerHTML = commentHTML;
detailsDiv.appendChild(contentDiv);
itemContainer.appendChild(avatarWrapper);
itemContainer.appendChild(detailsDiv);
return itemContainer;
}
// ====================================================================
// UNCHANGED: Initialization Logic (from v1.0.1)
// ====================================================================
setTimeout(() => {
initShareButtons();
const mainContentArea = document.getElementById('main');
if (mainContentArea) {
const observer = new MutationObserver(mutations => {
let needsReInit = false;
for(let mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('div.row.row_reply[id^="post_"], div.postTopic[id^="post_"], .action.dropdown ul') ||
node.querySelector('div.row.row_reply[id^="post_"], div.postTopic[id^="post_"], .action.dropdown ul')) {
needsReInit = true;
}
}
});
}
if (needsReInit) break;
}
if (needsReInit) {
initShareButtons();
}
});
observer.observe(mainContentArea, { childList: true, subtree: true });
}
}, 1500);
})();