// ==UserScript==
// @name Nodeloc 帖子增强工具
// @author 流浪开发者
// @namespace http://tampermonkey.net/
// @version 3.1
// @description 性能优化,交互体验再升级!在Nodeloc帖子列表和详情页添加“已读”、“隐藏”、“高亮”、“置顶”功能(置顶支持自定义标题),并提供一个带“不再提示”选项的美观、流畅、高效的记录管理中心。
// @match https://www.nodeloc.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=nodeloc.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const CONFIG_KEY_HIDE = 'tm_config_dont_ask_hide';
const CONFIG_KEY_DELETE = 'tm_config_dont_ask_delete';
// --- 菜单命令 ---
GM_registerMenuCommand('重置“隐藏不再提示”', () => {
GM_setValue(CONFIG_KEY_HIDE, false);
alert('“隐藏帖子不再提示”的设置已重置。');
});
GM_registerMenuCommand('重置“删除不再提示”', () => {
GM_setValue(CONFIG_KEY_DELETE, false);
alert('“删除记录不再提示”的设置已重置。');
});
// v3.0: 新增清空数据命令
GM_registerMenuCommand('清除所有帖子记录', () => {
if (confirm('您确定要清除所有“已读”、“隐藏”、“高亮”、“置顶”记录吗?此操作不可撤销。')) {
dataManager.clearAll();
}
});
// --- 样式定义 ---
GM_addStyle(`
/* 基础样式 */
.topic-list-item.tm-read .title { color: #888 !important; }
body.dark .topic-list-item.tm-read .title { color: #666 !important; }
.topic-list-item.tm-read .topic-excerpt, .topic-list-item.tm-read .badge-category, .topic-list-item.tm-read .posters { opacity: 0.6; }
.topic-list-item.tm-highlight { background-color: #fef8e0 !important; }
body.dark .topic-list-item.tm-highlight { background-color: #4a412a !important; }
.tm-controls { display: flex; gap: 10px; margin-top: 8px; align-items: center; flex-wrap: wrap; }
.tm-btn { background: none; border: 1px solid #ccc; border-radius: 4px; padding: 2px 6px; font-size: 12px; cursor: pointer; transition: background-color 0.2s, color 0.2s; white-space: nowrap; }
body.dark .tm-btn { border-color: #555; color: #ddd; }
.tm-btn:hover { background-color: #e9e9e9; }
body.dark .tm-btn:hover { background-color: #333; }
.tm-btn.active { background-color: #007bff; color: white; border-color: #007bff; }
body.dark .tm-btn.active { background-color: #009966; border-color: #009966; }
.tm-btn.tm-btn-read.active { background-color: #f0f0f0; color: #888; border-color: #ddd; }
body.dark .tm-btn.tm-btn-read.active { background-color: #3a3a3a; color: #777; border-color: #555; }
.tm-btn.tm-btn-danger { border-color: #dc3545; color: #dc3545; }
.tm-btn.tm-btn-danger:hover { background-color: #dc3545; color: white; }
body.dark .tm-btn.tm-btn-danger { border-color: #a02a37; color: #e56d79; }
body.dark .tm-btn.tm-btn-danger:hover { background-color: #a02a37; color: white; }
.tm-btn[disabled] { cursor: not-allowed; opacity: 0.5; }
/* 置顶容器 */
#tm-pinned-container-wrapper { border: 1px solid var(--tertiary-300, #cde8e1); border-radius: 5px; margin-bottom: 15px; overflow: hidden; display: none; position: relative; z-index: 100; background: var(--secondary, #fff); }
body.dark #tm-pinned-container-wrapper { border-color: #333; background: var(--secondary, #252525); }
.tm-pinned-header { font-size: 14px; font-weight: bold; padding: 8px 12px; background-color: var(--tertiary-100, #f2fbf8); }
body.dark .tm-pinned-header { background-color: #2c3e38; }
/* 帖子详情页按钮 */
.tm-topic-controls { display: flex; gap: 8px; margin-top: 10px; clear: both; align-items: center; }
#topic-title .title-wrapper.tm-topic-title-read h1 a, #topic-title .title-wrapper.tm-topic-title-read h1 span { color: #888 !important; }
body.dark #topic-title .title-wrapper.tm-topic-title-read h1 a, body.dark #topic-title .title-wrapper.tm-topic-title-read h1 span { color: #666 !important; }
#topic-title .title-wrapper.tm-topic-title-highlight { background-color: #fef8e0; padding: 2px 5px; border-radius: 3px; }
body.dark #topic-title .title-wrapper.tm-topic-title-highlight { background-color: #4a412a; }
/* 记录管理与模态框 */
#tm-records-btn { margin-left: 8px; }
.tm-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1001; display: flex; align-items: center; justify-content: center; }
.tm-modal-content { background: #fff; padding: 20px; border-radius: 8px; width: 90%; max-width: 650px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
body.dark .tm-modal-content { background: #252525; color: #ddd; }
.tm-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px; flex-shrink: 0; }
body.dark .tm-modal-header { border-bottom-color: #444; }
.tm-modal-title { font-size: 18px; font-weight: bold; margin: 0; }
.tm-modal-close { font-size: 24px; cursor: pointer; background: none; border: none; color: #888; padding: 0 5px; line-height: 1; }
body.dark .tm-modal-close { color: #aaa; }
.tm-modal-body { flex-grow: 1; overflow-y: auto; min-height: 0; }
.tm-modal-footer { text-align: right; display: flex; gap: 10px; justify-content: flex-end; margin-top: 15px; flex-shrink: 0; align-items: center; }
.tm-tabs-container { display: flex; gap: 20px; margin-bottom: 15px; padding: 8px 4px 0; flex-wrap: wrap; border-bottom: 1px solid #eee; min-height: 40px; }
body.dark .tm-tabs-container { border-bottom-color: #444; }
.tm-modal-tab-btn { position: relative; background: none; border: none; border-bottom: 3px solid transparent; padding: 8px 4px; font-size: 14px; cursor: pointer; color: #555; margin-bottom: -1px; transition: all 0.2s; }
body.dark .tm-modal-tab-btn { color: #aaa; }
.tm-modal-tab-btn:hover:not(.active) { color: #111; }
body.dark .tm-modal-tab-btn:hover:not(.active) { color: #eee; }
.tm-modal-tab-btn.active { color: var(--tertiary, #007bff); border-bottom-color: var(--tertiary, #007bff); font-weight: bold; }
body.dark .tm-modal-tab-btn.active { color: var(--tertiary, #009966); border-bottom-color: var(--tertiary, #009966); }
.tm-tab-count { position: absolute; top: 0; right: 0; transform: translate(60%, -40%); background-color: #e45735; color: white; border-radius: 10px; padding: 1px 6px; font-size: 11px; font-weight: bold; line-height: 1.4; }
body.dark .tm-tab-count { background-color: #c84a36; }
.tm-confirm-title, .tm-custom-title-input { font-weight: bold; margin: 10px 0; padding: 10px; background-color: #f5f5f5; border-radius: 4px; word-break: break-all; }
body.dark .tm-confirm-title, body.dark .tm-custom-title-input { background-color: #333; border-color: #555; color: #ddd;}
.tm-custom-title-input { width: 100%; border: 1px solid #ccc; box-sizing: border-box; }
.tm-records-list { list-style: none; padding: 0; margin:0; transition: opacity 0.3s; }
.tm-records-list.fade-out { opacity: 0; }
.tm-records-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 5px; border-bottom: 1px solid #f0f0f0; }
body.dark .tm-records-item { border-bottom-color: #444; }
.tm-records-item:last-child { border-bottom: none; }
.tm-records-item a { text-decoration: none; color: var(--primary-high, #222); flex-grow: 1; margin-right: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
body.dark .tm-records-item a { color: var(--primary-high, #ddd); }
.tm-records-item-controls { flex-shrink: 0; }
.tm-records-item-controls button { margin-left: 8px; }
.tm-empty-list-placeholder { color: #888; text-align: center; padding: 30px; font-style: italic; }
.tm-dont-ask-again { margin-right: auto; font-size: 12px; color: #666; display: flex; align-items: center; }
body.dark .tm-dont-ask-again { color: #999; }
.tm-dont-ask-again input { margin-right: 5px; }
`);
/**
* 数据管理器 (v3.0 性能重构)
* - 将分散的 GM_Value 整合为单个 JSON 对象,大幅提升读写性能。
* - 实现内存缓存和延迟保存,UI 操作更流畅。
* - 内置从旧版数据格式到新格式的自动迁移程序。
*/
const dataManager = {
_data: {},
_isDirty: false,
_saveTimeout: null,
STORAGE_KEY: 'nodeloc_enhancement_data',
OLD_STORAGE_PREFIX: 'discourse_topic_',
async load() {
await this._migrateData();
this._data = GM_getValue(this.STORAGE_KEY, {});
},
save() {
clearTimeout(this._saveTimeout);
if (!this._isDirty) return;
this._saveTimeout = setTimeout(() => {
GM_setValue(this.STORAGE_KEY, this._data);
this._isDirty = false;
Promise.resolve().then(uiManager.updateTabCounts);
}, 500);
},
get(topicId) {
return this._data[topicId] || {};
},
update(topicId, key, value) {
this._data[topicId] = this._data[topicId] || {};
this._data[topicId][key] = value;
const data = this._data[topicId];
const hasTrueValue = Object.keys(data).some(k => k !== 'title' && data[k]);
if (!hasTrueValue && !data.title) {
this.delete(topicId);
} else {
this._isDirty = true;
this.save();
}
},
delete(topicId) {
delete this._data[topicId];
this._isDirty = true;
this.save();
},
clearAll() {
this._data = {};
this._isDirty = true;
this.save();
alert('所有帖子增强记录已被清除。');
location.reload();
},
getAllData() {
return Object.entries(this._data).map(([topicId, d]) => ({ topicId, ...d }));
},
async _migrateData() {
const oldKeys = GM_listValues().filter(key => key.startsWith(this.OLD_STORAGE_PREFIX));
if (oldKeys.length === 0) return;
console.log(`Nodeloc Enhancer: Migrating ${oldKeys.length} old records to new format...`);
alert(`Nodeloc 帖子增强工具:\n\n检测到旧版数据,将为您自动升级到新版存储格式以提升性能。该过程仅需一次,请稍候...`);
const newData = GM_getValue(this.STORAGE_KEY, {});
let migratedCount = 0;
for (const key of oldKeys) {
const topicId = key.replace(this.OLD_STORAGE_PREFIX, '');
const oldData = GM_getValue(key, {});
if (topicId && Object.keys(oldData).length > 0) {
newData[topicId] = { ...(newData[topicId] || {}), ...oldData };
migratedCount++;
}
}
GM_setValue(this.STORAGE_KEY, newData);
for (const key of oldKeys) {
GM_deleteValue(key);
}
alert(`数据迁移完成!\n\n成功迁移了 ${migratedCount} 条记录。\n旧的存储数据已被移除以释放空间。\n页面将自动刷新。`);
console.log(`Nodeloc Enhancer: Migration complete. ${migratedCount} records moved.`);
location.reload();
}
};
/**
* UI 管理器
*/
const uiManager = {
createModal(id, title, bodyHtml, footerHtml = '') {
document.getElementById(id)?.remove();
const overlay = document.createElement('div');
overlay.id = id;
overlay.className = 'tm-modal-overlay';
overlay.style.display = 'none';
overlay.innerHTML = `<div class="tm-modal-content"><div class="tm-modal-header"><h2 class="tm-modal-title">${title}</h2><button class="tm-modal-close">×</button></div><div class="tm-modal-body">${bodyHtml}</div>${footerHtml ? `<div class="tm-modal-footer">${footerHtml}</div>` : ''}</div>`;
document.body.appendChild(overlay);
const close = () => { overlay.style.display = 'none'; overlay.remove(); };
overlay.querySelector('.tm-modal-close').onclick = close;
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
return overlay;
},
showConfirmation({ title, bodyHtml, onConfirm, confirmText = '确定', cancelText = '取消', confirmClass = 'active', showDontAskAgain = false, configKey = '' }) {
let footer = `<button id="tm-confirm-cancel" class="tm-btn">${cancelText}</button><button id="tm-confirm-ok" class="tm-btn ${confirmClass}">${confirmText}</button>`;
if (showDontAskAgain) {
footer = `<label class="tm-dont-ask-again"><input type="checkbox" id="tm-dont-ask-checkbox">不再提示</label>` + footer;
}
const modal = this.createModal('tm-confirm-modal', title, bodyHtml, footer);
modal.style.display = 'flex';
const okButton = document.getElementById('tm-confirm-ok');
const cancelButton = document.getElementById('tm-confirm-cancel');
const checkbox = document.getElementById('tm-dont-ask-checkbox');
const close = () => modal.remove();
okButton.onclick = () => {
if (showDontAskAgain && checkbox.checked) {
GM_setValue(configKey, true);
}
onConfirm();
close();
};
cancelButton.onclick = close;
},
showPinModal(currentTitle, onConfirm) {
const bodyHtml = `<p>您可以为这个置顶帖子设置一个自定义标题(可选):</p><input type="text" id="tm-custom-title-input" class="tm-custom-title-input" value="${currentTitle.replace(/"/g, '"')}">`;
const footerHtml = `<button id="tm-confirm-cancel" class="tm-btn">取消</button><button id="tm-confirm-ok" class="tm-btn active">置顶</button>`;
const modal = this.createModal('tm-pin-modal', '置顶帖子', bodyHtml, footerHtml);
modal.style.display = 'flex';
const titleInput = document.getElementById('tm-custom-title-input');
const okButton = document.getElementById('tm-confirm-ok');
const cancelButton = document.getElementById('tm-confirm-cancel');
const close = () => modal.remove();
okButton.onclick = () => { onConfirm(titleInput.value); close(); };
cancelButton.onclick = close;
},
showRecordsModal() {
const modal = this.createModal('tm-records-modal', '查看记录', `<div id="tm-tabs-container" class="tm-tabs-container"></div><ul id="tm-records-list-ul" class="tm-records-list"></ul>`);
const tabsContainer = modal.querySelector('#tm-tabs-container');
const listEl = modal.querySelector('#tm-records-list-ul');
const tabConfigs = [ { key: 'hidden', label: '隐藏' }, { key: 'pinned', label: '置顶' }, { key: 'highlight', label: '高亮' }, { key: 'read', label: '已读' }];
const populateList = (filterKey) => {
listEl.classList.add('fade-out');
setTimeout(() => {
listEl.innerHTML = '';
const allData = dataManager.getAllData();
const topics = allData.filter(item => item[filterKey]);
const currentTabConfig = tabConfigs.find(t => t.key === filterKey);
if (topics.length === 0) {
listEl.innerHTML = `<li class="tm-empty-list-placeholder">没有被标记为“${currentTabConfig.label}”的帖子。</li>`;
} else {
topics.forEach(({ topicId, title }) => {
const item = document.createElement('li');
item.className = 'tm-records-item';
item.innerHTML = `<a href="/t/${topicId}" target="_blank" title="${title}">${title || `帖子 #${topicId}`}</a><div class="tm-records-item-controls"><button class="tm-btn action-btn">取消${currentTabConfig.label}</button><button class="tm-btn tm-btn-danger delete-btn">删除记录</button></div>`;
item.querySelector('.action-btn').onclick = () => { dataManager.update(topicId, filterKey, false); this.updateTabCounts(); populateList(filterKey); document.querySelectorAll(`tr[data-topic-id="${topicId}"]`).forEach(row => features.applyRowStyles(row, dataManager.get(topicId))); };
item.querySelector('.delete-btn').onclick = () => {
const onConfirm = () => {
dataManager.delete(topicId); this.updateTabCounts(); populateList(filterKey);
const row = document.querySelector(`tr[data-topic-id="${topicId}"]`);
if (row) {
if (row.classList.contains('tm-injected')) { row.remove(); }
else { features.applyRowStyles(row, {}); row.querySelector('.tm-controls')?.remove(); features.addControlsToRow(row, topicId); }
}
};
if (GM_getValue(CONFIG_KEY_DELETE, false)) { onConfirm(); }
else { this.showConfirmation({ title: '确认删除记录', bodyHtml: `<p>您确定要永久删除此帖子的所有记录吗?此操作不可撤销。</p><div class="tm-confirm-title">${title || `帖子 #${topicId}`}</div>`, confirmText: '删除', confirmClass: 'tm-btn-danger', onConfirm, showDontAskAgain: true, configKey: CONFIG_KEY_DELETE }); }
};
listEl.appendChild(item);
});
}
listEl.classList.remove('fade-out');
}, 150);
};
tabConfigs.forEach(tab => {
const tabBtn = document.createElement('button');
tabBtn.className = 'tm-modal-tab-btn';
tabBtn.dataset.key = tab.key;
tabBtn.innerHTML = `${tab.label}<span class="tm-tab-count" style="display:none;"></span>`;
tabBtn.onclick = () => {
tabsContainer.querySelectorAll('.tm-modal-tab-btn').forEach(b => b.classList.remove('active'));
tabBtn.classList.add('active');
populateList(tab.key);
};
tabsContainer.appendChild(tabBtn);
});
this.updateTabCounts();
modal.style.display = 'flex';
tabsContainer.querySelector('button[data-key="hidden"]').click();
},
updateTabCounts() {
const tabsContainer = document.getElementById('tm-tabs-container');
if (!tabsContainer) return;
const allData = dataManager.getAllData();
tabsContainer.querySelectorAll('.tm-modal-tab-btn').forEach(tabBtn => {
const key = tabBtn.dataset.key;
const count = allData.filter(item => item[key]).length;
const countEl = tabBtn.querySelector('.tm-tab-count');
if (count > 0) { countEl.textContent = count; countEl.style.display = ''; }
else { countEl.style.display = 'none'; }
});
},
addManagementButton() {
// Check if the button already exists to prevent duplicates
if (document.getElementById('tm-records-btn')) return;
// Find a reliable anchor point in the header, like the user menu icon container (#current-user)
const currentUserLi = document.querySelector('.d-header .d-header-icons #current-user');
if (currentUserLi) {
// Create an LI element to wrap the button, ensuring correct HTML structure and styling within the UL
const newLi = document.createElement('li');
const btn = document.createElement('button');
btn.id = 'tm-records-btn';
btn.className = 'btn no-text btn-icon icon btn-flat';
btn.title = '查看记录';
btn.innerHTML = `<svg class="fa d-icon d-icon-list-ul svg-icon svg-string" aria-hidden="true"><use href="#list-ul"></use></svg>`;
btn.onclick = () => this.showRecordsModal();
// Append the button to the new list item
newLi.appendChild(btn);
// Insert the new list item before the user menu list item
currentUserLi.parentNode.insertBefore(newLi, currentUserLi);
}
}
};
/**
* 核心功能模块
*/
const features = {
_createListButton(text, key, topicId, row) {
const btn = document.createElement('button');
btn.textContent = text;
btn.className = 'tm-btn';
if (key === 'read') btn.classList.add('tm-btn-read');
btn.dataset.key = key;
btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
const currentData = dataManager.get(topicId);
const isCurrentlyActive = !!currentData[key];
const title = row.querySelector('a.title')?.textContent.trim() || currentData.title || `帖子 #${topicId}`;
if (key === 'pinned' && !isCurrentlyActive) {
uiManager.showPinModal(currentData.title || title, (newTitle) => {
dataManager.update(topicId, 'pinned', true);
dataManager.update(topicId, 'title', newTitle || title);
this.applyRowStyles(row, dataManager.get(topicId));
});
return;
}
if (key === 'hidden' && !isCurrentlyActive) {
if (!currentData.title) dataManager.update(topicId, 'title', title);
const onConfirm = () => {
dataManager.update(topicId, key, true);
this.applyRowStyles(row, dataManager.get(topicId));
};
if (GM_getValue(CONFIG_KEY_HIDE, false)) { onConfirm(); }
else { uiManager.showConfirmation({ title: '确认隐藏', bodyHtml: `<p>您确定要隐藏这个帖子吗?<br>您可以在记录管理中心找到并恢复它。</p><div class="tm-confirm-title">${title}</div>`, onConfirm, showDontAskAgain: true, configKey: CONFIG_KEY_HIDE }); }
return;
}
const isActive = !isCurrentlyActive;
dataManager.update(topicId, key, isActive);
this.applyRowStyles(row, dataManager.get(topicId));
});
return btn;
},
_createControls(topicId, row) {
const controlsContainer = document.createElement('div');
controlsContainer.className = 'tm-controls';
controlsContainer.append( this._createListButton('已读', 'read', topicId, row), this._createListButton('高亮', 'highlight', topicId, row), this._createListButton('置顶', 'pinned', topicId, row), this._createListButton('隐藏', 'hidden', topicId, row) );
return controlsContainer;
},
addControlsToRow(row, topicId) {
if (!row || !topicId || row.querySelector('.tm-controls')) return;
const targetCell = row.querySelector('td.main-link');
if (targetCell) {
const titleText = targetCell.querySelector('a.title')?.textContent.trim();
if (titleText && !dataManager.get(topicId).title) {
dataManager.update(topicId, 'title', titleText);
}
targetCell.appendChild(this._createControls(topicId, row));
}
},
applyRowStyles(row, data) {
row.classList.toggle('tm-read', !!data.read);
row.classList.toggle('tm-highlight', !!data.highlight);
row.style.display = data.hidden ? 'none' : '';
const pinnedContainerWrapper = document.getElementById('tm-pinned-container-wrapper');
const pinnedBody = pinnedContainerWrapper?.querySelector('.topic-list-body');
const topicListBody = document.querySelector('table.topic-list:not(#tm-pinned-container-wrapper table) tbody.topic-list-body');
if (data.pinned && !data.hidden) {
if (pinnedBody && row.parentElement !== pinnedBody) pinnedBody.prepend(row);
const titleLink = row.querySelector('a.title');
if (titleLink) titleLink.textContent = data.title || `帖子 #${row.dataset.topicId}`;
} else {
if (pinnedBody && row.parentElement === pinnedBody) {
if (row.classList.contains('tm-injected')) row.remove();
else topicListBody?.prepend(row);
}
}
if(pinnedContainerWrapper && pinnedBody) {
const hasVisiblePinnedItems = pinnedBody.querySelector('tr:not([style*="display: none"])');
pinnedContainerWrapper.style.display = hasVisiblePinnedItems ? 'block' : 'none';
}
row.querySelectorAll('.tm-controls .tm-btn').forEach(btn => {
const key = btn.dataset.key;
btn.classList.toggle('active', !!data[key]);
if (key === 'pinned') btn.textContent = data.pinned ? '取消置顶' : '置顶';
btn.disabled = !!(data.pinned && ['read', 'hidden'].includes(key));
});
},
createInjectedRow(topicData) {
const { topicId, title } = topicData;
const row = document.createElement('tr');
row.className = 'topic-list-item tm-injected tm-processed';
row.dataset.topicId = topicId;
const mainCell = document.createElement('td');
mainCell.className = 'main-link topic-list-data';
mainCell.colSpan = 5;
const titleLink = document.createElement('a');
titleLink.href = `/t/${topicId}`;
titleLink.className = 'title raw-link raw-topic-link';
titleLink.textContent = title || `帖子 #${topicId}`;
const linkTopLine = document.createElement('span');
linkTopLine.className = 'link-top-line';
linkTopLine.appendChild(titleLink);
mainCell.appendChild(linkTopLine);
mainCell.appendChild(this._createControls(topicId, row));
row.append(mainCell);
return row;
},
processTopicList() {
const listArea = document.getElementById('list-area');
if (!listArea || !listArea.querySelector('table.topic-list')) return;
const topicListBody = listArea.querySelector('table.topic-list tbody.topic-list-body');
if (!topicListBody) return;
// --- 1. 确保置顶容器存在 ---
let pinnedContainerWrapper = document.getElementById('tm-pinned-container-wrapper');
if (!pinnedContainerWrapper) {
pinnedContainerWrapper = document.createElement('div');
pinnedContainerWrapper.id = 'tm-pinned-container-wrapper';
pinnedContainerWrapper.innerHTML = `<div class="tm-pinned-header">📌 置顶的帖子</div><table class="topic-list"><tbody class="topic-list-body"></tbody></table>`;
const listContainer = topicListBody.closest('.topic-list');
const targetNodeForInsertion = listArea.querySelector('.show-more') || listContainer;
targetNodeForInsertion?.parentNode.insertBefore(pinnedContainerWrapper, targetNodeForInsertion);
}
// --- 2. 处理页面上可见的、未处理的行 ---
document.querySelectorAll('tr.topic-list-item:not(.tm-processed)').forEach(row => {
row.classList.add('tm-processed');
const topicId = row.dataset.topicId;
if (topicId) {
this.addControlsToRow(row, topicId);
this.applyRowStyles(row, dataManager.get(topicId));
}
});
// --- 3. 高效处理置顶帖子 (核心性能优化) ---
const pinnedBody = pinnedContainerWrapper?.querySelector('.topic-list-body');
if (pinnedBody) {
const pinnedTopics = dataManager.getAllData().filter(item => item.pinned && !item.hidden);
// 一次性获取 DOM 中所有帖子的 ID
const domTopicIds = new Set(Array.from(document.querySelectorAll('tr[data-topic-id]'), el => el.dataset.topicId));
// 移除不再置顶的注入行
pinnedBody.querySelectorAll('tr.tm-injected').forEach(injectedRow => {
if (!pinnedTopics.some(p => p.topicId === injectedRow.dataset.topicId)) {
injectedRow.remove();
}
});
// 仅注入当前页面不存在的置顶帖
pinnedTopics.forEach(topicData => {
if (!domTopicIds.has(topicData.topicId)) {
const newRow = this.createInjectedRow(topicData);
pinnedBody.prepend(newRow);
this.applyRowStyles(newRow, dataManager.get(topicData.topicId));
}
});
const hasVisiblePinnedItems = pinnedBody.querySelector('tr:not([style*="display: none"])');
pinnedContainerWrapper.style.display = hasVisiblePinnedItems ? 'block' : 'none';
}
},
processSingleTopicView() {
const topicContainer = document.getElementById('topic');
const topicId = topicContainer?.dataset.topicId;
const target = document.querySelector('#topic-title');
if (!topicId || !target) return;
target.querySelector('.tm-topic-controls')?.remove();
const controlsContainer = document.createElement('div');
controlsContainer.className = 'tm-topic-controls';
const topicTitleElement = target.querySelector('.title-wrapper');
const updateTitleStyles = () => {
if (!topicTitleElement) return;
const data = dataManager.get(topicId);
topicTitleElement.classList.toggle('tm-topic-title-read', !!data.read);
topicTitleElement.classList.toggle('tm-topic-title-highlight', !!data.highlight);
};
const createTopicButton = (text, key) => {
const btn = document.createElement('button');
btn.className = 'tm-btn';
if (key === 'read') btn.classList.add('tm-btn-read');
btn.textContent = text;
if (dataManager.get(topicId)[key]) btn.classList.add('active');
btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
const isActive = !dataManager.get(topicId)[key];
dataManager.update(topicId, key, isActive);
updateTitleStyles();
btn.classList.toggle('active', isActive);
});
return btn;
};
const titleText = topicTitleElement?.textContent.trim();
if (titleText && !dataManager.get(topicId).title) {
dataManager.update(topicId, 'title', titleText);
}
controlsContainer.append(createTopicButton('标记已读', 'read'), createTopicButton('高亮主题', 'highlight'));
target.appendChild(controlsContainer);
updateTitleStyles();
}
};
/**
* 主函数和监听器
*/
function runAllFeatures() {
uiManager.addManagementButton();
if (document.getElementById('list-area')) {
features.processTopicList();
}
if (document.getElementById('topic')) {
features.processSingleTopicView();
}
}
async function initialize() {
// 关键步骤:加载所有数据到内存,并执行一次性迁移(如果需要)
await dataManager.load();
let debounceTimer;
const observer = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(runAllFeatures, 150);
});
const mainOutlet = document.getElementById('main-outlet');
if (mainOutlet) {
runAllFeatures(); // 首次运行
observer.observe(mainOutlet, { childList: true, subtree: true });
} else {
// 兼容页面加载较慢的情况
setTimeout(initialize, 300);
}
}
// 启动脚本
initialize();
})();