// ==UserScript==
// @name 【云】XMXMKB关注作者管理
// @namespace http://tampermonkey.net/
// @version 1.5
// @description 为XMXMKB网站添加关注作者功能,可以管理关注的作者列表,显示每个作者的最新收藏内容,跟踪搜索时间和查看记录
// @author AI
// @match https://www.xmxmkb.com/search.php?mod=*
// @match https://www.xmxmkb.com/forum.php?mod=viewthread&tid=*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 添加样式
const style = document.createElement('style');
style.textContent = `
.follow-author-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: none;
justify-content: center;
align-items: center;
}
.follow-author-content {
background-color: white;
border-radius: 8px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.follow-author-header {
background-color: #4caf50;
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.follow-author-title {
font-size: 18px;
font-weight: bold;
}
.follow-author-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.follow-author-close:hover {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.follow-author-add-section {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
gap: 10px;
align-items: center;
}
.follow-author-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.follow-author-add-btn {
background-color: #4caf50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.follow-author-add-btn:hover {
background-color: #45a049;
}
.follow-author-list {
max-height: 400px;
overflow-y: auto;
padding: 0;
}
.follow-author-item {
display: flex;
align-items: flex-start;
padding: 15px 20px;
border-bottom: 1px solid #eee;
transition: background-color 0.2s;
gap: 15px;
}
.follow-author-item:hover {
background-color: #f5f5f5;
}
.follow-author-item:last-child {
border-bottom: none;
}
.follow-author-name {
font-weight: bold;
color: #333;
cursor: pointer;
min-width: 120px;
display: flex;
flex-direction: column;
gap: 5px;
}
.follow-author-name:hover {
color: #4caf50;
text-decoration: underline;
}
.follow-author-content-wrapper {
flex: 1;
display: flex;
gap: 15px;
align-items: flex-start;
}
.follow-author-last-viewed {
flex: 0 0 200px;
display: flex;
flex-direction: column;
gap: 8px;
}
.follow-author-favorites {
flex: 1;
display: flex;
gap: 10px;
flex-wrap: wrap;
max-width: 400px;
}
.follow-author-search-time {
font-size: 11px;
color: #999;
font-weight: normal;
}
.follow-author-section-title {
font-size: 12px;
color: #666;
font-weight: 500;
margin-bottom: 5px;
}
.follow-author-favorite-item {
background-color: #f0f0f0;
padding: 8px;
border-radius: 6px;
font-size: 12px;
color: #666;
cursor: pointer;
max-width: 180px;
display: flex;
flex-direction: column;
gap: 6px;
transition: all 0.2s;
}
.follow-author-last-viewed-item {
background-color: #e3f2fd;
padding: 8px;
border-radius: 6px;
font-size: 12px;
color: #666;
cursor: pointer;
max-width: 200px;
display: flex;
flex-direction: column;
gap: 6px;
transition: all 0.2s;
border: 1px solid #bbdefb;
}
.follow-author-last-viewed-item:hover {
background-color: #d1c4e9;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.follow-author-favorite-item:hover {
background-color: #e0e0e0;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.follow-author-favorite-title {
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.follow-author-favorite-image {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: 4px;
background-color: #f5f5f5;
border: 1px solid #eee;
}
.follow-author-favorite-time {
font-size: 10px;
color: #999;
text-align: right;
}
.follow-author-unfollow {
background-color: #f44336;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-left: 10px;
}
.follow-author-unfollow:hover {
background-color: #d32f2f;
}
.follow-author-empty {
text-align: center;
padding: 40px;
color: #999;
font-size: 14px;
}
.follow-author-stats {
padding: 10px 20px;
background-color: #f9f9f9;
border-bottom: 1px solid #eee;
font-size: 14px;
color: #666;
display: flex;
flex-wrap: wrap;
gap: 20px;
}
@media (max-width: 600px) {
.follow-author-content {
width: 95%;
margin: 10px;
}
.follow-author-add-section {
flex-direction: column;
gap: 8px;
}
.follow-author-add-btn {
width: 100%;
}
.follow-author-stats {
flex-direction: column;
gap: 5px;
}
.follow-author-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.follow-author-content-wrapper {
flex-direction: column;
width: 100%;
}
.follow-author-last-viewed {
flex: none;
width: 100%;
}
.follow-author-name {
min-width: auto;
margin-right: 0;
}
.follow-author-favorites {
width: 100%;
}
}
`;
document.head.appendChild(style);
// 关注作者管理类
class FollowAuthorManager {
constructor() {
this.followedAuthors = this.loadFollowedAuthors();
this.init();
}
init() {
this.createModal();
this.bindEvents();
console.log('关注作者管理器初始化完成');
}
// 从localStorage加载关注的作者列表
loadFollowedAuthors() {
try {
const stored = localStorage.getItem('xmxmkb_followed_authors');
const authors = stored ? JSON.parse(stored) : [];
// 如果是旧格式(字符串数组),转换为新格式(对象数组)
if (authors.length > 0 && typeof authors[0] === 'string') {
return authors.map(name => ({
name: name,
lastSearchTime: null,
lastViewedWork: null
}));
}
return authors;
} catch (e) {
console.error('加载关注作者列表失败:', e);
return [];
}
}
// 保存关注的作者列表到localStorage
saveFollowedAuthors() {
try {
localStorage.setItem('xmxmkb_followed_authors', JSON.stringify(this.followedAuthors));
console.log('关注作者列表已保存:', this.followedAuthors);
} catch (e) {
console.error('保存关注作者列表失败:', e);
}
}
// 创建弹窗
createModal() {
this.modal = document.createElement('div');
this.modal.className = 'follow-author-modal';
this.modal.innerHTML = `
<div class="follow-author-content">
<div class="follow-author-header">
<div class="follow-author-title">关注作者管理</div>
<button class="follow-author-close">×</button>
</div>
<div class="follow-author-stats">
<span>已关注作者: <strong id="followed-count">0</strong> 人</span>
<span style="margin-left: 20px;">收藏总数: <strong id="total-favorites">0</strong></span>
<span style="margin-left: 20px;">匹配收藏: <strong id="matched-favorites">0</strong></span>
</div>
<div class="follow-author-add-section">
<input type="text" class="follow-author-input" placeholder="输入作者名称" id="author-input">
<button class="follow-author-add-btn" id="add-author-btn">添加关注</button>
<button class="follow-author-add-btn" id="auto-extract-btn" style="background-color: #2196f3;">自动提取</button>
<button class="follow-author-add-btn" id="test-extract-btn" style="background-color: #ff9800; font-size: 12px;">测试提取</button>
</div>
<div class="follow-author-list" id="author-list">
<div class="follow-author-empty">暂无关注的作者</div>
</div>
</div>
`;
document.body.appendChild(this.modal);
}
// 绑定事件
bindEvents() {
// 监听来自主脚本的事件
document.addEventListener('followAuthorClick', () => {
this.showModal();
});
// 关闭弹窗
this.modal.querySelector('.follow-author-close').addEventListener('click', () => {
this.hideModal();
});
// 点击背景关闭弹窗
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) {
this.hideModal();
}
});
// 添加作者
const addBtn = this.modal.querySelector('#add-author-btn');
const authorInput = this.modal.querySelector('#author-input');
const autoExtractBtn = this.modal.querySelector('#auto-extract-btn');
addBtn.addEventListener('click', () => {
this.addAuthor();
});
authorInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.addAuthor();
}
});
// 自动提取作者
autoExtractBtn.addEventListener('click', () => {
this.autoExtractAuthors();
});
// 测试提取功能
const testExtractBtn = this.modal.querySelector('#test-extract-btn');
testExtractBtn.addEventListener('click', () => {
this.testExtractAuthors();
});
}
// 显示弹窗
showModal() {
this.updateAuthorList();
this.modal.style.display = 'flex';
}
// 隐藏弹窗
hideModal() {
this.modal.style.display = 'none';
}
// 添加关注作者
addAuthor() {
const input = this.modal.querySelector('#author-input');
const authorName = input.value.trim();
if (!authorName) {
alert('请输入作者名称');
return;
}
if (this.followedAuthors.some(author =>
(typeof author === 'string' ? author : author.name).toLowerCase() === authorName.toLowerCase())) {
alert('该作者已在关注列表中');
return;
}
this.followedAuthors.push({
name: authorName,
lastSearchTime: null,
lastViewedWork: null
});
this.saveFollowedAuthors();
this.updateAuthorList();
input.value = '';
}
// 取消关注作者
unfollowAuthor(authorName) {
const index = this.followedAuthors.findIndex(author =>
(typeof author === 'string' ? author : author.name) === authorName);
if (index > -1) {
this.followedAuthors.splice(index, 1);
this.saveFollowedAuthors();
this.updateAuthorList();
}
}
// 测试提取功能(仅显示结果,不添加)
testExtractAuthors() {
const favorites = this.getFavoritesData();
if (favorites.length === 0) {
alert('当前没有收藏数据');
return;
}
const extractResults = [];
favorites.slice(0, 10).forEach((fav, index) => {
const author = this.extractAuthorFromTitle(fav.title);
extractResults.push(`${index + 1}. "${fav.title}" → 作者: ${author || '无法提取'}`);
});
const message = `提取测试结果(前10条):\n\n${extractResults.join('\n')}\n\n这只是测试,未实际添加作者。`;
alert(message);
}
// 自动从收藏中提取作者
autoExtractAuthors() {
const favorites = this.getFavoritesData();
if (favorites.length === 0) {
alert('当前没有收藏数据');
return;
}
const extractedAuthors = new Set();
let extractedCount = 0;
favorites.forEach(fav => {
const author = this.extractAuthorFromTitle(fav.title);
if (author && !this.followedAuthors.some(followedAuthor =>
(typeof followedAuthor === 'string' ? followedAuthor : followedAuthor.name).toLowerCase() === author.toLowerCase())) {
extractedAuthors.add(author);
extractedCount++;
}
});
if (extractedCount === 0) {
alert('没有找到新的作者,可能已经全部关注或标题格式不支持自动提取');
return;
}
const authorsArray = Array.from(extractedAuthors);
const message = `找到 ${extractedCount} 个新作者:\n${authorsArray.slice(0, 10).join(', ')}${authorsArray.length > 10 ? '\n...' : ''}\n\n是否全部添加到关注列表?`;
if (confirm(message)) {
authorsArray.forEach(author => {
this.followedAuthors.push({
name: author,
lastSearchTime: null,
lastViewedWork: null
});
});
this.saveFollowedAuthors();
this.updateAuthorList();
alert(`成功添加 ${extractedCount} 个作者到关注列表`);
}
}
// 从收藏数据中提取作者名称
extractAuthorFromTitle(title) {
// 格式7: 作者名_标题 或 作者名 标题(空格分隔)
let match = title.match(/^([^_\s\[\]【】()()]+)[_\s]+/);
if (match && match[1].length >= 2 && match[1].length <= 20) {
return match[1].trim();
}
return null;
}
// 获取收藏数据
getFavoritesData() {
try {
// 优先从localStorage获取(与主脚本保持一致)
const stored = localStorage.getItem('xmxmkb_favorites');
if (stored) {
const data = JSON.parse(stored);
console.log(`获取到 ${data.length} 条收藏数据`);
return data;
}
// 备用:尝试从GM_getValue获取
if (typeof GM_getValue !== 'undefined') {
const data = GM_getValue('favorites', '[]');
return JSON.parse(data);
}
return [];
} catch (e) {
console.error('获取收藏数据失败:', e);
return [];
}
}
// 获取预加载数据
getPreloadedData() {
try {
const stored = localStorage.getItem('fubiji_preloaded_images');
if (stored) {
const data = JSON.parse(stored);
console.log(`获取到预加载数据,包含 ${Object.keys(data).length} 个URL`);
return data;
}
return {};
} catch (e) {
console.error('获取预加载数据失败:', e);
return {};
}
}
// 从预加载数据中获取作者的最近访问记录
getAuthorRecentViews(authorName) {
const preloadedData = this.getPreloadedData();
const authorViews = [];
Object.entries(preloadedData).forEach(([url, data]) => {
if (data.title && data.loaded) {
// 简化匹配逻辑:只要标题中包含作者名称即可
if (data.title.toLowerCase().includes(authorName.toLowerCase())) {
authorViews.push({
title: data.title,
url: url,
timestamp: data.timestamp,
images: data.images || [],
firstImage: (data.images && data.images.length > 0) ? data.images[0] : null
});
}
}
});
// 按时间戳降序排序,取最新的一条
return authorViews
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
.slice(0, 1);
}
// 获取指定作者的收藏数据(最新2条)
getAuthorFavorites(authorName) {
const favorites = this.getFavoritesData();
const authorFavorites = favorites.filter(fav => {
// 简化匹配逻辑:只要标题中包含作者名称即可
return fav.title && fav.title.toLowerCase().includes(authorName.toLowerCase());
});
// 按时间戳降序排序,取最新2条
return authorFavorites
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
.slice(0, 2);
}
// 更新作者搜索时间
updateAuthorSearchTime(authorName) {
const author = this.followedAuthors.find(a =>
(typeof a === 'string' ? a : a.name) === authorName);
if (author && typeof author === 'object') {
author.lastSearchTime = Date.now();
this.saveFollowedAuthors();
}
}
// 记录最后查看的作品
recordLastViewedWork(authorName, workTitle, workUrl) {
const author = this.followedAuthors.find(a =>
(typeof a === 'string' ? a : a.name) === authorName);
if (author && typeof author === 'object') {
author.lastViewedWork = {
title: workTitle,
url: workUrl,
timestamp: Date.now()
};
this.saveFollowedAuthors();
}
}
// 更新作者列表显示
updateAuthorList() {
const listContainer = this.modal.querySelector('#author-list');
const countElement = this.modal.querySelector('#followed-count');
const totalFavoritesElement = this.modal.querySelector('#total-favorites');
const matchedFavoritesElement = this.modal.querySelector('#matched-favorites');
const favorites = this.getFavoritesData();
let matchedCount = 0;
countElement.textContent = this.followedAuthors.length;
totalFavoritesElement.textContent = favorites.length;
if (this.followedAuthors.length === 0) {
listContainer.innerHTML = '<div class="follow-author-empty">暂无关注的作者<br><small style="color: #999; margin-top: 10px; display: block;">可以手动添加作者名称,或点击"自动提取"从现有收藏中提取作者</small></div>';
matchedFavoritesElement.textContent = 0;
return;
}
listContainer.innerHTML = '';
this.followedAuthors.forEach(authorData => {
const authorName = typeof authorData === 'string' ? authorData : authorData.name;
const authorObj = typeof authorData === 'object' ? authorData : { name: authorData, lastSearchTime: null, lastViewedWork: null };
const authorFavorites = this.getAuthorFavorites(authorName);
matchedCount += authorFavorites.length;
const item = document.createElement('div');
item.className = 'follow-author-item';
const nameElement = document.createElement('div');
nameElement.className = 'follow-author-name';
const nameText = document.createElement('div');
nameText.textContent = authorName;
nameText.style.cursor = 'pointer';
nameText.title = `点击搜索 ${authorName} 的作品`;
nameText.addEventListener('click', () => {
this.searchAuthor(authorName);
this.updateAuthorSearchTime(authorName);
});
nameElement.appendChild(nameText);
// 显示最近搜索时间
if (authorObj.lastSearchTime) {
const searchTimeElement = document.createElement('div');
searchTimeElement.className = 'follow-author-search-time';
searchTimeElement.textContent = `搜索: ${this.formatTime(authorObj.lastSearchTime)}`;
nameElement.appendChild(searchTimeElement);
}
// 创建内容包装器
const contentWrapper = document.createElement('div');
contentWrapper.className = 'follow-author-content-wrapper';
// 最后查看的作品(左边)- 优先从预加载数据获取
const lastViewedContainer = document.createElement('div');
lastViewedContainer.className = 'follow-author-last-viewed';
const lastViewedTitle = document.createElement('div');
lastViewedTitle.className = 'follow-author-section-title';
lastViewedTitle.textContent = '最后查看';
lastViewedContainer.appendChild(lastViewedTitle);
// 优先从预加载数据获取最近访问记录
const recentViews = this.getAuthorRecentViews(authorName);
let lastViewedWork = null;
if (recentViews.length > 0) {
lastViewedWork = recentViews[0];
} else if (authorObj.lastViewedWork) {
lastViewedWork = authorObj.lastViewedWork;
}
if (lastViewedWork) {
const viewedItem = document.createElement('div');
viewedItem.className = 'follow-author-last-viewed-item';
viewedItem.title = lastViewedWork.title;
const titleElement = document.createElement('div');
titleElement.className = 'follow-author-favorite-title';
titleElement.textContent = lastViewedWork.title;
viewedItem.appendChild(titleElement);
// 添加图片显示(如果有的话)
if (lastViewedWork.firstImage || (lastViewedWork.images && lastViewedWork.images.length > 0)) {
const imageElement = document.createElement('img');
imageElement.className = 'follow-author-favorite-image';
imageElement.src = lastViewedWork.firstImage || lastViewedWork.images[0];
imageElement.alt = lastViewedWork.title;
imageElement.loading = 'lazy';
imageElement.onerror = function() {
this.style.display = 'none';
};
viewedItem.appendChild(imageElement);
}
const timeElement = document.createElement('div');
timeElement.className = 'follow-author-favorite-time';
timeElement.textContent = this.formatTime(lastViewedWork.timestamp);
viewedItem.appendChild(timeElement);
viewedItem.addEventListener('click', () => {
window.open(lastViewedWork.url, '_blank');
});
lastViewedContainer.appendChild(viewedItem);
} else {
const emptyText = document.createElement('span');
emptyText.textContent = '暂无记录';
emptyText.style.color = '#999';
emptyText.style.fontSize = '12px';
lastViewedContainer.appendChild(emptyText);
}
// 最新收藏(右边)
const favoritesContainer = document.createElement('div');
favoritesContainer.className = 'follow-author-favorites';
const favoritesTitle = document.createElement('div');
favoritesTitle.className = 'follow-author-section-title';
favoritesTitle.textContent = '最新收藏';
favoritesTitle.style.width = '100%';
favoritesContainer.appendChild(favoritesTitle);
const favoritesWrapper = document.createElement('div');
favoritesWrapper.style.display = 'flex';
favoritesWrapper.style.gap = '10px';
favoritesWrapper.style.flexWrap = 'wrap';
favoritesWrapper.style.width = '100%';
if (authorFavorites.length === 0) {
const emptyText = document.createElement('span');
emptyText.textContent = '暂无收藏';
emptyText.style.color = '#999';
emptyText.style.fontSize = '12px';
favoritesWrapper.appendChild(emptyText);
} else {
authorFavorites.forEach(fav => {
const favItem = document.createElement('div');
favItem.className = 'follow-author-favorite-item';
favItem.title = fav.title;
// 标题元素
const titleElement = document.createElement('div');
titleElement.className = 'follow-author-favorite-title';
titleElement.textContent = fav.title;
favItem.appendChild(titleElement);
// 图片元素(优先显示收藏数据中的图片)
let imageToShow = null;
if (fav.firstImage) {
imageToShow = fav.firstImage;
} else if (fav.images && fav.images.length > 0) {
imageToShow = fav.images[0];
}
if (imageToShow) {
const imageElement = document.createElement('img');
imageElement.className = 'follow-author-favorite-image';
imageElement.src = imageToShow;
imageElement.alt = fav.title;
imageElement.loading = 'lazy';
imageElement.onerror = function() {
this.style.display = 'none';
};
favItem.appendChild(imageElement);
}
// 时间元素
if (fav.timestamp) {
const timeElement = document.createElement('div');
timeElement.className = 'follow-author-favorite-time';
timeElement.textContent = this.formatTime(fav.timestamp);
favItem.appendChild(timeElement);
}
favItem.addEventListener('click', () => {
window.open(fav.url, '_blank');
});
favoritesWrapper.appendChild(favItem);
});
}
favoritesContainer.appendChild(favoritesWrapper);
contentWrapper.appendChild(lastViewedContainer);
contentWrapper.appendChild(favoritesContainer);
const unfollowBtn = document.createElement('button');
unfollowBtn.className = 'follow-author-unfollow';
unfollowBtn.textContent = '取消关注';
unfollowBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`确定要取消关注 ${authorName} 吗?`)) {
this.unfollowAuthor(authorName);
}
});
item.appendChild(nameElement);
item.appendChild(contentWrapper);
item.appendChild(unfollowBtn);
listContainer.appendChild(item);
});
matchedFavoritesElement.textContent = matchedCount;
}
// 格式化时间戳
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
// 小于1分钟
if (diff < 60000) {
return '刚刚';
}
// 小于1小时
if (diff < 3600000) {
return `${Math.floor(diff / 60000)}分钟前`;
}
// 小于1天
if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}小时前`;
}
// 小于7天
if (diff < 604800000) {
return `${Math.floor(diff / 86400000)}天前`;
}
// 超过7天显示具体日期
return date.toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric'
});
}
// 搜索作者
searchAuthor(authorName) {
const searchUrl = `https://www.xmxmkb.com/search.php?mod=forum&searchid=85&orderby=lastpost&ascdesc=desc&searchsubmit=yes&kw=${encodeURIComponent(authorName)}`;
window.open(searchUrl, '_blank');
}
// 对外暴露的方法,供其他脚本调用
recordAuthorView(authorName, workTitle, workUrl) {
this.recordLastViewedWork(authorName, workTitle, workUrl);
console.log(`记录作者 ${authorName} 的查看记录:`, workTitle);
}
// 获取关注的作者列表(供其他脚本使用)
getFollowedAuthorNames() {
return this.followedAuthors.map(author =>
typeof author === 'string' ? author : author.name
);
}
}
// 全局实例
let followAuthorManagerInstance = null;
// 等待页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
followAuthorManagerInstance = new FollowAuthorManager();
// 将实例暴露到全局,供其他脚本使用
window.followAuthorManager = followAuthorManagerInstance;
});
} else {
followAuthorManagerInstance = new FollowAuthorManager();
window.followAuthorManager = followAuthorManagerInstance;
}
console.log('关注作者管理脚本已加载');
})();
Wrap
Beautify