YouTube Volume Booster 600% (with Clear Storage Menu)

Adds a floating volume slider with up to 600% boost. Features: Global Volume, Remember Per Video, One-Time Restore, Draggable UI. Fixes resize and idle-hide bugs.

// ==UserScript==
// @name        YouTube Volume Booster 600% (with Clear Storage Menu)
// @name:vi     YouTube - Tăng Âm Lượng 600% (có Menu Xóa Bộ nhớ)
// @namespace   http://tampermonkey.net/
// @version     3.9.21
// @description Adds a floating volume slider with up to 600% boost. Features: Global Volume, Remember Per Video, One-Time Restore, Draggable UI. Fixes resize and idle-hide bugs.
// @description:vi Thêm thanh trượt âm lượng nổi, tăng âm lượng đến 600%. Các tính năng: Âm lượng toàn tab, Ghi nhớ từng video, Khôi phục một lần, Giao diện kéo thả. Sửa lỗi thay đổi kích thước và ẩn khi không hoạt động.
// @author      Gemini & Developer
// @match       *://*.youtube.com/*
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_registerMenuCommand
// @icon        https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// ==/UserScript==

(function() {
    'use strict';

    // --- GLOBAL VARIABLES AND SETTINGS ---
    let audioContext, gainNode, sourceNode;
    let currentVideoId = null; // Current video ID
    let previousVideoId = null; // Stores the ID of the video watched just before the current one
    let currentTabVolume = 100; // Default volume for the current tab
    let tabId = null; // Unique ID for the current browser tab

    // Feature states
    let isGlobalVolumeEnabled = false; // Controls if volume is persisted per tab
    let isRememberPerVideoEnabled = false; // Controls if manual save button for per-video is active
    let isOneTimeRestoreEnabled = true; // Controls if one-time restore is active

    let currentLanguage = 'vi'; // Default language

    // Variables for draggable toolbar position
    let isDragging = false;
    let dragOffsetX, dragOffsetY;
    const STORAGE_KEY_TOOLBAR_POSITION = 'youtubeBoosterToolbarPosition'; // Key to save toolbar position

    const translations = {
        'vi': {
            globalVolumeTitle: 'Âm lượng toàn tab',
            globalVolumeEnabled: 'Âm lượng toàn tab: Đã bật (Nhấn để tắt)',
            globalVolumeDisabled: 'Âm lượng toàn tab: Đã tắt (Nhấn để bật)',
            rememberPerVideoTitle: 'Ghi nhớ từng video',
            rememberPerVideoEnabled: 'Ghi nhớ từng video: Đã bật (Nhấn để tắt)',
            rememberPerVideoDisabled: 'Ghi nhớ từng video: Đã tắt (Nhấn để bật)',
            savePerVideoTitle: 'Lưu âm lượng cho video này',
            savePerVideoEnabledHint: 'Lưu âm lượng cho video này (Tính năng ghi nhớ đang hoạt động)',
            savePerVideoDisabledHint: 'Chỉ hoạt động khi "Ghi nhớ từng video" bật.',
            oneTimeRestoreTitle: 'Khôi phục một lần',
            oneTimeRestoreEnabled: 'Khôi phục một lần: Đã bật (Nhấn để tắt)',
            oneTimeRestoreDisabled: 'Khôi phục một lần: Đã tắt (Nhấn để bật)',
            languageToggleTitle: 'Ngôn ngữ',
            languageToggleHint: 'Chuyển đổi ngôn ngữ (hiện tại: Tiếng Việt)',
            languageToggleHintEnglish: 'Switch language (current: English)',
            manualSaveSuccess: 'Đã lưu âm lượng thủ công',
            manualSaveNotAllowed: 'Lưu thủ công không được phép. "Ghi nhớ từng video" đang tắt hoặc không có ID video.',
            globalVolumeEnabledUser: '"Âm lượng toàn tab" đã BẬT bởi người dùng.',
            globalVolumeDisabledUser: '"Âm lượng toàn tab" đã TẮT bởi người dùng.',
            rememberPerVideoEnabledUser: '"Ghi nhớ từng video" đã BẬT.',
            rememberPerVideoDisabledUser: '"Ghi nhớ từng video" đã TẮT.',
            oneTimeRestoreEnabledUser: '"Khôi phục một lần" đã BẬT bởi người dùng.',
            oneTimeRestoreDisabledUser: '"Khôi phục một lần" đã TẮT bởi người dùng.',
            boosterInitializedGlobal: 'Khởi tạo - Trạng thái tính năng Âm lượng Toàn cầu:',
            boosterInitializedPerVideo: 'Khởi tạo - Trạng thái chuyển đổi tính năng Mỗi Video:',
            boosterInitializedOneTime: 'Khởi tạo - Trạng thái chuyển đổi tính năng Khôi phục Một lần:',
            tabIdNotInitialized: 'ID tab chưa được khởi tạo hoặc không có sẵn.',
            clearManualStorageTitle: "Xóa bộ nhớ 'Ghi nhớ từng video'",
            confirmClear: "Bạn có chắc chắn muốn xóa tất cả dữ liệu của 'Ghi nhớ từng video' không? Hành động này không thể hoàn tác.",
            clearManualStorageSuccess: "Đã xóa tất cả dữ liệu của 'Ghi nhớ từng video'."
        },
        'en': {
            globalVolumeTitle: 'Global Volume',
            globalVolumeEnabled: 'Global Volume: Enabled (Click to disable)',
            globalVolumeDisabled: 'Global Volume: Disabled (Click to enable)',
            rememberPerVideoTitle: 'Remember Per Video',
            rememberPerVideoEnabled: 'Remember Per Video: Enabled (Click to disable)',
            rememberPerVideoDisabled: 'Remember Per Video: Disabled (Click to enable)',
            savePerVideoTitle: 'Save volume for this video',
            savePerVideoEnabledHint: 'Save volume for this video (Remember feature is active)',
            savePerVideoDisabledHint: 'Only active when "Remember Per Video" is on.',
            oneTimeRestoreTitle: 'One-Time Restore',
            oneTimeRestoreEnabled: 'One-Time Restore: Enabled (Click to disable)',
            oneTimeRestoreDisabled: 'One-Time Restore: Disabled (Click to enable)',
            languageToggleTitle: 'Language',
            languageToggleHint: 'Switch language (current: Vietnamese)',
            languageToggleHintEnglish: 'Switch language (current: English)',
            manualSaveSuccess: 'Manually saved volume',
            manualSaveNotAllowed: 'Manual save not allowed. "Remember Volume Per Video" is off or no video ID.',
            globalVolumeEnabledUser: '"Global Volume for Current Tab" feature ENABLED by user.',
            globalVolumeDisabledUser: '"Global Volume for Current Tab" feature DISABLED by user.',
            rememberPerVideoEnabledUser: '"Remember Per Video" feature ENABLED.',
            rememberPerVideoDisabledUser: '"Remember Per Video" feature DISABLED.',
            oneTimeRestoreEnabledUser: '"One-Time Restore" feature ENABLED by user.',
            oneTimeRestoreDisabledUser: '"One-Time Restore" feature DISABLED by user.',
            boosterInitializedGlobal: 'Initializing - Global Volume Feature State:',
            boosterInitializedPerVideo: 'Initializing - Per Video Feature Toggle State:',
            boosterInitializedOneTime: 'Initializing - One-Time Restore Feature Toggle State:',
            tabIdNotInitialized: 'Tab ID not yet initialized or available.',
            clearManualStorageTitle: "Clear 'Remember Per Video' Storage",
            confirmClear: "Are you sure you want to delete all 'Remember Per Video' data? This action cannot be undone.",
            clearManualStorageSuccess: "Cleared all 'Remember Per Video' data."
        }
    };
    // Tampermonkey storage keys
    const STORAGE_KEY_PER_VIDEO = 'youtubeVolumeSettings_v2';
    const STORAGE_KEY_GLOBAL_FEATURE_STATE = 'youtubeGlobalVolumeFeatureState_v2';
    const STORAGE_KEY_PER_VIDEO_FEATURE_STATE = 'youtubePerVideoFeatureState_v2';
    const STORAGE_KEY_ONE_TIME_RESTORE_FEATURE_STATE = 'youtubeOneTimeRestoreFeatureState_v1';
    const STORAGE_KEY_TAB_VOLUME_PREFIX = 'youtubeGlobalVolumePerTab_v2_';
    const STORAGE_KEY_LANGUAGE = 'youtubeBoosterLanguage_v1';
    const STORAGE_KEY_ONE_TIME_RESTORE_VOLUMES = 'youtubeRestoreVolume_v3_videoId_map';

    // Session storage keys
    const SESSION_STORAGE_TAB_ID_KEY = 'youtubeBoosterTabId_v2';
    const SESSION_STORAGE_ONE_TIME_PROCESSED_KEY_PREFIX = 'youtubeBoosterOneTimeProcessed_';

    // Debounce variables
    let initializeTimeout = null;
    const DEBOUNCE_DELAY = 100;

    // --- CSS FOR UI ---
    GM_addStyle(`
        #volume-booster-container-abs {
            position: absolute;
            z-index: 9999;
            background-color: rgba(28, 28, 28, 0.85);
            padding: 6px 12px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            gap: 8px;
            opacity: 0;
            transition: opacity 0.3s ease-in-out, bottom 0.3s ease-in-out, right 0.3s ease-in-out;
            pointer-events: none;
            cursor: grab;
        }
        #volume-booster-container-abs.dragging {
            cursor: grabbing;
            transition: none;
        }
        #movie_player:not(.ytp-autohide) #volume-booster-container-abs {
            opacity: 1;
            pointer-events: auto;
        }
        .volume-booster-slider-abs {
            -webkit-appearance: none;
            appearance: none;
            width: 100px;
            height: 5px;
            background: #777;
            outline: none;
            cursor: pointer;
            border-radius: 3px;
            margin: 0;
        }
        .volume-booster-slider-abs::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 16px;
            height: 16px;
            background: #ff0000;
            cursor: pointer;
            border-radius: 50%;
        }
        .volume-booster-slider-abs::-moz-range-thumb {
            width: 16px;
            height: 16px;
            background: #ff0000;
            cursor: pointer;
            border-radius: 50%;
            border: none;
        }
        .volume-booster-label-abs {
            color: white;
            font-size: 13px;
            font-weight: bold;
            min-width: 50px;
            text-shadow: 1px 1px 2px black;
            text-align: right;
        }
        .volume-booster-setting-icon {
            cursor: pointer;
            width: 20px;
            height: 20px;
            background-color: #ffffff;
            mask-size: contain;
            mask-repeat: no-repeat;
            mask-position: center;
            transition: background-color 0.2s ease-in-out;
            border-radius: 4px;
            padding: 2px;
        }
        .volume-booster-setting-icon.enabled {
            background-color: #4CAF50;
        }
        .volume-booster-setting-icon.global-volume {
            mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.09-.75-1.72-1.03L13.73 2.2c-.06-.2-.25-.3-.46-.3h-4c-.21 0-.4.1-.46.3L9.23 4.87c-.63.28-1.2.63-1.72 1.03l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.12.22-.07.49.12.64l2.11 1.65c-.04.32-.07.64-.07.98s.03.66.07-.98l-2.11 1.65c-.19.15-.24-.42-.12-.64l2 3.46c.12.22.39-.3.61-.22l2.49-1c.52.4 1.09.75 1.72 1.03l.44 2.69c.06.2.25.3.46.3h4c.21 0 .4-.1.46-.3l.44-2.69c.63-.28 1.2-.63 1.72-1.03l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.93-1.93c.33.33.56.73.69 1.13l.11.4c.03.1.06.2.06.3s-.03.2-.06.3l-.11-.4c-.13.4-.36.8-.69 1.13-.33.33-.73-.56-1.13-.69l-.4-.11c-.1.03-.2.06-.3.06s-.2-.03-.3-.06l-.4-.11c-.4-.13-.8-.36-1.13-.69-.33-.33-.56-.73-.69-1.13l-.11-.4c-.03-.1-.06-.2-.06-.3s-.03.2.06-.3l-.11-.4c.13-.4.36-.8.69-1.13.33.33.73-.56 1.13-.69l-.4-.11c.1-.03.2-.06.3.06s-.2.03.3.06l-.4.11c.4.13.8.36 1.13.69zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></svg>');
        }
        .volume-booster-setting-icon.per-video-toggle {
            mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>');
        }
        .volume-booster-setting-icon.save-per-video {
            mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>');
            opacity: 0.5;
            pointer-events: none;
        }
        .volume-booster-setting-icon.one-time-restore-toggle {
            mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 4V1l-4 4 4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.01 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8V23l4-4-4-4v3z"/></svg>');
        }
        .volume-booster-setting-icon.language-toggle {
            mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.07-1.74-.27-3.4-.59-4.96C16.39 3.5 17.72 5.06 18.92 8zM12 4.04c.83 1.22 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.74 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2H4.26zm.82 2h2.95c.07 1.74-.27 3.4-.59 4.96C7.61 20.5 6.28 18.94 5.08 16zM8.07 19.96c-.83-1.22-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08-2.74-1.91 3.96zM11.99 20c-.83-1.22-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08-2.74-1.91 3.96zM12 19.96c-.83-1.22-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.74-1.91 3.96zM11.99 4.04c-.83-1.22-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.74-1.91 3.96z"/></svg>');
        }
        .volume-booster-setting-icon.clicked {
            background-color: #007bff;
        }
    `);
    // --- FUNCTIONS TO SAVE AND LOAD SETTINGS ---

    function getTabId() {
        let id = sessionStorage.getItem(SESSION_STORAGE_TAB_ID_KEY);
        if (!id) {
            id = crypto.randomUUID();
            sessionStorage.setItem(SESSION_STORAGE_TAB_ID_KEY, id);
        }
        return id;
    }

    function getVideoId() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('v');
    }

    async function getVolumeSetting(videoId) {
        if (!videoId) return null;
        const allSettings = await GM_getValue(STORAGE_KEY_PER_VIDEO, {});
        return allSettings[videoId] !== undefined ? allSettings[videoId] : null;
    }

    async function saveVolumeSetting(videoId, volume) {
        if (!videoId) return;
        const allSettings = await GM_getValue(STORAGE_KEY_PER_VIDEO, {});
        if (volume === 100) {
            delete allSettings[videoId];
        } else {
            allSettings[videoId] = volume;
        }
        await GM_setValue(STORAGE_KEY_PER_VIDEO, allSettings);
    }

    async function getFeatureState(key, defaultValue = false) {
        return await GM_getValue(key, defaultValue);
    }

    async function saveFeatureState(key, state) {
        await GM_setValue(key, state);
    }

    async function saveOneTimeRestoreVolume(videoId, volume) {
        if (!videoId) return;
        if (!isOneTimeRestoreEnabled) {
            return;
        }
        let restoreData = await GM_getValue(STORAGE_KEY_ONE_TIME_RESTORE_VOLUMES, {});
        restoreData[videoId] = volume;
        await GM_setValue(STORAGE_KEY_ONE_TIME_RESTORE_VOLUMES, restoreData);
    }

    async function loadAndClearOneTimeRestoreVolume(videoId) {
        if (!videoId) return null;
        if (!isOneTimeRestoreEnabled) {
            return null;
        }
        const oneTimeProcessedKey = SESSION_STORAGE_ONE_TIME_PROCESSED_KEY_PREFIX + videoId;
        const hasProcessedOneTime = sessionStorage.getItem(oneTimeProcessedKey);
        if (hasProcessedOneTime) {
            return null;
        }
        let restoreData = await GM_getValue(STORAGE_KEY_ONE_TIME_RESTORE_VOLUMES, {});
        let restoredVolume = restoreData[videoId];
        if (restoredVolume !== undefined) {
            sessionStorage.setItem(oneTimeProcessedKey, 'true');
            return restoredVolume;
        }
        return null;
    }

    async function clearOneTimeRestoreVolume(videoIdToClear) {
        if (!videoIdToClear) return;
        let restoreData = await GM_getValue(STORAGE_KEY_ONE_TIME_RESTORE_VOLUMES, {});
        if (restoreData.hasOwnProperty(videoIdToClear)) {
            delete restoreData[videoIdToClear];
            await GM_setValue(STORAGE_KEY_ONE_TIME_RESTORE_VOLUMES, restoreData);
        }
        sessionStorage.removeItem(SESSION_STORAGE_ONE_TIME_PROCESSED_KEY_PREFIX + videoIdToClear);
    }

    async function saveToolbarPosition(bottom, right) {
        await GM_setValue(STORAGE_KEY_TOOLBAR_POSITION, { bottom, right });
    }

    async function loadToolbarPosition() {
        return await GM_getValue(STORAGE_KEY_TOOLBAR_POSITION, null);
    }

    // --- CORE SCRIPT FUNCTIONS ---

    function setupAudioBoosterOnce(videoElement) {
        if (audioContext) return;
        audioContext = new(window.AudioContext || window.webkitAudioContext)();
        sourceNode = audioContext.createMediaElementSource(videoElement);
        gainNode = audioContext.createGain();
        sourceNode.connect(gainNode);
        gainNode.connect(audioContext.destination);
    }

    function applyVolumeToUIAndGain(volume) {
        if (!gainNode) return;
        gainNode.gain.value = volume / 100;
        const slider = document.querySelector('.volume-booster-slider-abs');
        const label = document.querySelector('.volume-booster-label-abs');
        if (slider) slider.value = volume;
        if (label) label.textContent = `${volume}%`;
    }

    function updateUIText() {
        const lang = currentLanguage;
        const settingsIconGlobal = document.querySelector('.volume-booster-setting-icon.global-volume');
        if (settingsIconGlobal) {
            settingsIconGlobal.title = isGlobalVolumeEnabled ?
                translations[lang].globalVolumeEnabled : translations[lang].globalVolumeDisabled;
        }
        const settingsIconPerVideo = document.querySelector('.volume-booster-setting-icon.per-video-toggle');
        if (settingsIconPerVideo) {
            settingsIconPerVideo.title = isRememberPerVideoEnabled ?
                translations[lang].rememberPerVideoEnabled : translations[lang].rememberPerVideoDisabled;
        }
        const saveVolumeIcon = document.querySelector('.volume-booster-setting-icon.save-per-video');
        if (saveVolumeIcon) {
            saveVolumeIcon.title = isRememberPerVideoEnabled ?
                translations[lang].savePerVideoEnabledHint : translations[lang].savePerVideoDisabledHint;
        }
        const oneTimeRestoreToggle = document.querySelector('.volume-booster-setting-icon.one-time-restore-toggle');
        if (oneTimeRestoreToggle) {
            oneTimeRestoreToggle.title = isOneTimeRestoreEnabled ?
                translations[lang].oneTimeRestoreEnabled : translations[lang].oneTimeRestoreDisabled;
        }
        const languageToggleIcon = document.querySelector('.volume-booster-setting-icon.language-toggle');
        if (languageToggleIcon) {
            languageToggleIcon.title = lang === 'vi' ?
                translations['vi'].languageToggleHint : translations['en'].languageToggleHintEnglish;
        }
    }

    async function createVolumeSliderUI() {
        if (document.getElementById('volume-booster-container-abs')) return;
        const playerContainer = document.querySelector('#movie_player');
        if (!playerContainer) return;

        const container = document.createElement('div');
        container.id = 'volume-booster-container-abs';
        const slider = document.createElement('input');
        slider.className = 'volume-booster-slider-abs';
        slider.type = 'range';
        slider.min = '0';
        slider.max = '600';
        slider.step = '10';
        const label = document.createElement('span');
        label.className = 'volume-booster-label-abs';

        const settingsIconGlobal = document.createElement('div');
        settingsIconGlobal.className = 'volume-booster-setting-icon global-volume';
        const settingsIconPerVideo = document.createElement('div');
        settingsIconPerVideo.className = 'volume-booster-setting-icon per-video-toggle';
        if (isRememberPerVideoEnabled) settingsIconPerVideo.classList.add('enabled');
        const saveVolumeIcon = document.createElement('div');
        saveVolumeIcon.className = 'volume-booster-setting-icon save-per-video';
        const oneTimeRestoreToggle = document.createElement('div');
        oneTimeRestoreToggle.className = 'volume-booster-setting-icon one-time-restore-toggle';
        if (isOneTimeRestoreEnabled) oneTimeRestoreToggle.classList.add('enabled');
        const languageToggleIcon = document.createElement('div');
        languageToggleIcon.className = 'volume-booster-setting-icon language-toggle';

        function updateSaveButtonState() {
            if (isRememberPerVideoEnabled) {
                saveVolumeIcon.style.opacity = '1';
                saveVolumeIcon.style.pointerEvents = 'auto';
            } else {
                saveVolumeIcon.style.opacity = '0.5';
                saveVolumeIcon.style.pointerEvents = 'none';
            }
        }
        updateSaveButtonState();

        function updateGlobalVolumeIconState() {
            if (isGlobalVolumeEnabled) {
                settingsIconGlobal.classList.add('enabled');
            } else {
                settingsIconGlobal.classList.remove('enabled');
            }
        }
        updateGlobalVolumeIconState();

        function updateOneTimeRestoreIconState() {
            if (isOneTimeRestoreEnabled) {
                oneTimeRestoreToggle.classList.add('enabled');
            } else {
                oneTimeRestoreToggle.classList.remove('enabled');
            }
        }
        updateOneTimeRestoreIconState();

        slider.addEventListener('input', async () => {
            const boostValue = parseInt(slider.value, 10);
            applyVolumeToUIAndGain(boostValue);
            currentTabVolume = boostValue;
            if (isGlobalVolumeEnabled) {
                await GM_setValue(STORAGE_KEY_TAB_VOLUME_PREFIX + tabId, currentTabVolume);
            }
            if (isOneTimeRestoreEnabled) {
                await saveOneTimeRestoreVolume(currentVideoId, currentTabVolume);
            }
        });
        settingsIconGlobal.addEventListener('click', async () => {
            isGlobalVolumeEnabled = !isGlobalVolumeEnabled;
            await saveFeatureState(STORAGE_KEY_GLOBAL_FEATURE_STATE, isGlobalVolumeEnabled);
            updateGlobalVolumeIconState();
            updateUIText();
            settingsIconGlobal.classList.add('clicked');
            setTimeout(() => settingsIconGlobal.classList.remove('clicked'), 200);
            if (isGlobalVolumeEnabled) {
                await GM_setValue(STORAGE_KEY_TAB_VOLUME_PREFIX + tabId, currentTabVolume);
            }
            debouncedInitialize();
        });
        settingsIconPerVideo.addEventListener('click', async () => {
            isRememberPerVideoEnabled = !isRememberPerVideoEnabled;
            await saveFeatureState(STORAGE_KEY_PER_VIDEO_FEATURE_STATE, isRememberPerVideoEnabled);
            if (isRememberPerVideoEnabled) {
                settingsIconPerVideo.classList.add('enabled');
            } else {
                settingsIconPerVideo.classList.remove('enabled');
            }
            updateSaveButtonState();
            updateUIText();
            settingsIconPerVideo.classList.add('clicked');
            setTimeout(() => settingsIconPerVideo.classList.remove('clicked'), 200);
            debouncedInitialize();
        });
        saveVolumeIcon.addEventListener('click', async () => {
            if (isRememberPerVideoEnabled && currentVideoId) {
                const currentSliderValue = parseInt(slider.value, 10);
                await saveVolumeSetting(currentVideoId, currentSliderValue);
                currentTabVolume = currentSliderValue;
                applyVolumeToUIAndGain(currentTabVolume);
                if (isOneTimeRestoreEnabled) {
                    await saveOneTimeRestoreVolume(currentVideoId, currentTabVolume);
                }
                saveVolumeIcon.classList.add('clicked');
                setTimeout(() => saveVolumeIcon.classList.remove('clicked'), 500);
            }
        });

        oneTimeRestoreToggle.addEventListener('click', async () => {
            isOneTimeRestoreEnabled = !isOneTimeRestoreEnabled;
            await saveFeatureState(STORAGE_KEY_ONE_TIME_RESTORE_FEATURE_STATE, isOneTimeRestoreEnabled);
            updateOneTimeRestoreIconState();
            updateUIText();
            oneTimeRestoreToggle.classList.add('clicked');
            setTimeout(() => oneTimeRestoreToggle.classList.remove('clicked'), 200);
            if (isOneTimeRestoreEnabled) {
                await saveOneTimeRestoreVolume(currentVideoId, currentTabVolume);
            } else {
                await clearOneTimeRestoreVolume(currentVideoId);
            }
            debouncedInitialize();
        });
        languageToggleIcon.addEventListener('click', async () => {
            currentLanguage = (currentLanguage === 'vi') ? 'en' : 'vi';
            await saveFeatureState(STORAGE_KEY_LANGUAGE, currentLanguage);
            updateUIText();
            languageToggleIcon.classList.add('clicked');
            setTimeout(() => languageToggleIcon.classList.remove('clicked'), 200);
        });
        container.appendChild(slider);
        container.appendChild(label);
        container.appendChild(settingsIconGlobal);
        container.appendChild(settingsIconPerVideo);
        container.appendChild(saveVolumeIcon);
        container.appendChild(oneTimeRestoreToggle);
        container.appendChild(languageToggleIcon);
        playerContainer.appendChild(container);

        const savedPos = await loadToolbarPosition();
        if (savedPos) {
            container.style.bottom = `${savedPos.bottom}px`;
            container.style.right = `${savedPos.right}px`;
        } else {
            container.style.bottom = '55px';
            container.style.right = '15px';
        }
        container.style.position = 'absolute';

        container.addEventListener('mousedown', (e) => {
            if (e.button === 0 && !e.target.closest('input, .volume-booster-setting-icon')) {
                isDragging = true;
                container.classList.add('dragging');
                const containerRect = container.getBoundingClientRect();
                dragOffsetX = e.clientX - containerRect.left;
                dragOffsetY = e.clientY - containerRect.top;
                container.style.transition = 'none';
                e.preventDefault();
            }
        });
        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const playerRect = playerContainer.getBoundingClientRect();
            const containerWidth = container.offsetWidth;
            const containerHeight = container.offsetHeight;

            const mouseXRelativeToPlayer = e.clientX - playerRect.left;
            const mouseYRelativeToPlayer = e.clientY - playerRect.top;

            let newContainerLeftRelativeToPlayer = mouseXRelativeToPlayer - dragOffsetX;
            let newContainerTopRelativeToPlayer = mouseYRelativeToPlayer - dragOffsetY;

            newContainerLeftRelativeToPlayer = Math.max(0, Math.min(newContainerLeftRelativeToPlayer, playerRect.width - containerWidth));
            newContainerTopRelativeToPlayer = Math.max(0, Math.min(newContainerTopRelativeToPlayer, playerRect.height - containerHeight));

            let newRight = playerRect.width - (newContainerLeftRelativeToPlayer + containerWidth);
            let newBottom = playerRect.height - (newContainerTopRelativeToPlayer + containerHeight);

            container.style.right = `${newRight}px`;
            container.style.bottom = `${newBottom}px`;
        });

        document.addEventListener('mouseup', async () => {
            if (isDragging) {
                isDragging = false;
                container.classList.remove('dragging');
                container.style.transition = 'opacity 0.3s ease-in-out, bottom 0.3s ease-in-out, right 0.3s ease-in-out';
                const currentBottom = parseFloat(container.style.bottom);
                const currentRight = parseFloat(container.style.right);
                await saveToolbarPosition(currentBottom, currentRight);
            }
        });
        const resizeObserver = new ResizeObserver(entries => {
            for (let entry of entries) {
                const playerRect = entry.contentRect;
                const containerWidth = container.offsetWidth;
                const containerHeight = container.offsetHeight;
                let currentBottom = parseFloat(container.style.bottom);
                let currentRight = parseFloat(container.style.right);
                let currentTop = playerRect.height - currentBottom - containerHeight;
                let currentLeft = playerRect.width - currentRight - containerWidth;
                const clampedLeft = Math.max(0, Math.min(currentLeft, playerRect.width - containerWidth));
                const clampedTop = Math.max(0, Math.min(currentTop, playerRect.height - containerHeight));
                const newFinalRight = playerRect.width - clampedLeft - containerWidth;
                const newFinalBottom = playerRect.height - clampedTop - containerHeight;
                container.style.right = `${newFinalRight}px`;
                container.style.bottom = `${newFinalBottom}px`;
            }
        });
        resizeObserver.observe(playerContainer);

        updateUIText();
    }

    async function initialize() {
        if (!window.location.pathname.startsWith('/watch')) {
            const container = document.getElementById('volume-booster-container-abs');
            if (container) container.remove();
            currentVideoId = null;
            previousVideoId = null;
            return;
        }

        const newVideoId = getVideoId();
        const videoElement = document.querySelector('video');
        if (!videoElement || !newVideoId) {
            const container = document.getElementById('volume-booster-container-abs');
            if (container) container.remove();
            currentVideoId = null;
            previousVideoId = null;
            return;
        }

        if (newVideoId === currentVideoId && currentVideoId !== null) {
            return;
        }

        if (previousVideoId && previousVideoId !== newVideoId && isOneTimeRestoreEnabled) {
            await clearOneTimeRestoreVolume(previousVideoId);
        }

        previousVideoId = currentVideoId;
        currentVideoId = newVideoId;
        tabId = getTabId();

        isGlobalVolumeEnabled = await getFeatureState(STORAGE_KEY_GLOBAL_FEATURE_STATE, false);
        isRememberPerVideoEnabled = await getFeatureState(STORAGE_KEY_PER_VIDEO_FEATURE_STATE, false);
        isOneTimeRestoreEnabled = await getFeatureState(STORAGE_KEY_ONE_TIME_RESTORE_FEATURE_STATE, true);
        currentLanguage = await getFeatureState(STORAGE_KEY_LANGUAGE, 'vi');

        let volumeToDetermine = 100;
        const perVideoVolume = await getVolumeSetting(currentVideoId);
        const restoredVolumeOnBrowserRestore = await loadAndClearOneTimeRestoreVolume(currentVideoId);
        const globalTabVolume = await GM_getValue(STORAGE_KEY_TAB_VOLUME_PREFIX + tabId, 100);

        if (isRememberPerVideoEnabled && perVideoVolume !== null) {
            volumeToDetermine = perVideoVolume;
        } else if (restoredVolumeOnBrowserRestore !== null) {
            volumeToDetermine = restoredVolumeOnBrowserRestore;
        } else if (isGlobalVolumeEnabled) {
            volumeToDetermine = globalTabVolume;
        } else {
            volumeToDetermine = 100;
        }

        currentTabVolume = volumeToDetermine;
        if (isGlobalVolumeEnabled) {
            await GM_setValue(STORAGE_KEY_TAB_VOLUME_PREFIX + tabId, currentTabVolume);
        }
        if (isOneTimeRestoreEnabled) {
            await saveOneTimeRestoreVolume(currentVideoId, currentTabVolume);
        }

        setupAudioBoosterOnce(videoElement);
        await createVolumeSliderUI();
        applyVolumeToUIAndGain(currentTabVolume);
        if (audioContext && audioContext.state === 'suspended') {
            audioContext.resume();
        }
    }

    function debouncedInitialize() {
        clearTimeout(initializeTimeout);
        initializeTimeout = setTimeout(initialize, DEBOUNCE_DELAY);
    }

    const observer = new MutationObserver(mutations => {
        let shouldDebounceInitialize = false;
        const newVideoIdInURL = getVideoId();
        if (newVideoIdInURL && newVideoIdInURL !== currentVideoId) {
            shouldDebounceInitialize = true;
        }
        if (shouldDebounceInitialize) {
            debouncedInitialize();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
    window.addEventListener('yt-page-data-updated', debouncedInitialize);
    window.addEventListener('yt-navigate-finish', debouncedInitialize);
    debouncedInitialize();

    window.checkBoosterStorage = async function() {
        console.log("--- YouTube Volume Booster Storage Inspection ---");
        console.log("Global Volume Feature State:", await GM_getValue(STORAGE_KEY_GLOBAL_FEATURE_STATE, false));
        console.log("Remember Per Video Feature State:", await GM_getValue(STORAGE_KEY_PER_VIDEO_FEATURE_STATE, false));
        console.log("One-Time Restore Feature State:", await GM_getValue(STORAGE_KEY_ONE_TIME_RESTORE_FEATURE_STATE, true));
        console.log("Per Video Saved Volumes:", await GM_getValue(STORAGE_KEY_PER_VIDEO, {}));
        console.log("One-Time Restore Volumes (Map of videoId -> volume):", await GM_getValue(STORAGE_KEY_ONE_TIME_RESTORE_VOLUMES, {}));
        if (tabId) {
            console.log(`Global Volume for current tab (${tabId}):`, await GM_getValue(STORAGE_KEY_TAB_VOLUME_PREFIX + tabId, 100));
        } else {
            console.log(translations[currentLanguage].tabIdNotInitialized);
        }
        console.log("Saved Toolbar Position:", await GM_getValue(STORAGE_KEY_TOOLBAR_POSITION, null));
        console.log("Current Language:", currentLanguage);
        console.log("------------------------------------------");
    };

    // --- NEW FUNCTIONALITY: CLEAR MANUAL STORAGE (WITH CONFIRMATION) ---
    async function clearManualVideoSettings() {
        const isConfirmed = confirm(translations[currentLanguage].confirmClear);

        if (isConfirmed) {
            await GM_setValue(STORAGE_KEY_PER_VIDEO, {});
            alert(translations[currentLanguage].clearManualStorageSuccess);
        }
    }

    (async () => {
        currentLanguage = await getFeatureState(STORAGE_KEY_LANGUAGE, 'vi');
        GM_registerMenuCommand(translations[currentLanguage].clearManualStorageTitle, clearManualVideoSettings);
    })();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址