// ==UserScript==
// @name Bangumi 社区月刊组件[测试]
// @version 1.2.3
// @description 在小组话题/条目讨论/日志/目录页右侧添加推荐面板;在首页右侧显示「Bangumi社区月刊」卡片
// @author zin
// @match https://bgm.tv/*
// @match https://bangumi.tv/*
// @match https://chii.in/*
// @connect raw.githubusercontent.com
// @license MIT
// @namespace https://greasyfork.org/users/1386262
// ==/UserScript==
(async function () {
'use strict';
const JSON_URL = 'https://raw.githubusercontent.com/zintop/bangumi_monthly/main/bangumi_monthly_1.json';
let COVER_IMG = '', COVER_THUMB = '', ISSUE_TITLE = '', ISSUE_VOL = '', ISSUE_URL_CHAHUA = '', ISSUE_URL_YUEKAN = '', RECOMMEND_LOG_ID = '';
// 获取设置项的值
const getSetting = (name, defaultValue) => {
const value = localStorage.getItem(`bangumi_monthly_${name}`);
return value !== null ? value === 'true' : defaultValue;
};
// 保存设置项的值
const setSetting = (name, value) => {
localStorage.setItem(`bangumi_monthly_${name}`, value);
};
// 初始化设置
let homeCardEnabled = getSetting('homeCardEnabled', true);
let recommendPanelEnabled = getSetting('recommendPanelEnabled', true);
try {
const res = await fetch(JSON_URL);
const data = await res.json();
({ coverImg: COVER_IMG = '', coverThumb: COVER_THUMB = '', issueTitle: ISSUE_TITLE = '', issueVol: ISSUE_VOL = '', issueUrlChahua: ISSUE_URL_CHAHUA = '', issueUrlYuekan: ISSUE_URL_YUEKAN = '', recommendLogId: RECOMMEND_LOG_ID = '' } = data);
} catch (err) {
console.error('无法加载 Bangumi 月刊 JSON:', err);
return;
}
const injectStyles = () => {
const styles = `
#monthly-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.15);z-index:9999;justify-content:center;align-items:center;backdrop-filter:blur(8px);animation:fadeIn .3s}
#monthly-modal.active{display:flex}
#monthly-modal .modal-content{position:relative;border-radius:20px;padding:8px;background:rgba(255,255,255,0.02);backdrop-filter:blur(25px);max-width:90%;max-height:90%;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 12px 40px rgba(0,0,0,.35);animation:slideUp .35s}
#monthly-modal .inner-frame{border-radius:16px;border:6px solid rgba(255,255,255,0.12);padding:4px;background:rgba(255,255,255,0.02);display:flex;justify-content:center;align-items:center}
#monthly-modal .inner-frame img{border-radius:12px;max-width:100%;max-height:80vh;object-fit:contain}
#monthly-modal .footer{padding:10px 12px;text-align:center;font-size:1em;color:#e0e0e0;font-weight:700}
#monthly-modal .footer a{color:#a0d8ff;text-decoration:none;margin:0 6px;transition:color .2s,text-shadow .2s}
#monthly-modal .footer a:hover{color:#66bfff;text-shadow:0 0 4px rgba(0,0,0,.5)}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
@keyframes slideUp{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
#bangumi-recommend-panel {
margin-top: 15px;
}
#bangumi-recommend-panel .SidePanel {
padding: 10px;
}
#bangumi-recommend-panel h2 {
margin-bottom: 10px;
font-size: 13px;
font-weight: 400;
line-height: 1.5;
text-align: left;
}
#bangumi-recommend-panel h2 a {
text-decoration: none;
}
#bangumi-recommend-panel h2 a:hover {
color: #f09199;
}
#bangumi-recommend-panel textarea {
width: 100%;
min-height: 80px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
box-sizing: border-box;
margin-bottom: 8px;
}
#bangumi-recommend-panel button {
width: 100%;
padding: 8px 0;
background-color: #f09199;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
#bangumi-recommend-panel button:hover {
background-color: #e07179;
}
`;
document.head.appendChild(Object.assign(document.createElement("style"), { textContent: styles }));
};
const injectCard = () => {
if (window.location.pathname !== "/" || !homeCardEnabled) return;
setTimeout(() => {
const info = { id: "bangumi_monthly", title: "Bangumi 社区月刊", desc: `${ISSUE_TITLE} ${ISSUE_VOL}`, banner_url: COVER_THUMB || COVER_IMG, url: "javascript:void(0)" };
const html = `<div class="featuredItems"><div id="${info.id}" class="appItem" style="background-image:linear-gradient(to right,rgba(0,0,0,.6),transparent 60%),url('${info.banner_url}');background-size:cover"><a href="${info.url}"><p class="title">${info.title}</p><p>${info.desc}</p></a></div></div>`;
$("#columnHomeB .featuredItems").length ? $("#columnHomeB .featuredItems").first().after(html) : $("#columnHomeB").prepend(html);
const modalHTML = `
<div id="monthly-modal">
<div class="modal-content">
<div class="inner-frame"><img src="${COVER_IMG}" alt="社区月刊封面" /></div>
<div class="footer">
月刊上新 点击直达:
<a href="${ISSUE_URL_CHAHUA}" target="_blank">靠谱人生茶话会</a> |
<a href="${ISSUE_URL_YUEKAN}" target="_blank">Bangumi社区月刊</a>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML("beforeend", modalHTML);
const banner = document.getElementById("bangumi_monthly");
const modal = document.getElementById("monthly-modal");
banner.addEventListener("click", () => modal.classList.add("active"));
modal.addEventListener("click", (e) => { if (e.target === modal) modal.classList.remove("active"); });
}, 0);
};
const path = location.pathname, BASE_URL = location.hostname;
const shouldShowRecommendPanel = () => RECOMMEND_LOG_ID && recommendPanelEnabled && (/^\/group\/topic\/\d+$/.test(path) || /^\/blog\/\d+$/.test(path) || /^\/subject\/topic\/\d+$/.test(path) || /^\/index\/\d+$/.test(path));
const getPostIdFromUrl = () => path.split('/').pop();
const getAuthorInfo = () => {
if (document.querySelector('.postTopic')) {
const userLink = document.querySelector('.postTopic .inner strong a');
return { id: document.querySelector('.postTopic').getAttribute('data-item-user') || userLink?.href?.split('/').pop() || '用户', name: userLink?.textContent.trim() || '用户' };
}
if (path.startsWith('/blog/')) {
const authorLink = document.querySelector('#pageHeader h1 a[href^="/user/"], .blog_info a[href^="/user/"]');
return { id: authorLink?.href.split('/').pop() || '用户', name: authorLink?.textContent.trim() || '用户' };
}
if (path.startsWith('/index/')) {
const authorLink = document.querySelector('a.l.bve-processed[href^="/user/"]');
return { id: authorLink?.href.split('/').pop() || '用户', name: authorLink?.textContent.trim() || '用户' };
}
return { id: '用户', name: '用户' };
};
const getSubjectInfo = () => {
const subjectLink = document.querySelector('#subject_inner_info > a.avatar[href^="/subject/"]');
return { id: subjectLink?.href.split('/')[4] || null, name: subjectLink?.title || subjectLink?.textContent.trim() || null };
};
const getBlogFormData = async (blogId) => {
const response = await fetch(`https://${BASE_URL}/blog/${blogId}`);
if (!response.ok) throw new Error('无法获取日志');
const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
const formhash = doc.querySelector('input[name="formhash"]')?.value;
const lastview = doc.querySelector('input[name="lastview"]')?.value;
if (!formhash || !lastview) throw new Error('找不到日志信息');
return { formhash, lastview };
};
const submitComment = async (blogId, formData, content) => {
const response = await fetch(`https://${BASE_URL}/blog/entry/${blogId}/new_reply`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ formhash: formData.formhash, lastview: formData.lastview, content, submit: '加上去' }),
});
return response.ok || response.status === 302;
};
const handleRecommend = async (recommendReason = '') => {
try {
if (!RECOMMEND_LOG_ID) return alert('推荐日志ID未配置');
const isBlog = path.startsWith('/blog/'), isSubjectTopic = path.startsWith('/subject/topic/'), isIndex = path.startsWith('/index/'), postId = getPostIdFromUrl(), authorInfo = getAuthorInfo(), formData = await getBlogFormData(RECOMMEND_LOG_ID);
let postUrl = location.href, postTitle, bbcode;
if (isBlog) {
postTitle = (document.querySelector('#pageHeader h1')?.innerHTML.split('<br>')[1] || document.title).trim();
bbcode = `[url=https://${BASE_URL}/blog/${postId}]${postTitle}[/url]\n[日志] ${authorInfo.name}([url=https://${BASE_URL}/user/${authorInfo.id}]${authorInfo.id}[/url])`;
} else if (isSubjectTopic) {
postTitle = document.querySelector('#header h1')?.textContent.trim() || document.title.replace(' - Bangumi', '');
const subjectInfo = getSubjectInfo();
bbcode = `[url=https://${BASE_URL}/subject/topic/${postId}]${postTitle}[/url]\n${subjectInfo.id ? `[[url=https://${BASE_URL}/subject/${subjectInfo.id}]${subjectInfo.name}[/url]] ` : ''}${authorInfo.name}([url=https://${BASE_URL}/user/${authorInfo.id}]${authorInfo.id}[/url])`;
} else if (isIndex) {
postTitle = document.querySelector('#pageHeader h1')?.textContent.trim() || document.title.replace(' - Bangumi', '');
bbcode = `[url=https://${BASE_URL}/index/${postId}]${postTitle}[/url]\n[目录] ${authorInfo.name}([url=https://${BASE_URL}/user/${authorInfo.id}]${authorInfo.id}[/url])`;
} else {
postTitle = document.title.replace(' - Bangumi', '');
const groupName = document.querySelector('#pageHeader h1 a')?.textContent.trim() || '未知小组';
bbcode = `[url=https://${BASE_URL}/group/topic/${postId}]${postTitle}[/url]\n[${groupName}] ${authorInfo.name}([url=https://${BASE_URL}/user/${authorInfo.id}]${authorInfo.id}[/url])`;
}
if (recommendReason.trim()) bbcode += `\n\n推荐理由:${recommendReason.trim()}`;
if (!await submitComment(RECOMMEND_LOG_ID, formData, bbcode)) throw new Error('提交失败');
alert('推荐成功!');
document.getElementById('bangumi-recommend-reason-textarea').value = '';
} catch (err) {
alert('推荐失败: ' + (err.message || '未知错误'));
}
};
const updateTextColor = () => {
const theme = document.documentElement.getAttribute('data-theme');
const textElement = document.querySelector('#bangumi-recommend-panel h2');
if (textElement) {
textElement.style.color = theme === 'dark' ? '#fff' : '#444';
}
};
const addRecommendPanel = () => {
const panelHTML = `
<div id="bangumi-recommend-panel">
<div class="SidePanel png_bg clearit">
<h2>推荐到 <a href="https://${BASE_URL}/blog/${RECOMMEND_LOG_ID}" target="_blank">社区月刊协力楼</a></h2>
<textarea id="bangumi-recommend-reason-textarea" placeholder="推荐原因(可选,写了更好)"></textarea>
<button id="bangumi-recommend-submit-button">确认推荐</button>
</div>
</div>
`;
// v1.1.1:适配目录页
if (path.startsWith('/index/')) {
const columnElement = document.getElementById('columnSubjectBrowserB');
if (columnElement) {
const lastMenuInner = columnElement.querySelector('.menu_inner:last-child');
if (lastMenuInner) {
lastMenuInner.insertAdjacentHTML('afterend', panelHTML);
} else {
columnElement.insertAdjacentHTML('beforeend', panelHTML);
}
} else {
console.warn('找不到目录页侧栏,推荐面板未添加');
return;
}
}
else {
const ANCHOR_1 = '#columnB';
const ANCHOR_2 = '#columnInSubjectB';
const ANCHOR_3 = 'div.columns';
if ($(ANCHOR_1).length) {
$(ANCHOR_1).append(panelHTML);
} else if ($(ANCHOR_2).length) {
$(ANCHOR_2).append(panelHTML);
} else if ($(ANCHOR_3).length) {
$(ANCHOR_3).append(panelHTML);
} else {
console.warn('找不到合适的插入位置,推荐面板未添加');
return;
}
}
updateTextColor();
const observer = new MutationObserver(updateTextColor);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
document.getElementById('bangumi-recommend-submit-button').addEventListener('click', () => {
handleRecommend(document.getElementById('bangumi-recommend-reason-textarea').value);
});
};
// v1.1.2:添加个性化面板设置
const addCustomPanelTab = () => {
if (typeof chiiLib === 'undefined' || typeof chiiLib.ukagaka === 'undefined' || typeof chiiLib.ukagaka.addPanelTab !== 'function') {
return;
}
chiiLib.ukagaka.addPanelTab({
tab: 'bangumi_monthly',
label: '社区月刊',
type: 'options',
config: [
{
title: '首页卡片',
name: 'homeCardEnabled',
type: 'radio',
defaultValue: 'true',
getCurrentValue: function() {
return homeCardEnabled.toString();
},
onChange: function(value) {
const enabled = value === 'true';
setSetting('homeCardEnabled', enabled);
homeCardEnabled = enabled;
// 立即生效
const card = document.getElementById('bangumi_monthly');
if (card) {
card.style.display = enabled ? 'block' : 'none';
} else if (enabled && window.location.pathname === '/') {
// 如果卡片不存在但需要显示,重新注入
injectCard();
}
},
options: [
{ value: 'true', label: '开启' },
{ value: 'false', label: '关闭' }
]
},
{
title: '推荐面板',
name: 'recommendPanelEnabled',
type: 'radio',
defaultValue: 'true',
getCurrentValue: function() {
return recommendPanelEnabled.toString();
},
onChange: function(value) {
const enabled = value === 'true';
setSetting('recommendPanelEnabled', enabled);
recommendPanelEnabled = enabled;
// 立即生效
const panel = document.getElementById('bangumi-recommend-panel');
if (panel) {
panel.style.display = enabled ? 'block' : 'none';
} else if (enabled && shouldShowRecommendPanel()) {
// 如果面板不存在但需要显示,重新注入
addRecommendPanel();
}
},
options: [
{ value: 'true', label: '开启' },
{ value: 'false', label: '关闭' }
]
}
]
});
};
injectStyles();
injectCard();
if (shouldShowRecommendPanel()) addRecommendPanel();
addCustomPanelTab();
})();