// ==UserScript==
// @name 抖音自律助手终极版(右键可自定义时长)
// @namespace http://tampermonkey.net/
// @version 1.6
// @description 带可拖动悬浮计时器、智能黑屏及网页端自定义时长
// @author potato
// @match *://*.douyin.com/*
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
/*************** 样式 ***************/
GM_addStyle(`
/* 黑屏遮罩 */
#timeout-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,.9);z-index:999999;display:none;justify-content:center;align-items:center;color:#fff;font-size:2em;text-align:center;cursor:not-allowed;flex-direction:column;}
/* 倒计时小球 */
#countdown-ball{position:fixed;right:20px;bottom:20px;width:60px;height:60px;background:linear-gradient(135deg,#ff6b6b,#ff4757);border-radius:50%;box-shadow:0 4px 15px rgba(255,107,107,.3);cursor:move;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;font-size:14px;text-align:center;line-height:1.2;z-index:999998;user-select:none;transition:all .3s cubic-bezier(.4,0,.2,1);}
#countdown-ball:hover{transform:scale(1.1);box-shadow:0 6px 20px rgba(255,107,107,.4);}
#countdown-ball.dragging{transition:none;opacity:.9;}
/* 黑屏提示文字 */
.timeout-text{animation:pulse 2s infinite;}
@keyframes pulse{0%{transform:scale(1);}50%{transform:scale(1.05);}100%{transform:scale(1);}}
`);
/*************** DOM 构造 ***************/
const createOverlay = () => {
const overlay = document.createElement('div');
overlay.id = 'timeout-overlay';
document.body.appendChild(overlay);
return overlay;
};
const createCountdownBall = () => {
const ball = document.createElement('div');
ball.id = 'countdown-ball';
document.body.appendChild(ball);
return ball;
};
/*************** 定时器控制 ***************/
class TimerController {
STORAGE_KEY = 'douyin_focus_duration_ms';
constructor() {
// 读取持久化时长,默认 10 分钟
const saved = parseInt(localStorage.getItem(this.STORAGE_KEY), 10);
this.totalTime = Number.isFinite(saved) && saved > 0 ? saved : 10 * 60 * 1000;
this.startTime = Date.now();
this.remaining = this.totalTime;
this.timer = null;
this.ball = createCountdownBall();
this.overlay = createOverlay();
this.setupOverlayText(); // 根据时长渲染提示
this.init();
}
/***** 初始化 *****/
init() {
this.setupDrag();
this.setupCustomTimeListener(); // 新增:自定义时长
this.setupVisibilityListener();
this.startCountdown();
}
/***** 倒计时循环 *****/
startCountdown() {
this.clearTimer();
this.startTime = Date.now();
this.updateDisplay();
this.timer = setInterval(() => {
this.remaining = this.totalTime - (Date.now() - this.startTime);
if (this.remaining <= 0) return this.triggerTimeout();
this.updateDisplay();
}, 200);
}
clearTimer() {
if (this.timer) clearInterval(this.timer);
this.timer = null;
}
/***** UI 更新 *****/
updateDisplay() {
const m = Math.floor(this.remaining / 60000);
const s = Math.floor((this.remaining % 60000) / 1000)
.toString()
.padStart(2, '0');
this.ball.textContent = `${m}:${s}`;
// 渐变色随进度变化
const p = this.remaining / this.totalTime;
this.ball.style.background = `linear-gradient(135deg,
hsl(${p * 120},70%,50%),
hsl(${p * 120},80%,45%)
)`;
}
setupOverlayText() {
const minutes = this.totalTime / 60000;
this.overlay.innerHTML = `
<div class="timeout-text">
<div>🕒 已连续使用${minutes}分钟</div>
<div style="font-size:0.6em;margin-top:20px;">请休息片刻再继续</div>
<div style="font-size:0.4em;margin-top:10px;">(刷新页面可恢复)</div>
</div>
`;
}
/***** 超时处理 *****/
triggerTimeout() {
this.clearTimer();
document.querySelectorAll('video').forEach(v => v.pause());
this.overlay.style.display = 'flex';
this.ball.style.display = 'none';
document.body.style.overflow = 'hidden';
this.addOverlayBlockers();
}
addOverlayBlockers() {
const blocker = e => {
e.preventDefault();
e.stopPropagation();
};
['click', 'touchstart', 'keydown'].forEach(evt =>
document.addEventListener(evt, blocker, true)
);
}
/***** 自定义时长 *****/
setupCustomTimeListener() {
const handler = e => {
e.preventDefault(); // 阻止默认菜单
const currentMin = Math.round(this.totalTime / 60000);
const input = prompt('设置专注时长 (分钟):', currentMin);
if (input === null) return;
const mins = parseInt(input.trim(), 10);
if (!Number.isFinite(mins) || mins <= 0) {
alert('请输入正整数分钟数');
return;
}
// 更新时长 & 持久化
this.totalTime = mins * 60 * 1000;
localStorage.setItem(this.STORAGE_KEY, this.totalTime);
this.remaining = this.totalTime;
this.setupOverlayText(); // 重新渲染遮罩文案
this.ball.style.display = 'flex';
this.overlay.style.display = 'none';
document.body.style.overflow = '';
this.startCountdown();
};
// 双击 or 右键均可触发
this.ball.addEventListener('contextmenu', handler);
this.ball.addEventListener('dblclick', handler);
}
/***** 可见性监控 *****/
setupVisibilityListener() {
let hiddenAt = 0;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
hiddenAt = Date.now();
this.clearTimer();
} else {
this.startTime += Date.now() - hiddenAt;
this.startCountdown();
}
});
}
/***** 拖动 *****/
setupDrag() {
let dragging = false, startX, startY, origX, origY;
const move = e => {
if (!dragging) return;
const dx = e.clientX - startX, dy = e.clientY - startY;
const maxX = window.innerWidth - this.ball.offsetWidth;
const maxY = window.innerHeight - this.ball.offsetHeight;
const newX = Math.min(Math.max(0, origX + dx), maxX);
const newY = Math.min(Math.max(0, origY + dy), maxY);
this.ball.style.left = `${newX}px`;
this.ball.style.top = `${newY}px`;
};
this.ball.addEventListener('mousedown', e => {
dragging = true;
this.ball.classList.add('dragging');
startX = e.clientX; startY = e.clientY;
const rect = this.ball.getBoundingClientRect();
origX = rect.left; origY = rect.top;
document.addEventListener('mousemove', move);
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
this.ball.classList.remove('dragging');
document.removeEventListener('mousemove', move);
});
}
}
// 启动
new TimerController();
})();