Lolz Time Tracker

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