Lolz Time Tracker

Track time spent on lolz.live with detailed statistics

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();