Lolz Time Tracker

Track time spent on lolz.live with detailed statistics

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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