// ==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();
}
});
})();