您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Track time spent on lolz.live with detailed statistics
// ==UserScript== // @name Lolz Time Tracker // @namespace http://tampermonkey.net/ // @version 1.0 // @description Track time spent on lolz.live with detailed statistics // @author Yowori // @match https://lolz.live/* // @match https://lolz.guru/* // @match https://zelenka.guru/* // @license MIT // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; const SECTION_CONFIG = { forums: { pattern: /\/forums\//, name: 'Разделы ' }, threads: { pattern: /\/threads\//, name: 'Темы ' }, members: { pattern: /\/members\//, name: 'Пользователи ' }, account: { pattern: /\/account\//, name: 'Профиль ' }, other: { pattern: /.*/, name: 'Другое ' } }; let trackerState = { currentSection: null, currentThread: null, currentUser: null, isPageActive: true, lastUpdateTime: Date.now(), totalSeconds: 0, sectionSeconds: {}, threadSeconds: {}, userSeconds: {} }; const storageKey = 'lolzTimeTracker_v4'; const uiStateKey = 'lolzTimeTrackerUIState'; let updateInterval; let statsVisible = false; let detailedView = 'main'; let confirmationOpen = false; GM_addStyle(` #time-tracker-container { position: fixed; left: 10px; bottom: 10px; z-index: 9999; font-family: Arial, sans-serif; } #time-tracker-toggle { background: #4CAF50; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.2s ease; margin-right: 8px; } #time-tracker-toggle:hover { background: #45a049; transform: translateY(-1px); } #time-tracker-reset { background: #f44336; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.2s ease; } #time-tracker-reset:hover { background: #d32f2f; transform: translateY(-1px); } #time-tracker-buttons { display: flex; } #time-tracker-widget { display: none; background: #272727; color: #fff; padding: 12px; border-radius: 6px; margin-top: 8px; width: 280px; backdrop-filter: blur(5px); box-shadow: 0 4px 8px rgba(0,0,0,0.3); border: 1px solid #383838; max-height: 60vh; overflow-y: auto; } .time-tracker-header { font-size: 16px; font-weight: bold; margin-bottom: 10px; color: #4CAF50; border-bottom: 1px solid #444; padding-bottom: 5px; display: flex; justify-content: space-between; align-items: center; } .time-tracker-back-btn { background: none; border: none; color: #4CAF50; cursor: pointer; font-size: 14px; } .time-tracker-confirm-reset { background: none; border: none; color: #f44336; cursor: pointer; font-size: 14px; margin-left: 5px; } .time-tracker-section { margin: 8px 0; display: flex; justify-content: space-between; } .time-tracker-section-name { color: #ddd; cursor: pointer; } .time-tracker-section-time { font-weight: bold; color: #fff; } .time-tracker-detail-item { margin: 6px 0; padding-left: 10px; border-left: 2px solid #444; } .time-tracker-nav-btn { background: none; border: none; color: #ddd; cursor: pointer; margin: 0 5px; padding: 2px 5px; } .time-tracker-nav-btn:hover { color: #4CAF50; } .time-tracker-nav-btn.active { color: #4CAF50; border-bottom: 1px solid #4CAF50; } `); function init() { loadData(); loadUIState(); createUI(); setupVisibilityListener(); setupPageAnalyzers(); startTracking(); GM_registerMenuCommand("Показать статистику Lolz.live", toggleStats); } function loadUIState() { const savedState = GM_getValue(uiStateKey, { statsVisible: false }); statsVisible = savedState.statsVisible; } function saveUIState() { GM_setValue(uiStateKey, { statsVisible: statsVisible }); } function createUI() { const container = document.createElement('div'); container.id = 'time-tracker-container'; const buttonsContainer = document.createElement('div'); buttonsContainer.id = 'time-tracker-buttons'; const toggleBtn = document.createElement('button'); toggleBtn.id = 'time-tracker-toggle'; toggleBtn.textContent = statsVisible ? '❌ Скрыть' : '📊 Статистика'; toggleBtn.addEventListener('click', toggleStats); const resetBtn = document.createElement('button'); resetBtn.id = 'time-tracker-reset'; resetBtn.textContent = '🔄 Сбросить'; resetBtn.addEventListener('click', confirmResetStats); buttonsContainer.appendChild(toggleBtn); buttonsContainer.appendChild(resetBtn); const widget = document.createElement('div'); widget.id = 'time-tracker-widget'; widget.style.display = statsVisible ? 'block' : 'none'; container.appendChild(buttonsContainer); container.appendChild(widget); document.body.appendChild(container); updateUI(); } function confirmResetStats() { const widget = document.getElementById('time-tracker-widget'); if (!widget) return; confirmationOpen = true; widget.innerHTML = ` <div class="time-tracker-header"> <span>Сброс статистики</span> </div> <div style="margin: 10px 0;"> Вы уверены, что хотите сбросить статистику за сегодня? </div> <div style="display: flex; justify-content: flex-end;"> <button id="time-tracker-cancel-reset" style="background: #4CAF50; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.2s ease; margin-right: 8px;">Отмена</button> <button id="time-tracker-confirm-reset" style="background: #f44336; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.2s ease;">Сбросить</button> </div> `; document.getElementById('time-tracker-cancel-reset').addEventListener('click', () => { confirmationOpen = false; updateUI(); }); document.getElementById('time-tracker-confirm-reset').addEventListener('click', resetStats); } function resetStats() { const today = getTodayKey(); const savedData = GM_getValue(storageKey) || {}; savedData[today] = { totalSeconds: 0, sectionSeconds: {}, threadSeconds: {}, userSeconds: {} }; GM_setValue(storageKey, savedData); trackerState.totalSeconds = 0; trackerState.sectionSeconds = {}; trackerState.threadSeconds = {}; trackerState.userSeconds = {}; for (const section in SECTION_CONFIG) { trackerState.sectionSeconds[section] = 0; } confirmationOpen = false; updateUI(); } function toggleStats() { statsVisible = !statsVisible; const widget = document.getElementById('time-tracker-widget'); if (widget) widget.style.display = statsVisible ? 'block' : 'none'; const toggleBtn = document.getElementById('time-tracker-toggle'); if (toggleBtn) toggleBtn.textContent = statsVisible ? '❌ Скрыть' : '📊 Статистика'; saveUIState(); if (statsVisible && !confirmationOpen) { updateUI(); } } function loadData() { const today = getTodayKey(); const savedData = GM_getValue(storageKey) || {}; const todayData = savedData[today] || { totalSeconds: 0, sectionSeconds: {}, threadSeconds: {}, userSeconds: {} }; trackerState = { ...trackerState, totalSeconds: todayData.totalSeconds || 0, sectionSeconds: todayData.sectionSeconds || {}, threadSeconds: todayData.threadSeconds || {}, userSeconds: todayData.userSeconds || {} }; for (const section in SECTION_CONFIG) { if (!trackerState.sectionSeconds[section]) { trackerState.sectionSeconds[section] = 0; } } } function saveData() { const today = getTodayKey(); const savedData = GM_getValue(storageKey) || {}; savedData[today] = { totalSeconds: trackerState.totalSeconds, sectionSeconds: trackerState.sectionSeconds, threadSeconds: trackerState.threadSeconds, userSeconds: trackerState.userSeconds }; GM_setValue(storageKey, savedData); } function detectSection() { const path = window.location.pathname; for (const [section, config] of Object.entries(SECTION_CONFIG)) { if (config.pattern.test(path)) { return section; } } return 'other'; } function detectThread() { const threadMatch = window.location.pathname.match(/\/threads\/(\d+)/); if (threadMatch) { return threadMatch[1]; } return null; } function detectUser() { const userMatch = window.location.pathname.match(/\/members\/(\d+)/); if (userMatch) { return userMatch[1]; } const prettyUrlMatch = window.location.pathname.match(/^\/([^\/]+)\/?$/); if (prettyUrlMatch && !['forums', 'threads', 'account', 'members'].includes(prettyUrlMatch[1])) { return prettyUrlMatch[1]; } return null; } function setupPageAnalyzers() { trackerState.currentSection = detectSection(); trackerState.currentThread = detectThread(); trackerState.currentUser = detectUser(); } function startTracking() { if (updateInterval) clearInterval(updateInterval); updateInterval = setInterval(() => { if (!trackerState.isPageActive) return; const now = Date.now(); const elapsedSeconds = Math.floor((now - trackerState.lastUpdateTime) / 1000); if (elapsedSeconds > 0) { trackerState.lastUpdateTime = now; setupPageAnalyzers(); trackerState.totalSeconds += elapsedSeconds; trackerState.sectionSeconds[trackerState.currentSection] = (trackerState.sectionSeconds[trackerState.currentSection] || 0) + elapsedSeconds; if (trackerState.currentThread) { trackerState.threadSeconds[trackerState.currentThread] = (trackerState.threadSeconds[trackerState.currentThread] || 0) + elapsedSeconds; } if (trackerState.currentUser) { trackerState.userSeconds[trackerState.currentUser] = (trackerState.userSeconds[trackerState.currentUser] || 0) + elapsedSeconds; } saveData(); if (statsVisible && !confirmationOpen) { updateUI(); } } }, 1000); } function updateUI() { if (confirmationOpen) return; const widget = document.getElementById('time-tracker-widget'); if (!widget) return; let html = ''; switch (detailedView) { case 'main': html = getMainView(); break; case 'sections': html = getSectionsDetailView(); break; case 'threads': html = getThreadsDetailView(); break; case 'users': html = getUsersDetailView(); break; } widget.innerHTML = html; addEventHandlers(); } function getMainView() { let html = `<div class="time-tracker-header"> <span>Статистика за сегодня</span> </div>`; html += `<div class="time-tracker-section"> <span class="time-tracker-section-name">Всего:</span> <span class="time-tracker-section-time">${formatTime(trackerState.totalSeconds)}</span> </div>`; html += `<div style="margin: 10px 0; display: flex; justify-content: space-around;"> <button class="time-tracker-nav-btn ${detailedView === 'threads' ? 'active' : ''}" data-view="threads">Темы</button> <button class="time-tracker-nav-btn ${detailedView === 'users' ? 'active' : ''}" data-view="users">Пользователи</button> </div>`; const sortedSections = Object.entries(trackerState.sectionSeconds) .sort((a, b) => b[1] - a[1]); html += `<div style="margin-top: 10px; font-weight: bold; color: #ddd;">Общая информация :</div>`; for (const [sectionId, seconds] of sortedSections.slice(0, 5)) { if (seconds > 0) { const sectionName = SECTION_CONFIG[sectionId].name; html += `<div class="time-tracker-section"> <span class="time-tracker-section-name">${sectionName}:</span> <span class="time-tracker-section-time">${formatTime(seconds)}</span> </div>`; } } return html; } function getSectionsDetailView() { let html = `<div class="time-tracker-header"> <button class="time-tracker-back-btn" data-view="main">Назад</button> <span>Статистика по разделам</span> </div>`; const sortedSections = Object.entries(trackerState.sectionSeconds) .sort((a, b) => b[1] - a[1]); for (const [sectionId, seconds] of sortedSections) { if (seconds > 0) { const sectionName = SECTION_CONFIG[sectionId].name; html += `<div class="time-tracker-section"> <span class="time-tracker-section-name">${sectionName}:</span> <span class="time-tracker-section-time">${formatTime(seconds)}</span> </div>`; } } return html; } function getThreadsDetailView() { let html = `<div class="time-tracker-header"> <button class="time-tracker-back-btn" data-view="main">Назад</button> <span>Темы</span> </div>`; const sortedThreads = Object.entries(trackerState.threadSeconds) .sort((a, b) => b[1] - a[1]); for (const [threadId, seconds] of sortedThreads.slice(0, 20)) { if (seconds > 0) { html += `<div class="time-tracker-section"> <span class="time-tracker-section-name" data-thread-id="${threadId}">Тема #${threadId}:</span> <span class="time-tracker-section-time">${formatTime(seconds)}</span> </div>`; } } return html; } function getUsersDetailView() { let html = `<div class="time-tracker-header"> <button class="time-tracker-back-btn" data-view="main">Назад</button> <span>Пользователи</span> </div>`; const sortedUsers = Object.entries(trackerState.userSeconds) .sort((a, b) => b[1] - a[1]); for (const [userId, seconds] of sortedUsers.slice(0, 20)) { if (seconds > 0) { const isNumericId = /^\d+$/.test(userId); const userLink = isNumericId ? `/members/${userId}/` : `/${userId}/`; html += `<div class="time-tracker-section"> <span class="time-tracker-section-name" data-user-id="${userId}" data-is-numeric="${isNumericId}">Пользователь ${isNumericId ? '#' + userId : userId}:</span> <span class="time-tracker-section-time">${formatTime(seconds)}</span> </div>`; } } return html; } function addEventHandlers() { document.querySelectorAll('.time-tracker-nav-btn').forEach(btn => { btn.addEventListener('click', () => { detailedView = btn.dataset.view; updateUI(); }); }); document.querySelectorAll('.time-tracker-back-btn').forEach(btn => { btn.addEventListener('click', () => { detailedView = btn.dataset.view; updateUI(); }); }); document.querySelectorAll('.time-tracker-section-name[data-thread-id]').forEach(el => { el.addEventListener('click', (e) => { e.preventDefault(); const threadId = el.dataset.threadId; window.location.href = `/threads/${threadId}/`; }); }); document.querySelectorAll('.time-tracker-section-name[data-user-id]').forEach(el => { el.addEventListener('click', (e) => { e.preventDefault(); const userId = el.dataset.userId; const isNumeric = el.dataset.isNumeric === 'true'; if (isNumeric) { window.location.href = `/members/${userId}/`; } else { window.location.href = `/${userId}/`; } }); }); } function formatTime(totalSeconds) { const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (totalSeconds < 60) { return `${seconds} сек`; } else { return `${hours > 0 ? hours + ' ч ' : ''}${minutes} мин`; } } function setupVisibilityListener() { document.addEventListener('visibilitychange', () => { trackerState.isPageActive = !document.hidden; trackerState.lastUpdateTime = Date.now(); }); } function getTodayKey() { const now = new Date(); return `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}`; } window.addEventListener('load', init); document.addEventListener('DOMContentLoaded', init); })();