// ==UserScript==
// @name Aizex增强插件
// @namespace https://www.klaio.top/
// @version 1.0.0
// @description 为Aizex相关网站提供一系列增强功能,包括高级设置面板、积分显示、界面元素显隐控制、界面优化及自定义头像等。
// @author NianBroken
// @match *://*.mana-x.aizex.net/*
// @match *://*.leopard-x.memofun.net/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect aizex.me
// @run-at document-start
// @icon https://aizex.me/favicon.ico
// @copyright Copyright © 2024 NianBroken. All rights reserved.
// @license Apache-2.0 license
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
// ===================================================================================
// === 全局配置 (CONSTANTS & CONFIGURATION) ===
// ===================================================================================
// 本区域集中了所有用户将来可能需要调整的参数。
// 修改前请务必理解各参数的含义及其对脚本功能的影响。
// ===================================================================================
const CONFIG = {
// --- 脚本基础信息 ---
SCRIPT_NAME: 'Aizex增强插件', // 脚本名称,用于日志输出等场合,方便识别。
SCRIPT_VERSION: '1.0.0', // 脚本版本号,用于日志输出和问题追踪。
// --- 目标元素选择器 (CSS Selectors) ---
// 这些选择器用于在目标网页上定位特定的HTML元素。
// !! 警告:如果目标网站的HTML结构发生变化,导致这些选择器失效,脚本的相应功能将无法正常工作。!!
// !! 在不完全理解其作用前,请勿随意修改这些路径,以免导致功能异常。!!
SELECTORS: {
// “高级设置”按钮将被注入到此选择器指向的容器的起始位置。
ADVANCED_SETTINGS_BUTTON_TARGET_CONTAINER: "#conversation-header-actions",
// “高级设置”按钮将尝试复制此选择器指向的按钮的样式。
ADVANCED_SETTINGS_BUTTON_STYLE_REFERENCE: "#conversation-header-actions .btn-secondary",
// “积分显示”面板将被注入到此选择器指向的容器的末尾。
POINTS_PANEL_TARGET_CONTAINER: "body > div.flex.h-full.w-full.flex-col > div > div.relative.flex.h-full.w-full.flex-row.overflow-hidden > div.bg-token-sidebar-surface-primary.z-21.shrink-0.overflow-x-hidden.\\[view-transition-name\\:var\\(--sidebar-slideover\\)\\].max-md\\:w-0\\! > div > div > div > nav",
// “隐藏侧边工具栏入口”功能将尝试移除此选择器指向的按钮。
SIDEBAR_TOGGLE_BUTTON: "#toggleButton",
// “隐藏滚动至末尾按钮”功能将尝试移除此选择器指向的按钮。
SCROLL_TO_END_BUTTON: "#thread > div > div.flex.shrink.basis-auto.flex-col.overflow-hidden.-mb-\\(--composer-overlap-px\\).\\[--composer-overlap-px\\:24px\\].grow > div > div.sticky.bottom-6.z-10.flex.h-0.items-end.justify-center.motion-safe\\:transition-all.motion-safe\\:delay-300.motion-safe\\:duration-300.group-\\[\\:not\\(\\[data-scroll-from-end\\]\\)\\]\\/thread\\:scale-50.group-\\[\\:not\\(\\[data-scroll-from-end\\]\\)\\]\\/thread\\:opacity-0.group-\\[\\:not\\(\\[data-scroll-from-end\\]\\)\\]\\/thread\\:pointer-events-none.group-\\[\\:not\\(\\[data-scroll-from-end\\]\\)\\]\\/thread\\:duration-100.group-\\[\\:not\\(\\[data-scroll-from-end\\]\\)\\]\\/thread\\:delay-0 > button",
// “优化界面”功能将尝试移除此选择器指向的元素,并用一个占位符替换。
OPTIMIZE_UI_TARGET_ELEMENT: "#thread-bottom-container > div.text-token-text-secondary.relative.mt-auto.flex.min-h-8.w-full.items-center.justify-center.p-2.text-center.text-xs.md\\:px-\\[60px\\]",
// “自定义头像”功能将修改此选择器指向的元素的src属性。
// 使用 data-testid 和 alt 属性进行定位,期望能比动态ID更稳定。
CUSTOM_AVATAR_IMAGE: 'button[data-testid="profile-button"] img[alt="User"]',
},
// --- 脚本创建元素的ID ---
// 为脚本动态创建的DOM元素分配的ID,用于后续的查找、修改或移除操作。
// 保持这些ID的独特性,以避免与页面原有元素的ID冲突。
ELEMENT_IDS: {
ADVANCED_SETTINGS_BUTTON: 'aizex-enhancer-adv-settings-btn',
SETTINGS_PANEL: 'aizex-enhancer-settings-panel',
SCROLLABLE_CONTENT_AREA: 'aizex-enhancer-scrollable-content',
OVERLAY: 'aizex-enhancer-overlay',
QUOTA_PANEL_CONTAINER: 'aizex-enhancer-quota-panel',
OPTIMIZE_UI_PLACEHOLDER: 'aizex-enhancer-optimize-ui-placeholder',
},
// --- 本地存储键名 ---
// 用于在油猴脚本管理器提供的存储中保存用户设置和缓存数据。
// 修改这些键名将导致用户之前保存的设置和数据无法被脚本识别。
STORAGE_KEYS: {
MAIN_SETTINGS: 'aizex_enhancer_settings_v1.0.0', // 主设置对象存储键
QUOTA_DATA: 'aizex_enhancer_quota_data_v1.0.0', // 积分数据对象存储键
},
// --- API 相关配置 ---
API: {
QUOTA_URL: 'https://aizex.me/be/auth/get-quota', // 获取积分数据的接口地址
TIMEOUT_MS: 15000, // API请求的超时时间(单位:毫秒)
QUOTA_AUTO_REFRESH_INTERVAL_S: 60, // 积分数据自动刷新的时间间隔(单位:秒)
},
// --- 自定义头像功能相关属性名 ---
// 在被修改的头像
元素上添加这些自定义HTML属性,用于追踪脚本的操作状态。
CUSTOM_AVATAR_ATTRIBUTES: {
APPLIED: 'data-aizex-enhancer-avatar-applied', // 标记此
已被自定义头像替换
// FALLBACK_SRC: 'data-aizex-enhancer-original-src', // 已根据用户新需求移除
},
// --- 积分面板UI默认及占位符值 ---
// 当无法从API获取有效数据,且本地也无缓存或缓存数据不完整时,积分面板中对应项显示的文本。
POINTS_PANEL_DEFAULTS: {
TIME_STRING: '1970-01-01 00:00:00', // 时间戳无效或缺失时的默认显示时间
MAX_QUOTA_FALLBACK: "None", // 当最大积分值未知时的最终回退显示值
USED_FALLBACK: "None", // 当已用积分值未知时的最终回退显示值
NONE_PLACEHOLDER: "None", // 当某个具体数值或信息缺失时的通用占位符
},
// --- UI文本字符串 ---
// 设置面板等UI界面上显示的固定文本。便于统一管理和未来可能的修改。
TEXT: {
ADV_SETTINGS_BUTTON: '高级设置',
SETTINGS_PANEL_TITLE: 'Aizex增强插件',
CLOSE_PANEL_BUTTON: '关闭面板',
AVATAR_BTN_SELECT: '选择文件',
AVATAR_BTN_SELECTED: '已设置头像',
AVATAR_BTN_RESET: '重置',
SETTING_ITEM_TITLES: { // 各设置项在面板中的显示标题
showPoints: '开启积分显示',
hideSidebarEntry: '隐藏侧边工具栏入口',
hideScrollToEnd: '隐藏滚动至末尾按钮',
optimizeUI: '优化界面',
enableLogging: '开启日志输出',
customAvatar: '自定义头像',
}
},
};
// --- 默认设置对象 ---
// 定义了脚本所有可配置项的初始默认状态。
// 确保所有功能性开关默认为关闭 (false),符合用户要求。
const DEFAULT_SETTINGS = {
showPoints: false, // 是否显示积分面板
hideSidebarEntry: false, // 是否隐藏侧边栏入口按钮(#toggleButton)
hideScrollToEnd: false, // 是否隐藏“滚动至末尾”按钮
optimizeUI: false, // 是否启用界面优化(移除特定页脚元素并替换为占位符)
enableLogging: false, // 是否在控制台输出详细日志
customAvatar: null, // 自定义头像数据 { isSet: boolean, originalName?: string, dataUrl?: string }
};
// --- 全局状态变量 ---
let currentSettings = {
...DEFAULT_SETTINGS
}; // 存储当前所有设置项的值
let previousUrl = window.location.href; // 用于检测浏览器地址栏URL的变化
// 各独立功能模块的状态标志
let isPointsDisplayFeatureActive = false; // 积分显示功能是否已实际激活并运行
let isHideSidebarEntryFeatureActive = false; // 隐藏侧边栏入口功能是否已实际激活并运行
let isHideScrollToEndButtonFeatureActive = false; // 隐藏滚动至末尾按钮功能是否已实际激活并运行
let isOptimizeUIFeatureActive = false; // 优化界面功能是否已实际激活并运行
// 自定义头像功能的激活状态直接通过 currentSettings.customAvatar.dataUrl 是否存在来判断
// 积分功能相关状态
let lastQuotaData = null; // 上次成功从API获取或本地加载的积分数据
let isQuotaRequestPending = false; // 标记当前是否有积分API请求正在进行中,用于请求锁定
let autoRefreshTimerId = null; // 积分数据自动刷新的 setInterval ID
let autoRefreshCountdown = CONFIG.API.QUOTA_AUTO_REFRESH_INTERVAL_S; // 自动刷新倒计时
// MutationObserver 实例,用于异步等待特定DOM元素的出现
let mainSettingsButtonObserver = null;
let pointsPanelTargetObserver = null;
let toggleButtonObserver = null;
let scrollToEndButtonObserver = null;
let optimizeUITargetObserver = null;
let customAvatarObserver = null;
// ===================================================================================
// === 工具函数 (Utilities) ===
// ===================================================================================
/**
* @description 标准化的日志输出函数。仅当 CONFIG.DEFAULT_SETTINGS.enableLogging (通过 currentSettings 反映) 为 true 时执行。
* 所有日志输出均以此函数为入口,确保格式统一和条件输出。
* @param {string} functionName - 调用日志的函数名,用于日志追溯。
* @param {string} message - 要输出的核心日志消息。
* @param {...any} [args] - (可选) 附加的日志参数,可以是任何类型,会追加在核心消息后。
*/
function log(functionName, message, ...args) {
if (currentSettings.enableLogging) {
const timestamp = new Date().toLocaleString(); // 使用浏览器本地时区和时间格式
// 为确保可读性,函数名和消息之间用冒号分隔,附加参数直接传递给 console.log
console.log(`[${CONFIG.SCRIPT_NAME} v${CONFIG.SCRIPT_VERSION}] ${timestamp} [${functionName}]: ${message}`, ...args);
}
}
// ===================================================================================
// === 设置管理 (Settings Management) ===
// ===================================================================================
/**
* @description 从Tampermonkey的存储中异步加载脚本的设置。
* 如果存储中没有设置或设置格式不正确,则使用 DEFAULT_SETTINGS 并将其保存回存储。
* 此函数应在脚本初始化早期被调用。
*/
async function loadSettings() {
const FN = 'loadSettings'; // 当前函数名,用于日志
log(FN, '函数开始执行。准备从本地存储加载主设置。');
log(FN, ' 存储键名:', CONFIG.STORAGE_KEYS.MAIN_SETTINGS);
try {
const savedSettings = await GM_getValue(CONFIG.STORAGE_KEYS.MAIN_SETTINGS);
log(FN, ' 从 GM_getValue 获取的原始主设置数据:', savedSettings);
if (savedSettings && typeof savedSettings === 'object' && Object.keys(savedSettings).length > 0) {
// 使用保存的设置,并用默认设置补充任何可能缺失的新设置项
currentSettings = {
...DEFAULT_SETTINGS,
...savedSettings
};
log(FN, ' 成功合并已保存的主设置与默认设置。当前生效的设置为:', currentSettings);
} else {
// 无有效保存设置,使用全新默认设置
currentSettings = {
...DEFAULT_SETTINGS
};
log(FN, ' 本地存储中未找到有效的主设置、格式错误或为空对象。已恢复使用全新的默认主设置:', currentSettings);
// 将全新的默认设置保存到存储中,供下次加载
await GM_setValue(CONFIG.STORAGE_KEYS.MAIN_SETTINGS, currentSettings);
log(FN, ' 全新的默认主设置已自动保存至本地存储。');
}
} catch (error) {
// 捕获 GM_getValue 或 GM_setValue 可能抛出的异常
console.error(`[${CONFIG.SCRIPT_NAME}] ${new Date().toLocaleString()}: ${FN}: 加载主设置时发生严重错误:`, error);
currentSettings = {
...DEFAULT_SETTINGS
}; // 任何异常均回退到默认设置,保证脚本基本可用性
log(FN, ' 加载主设置过程中发生异常,已强制恢复为默认主设置:', currentSettings);
}
log(FN, '主设置加载完成。最终 currentSettings 内容:', currentSettings);
// 特殊处理日志设置的初始状态输出,确保用户了解如何开启日志
if (currentSettings.enableLogging) {
log(FN, '根据已加载的设置,日志输出功能当前为:已激活。');
} else {
// 此条 console.log 会在 enableLogging 为 false 时也输出,作为对用户的提示
console.log(`[${CONFIG.SCRIPT_NAME} v${CONFIG.SCRIPT_VERSION}] ${new Date().toLocaleString()}: 日志输出功能当前为:未激活。如需查看详细操作日志,请在插件“${CONFIG.TEXT.ADV_SETTINGS_BUTTON}”面板中的“${CONFIG.TEXT.SETTING_ITEM_TITLES.enableLogging}”项将其开启。`);
}
log(FN, '函数执行完毕。');
}
/**
* @description 保存单个设置项的值。
* 更新内存中的 currentSettings 对象,然后将其完整地持久化到Tampermonkey存储。
* 在值更新后,会根据被修改的设置项键名,调用相应的功能模块切换函数。
* @param {string} key - 要保存的设置项的键名 (必须是 DEFAULT_SETTINGS 中的一个键)。
* @param {any} value - 要保存的设置项的新值。
*/
async function saveSetting(key, value) {
const FN = 'saveSetting';
log(FN, `函数开始执行。准备保存设置项 - 键: "${key}"。`);
log(FN, ` 保存前,键 "${key}" 在 currentSettings 中的当前值为:`, currentSettings[key]);
log(FN, ` 即将为键 "${key}" 设置的新值为:`, value);
currentSettings[key] = value; // 步骤1: 更新内存中的设置对象
try {
// 步骤2: 将整个更新后的 currentSettings 对象持久化保存
await GM_setValue(CONFIG.STORAGE_KEYS.MAIN_SETTINGS, currentSettings);
log(FN, ` 主设置对象已成功通过 GM_setValue 持久化保存。键: "${key}" 的新值已在存储中生效。`);
log(FN, " 当前完整的 currentSettings 对象内容:", currentSettings);
} catch (error) {
console.error(`[${CONFIG.SCRIPT_NAME}] ${new Date().toLocaleString()}: ${FN}: 保存主设置时发生严重错误 - 键: "${key}"`, error);
log(FN, ` 持久化保存主设置失败! 键: "${key}", 值:`, value, "错误详情:", error);
}
// 步骤3: 根据被修改的设置项,触发相应功能模块的逻辑切换
log(FN, ` 检查键 "${key}" 是否需要触发特定功能模块的状态更新...`);
switch (key) {
case 'showPoints':
log(FN, ` 检测到 "${key}" (积分显示) 设置项被更改为 ${value},将调用 togglePointsDisplayFeature。`);
togglePointsDisplayFeature(value);
break;
case 'hideSidebarEntry':
log(FN, ` 检测到 "${key}" (隐藏侧边栏入口) 设置项被更改为 ${value},将调用 toggleHideSidebarEntryFeature。`);
toggleHideSidebarEntryFeature(value);
break;
case 'hideScrollToEnd':
log(FN, ` 检测到 "${key}" (隐藏滚动至末尾按钮) 设置项被更改为 ${value},将调用 toggleHideScrollToEndButtonFeature。`);
toggleHideScrollToEndButtonFeature(value);
break;
case 'optimizeUI':
log(FN, ` 检测到 "${key}" (优化界面) 设置项被更改为 ${value},将调用 toggleOptimizeUIFeature。`);
toggleOptimizeUIFeature(value);
break;
case 'customAvatar':
log(FN, ` 检测到 "${key}" (自定义头像) 设置项被更改。新值详情:`, value);
if (value && value.dataUrl) { // 如果设置了新头像(包含dataUrl)
log(FN, ' 新头像数据包含有效的 dataUrl,将尝试将其应用到页面上。');
applyCustomAvatarToPage();
} else { // 如果是重置头像 (value 为 null 或无效)
log(FN, ' 自定义头像数据被清除 (可能通过重置操作或选择了无效文件)。将尝试恢复页面上的原始头像。');
revertCustomAvatarOnPage();
}
break;
case 'enableLogging':
// 日志设置本身的变化会立即影响后续的 log() 函数行为,无需额外调用。
// 但可以输出一条明确的提示。
if (value) {
log(FN, '日志输出功能现已开启。此条及后续符合条件的日志将被记录。');
} else {
// 当日志关闭时,这是最后一条由脚本log函数输出的消息。
console.log(`[${CONFIG.SCRIPT_NAME} v${CONFIG.SCRIPT_VERSION}] ${new Date().toLocaleString()}: ${FN}: 日志输出功能现已关闭。`);
}
break;
default:
log(FN, ` 键 "${key}" 未匹配到需要特殊处理的功能模块,或其处理逻辑已包含在其他地方。`);
break;
}
log(FN, `函数执行完毕 - 键: "${key}"。`);
}
// ===================================================================================
// === 功能模块: 自定义头像 (Custom Avatar) ===
// ===================================================================================
/**
* @description 根据 currentSettings 中自定义头像的设置,初始化页面上的头像显示。
* 如果设置了自定义头像,则应用它;否则,尝试恢复(或确保为)页面的默认头像。
*/
function initializeCustomAvatarState() {
const FN = 'initializeCustomAvatarState';
log(FN, '函数开始执行。根据当前设置初始化自定义头像的显示状态。');
if (currentSettings.customAvatar && currentSettings.customAvatar.dataUrl) {
log(FN, ' 检测到 currentSettings 中已存在有效的自定义头像数据。将调用 applyCustomAvatarToPage 尝试应用。');
applyCustomAvatarToPage();
} else {
log(FN, ' currentSettings 中未设置自定义头像,或头像数据无效 (缺少dataUrl)。将调用 revertCustomAvatarOnPage 清理可能存在的自定义头像状态。');
revertCustomAvatarOnPage();
}
log(FN, '函数执行完毕。');
}
/**
* @description 使用配置的选择器查找页面上用于显示用户头像的
元素。
* @returns {HTMLImageElement|null} 找到的
元素;如果未找到,则返回null。
*/
function findTargetAvatarImageElement() {
const FN = 'findTargetAvatarImageElement';
// log(FN, '尝试查找目标头像IMG元素。选择器:', CONFIG.SELECTORS.CUSTOM_AVATAR_IMAGE); // 此日志在频繁调用时可能过于冗余
const element = document.querySelector(CONFIG.SELECTORS.CUSTOM_AVATAR_IMAGE);
// if(element) { log(FN, ' 成功找到目标头像IMG元素:', element); } else { log(FN, ' 未找到目标头像IMG元素。'); }
return element;
}
/**
* @description 将 currentSettings 中存储的自定义头像(Base64 DataURL)应用到页面上找到的头像
元素。
* 如果目标元素当前未找到,则会启动一个MutationObserver来等待其出现。
* 此函数包含防止重复应用的逻辑。
*/
function applyCustomAvatarToPage() {
const FN = 'applyCustomAvatarToPage';
log(FN, '函数开始执行,尝试应用自定义头像。');
if (!currentSettings.customAvatar || !currentSettings.customAvatar.dataUrl) {
log(FN, ' 当前配置中未设置有效的自定义头像 (currentSettings.customAvatar.dataUrl 为空)。取消应用操作,并确保恢复原始头像状态。');
revertCustomAvatarOnPage(); // 确保如果之前有自定义头像,现在被清除了,则恢复原始状态
// 如果没有有效头像数据,也应该停止相关的观察器(revertCustomAvatarOnPage会处理)
return;
}
log(FN, ' 检测到有效的自定义头像数据,准备查找目标IMG元素并应用。DataURL 长度:', currentSettings.customAvatar.dataUrl.length);
const targetImg = findTargetAvatarImageElement();
if (targetImg) {
log(FN, ' 成功找到目标头像IMG元素:', targetImg);
// 检查头像是否已被本脚本应用且SRC与当前设置一致,避免不必要的DOM操作和潜在的闪烁
if (targetImg.getAttribute(CONFIG.CUSTOM_AVATAR_ATTRIBUTES.APPLIED) === 'true' &&
targetImg.src === currentSettings.customAvatar.dataUrl) {
log(FN, ' 检测到此IMG元素已被正确应用了当前设置的自定义头像,无需重复修改。');
// 如果是通过观察器找到并且确认无需操作,可以停止该观察器实例
if (customAvatarObserver) {
log(FN, ' 自定义头像已正确应用。此 customAvatarObserver 实例的任务已完成,将停止并断开它。');
customAvatarObserver.disconnect();
customAvatarObserver = null;
}
return; // 无需进一步操作
}
log(FN, ' 准备将自定义头像应用到目标IMG元素 (之前未应用,或SRC不匹配)。');
// 注意:根据新需求,不再保存原始SRC。
// targetImg.setAttribute(CONFIG.CUSTOM_AVATAR_ATTRIBUTES.FALLBACK_SRC, targetImg.src); // 已移除
targetImg.src = currentSettings.customAvatar.dataUrl;
log(FN, ' 目标IMG元素的 src 属性已成功更新为自定义头像的 DataURL。');
targetImg.removeAttribute('srcset'); // 移除 srcset 属性,它可能覆盖通过 src 设置的图像
log(FN, ' 目标IMG元素的 srcset 属性 (如果存在) 已被移除,以确保自定义头像正确显示。');
targetImg.setAttribute(CONFIG.CUSTOM_AVATAR_ATTRIBUTES.APPLIED, 'true');
log(FN, ` 目标IMG元素已添加标记属性 "${CONFIG.CUSTOM_AVATAR_ATTRIBUTES.APPLIED}=true"。`);
// 自定义头像成功应用后,此 specific 观察器实例的任务完成。
if (customAvatarObserver) {
log(FN, ' 自定义头像已成功应用。将停止并断开当前的 customAvatarObserver 实例。');
customAvatarObserver.disconnect();
customAvatarObserver = null;
}
} else {
log(FN, ' 当前DOM中未找到目标头像IMG元素。');
// 仅在未找到元素、功能已启用(有头像数据)、且观察器未运行时,才启动观察器
if (!customAvatarObserver && currentSettings.customAvatar && currentSettings.customAvatar.dataUrl) {
log(FN, ' 当前没有活动的 customAvatarObserver,且已设置自定义头像。将创建并启动一个新的 MutationObserver 等待目标IMG元素出现。');
customAvatarObserver = new MutationObserver((mutationsList, obs) => {
log(FN, 'customAvatarObserver: MutationObserver 回调被触发。');
// 在回调中再次检查头像数据是否仍然有效,以防在等待期间被重置
if (!currentSettings.customAvatar || !currentSettings.customAvatar.dataUrl) {
log(FN, ' customAvatarObserver 回调:但此时已无有效的自定义头像数据。将停止此观察器。');
obs.disconnect(); // 停止观察
customAvatarObserver = null; // 清除引用
return;
}
const newlyFoundImg = findTargetAvatarImageElement();
if (newlyFoundImg) {
log(FN, ' customAvatarObserver 回调:成功检测到目标头像IMG元素已出现在DOM中!将调用 applyCustomAvatarToPage 进行处理。');
// 注意:这里不需要手动停止观察器 obs,因为 applyCustomAvatarToPage 内部的逻辑
// 在成功应用头像后,会把全局的 customAvatarObserver 置为 null 并断开。
applyCustomAvatarToPage();
} else {
// log(FN, ' customAvatarObserver 回调:DOM发生变化,但仍未找到目标头像IMG元素。继续观察...'); // 此日志可能过于频繁
}
});
try {
customAvatarObserver.observe(document.documentElement, {
childList: true,
subtree: true
});
log(FN, ' customAvatarObserver 已成功启动,正在监视整个文档的DOM变化。');
} catch (e) {
log(FN, ' 严重错误: customAvatarObserver 启动失败:', e);
customAvatarObserver = null; // 确保启动失败时清除引用
}
} else if (customAvatarObserver && currentSettings.customAvatar && currentSettings.customAvatar.dataUrl) {
log(FN, ' customAvatarObserver 已在运行中,将继续等待目标头像IMG元素出现。');
} else if (!currentSettings.customAvatar || !currentSettings.customAvatar.dataUrl) {
log(FN, ' 未设置自定义头像数据,不启动 customAvatarObserver。');
if (customAvatarObserver) { // 如果因为某种原因观察器还在但头像数据没了,也停掉
customAvatarObserver.disconnect();
customAvatarObserver = null;
}
}
}
log(FN, '函数执行完毕。');
}
/**
* @description 当自定义头像被重置(清除)时,调用此函数。
* 它会找到可能已被修改的头像
元素,并移除脚本添加的自定义属性。
* 网页应自行负责恢复显示其原始或默认头像。
* 同时停止相关的MutationObserver。
*/
function revertCustomAvatarOnPage() {
const FN = 'revertCustomAvatarOnPage';
log(FN, '函数开始执行,尝试移除自定义头像状态并让页面恢复其默认头像。');
const targetImg = findTargetAvatarImageElement();
if (targetImg) {
log(FN, ' 找到可能曾被修改的头像IMG元素:', targetImg);
// 检查是否存在脚本添加的标记属性
if (targetImg.hasAttribute(CONFIG.CUSTOM_AVATAR_ATTRIBUTES.APPLIED)) {
log(FN, ' 检测到此IMG元素曾被自定义头像功能修改过。准备移除自定义状态标记。');
targetImg.removeAttribute(CONFIG.CUSTOM_AVATAR_ATTRIBUTES.APPLIED);
// 不再需要恢复 CONFIG.CUSTOM_AVATAR_ATTRIBUTES.FALLBACK_SRC,因为已不保存
// targetImg.src = ''; // 可选: 清空src强制浏览器重新加载原始src,但通常移除标记就够了,让页面自己处理
log(FN, ` 自定义头像标记属性 ("${CONFIG.CUSTOM_AVATAR_ATTRIBUTES.APPLIED}") 已从目标IMG元素上移除。`);
} else {
log(FN, ' 目标IMG元素未被标记为已应用自定义头像,无需执行移除标记操作。');
}
} else {
log(FN, ' 未在当前DOM中找到目标头像IMG元素,无法执行恢复操作。');
}
// 无论是否找到IMG元素,如果自定义头像功能被禁用或重置,都应停止观察器
if (customAvatarObserver) {
log(FN, ' 由于正在恢复/清除自定义头像状态,将停止并清除 customAvatarObserver。');
customAvatarObserver.disconnect();
customAvatarObserver = null;
}
log(FN, '函数执行完毕。');
}
// --- 功能模块: 优化界面 (Optimize UI) ---
function toggleOptimizeUIFeature(enable) {
const FN = 'toggleOptimizeUIFeature';
log(FN, `请求设置“优化界面”功能为: ${enable}`);
isOptimizeUIFeatureActive = enable;
if (isOptimizeUIFeatureActive) {
log(FN, ' “优化界面”功能已激活。调用 manageOptimizeUITarget 处理目标元素。');
manageOptimizeUITarget();
} else {
log(FN, ' “优化界面”功能已禁用。');
if (optimizeUITargetObserver) {
log(FN, ' 停止活动的 optimizeUITargetObserver。');
optimizeUITargetObserver.disconnect();
optimizeUITargetObserver = null;
}
const placeholder = document.getElementById(CONFIG.ELEMENT_IDS.OPTIMIZE_UI_PLACEHOLDER);
if (placeholder) {
log(FN, ' 找到并移除“优化界面”的占位符 (ID:', CONFIG.ELEMENT_IDS.OPTIMIZE_UI_PLACEHOLDER, ')。');
placeholder.remove();
} else {
log(FN, ' 未找到需要移除的“优化界面”占位符。');
}
}
log(FN, `执行完毕。当前状态 (isOptimizeUIFeatureActive): ${isOptimizeUIFeatureActive}`);
}
function manageOptimizeUITarget() {
const FN = 'manageOptimizeUITarget';
log(FN, '开始管理“优化界面”目标元素。');
if (!isOptimizeUIFeatureActive) {
log(FN, ' 功能未激活,取消操作。');
if (optimizeUITargetObserver) {
optimizeUITargetObserver.disconnect();
optimizeUITargetObserver = null;
}
return;
}
const targetElement = document.querySelector(CONFIG.SELECTORS.OPTIMIZE_UI_TARGET_ELEMENT);
if (targetElement) {
log(FN, ' 找到“优化界面”目标元素:', targetElement);
const parent = targetElement.parentNode;
if (!parent) {
log(FN, ' 错误: 目标元素无父节点!');
return;
}
let placeholder = document.getElementById(CONFIG.ELEMENT_IDS.OPTIMIZE_UI_PLACEHOLDER);
if (placeholder && placeholder.parentNode !== parent) {
placeholder = null;
}
if (!placeholder) {
log(FN, ' 未找到有效占位符,创建并插入1rem高占位符。');
placeholder = document.createElement('div');
placeholder.id = CONFIG.ELEMENT_IDS.OPTIMIZE_UI_PLACEHOLDER;
placeholder.style.height = '1rem';
placeholder.style.width = '100%';
placeholder.setAttribute('data-comment', `${CONFIG.SCRIPT_NAME} 创建的优化界面占位符`);
parent.insertBefore(placeholder, targetElement);
log(FN, ' 占位符已插入目标元素之前。');
} else {
log(FN, ' 已存在有效占位符。');
}
try {
targetElement.remove();
log(FN, ' 目标“优化界面”元素已移除。');
} catch (e) {
log(FN, ' 移除目标“优化界面”元素时出错:', e);
}
} else {
log(FN, ' 未找到“优化界面”目标元素。');
const existingPlaceholder = document.getElementById(CONFIG.ELEMENT_IDS.OPTIMIZE_UI_PLACEHOLDER);
if (existingPlaceholder) {
log(FN, ' 目标元素未找到,但占位符已存在。无需操作。');
return;
}
if (!optimizeUITargetObserver && isOptimizeUIFeatureActive) {
log(FN, ' 启动 optimizeUITargetObserver 等待目标元素出现...');
optimizeUITargetObserver = new MutationObserver(() => {
if (!isOptimizeUIFeatureActive) {
if (optimizeUITargetObserver) {
optimizeUITargetObserver.disconnect();
optimizeUITargetObserver = null;
}
return;
}
if (document.querySelector(CONFIG.SELECTORS.OPTIMIZE_UI_TARGET_ELEMENT)) {
log(FN, ' optimizeUITargetObserver: 目标元素已出现!');
manageOptimizeUITarget();
}
});
try {
optimizeUITargetObserver.observe(document.documentElement, {
childList: true,
subtree: true
});
} catch (e) {
log(FN, ' 错误: optimizeUITargetObserver 启动失败:', e);
optimizeUITargetObserver = null;
}
}
}
log(FN, '执行完毕。');
}
// --- 功能模块: 隐藏滚动至末尾按钮 ---
function toggleHideScrollToEndButtonFeature(enable) {
const FN = 'toggleHideScrollToEndButtonFeature';
log(FN, `请求设置“隐藏滚动至末尾按钮”功能为: ${enable}`);
isHideScrollToEndButtonFeatureActive = enable;
if (isHideScrollToEndButtonFeatureActive) {
log(FN, ' 功能已激活,调用 manageScrollToEndButtonVisibility。');
manageScrollToEndButtonVisibility();
} else {
log(FN, ' 功能已禁用。如果观察器在运行,则停止。');
if (scrollToEndButtonObserver) {
scrollToEndButtonObserver.disconnect();
scrollToEndButtonObserver = null;
log(FN, ' scrollToEndButtonObserver 已停止。');
}
}
log(FN, `执行完毕。状态: ${isHideScrollToEndButtonFeatureActive}`);
}
function manageScrollToEndButtonVisibility() {
const FN = 'manageScrollToEndButtonVisibility';
log(FN, '开始管理“滚动至末尾按钮”。');
if (!isHideScrollToEndButtonFeatureActive) {
if (scrollToEndButtonObserver) {
scrollToEndButtonObserver.disconnect();
scrollToEndButtonObserver = null;
}
return;
}
const btn = document.querySelector(CONFIG.SELECTORS.SCROLL_TO_END_BUTTON);
if (btn) {
log(FN, ' 找到“滚动至末尾按钮”,准备移除:', btn);
try {
btn.remove();
log(FN, ' “滚动至末尾按钮”已移除。');
} catch (error) {
log(FN, ' 移除“滚动至末尾按钮”时出错:', error);
}
} else {
log(FN, ' 未找到“滚动至末尾按钮”。');
if (!scrollToEndButtonObserver && isHideScrollToEndButtonFeatureActive) {
log(FN, ' 启动 scrollToEndButtonObserver 等待按钮出现...');
scrollToEndButtonObserver = new MutationObserver(() => {
if (!isHideScrollToEndButtonFeatureActive) {
if (scrollToEndButtonObserver) {
scrollToEndButtonObserver.disconnect();
scrollToEndButtonObserver = null;
}
return;
}
if (document.querySelector(CONFIG.SELECTORS.SCROLL_TO_END_BUTTON)) {
log(FN, ' scrollToEndButtonObserver: 按钮已出现!');
manageScrollToEndButtonVisibility();
}
});
try {
scrollToEndButtonObserver.observe(document.documentElement, {
childList: true,
subtree: true
});
} catch (e) {
log(FN, ' 错误: scrollToEndButtonObserver 启动失败:', e);
scrollToEndButtonObserver = null;
}
}
}
log(FN, '执行完毕。');
}
// --- 功能模块: 隐藏侧边工具栏入口 ---
function toggleHideSidebarEntryFeature(enable) {
const FN = 'toggleHideSidebarEntryFeature';
log(FN, `请求设置“隐藏侧边工具栏入口”功能为: ${enable}`);
isHideSidebarEntryFeatureActive = enable;
if (isHideSidebarEntryFeatureActive) {
log(FN, ' 功能已激活,调用 manageToggleButtonVisibility。');
manageToggleButtonVisibility();
} else {
log(FN, ' 功能已禁用。如果观察器在运行,则停止。');
if (toggleButtonObserver) {
toggleButtonObserver.disconnect();
toggleButtonObserver = null;
log(FN, ' toggleButtonObserver 已停止。');
}
}
log(FN, `执行完毕。状态: ${isHideSidebarEntryFeatureActive}`);
}
function manageToggleButtonVisibility() {
const FN = 'manageToggleButtonVisibility';
log(FN, '开始管理“侧边工具栏入口按钮”。');
if (!isHideSidebarEntryFeatureActive) {
if (toggleButtonObserver) {
toggleButtonObserver.disconnect();
toggleButtonObserver = null;
}
return;
}
const btn = document.querySelector(CONFIG.SELECTORS.SIDEBAR_TOGGLE_BUTTON);
if (btn) {
log(FN, ' 找到“侧边工具栏入口按钮” (#toggleButton),准备移除:', btn);
try {
btn.remove();
log(FN, ' “侧边工具栏入口按钮” (#toggleButton) 已移除。');
} catch (error) {
log(FN, ' 移除“侧边工具栏入口按钮” (#toggleButton) 时出错:', error);
}
} else {
log(FN, ' 未找到“侧边工具栏入口按钮” (#toggleButton)。');
if (!toggleButtonObserver && isHideSidebarEntryFeatureActive) {
log(FN, ' 启动 toggleButtonObserver 等待按钮出现...');
toggleButtonObserver = new MutationObserver(() => {
if (!isHideSidebarEntryFeatureActive) {
if (toggleButtonObserver) {
toggleButtonObserver.disconnect();
toggleButtonObserver = null;
}
return;
}
if (document.querySelector(CONFIG.SELECTORS.SIDEBAR_TOGGLE_BUTTON)) {
log(FN, ' toggleButtonObserver: 按钮已出现!');
manageToggleButtonVisibility();
}
});
try {
toggleButtonObserver.observe(document.documentElement, {
childList: true,
subtree: true
});
} catch (e) {
log(FN, ' 错误: toggleButtonObserver 启动失败:', e);
toggleButtonObserver = null;
}
}
}
log(FN, '执行完毕。');
}
// --- 功能模块: 积分显示 ---
async function togglePointsDisplayFeature(enable) {
const FN = 'togglePointsDisplayFeature';
log(FN, `请求设置“积分显示”功能为: ${enable}`);
isPointsDisplayFeatureActive = enable;
if (enable) {
log(FN, ' “积分显示”功能已激活。');
await loadSavedQuotaData();
updateQuotaPanelUI(lastQuotaData);
attemptQuotaPanelInsertion();
fetchQuotaData('功能启用');
startAutoRefreshTimer();
} else {
log(FN, ' “积分显示”功能已禁用。');
removeQuotaPanel();
stopAutoRefreshTimer();
}
log(FN, `执行完毕。状态: ${isPointsDisplayFeatureActive}`);
}
function getPointsTargetElement() {
return document.querySelector(CONFIG.SELECTORS.POINTS_PANEL_TARGET_CONTAINER);
}
function createQuotaPanelHTML() {
log('createQuotaPanelHTML: 生成积分面板HTML。');
return `