您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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或关注我们的公众号极客氢云获取最新地址