您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
统计特定网站的浏览时间,提供可自定义的中央提醒和快捷操作
// ==UserScript== // @name 水源戒手 // @namespace https://shuiyuan.sjtu.edu.cn/ // @version 1.4.1 // @description 统计特定网站的浏览时间,提供可自定义的中央提醒和快捷操作 // @match https://shuiyuan.sjtu.edu.cn/* // @match https://shuiyuan.sjtu.edu.cn/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @author claude-3-5-sonnet-20240620 ,十一世纪 // @license MIT // ==/UserScript== (function () { 'use strict'; // 默认配置 const defaultConfig = { buttons: [ { name: "戒!", url: "https://www.bilibili.com/video/BV1UT42167xb" }, { name: "学!", url: "https://oc.sjtu.edu.cn/" }, { name: "源!", url: "" } ], popupInterval: -1, popupEnabled: true, nextPopupTime: 0, suppressDuration: 0, lastResetTime: Date.now() }; // 全局变量 let config = GM_getValue('config', defaultConfig); let startTime = Date.now(); let totalTime = GM_getValue('totalTime', 0); let lastActive = Date.now(); let isActive = true; // 添加样式 GM_addStyle(` .time-tracker-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; } .time-tracker-popup { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); width: 400px; max-width: 90%; } .popup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .popup-header h2 { margin: 0; color: #333; } .close-btn { cursor: pointer; font-size: 24px; color: #666; } .time-display { text-align: center; font-size: 18px; margin-bottom: 20px; } .button-container { display: flex; justify-content: space-around; gap: 10px; margin: 15px 0; } .button-wrapper { position: relative; width: 100%; } .custom-button { position: relative; width: 100%; padding: 10px 20px; font-size: 16px; border: none; border-radius: 5px; cursor: pointer; transition: all 0.3s ease; color: white; margin-bottom: 5px; } .custom-button:nth-child(1) { background-color: #ff4444; } .custom-button:nth-child(2) { background-color: #4CAF50; } .custom-button:nth-child(3) { background-color: #2196F3; } /* 如果需要调整气泡提示的位置,可以修改这些值 */ .tooltip-trigger { position: absolute; left: 0; right: 0; top: -40px; height: 60px; z-index: 999; } .tooltip { visibility: hidden; position: absolute; bottom: 120%; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 8px; border-radius: 4px; white-space: nowrap; font-size: 14px; opacity: 0; transition: opacity 0.2s; z-index: 1000; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .tooltip input { margin: 0 5px; padding: 2px 5px; border: 1px solid #666; border-radius: 3px; width: 200px; } .tooltip-trigger:hover + .tooltip, .tooltip:hover { visibility: visible; opacity: 1; } .settings-container { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; } .settings-group { margin-bottom: 15px; } .settings-label { display: block; margin-bottom: 5px; color: #666; } .settings-select { width: 100%; padding: 5px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 10px; } .suppress-controls { display: flex; gap: 10px; align-items: center; /* 垂直居中对齐 */ justify-content: flex-start; /* 靠左对齐 */ } .settings-button { padding: 8px 15px; /* 调整内边距使按钮更宽 */ background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; white-space: nowrap; /* 防止文字换行 */ min-width: 120px; /* 设置最小宽度 */ text-align: center; /* 文字居中 */ } .settings-button:hover { background: #1976D2; } #quit { background-color: #ff4444; } #study { background-color: #4CAF50; } #continue { background-color: #2196F3; } #quit:hover { background-color: #ff6666; } #study:hover { background-color: #66BB6A; } #continue:hover { background-color: #42A5F5; } `); // 创建水源图标 function createWaterSourceIcon() { const headerIcons = document.querySelector('.icons.d-header-icons'); if (!headerIcons) return; // 检查是否存在“聊天”或“搜索”图标 const hasChatOrSearchIcon = headerIcons.querySelector('.chat-header-icon, .search-dropdown'); if (!hasChatOrSearchIcon) return; // 如果两者都不存在,则不插入图标 const listItem = document.createElement('li'); listItem.className = 'header-dropdown-toggle'; const button = document.createElement('button'); button.className = 'btn no-text icon btn-flat'; button.title = '水源戒手'; button.innerHTML = ` <svg class="fa d-icon svg-icon svg-string" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <path fill="currentColor" d="M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22A9,9 0 0,0 21,13A9,9 0 0,0 12,4M12.5,8H11V14L15.75,16.85L16.5,15.62L12.5,13.25V8M7.88,3.39L6.6,1.86L2,5.71L3.29,7.24L7.88,3.39M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25L22,5.72Z"/> </svg> `; button.onclick = () => createPopup(true); listItem.appendChild(button); // 插入到“聊天”或“搜索”图标之前 headerIcons.insertBefore(listItem, hasChatOrSearchIcon); } // 更新配置 function updateConfig() { GM_setValue('config', config); } // 更新时间 function updateTime() { if (isActive) { const now = Date.now(); // 检查是否需要重置 if (!checkAndResetTime()) { totalTime += (now - startTime) / 1000; GM_setValue('totalTime', totalTime); } startTime = now; } } // 重置活动状态 function resetActivity() { if (!isActive) { startTime = Date.now(); } isActive = true; lastActive = Date.now(); } // 检查不活跃状态 function checkInactivity() { if (Date.now() - lastActive > 60000) { isActive = false; } } // 创建弹窗 function createPopup(isManualClick = false) { // 检查是否应该显示弹窗 if (!isManualClick && config.nextPopupTime > Date.now()) { return; } updateTime(); const overlay = document.createElement('div'); overlay.className = 'time-tracker-overlay'; let buttonsHtml = config.buttons.map((button, index) => ` <div class="button-wrapper"> <button class="custom-button" id="${['quit', 'study', 'continue'][index]}" data-url="${button.url}"> ${button.name} </button> ${index !== 2 ? ` <div class="tooltip-trigger"></div> <div class="tooltip"> <input type="text" value="${button.url}"> </div> ` : ''} </div> `).join(''); overlay.innerHTML = ` <div class="time-tracker-popup"> <div class="popup-header"> <h2>水源戒手</h2> <span class="close-btn">×</span> </div> <div class="popup-content"> <p class="time-display">你已经看了 ${Math.round(totalTime / 60)} 分钟的水源了!</p> <div class="button-container"> ${buttonsHtml} </div> <div class="settings-container"> <div class="settings-group"> <label class="settings-label">弹窗提醒间隔:</label> <select class="settings-select" id="popupSettings"> <option value="-1">不设置定时弹窗</option> <option value="3">3分钟</option> <option value="10">10分钟</option> <option value="15">15分钟</option> <option value="30">30分钟</option> <option value="60">60分钟</option> </select> </div> <div class="settings-group"> <label class="settings-label">本次浏览提醒设置:</label> <div class="suppress-controls"> <select class="settings-select" id="suppressDuration"> <option value="0">每次刷新都提醒</option> <option value="0.05">3分钟内不再提醒</option> <option value="0.17">10分钟内不再提醒</option> <option value="0.25">15分钟内不再提醒</option> <option value="0.5">30分钟内不再提醒</option> <option value="1">1小时内不再提醒</option> <option value="3">3小时内不再提醒</option> <option value="12">12小时内不再提醒</option> <option value="24">24小时内不再提醒</option> </select> <button class="settings-button" id="suppressButton">确定</button> </div> </div> </div> </div> </div> `; document.body.appendChild(overlay); // 设置选项的初始值 const popupSelect = document.getElementById('popupSettings'); const suppressSelect = document.getElementById('suppressDuration'); popupSelect.value = config.popupInterval.toString(); suppressSelect.value = config.suppressDuration.toString(); // 事件监听 overlay.querySelector('.close-btn').addEventListener('click', () => overlay.remove()); // 按钮点击事件 overlay.querySelectorAll('.custom-button').forEach((button, index) => { button.addEventListener('click', () => { if (index === 2) { // "源!"按钮 overlay.remove(); } else { const url = button.dataset.url; if (url) { window.location.href = url; } } }); }); // URL输入事件 overlay.querySelectorAll('.tooltip input').forEach((input, index) => { input.addEventListener('click', e => e.stopPropagation()); input.addEventListener('change', e => { const button = e.target.closest('.custom-button'); const newUrl = e.target.value; button.dataset.url = newUrl; config.buttons[index].url = newUrl; updateConfig(); }); }); // 设置相关事件 popupSelect.addEventListener('change', (e) => { config.popupInterval = parseInt(e.target.value); config.popupEnabled = (config.popupInterval !== -1); updateConfig(); }); document.getElementById('suppressButton').addEventListener('click', () => { const duration = parseFloat(suppressSelect.value); config.nextPopupTime = duration === 0 ? 0 : Date.now() + (duration * 60 * 60 * 1000); config.suppressDuration = duration; updateConfig(); overlay.remove(); }); } // 重置函数 function checkAndResetTime() { const now = new Date(); const lastResetTime = new Date(config.lastResetTime); // 获取今天的凌晨4点时间 const today4AM = new Date(now); today4AM.setHours(4, 0, 0, 0); // 获取昨天的凌晨4点时间 const yesterday4AM = new Date(today4AM); yesterday4AM.setDate(yesterday4AM.getDate() - 1); // 如果上次重置时间在昨天4点之前,且现在时间已过今天4点 if (lastResetTime < yesterday4AM && now >= today4AM) { totalTime = 0; GM_setValue('totalTime', 0); config.lastResetTime = now.getTime(); updateConfig(); return true; } return false; } // 初始化 function initialize() { createWaterSourceIcon(); // 检查是否需要重置时间 checkAndResetTime(); // 检查是否需要显示初始弹窗 if (config.nextPopupTime <= Date.now()) { setTimeout(() => createPopup(false), 1000); } } initialize(); // 定时检查是否需要弹窗 setInterval(() => { if (config.popupEnabled && config.popupInterval !== -1) { createPopup(false); } }, 60000); // 添加定时检查重置 setInterval(() => { checkAndResetTime(); }, 60000); // 每分钟检查一次 // 全局事件监听 window.addEventListener('beforeunload', updateTime); ['mousemove', 'keydown', 'click', 'scroll'].forEach(eventType => { document.addEventListener(eventType, resetActivity); }); setInterval(checkInactivity, 10000); document.addEventListener('visibilitychange', () => { if (document.hidden) { updateTime(); isActive = false; } else { resetActivity(); } }); })();