// ==UserScript==
// @name YouTube Volume Booster 600% (v3.9.18 Resize & Idle-Hide Fix)
// @name:vi YouTube - Tăng Âm Lượng 600% (v3.9.18 Sửa lỗi Resize & Ẩn khi không hoạt động)
// @namespace http://tampermonkey.net/
// @version 3.9.18 // FIX: Panel now resizes correctly and hides with native YouTube controls.
// @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
// @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.' //
},
'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.' //
}
};
// 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; /* */
}
/* FIX: Panel only shows when native controls are visible (player does NOT have .ytp-autohide class) */
#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);
}
});
// FIX: Add ResizeObserver to handle player size changes
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("------------------------------------------");
};
})();