Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @require https://update.greasyfork.org/scripts/537107/1595129/dailyLimitManager.js
// ==UserScript==
// @name 岐黄助手-每日上限管理器
// @namespace http://tampermonkey.net/
// @version 1.0.4 // 版本更新
// @description 管理岐黄天使学习平台的每日学习上限并更新UI显示
// @author AI 助手
// @match *://www.tcm512.com/*
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
class DailyLimitManager {
constructor(options = {}) {
this.config = {
limitVideos: options.limitVideos || 10, // 默认每天10个视频
resetHour: GM_getValue('qh_daily_limit_reset_hour', options.resetHour || 8), // 默认早上8点重置
selectors: options.selectors || {
limitReachedHintBox: '.sc_tips_box[style*="display: block"]', // 网站提示达到上限的容器选择器
limitReachedTextHint: '.title', // 在上述容器中包含特定文本的元素
limitReachedTextContent: '每天最多只能学习10个视频', // 指示达到上限的具体文本
confirmButton: '.layui-layer-btn0' // "我知道了"按钮的选择器
},
onLimitReached: options.onLimitReached || function() { console.warn('[每日上限管理器] onLimitReached 回调未设置。'); },
onLimitReset: options.onLimitReset || function() { console.info('[每日上限管理器] 每日上限已重置。'); }
};
this.state = {
videosWatchedToday: GM_getValue('qh_videos_watched_today', 0), // 今天已观看视频数
lastResetDate: GM_getValue('qh_daily_limit_last_reset_date', new Date().toDateString()), // 上次重置日期
isLimitReached: GM_getValue('qh_daily_limit_reached', false), // 是否已达到上限
lastLimitDate: GM_getValue('qh_daily_limit_date', null), // 记录达到上限的日期
autoResetTimer: null, // 自动重置的setTimeout ID
countdownInterval: null, // UI倒计时更新的setInterval ID
pageCheckInterval: null, // 页面检查的setInterval ID
};
this._checkAndResetOnNewDay(); // 初始化检查
if (this.state.isLimitReached) {
this._scheduleAutoReset(); // 如果已达上限,确保已安排重置并启动倒计时
}
this._updateUIDisplay(); // 初始化UI更新
console.log('[每日上限管理器] 初始化完成。状态:', JSON.parse(JSON.stringify(this.state)));
}
_getMillisecondsUntilReset() {
const now = new Date();
let resetTime = new Date();
resetTime.setHours(this.config.resetHour, 0, 0, 0);
if (now.getHours() >= this.config.resetHour) { // 如果当前时间已过重置小时,则安排到下一天
resetTime.setDate(resetTime.getDate() + 1);
}
return resetTime.getTime() - now.getTime();
}
_formatCountdown(ms) {
if (ms <= 0) return "00:00:00";
let seconds = Math.floor((ms / 1000) % 60);
let minutes = Math.floor((ms / (1000 * 60)) % 60);
let hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return `${hours}:${minutes}:${seconds}`;
}
_updateUIDisplay() {
if (typeof window.updateDailyLimitDisplay !== 'function') {
// console.warn('[每日上限管理器] window.updateDailyLimitDisplay 函数不可用。');
return;
}
let statusText = '';
let countdownText = '--:--:--';
if (this.state.isLimitReached) {
statusText = `已达上限 (${this.state.videosWatchedToday}/${this.config.limitVideos} 个视频)`;
const msUntilReset = this._getMillisecondsUntilReset();
countdownText = this._formatCountdown(msUntilReset);
} else {
statusText = `今日已看 ${this.state.videosWatchedToday}/${this.config.limitVideos} 个视频`;
const msUntilReset = this._getMillisecondsUntilReset();
countdownText = this._formatCountdown(msUntilReset);
}
if (this.config.limitVideos <= 0) {
statusText = '无每日学习上限';
countdownText = '不适用'; // N/A
}
// 防抖机制:只在状态真正变化时更新UI和输出日志
const newDisplayState = `${statusText}|${countdownText}`;
if (this.state.lastDisplayState !== newDisplayState) {
this.state.lastDisplayState = newDisplayState;
window.updateDailyLimitDisplay(statusText, countdownText);
}
}
_checkAndResetOnNewDay() {
const today = new Date().toDateString();
// console.log(`[每日上限管理器] 检查新的一天。今天是: ${today}, 上次重置: ${this.state.lastResetDate}`);
if (this.state.lastResetDate !== today) {
const now = new Date();
if (this.state.lastResetDate !== today && now.getHours() >= this.config.resetHour) {
console.log('[每日上限管理器] 检测到新的一天且已过重置时间,正在重置每日上限。');
this.state.videosWatchedToday = 0;
this.state.isLimitReached = false;
this.state.lastResetDate = today;
this.state.lastLimitDate = null;
GM_setValue('qh_videos_watched_today', 0);
GM_setValue('qh_daily_limit_reached', false);
GM_setValue('qh_daily_limit_last_reset_date', today);
GM_setValue('qh_daily_limit_date', null);
if (this.state.autoResetTimer) {
clearTimeout(this.state.autoResetTimer);
this.state.autoResetTimer = null;
}
if (this.state.countdownInterval) {
clearInterval(this.state.countdownInterval);
this.state.countdownInterval = null;
}
this.config.onLimitReset();
this._scheduleAutoReset();
} else if (this.state.lastResetDate !== today && now.getHours() < this.config.resetHour) {
if (!this.state.isLimitReached) {
this.state.videosWatchedToday = 0;
GM_setValue('qh_videos_watched_today', 0);
this.state.lastResetDate = today;
GM_setValue('qh_daily_limit_last_reset_date', today);
console.log('[每日上限管理器] 新的一天,未到重置时间,学习上限未满。计数器已重置,等待今日的计划重置。');
} else {
console.log('[每日上限管理器] 新的一天,未到重置时间,但学习上限已满。等待计划重置。');
}
if (this.state.isLimitReached && !this.state.autoResetTimer) {
this._scheduleAutoReset();
}
}
}
this._updateUIDisplay();
}
/**
* 通过检查页面内容来判断是否已达到每日学习上限。
* @param {Document} docContext - 要检查的文档上下文 (主窗口或 iframe)。
* @returns {boolean} 如果检测到上限则为 true,否则为 false。
*/
checkLimitReachedOnPage(docContext) {
if (!docContext) {
console.warn('[每日上限管理器] 在 checkLimitReachedOnPage 中 docContext 为空');
return false;
}
try {
const hintBox = docContext.querySelector(this.config.selectors.limitReachedHintBox);
if (hintBox) {
const titleElement = hintBox.querySelector(this.config.selectors.limitReachedTextHint);
if (titleElement && titleElement.textContent.includes(this.config.selectors.limitReachedTextContent)) {
console.log('[每日上限管理器] 检测到每日上限提示:', titleElement.textContent);
return true;
}
}
} catch (e) {
console.error('[每日上限管理器] 检查页面每日上限时出错:', e);
}
return false;
}
/**
* 当一个视频被视为已观看时调用此方法,以可能更新上限状态。
*/
notifyVideoWatched() {
this._checkAndResetOnNewDay(); // 计数前确保日期是最新的
if (this.state.isLimitReached) {
console.log('[每日上限管理器] 已达到上限,不增加视频计数。');
this._updateUIDisplay();
return; // 如果已通过程序设置上限,则不计数
}
this.state.videosWatchedToday++;
GM_setValue('qh_videos_watched_today', this.state.videosWatchedToday);
console.log(`[每日上限管理器] 视频已观看。计数: ${this.state.videosWatchedToday}`);
if (this.config.limitVideos > 0 && this.state.videosWatchedToday >= this.config.limitVideos) {
console.log('[每日上限管理器] 视频观看数量已达上限。');
this.handleLimitReached(); // 处理达到上限的情况
} else {
// 如果因为页面检测到上限而调用了 handleLimitReached,则 isLimitReached 可能已经是 true
// 但如果仅通过计数达到上限,则在此处调用
const pageLimit = this.checkLimitReachedOnPage(document); // 检查当前页面是否也提示上限
if (pageLimit) {
console.log('[每日上限管理器] 页面也检测到上限信息。');
this.handleLimitReached();
}
}
this._updateUIDisplay();
}
/**
* Handles the scenario when the daily limit is reached.
* Sets internal state, informs UI, and schedules auto-reset.
*/
handleLimitReached() {
// console.warn('[每日上限管理器] handleLimitReached 未完全实现。');
if (this.state.isLimitReached && this.state.lastLimitDate === new Date().toDateString()) {
// 今天已经处理过并且记录了。
// 如果倒计时不存在或需要更新,则重新调度
if (!this.state.countdownInterval) {
this._scheduleAutoReset(); // 确保倒计时启动
}
this._updateUIDisplay(); // 确保UI是最新的
return; // 避免重复处理
}
console.log('[每日上限管理器] 每日上限已通过页面检测或计数达到。');
this.state.isLimitReached = true;
this.state.lastLimitDate = new Date().toDateString(); // 记录达到上限的日期
GM_setValue('qh_daily_limit_reached', true);
GM_setValue('qh_daily_limit_date', this.state.lastLimitDate);
this.config.onLimitReached(); // 调用外部回调
this._scheduleAutoReset(); // 安排自动重置并启动UI倒计时
this._updateUIDisplay(); // 更新UI显示
}
/**
* Starts a periodic check for the on-page limit hints.
*/
startPeriodicPageCheck(docContextRoot, interval = 300000) { // 默认5分钟检查一次
// console.warn('[每日上限管理器] startPeriodicPageCheck 未完全实现。');
if (this.state.pageCheckIntervalId) {
clearInterval(this.state.pageCheckIntervalId);
}
this.state.lastLimitCheckTime = Date.now(); // 初始化上次检查时间
this.state.pageCheckIntervalId = setInterval(() => {
const now = Date.now();
// 避免过于频繁的检查,例如当浏览器标签页在后台时定时器可能行为异常
if (now - this.state.lastLimitCheckTime > interval - 1000) { // 稍微提前一点,以防万一
this.state.lastLimitCheckTime = now;
console.log('[每日上限管理器] 定期检查页面上的上限提示。');
if (this.checkLimitReachedOnPage(docContextRoot || document)) {
console.log('[每日上限管理器] 通过定期检查在页面上检测到上限提示。');
this.handleLimitReached();
} else {
// 如果页面不再提示上限,但脚本内部状态仍是 isLimitReached
// 这可能意味着上限通过其他方式被解除了,或者之前是误报
// 暂时不自动重置,依赖于每日自动重置或手动重置
// console.log('[每日上限管理器] 定期检查未发现页面上限提示。');
}
}
}, Math.max(interval, 60000)); // 确保检查间隔至少为1分钟
console.log(`[每日上限管理器] 已启动页面上限提示的定期检查 (间隔: ${interval / 1000} 秒)。`);
}
stopPeriodicPageCheck() {
if (this.state.pageCheckIntervalId) {
clearInterval(this.state.pageCheckIntervalId);
this.state.pageCheckIntervalId = null;
console.log('[每日上限管理器] 已停止页面上限提示的定期检查。');
}
}
/**
* Gets the current state of whether the limit is reached.
* @returns {boolean}
*/
isLimitActive() {
this._checkAndResetOnNewDay(); // 确保状态是最新的
return this.state.isLimitReached;
}
getVideosWatchedToday() {
this._checkAndResetOnNewDay();
return this.state.videosWatchedToday;
}
getLimitVideos() {
return this.config.limitVideos;
}
/**
* Calculates and returns the countdown string to the next reset time.
* @returns {string} Formatted countdown string (e.g., "HH:MM:SS") or "--:--:--"
*/
getCountdownString() {
if (!this.state.isLimitReached) return "未达上限";
const msUntilReset = this._getMillisecondsUntilReset();
return this._formatCountdown(msUntilReset);
}
/**
* Manually resets the daily limit.
*/
manualReset(callOnResetCallback = true) {
// console.warn('[每日上限管理器] manualReset 未完全实现。');
console.log('[每日上限管理器] 手动重置已触发。');
this.state.videosWatchedToday = 0;
this.state.isLimitReached = false;
this.state.lastResetDate = new Date().toDateString(); // 将最后重置日期更新为今天
this.state.lastLimitDate = null; // 清除上次达到上限的日期
GM_setValue('qh_videos_watched_today', 0);
GM_setValue('qh_daily_limit_reached', false);
GM_setValue('qh_daily_limit_last_reset_date', this.state.lastResetDate);
GM_setValue('qh_daily_limit_date', null);
if (this.state.autoResetTimer) {
clearTimeout(this.state.autoResetTimer);
this.state.autoResetTimer = null;
}
if (this.state.countdownInterval) {
clearInterval(this.state.countdownInterval);
this.state.countdownInterval = null;
}
if (callOnResetCallback) {
this.config.onLimitReset();
}
this._scheduleAutoReset(); // 重新安排下一次自动重置并更新倒计时
this._updateUIDisplay();
console.log('[每日上限管理器] 手动重置后状态:', JSON.parse(JSON.stringify(this.state)));
}
/**
* Updates the configured reset hour.
* @param {number} hour - The new hour (0-23) for reset.
*/
setResetHour(hour) {
// console.warn('[每日上限管理器] setResetHour 未完全实现。');
const hourInt = parseInt(hour, 10);
if (!isNaN(hourInt) && hourInt >= 0 && hourInt <= 23) {
console.log(`[每日上限管理器] 设置重置小时为: ${hourInt}`);
this.config.resetHour = hourInt;
GM_setValue('qh_daily_limit_reset_hour', hourInt);
// 如果当前已达上限或只是普通情况,都需要重新计算并安排下一次重置
console.log('[每日上限管理器] 重置小时已更改,重新安排自动重置并更新UI。');
this._scheduleAutoReset(); // 会清除旧的timer和interval,并用新时间启动
this._updateUIDisplay(); // 立即更新UI倒计时
} else {
console.error('[每日上限管理器] 提供了无效的重置小时:', hour);
}
}
getResetHour() {
return this.config.resetHour;
}
getState() {
return JSON.parse(JSON.stringify(this.state)); // 返回状态的深拷贝
}
setConfig(newConfig) {
let changed = false;
for (const key in newConfig) {
if (Object.hasOwnProperty.call(this.config, key) && this.config[key] !== newConfig[key]) {
this.config[key] = newConfig[key];
changed = true;
}
}
if (changed) {
console.log('[每日上限管理器] 配置已更新。正在检查是否需要调整上限状态...');
// 如果视频上限数量改变,可能需要重新评估 isLimitReached状态
if (newConfig.limitVideos !== undefined) {
if (this.config.limitVideos > 0 && this.state.videosWatchedToday >= this.config.limitVideos) {
if (!this.state.isLimitReached) {
console.log('[每日上限管理器] 根据新配置,视频数量已达上限。');
this.handleLimitReached(); // 触发上限处理
}
} else if (this.state.isLimitReached && (this.config.limitVideos <= 0 || this.state.videosWatchedToday < this.config.limitVideos)) {
// 如果之前是上限状态,但新配置允许更多视频或无限制,则解除上限
console.log('[每日上限管理器] 根据新配置,视频数量未达上限或无限制。解除上限状态。');
this.manualReset(true); // 使用manualReset来重置状态并触发回调和重新调度
}
}
if (newConfig.resetHour !== undefined) {
this.setResetHour(newConfig.resetHour); // 调用setResetHour来处理重置小时的变更逻辑
}
this._updateUIDisplay();
console.log('[每日上限管理器] 配置更新完毕:', this.config);
} else {
console.log('[每日上限管理器] 无配置变更。');
}
}
// --- Internal methods ---
_scheduleAutoReset() {
if (this.state.autoResetTimer) {
clearTimeout(this.state.autoResetTimer);
this.state.autoResetTimer = null;
}
if (this.state.countdownInterval) {
clearInterval(this.state.countdownInterval);
this.state.countdownInterval = null;
}
const msUntilReset = this._getMillisecondsUntilReset();
console.log(`[每日上限管理器] 计划在 ${msUntilReset / 1000 / 60} 分钟后自动重置。`);
this.state.autoResetTimer = setTimeout(() => {
console.log('[每日上限管理器] 自动每日上限重置已触发。');
this._checkAndResetOnNewDay(); // 这个方法会处理实际的重置逻辑和回调
// _checkAndResetOnNewDay 会调用 _updateUIDisplay 和 _scheduleAutoReset (如果需要再次调度)
}, msUntilReset);
// 启动UI倒计时更新(降低频率,减少日志输出)
this.state.countdownInterval = setInterval(() => {
this._updateUIDisplay(); // 每30秒更新倒计时显示
}, 30000); // 从1秒改为30秒
this._updateUIDisplay(); // 立即更新一次UI
}
}
// 确保 qh 对象存在
window.qh = window.qh || {};
window.qh.DailyLimitManager = DailyLimitManager;
// 导出全局函数供UI模块使用
window.updateDailyLimitDisplay = function(statusText, countdownText) {
// 减少日志输出频率,只在debug模式下输出
if (window.qh && window.qh.debugMode) {
console.log('[每日上限管理器] 全局 updateDailyLimitDisplay 被调用:', statusText, countdownText);
}
// 尝试调用UI模块的函数
if (window.qh && typeof window.qh.updateDailyLimitDisplay === 'function') {
window.qh.updateDailyLimitDisplay(statusText, countdownText);
} else {
// 直接更新DOM元素
const limitStatusEl = document.getElementById('qh-daily-limit-status');
const countdownEl = document.getElementById('qh-daily-limit-countdown');
if (limitStatusEl) {
limitStatusEl.textContent = statusText;
}
if (countdownEl) {
countdownEl.textContent = countdownText;
}
}
};
console.log('[模块加载] dailyLimitManager 模块已加载, 版本 1.0.2');
})();