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

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

当前为 2025-01-06 提交的版本,查看 最新版本

// ==UserScript==
// @name Сохранение прогресса видео с настройками и управлением скорости воспроизведения
// @name:en Video Progress Saver with Settings and playbackrate
// @namespace http://tampermonkey.net/
// @version 2.8
// @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';

        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 notificationsContainer = document.createElement('div');
        notificationsContainer.style.marginBottom = '20px';

        const notificationsLabel = document.createElement('label');
        notificationsLabel.innerText = langTexts[settings.language].notificationsLabel;
        notificationsLabel.style.display = 'block';
        notificationsLabel.style.marginBottom = '10px';
        notificationsLabel.style.fontSize = '16px';
        notificationsContainer.appendChild(notificationsLabel);

        const notificationsCheckbox = document.createElement('input');
        notificationsCheckbox.type = 'checkbox';
        notificationsCheckbox.checked = settings.notificationsEnabled;
        notificationsCheckbox.style.marginRight = '10px';
        notificationsCheckbox.addEventListener('change', () => {
            settings.notificationsEnabled = notificationsCheckbox.checked;
            localStorage.setItem('notificationsEnabled', settings.notificationsEnabled);
        });
        notificationsContainer.appendChild(notificationsCheckbox);
        settingsWindow.appendChild(notificationsContainer);

        // Интервал авто-сохранения
        const intervalContainer = document.createElement('div');
        intervalContainer.style.marginBottom = '20px';

        const intervalLabel = document.createElement('label');
        intervalLabel.innerText = langTexts[settings.language].autoSaveLabel;
        intervalLabel.style.display = 'block';
        intervalLabel.style.marginBottom = '10px';
        intervalLabel.style.fontSize = '16px';
        intervalContainer.appendChild(intervalLabel);

        const intervalInput = document.createElement('input');
        intervalInput.type = 'number';
        intervalInput.value = settings.autoSaveInterval;
        intervalInput.style.width = '100%';
        intervalInput.style.padding = '8px';
        intervalInput.style.borderRadius = '5px';
        intervalInput.style.fontSize = '16px';
        intervalInput.addEventListener('input', () => {
            settings.autoSaveInterval = parseInt(intervalInput.value, 10);
            localStorage.setItem('autoSaveInterval', settings.autoSaveInterval);
        });
        intervalContainer.appendChild(intervalInput);
        settingsWindow.appendChild(intervalContainer);

        // Максимальная скорость воспроизведения
        const maxSpeedContainer = document.createElement('div');
        maxSpeedContainer.style.marginBottom = '20px';

        const maxSpeedLabel = document.createElement('label');
        maxSpeedLabel.innerText = langTexts[settings.language].maxSpeedLabel;
        maxSpeedLabel.style.display = 'block';
        maxSpeedLabel.style.marginBottom = '10px';
        maxSpeedLabel.style.fontSize = '16px';
        maxSpeedContainer.appendChild(maxSpeedLabel);

        const maxSpeedInput = document.createElement('input');
        maxSpeedInput.type = 'number';
        maxSpeedInput.step = '0.1';
        maxSpeedInput.value = settings.maxPlaybackRate;
        maxSpeedInput.style.width = '100%';
        maxSpeedInput.style.padding = '8px';
        maxSpeedInput.style.borderRadius = '5px';
        maxSpeedInput.style.fontSize = '16px';
        maxSpeedInput.addEventListener('input', () => {
            settings.maxPlaybackRate = parseFloat(maxSpeedInput.value);
            localStorage.setItem('maxPlaybackRate', settings.maxPlaybackRate);
        });
        maxSpeedContainer.appendChild(maxSpeedInput);
        settingsWindow.appendChild(maxSpeedContainer);

        // Включение/выключение громкости
        const volumeControlContainer = document.createElement('div');
        volumeControlContainer.style.marginBottom = '20px';

        const volumeControlLabel = document.createElement('label');
        volumeControlLabel.innerText = langTexts[settings.language].volumeControlLabel;
        volumeControlLabel.style.display = 'block';
        volumeControlLabel.style.marginBottom = '10px';
        volumeControlLabel.style.fontSize = '16px';
        volumeControlContainer.appendChild(volumeControlLabel);

        const volumeControlCheckbox = document.createElement('input');
        volumeControlCheckbox.type = 'checkbox';
        volumeControlCheckbox.checked = settings.volumeControlEnabled;
        volumeControlCheckbox.style.marginRight = '10px';
        volumeControlCheckbox.addEventListener('change', () => {
            settings.volumeControlEnabled = volumeControlCheckbox.checked;
            localStorage.setItem('volumeControlEnabled', settings.volumeControlEnabled);
        });
        volumeControlContainer.appendChild(volumeControlCheckbox);
        settingsWindow.appendChild(volumeControlContainer);

        // Включение/выключение горячих клавиш
        const hotkeysContainer = document.createElement('div');
        hotkeysContainer.style.marginBottom = '20px';

        const hotkeysLabel = document.createElement('label');
        hotkeysLabel.innerText = langTexts[settings.language].hotkeysLabel;
        hotkeysLabel.style.display = 'block';
        hotkeysLabel.style.marginBottom = '10px';
        hotkeysLabel.style.fontSize = '16px';
        hotkeysContainer.appendChild(hotkeysLabel);

        const hotkeysCheckbox = document.createElement('input');
        hotkeysCheckbox.type = 'checkbox';
        hotkeysCheckbox.checked = settings.hotkeysEnabled;
        hotkeysCheckbox.style.marginRight = '10px';
        hotkeysCheckbox.addEventListener('change', () => {
            settings.hotkeysEnabled = hotkeysCheckbox.checked;
            localStorage.setItem('hotkeysEnabled', settings.hotkeysEnabled);
        });
        hotkeysContainer.appendChild(hotkeysCheckbox);
        settingsWindow.appendChild(hotkeysContainer);

        // Выбор языка
        const languageContainer = document.createElement('div');
        languageContainer.style.marginBottom = '20px';

        const languageLabel = document.createElement('label');
        languageLabel.innerText = langTexts[settings.language].languageLabel;
        languageLabel.style.display = 'block';
        languageLabel.style.marginBottom = '10px';
        languageLabel.style.fontSize = '16px';
        languageContainer.appendChild(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';
            if (lang === settings.language) option.selected = true;
            languageSelect.appendChild(option);
        });

        languageSelect.addEventListener('change', () => {
    settings.language = languageSelect.value;
    localStorage.setItem('language', settings.language);
    updateSettingsWindow(); // Обновить окно с новым языком

    // Перезагрузка страницы при смене языка
    location.reload(); // Перезагружает страницу
});
        languageContainer.appendChild(languageSelect);
        settingsWindow.appendChild(languageContainer);

        // Кнопка закрытия
        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') {
            createSettingsWindow();
        }
    });

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

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

        let saveProgress;

        const saveInterval = setInterval(() => {
            // Сохраняем прогресс только если видео воспроизводится
            if (!video.paused && !video.ended) {
                saveProgress();
            }
        }, settings.autoSaveInterval);

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

        video.addEventListener('volumechange', () => {
            localStorage.setItem('videoVolume', video.volume);
        });

        window.addEventListener('beforeunload', () => {
            clearInterval(saveInterval);
        });

        saveProgress = () => {
            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)');
            }
        }
    });
})();