Сохранение прогресса видео с настройками и управлением скорости воспроизведения

Сохраняет прогресс для каждого видео автоматически, добавляет управление скоростью воспроизведения, уведомления и окно настроек с возможностью настройки. Поддержка двух языков (русский и английский).

// ==UserScript==
// @name Сохранение прогресса видео с настройками и управлением скорости воспроизведения
// @name:en Saving Video Progress with Settings and Playback Speed Control
// @namespace http://tampermonkey.net/
// @version 2.9.1
// @description Сохраняет прогресс для каждого видео автоматически, добавляет управление скоростью воспроизведения, уведомления и окно настроек с возможностью настройки. Поддержка двух языков (русский и английский).
// @description:en Automatically saves progress for each video, adds playback speed control, notifications, and a settings window with customizable options. Supports two languages (Russian and English).
// @author Egor Fox
// @match *://*/*
// @grant none
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const videos = document.querySelectorAll('video'); // Находим все видео на странице

    if (!videos.length) return; // Если видео нет, завершаем выполнение скрипта

    let currentNotification = null;
    let settingsWindowOpen = false; // Флаг для отслеживания открыто ли окно настроек

    // Загрузить настройки из localStorage или установить значения по умолчанию
    let settings = {
        notificationsEnabled: localStorage.getItem('notificationsEnabled') !== 'false',
        autoSaveInterval: parseInt(localStorage.getItem('autoSaveInterval'), 10) || 1000,
        maxPlaybackRate: parseFloat(localStorage.getItem('maxPlaybackRate')) || 3,
        hotkeysEnabled: localStorage.getItem('hotkeysEnabled') !== 'false',
        volumeControlEnabled: localStorage.getItem('volumeControlEnabled') !== 'false', // Добавляем настройку громкости
        language: localStorage.getItem('language') || 'ru' // Поддержка языка
    };

    // Тексты для уведомлений и интерфейса
    const langTexts = {
        ru: {
            settingsTitle: 'Настройки скрипта',
            notificationsLabel: 'Включить уведомления:',
            autoSaveLabel: 'Интервал авто-сохранения (мс):',
            maxSpeedLabel: 'Максимальная скорость (x):',
            volumeControlLabel: 'Включить изменение громкости при прокрутке:',
            hotkeysLabel: 'Включить горячие клавиши:',
            languageLabel: 'Выберите язык:',
            closeButton: 'Закрыть',
            progressRestored: 'Прогресс видео №{index} восстановлен!',
            speedChanged: 'Скорость видео изменена на {speed}x',
            volumeChanged: 'Громкость изменена на {volume}%',
        },
        en: {
            settingsTitle: 'Script Settings',
            notificationsLabel: 'Enable notifications:',
            autoSaveLabel: 'Auto-save interval (ms):',
            maxSpeedLabel: 'Max playback speed (x):',
            volumeControlLabel: 'Enable volume control on scroll:',
            hotkeysLabel: 'Enable hotkeys:',
            languageLabel: 'Choose language:',
            closeButton: 'Close',
            progressRestored: 'Video progress #{index} restored!',
            speedChanged: 'Playback speed changed to {speed}x',
            volumeChanged: 'Volume changed to {volume}%',
        }
    };

    // Функция для отображения уведомлений
    function showNotification(message, backgroundColor) {
        if (!settings.notificationsEnabled) return;

        if (currentNotification) {
            currentNotification.style.display = 'none';
        }

        const notification = document.createElement('div');
        notification.style.position = 'fixed';
        notification.style.top = '10%';
        notification.style.left = '50%';
        notification.style.transform = 'translate(-50%, -50%)';
        notification.style.backgroundColor = backgroundColor;
        notification.style.color = 'white';
        notification.style.padding = '10px 20px';
        notification.style.borderRadius = '8px';
        notification.style.fontSize = '16px';
        notification.style.zIndex = '9999';
        notification.innerText = message;

        document.body.appendChild(notification);

        setTimeout(() => {
            notification.style.display = 'none';
        }, 3000);

        currentNotification = notification;
    }

    // Функция для создания окна настроек
    function createSettingsWindow() {
        if (settingsWindowOpen) {
            closeSettingsWindow(); // Закрыть окно, если оно уже открыто
            return;
        }

        settingsWindowOpen = true; // Устанавливаем флаг, что окно открыто

        const settingsOverlay = document.createElement('div');
        settingsOverlay.id = 'settingsOverlay';
        settingsOverlay.style.position = 'fixed';
        settingsOverlay.style.top = '0';
        settingsOverlay.style.left = '0';
        settingsOverlay.style.width = '100%';
        settingsOverlay.style.height = '100%';
        settingsOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        settingsOverlay.style.zIndex = '10000';
        settingsOverlay.style.display = 'flex';
        settingsOverlay.style.justifyContent = 'center';
        settingsOverlay.style.alignItems = 'center';

        const settingsWindow = document.createElement('div');
settingsWindow.style.backgroundColor = '#333';
settingsWindow.style.padding = '30px';
settingsWindow.style.borderRadius = '10px';
settingsWindow.style.width = '500px';
settingsWindow.style.textAlign = 'left';
settingsWindow.style.color = '#fff';
settingsWindow.style.fontFamily = 'Arial, sans-serif';
settingsWindow.style.fontSize = '18px'; // Увеличиваем шрифт


        const title = document.createElement('h2');
        title.innerText = langTexts[settings.language].settingsTitle;
        title.style.textAlign = 'center';
        title.style.marginBottom = '30px';
        title.style.fontSize = '20px';
        settingsWindow.appendChild(title);

        // Настройки уведомлений
        const notificationsLabel = document.createElement('label');
        notificationsLabel.innerText = langTexts[settings.language].notificationsLabel;
        const notificationsCheckbox = document.createElement('input');
        notificationsCheckbox.type = 'checkbox';
        notificationsCheckbox.checked = settings.notificationsEnabled;
        notificationsCheckbox.addEventListener('change', () => {
            settings.notificationsEnabled = notificationsCheckbox.checked;
            localStorage.setItem('notificationsEnabled', settings.notificationsEnabled);
        });
        settingsWindow.appendChild(notificationsLabel);
        settingsWindow.appendChild(notificationsCheckbox);
        settingsWindow.appendChild(document.createElement('br'));

        // Интервал авто-сохранения
        const autoSaveLabel = document.createElement('label');
        autoSaveLabel.innerText = langTexts[settings.language].autoSaveLabel;
        const autoSaveInput = document.createElement('input');
        autoSaveInput.type = 'number';
        autoSaveInput.value = settings.autoSaveInterval;
        autoSaveInput.addEventListener('change', () => {
            settings.autoSaveInterval = parseInt(autoSaveInput.value, 10);
            localStorage.setItem('autoSaveInterval', settings.autoSaveInterval);
        });
        settingsWindow.appendChild(autoSaveLabel);
        settingsWindow.appendChild(autoSaveInput);
        settingsWindow.appendChild(document.createElement('br'));

        // Максимальная скорость воспроизведения
        const maxSpeedLabel = document.createElement('label');
        maxSpeedLabel.innerText = langTexts[settings.language].maxSpeedLabel;
        const maxSpeedInput = document.createElement('input');
        maxSpeedInput.type = 'number';
        maxSpeedInput.step = '0.1';
        maxSpeedInput.value = settings.maxPlaybackRate;
        maxSpeedInput.addEventListener('change', () => {
            settings.maxPlaybackRate = parseFloat(maxSpeedInput.value);
            localStorage.setItem('maxPlaybackRate', settings.maxPlaybackRate);
        });
        settingsWindow.appendChild(maxSpeedLabel);
        settingsWindow.appendChild(maxSpeedInput);
        settingsWindow.appendChild(document.createElement('br'));

        // Включить изменение громкости при прокрутке
        const volumeControlLabel = document.createElement('label');
        volumeControlLabel.innerText = langTexts[settings.language].volumeControlLabel;
        const volumeControlCheckbox = document.createElement('input');
        volumeControlCheckbox.type = 'checkbox';
        volumeControlCheckbox.checked = settings.volumeControlEnabled;
        volumeControlCheckbox.addEventListener('change', () => {
            settings.volumeControlEnabled = volumeControlCheckbox.checked;
            localStorage.setItem('volumeControlEnabled', settings.volumeControlEnabled);
        });
        settingsWindow.appendChild(volumeControlLabel);
        settingsWindow.appendChild(volumeControlCheckbox);
        settingsWindow.appendChild(document.createElement('br'));

        // Включить горячие клавиши
        const hotkeysLabel = document.createElement('label');
        hotkeysLabel.innerText = langTexts[settings.language].hotkeysLabel;
        const hotkeysCheckbox = document.createElement('input');
        hotkeysCheckbox.type = 'checkbox';
        hotkeysCheckbox.checked = settings.hotkeysEnabled;
        hotkeysCheckbox.addEventListener('change', () => {
            settings.hotkeysEnabled = hotkeysCheckbox.checked;
            localStorage.setItem('hotkeysEnabled', settings.hotkeysEnabled);
        });
        settingsWindow.appendChild(hotkeysLabel);
        settingsWindow.appendChild(hotkeysCheckbox);
        settingsWindow.appendChild(document.createElement('br'));

        // Выбор языка
        const languageLabel = document.createElement('label');
        languageLabel.innerText = langTexts[settings.language].languageLabel;
        const languageSelect = document.createElement('select');
        const languages = ['ru', 'en'];
        languages.forEach(lang => {
            const option = document.createElement('option');
            option.value = lang;
            option.innerText = lang === 'ru' ? 'Русский' : 'English';
            languageSelect.appendChild(option);
        });
        languageSelect.value = settings.language;
        languageSelect.addEventListener('change', () => {
            settings.language = languageSelect.value;
            localStorage.setItem('language', settings.language);
            updateSettingsWindow();
        });
        settingsWindow.appendChild(languageLabel);
        settingsWindow.appendChild(languageSelect);
        settingsWindow.appendChild(document.createElement('br'));

        // Кнопка закрытия
        const closeButton = document.createElement('button');
        closeButton.innerText = langTexts[settings.language].closeButton;
        closeButton.style.marginTop = '20px';
        closeButton.style.padding = '10px 20px';
        closeButton.style.border = 'none';
        closeButton.style.borderRadius = '5px';
        closeButton.style.backgroundColor = '#007BFF';
        closeButton.style.color = '#fff';
        closeButton.style.cursor = 'pointer';
        closeButton.style.fontSize = '16px';

        closeButton.addEventListener('click', closeSettingsWindow);
        settingsWindow.appendChild(closeButton);
        settingsOverlay.appendChild(settingsWindow);
        document.body.appendChild(settingsOverlay);
    }

    // Функция для обновления окна настроек при смене языка
    function updateSettingsWindow() {
        const settingsOverlay = document.getElementById('settingsOverlay');
        if (settingsOverlay) {
            document.body.removeChild(settingsOverlay);
        }
        createSettingsWindow(); // Пересоздаем окно с новыми данными
    }

    // Функция для закрытия окна настроек
    function closeSettingsWindow() {
        const settingsOverlay = document.getElementById('settingsOverlay');
        if (settingsOverlay) {
            document.body.removeChild(settingsOverlay);
            settingsWindowOpen = false; // Сбрасываем флаг
        }
    }

    window.addEventListener('keydown', (event) => {
        if (event.code === 'KeyN' || event.code === 'KeyT') {
            createSettingsWindow();
        }
    });

    // Обрабатываем каждое видео
    videos.forEach((video, index) => {
        const videoKey = `videoProgress_${window.location.href}_${index}`;
        const savedTime = localStorage.getItem(videoKey);
        const savedVolume = localStorage.getItem(videoKey + "_volume");

        // Отложим восстановление прогресса на 1 секунды
        video.addEventListener('loadedmetadata', () => {
            setTimeout(() => {
                if (savedTime) {
                    video.currentTime = parseFloat(savedTime);
                    showNotification(langTexts[settings.language].progressRestored.replace("{index}", index + 1), 'rgba(0, 0, 0, 0.6)');
                }

                if (savedVolume !== null) {
                    video.volume = parseFloat(savedVolume);
                }
            }, 1000); // Задержка 1 секундa
        });

        video.addEventListener('timeupdate', () => {
            if (!video.paused && !video.ended) {
                localStorage.setItem(videoKey, video.currentTime); // Сохраняем прогресс при каждом обновлении времени
            }
        });

        video.addEventListener('ended', () => {
            localStorage.removeItem(videoKey);
        });

        video.addEventListener('volumechange', () => {
            localStorage.setItem(videoKey + "_volume", video.volume);
        });

        window.addEventListener('beforeunload', () => {
            localStorage.setItem(videoKey, video.currentTime); // Обновляем прогресс перед закрытием страницы
        });
    });

    window.addEventListener('wheel', (event) => {
        if (!settings.volumeControlEnabled || !event.shiftKey) return;

        const focusedVideo = document.activeElement.tagName === 'VIDEO' ? document.activeElement : videos[0];
        if (!focusedVideo) return;

        const volumeChange = event.deltaY < 0 ? 0.01 : -0.01;
        let newVolume = Math.min(Math.max(focusedVideo.volume + volumeChange, 0), 1);

        // Обновляем громкость плеера
        focusedVideo.volume = newVolume;

        // Показ уведомления при изменении громкости
        showNotification(langTexts[settings.language].volumeChanged.replace("{volume}", (newVolume * 100).toFixed(0)), 'rgba(0, 0, 0, 0.6)');
    });

    window.addEventListener('keydown', (event) => {
        if (!settings.hotkeysEnabled) return;

        const focusedVideo = document.activeElement.tagName === 'VIDEO' ? document.activeElement : videos[0];

        if (!focusedVideo) return;

        if (event.key === 'z' || event.key === 'я') {
            if (focusedVideo.playbackRate > 0.1) {
                focusedVideo.playbackRate -= 0.1;
                showNotification(langTexts[settings.language].speedChanged.replace("{speed}", focusedVideo.playbackRate.toFixed(1)), 'rgba(0, 0, 0, 0.6)');
            }
        } else if (event.key === 'x' || event.key === 'ч') {
            if (focusedVideo.playbackRate < settings.maxPlaybackRate) {
                focusedVideo.playbackRate += 0.1;
                showNotification(langTexts[settings.language].speedChanged.replace("{speed}", focusedVideo.playbackRate.toFixed(1)), 'rgba(0, 0, 0, 0.6)');
            }
        }
    });

})();