// ==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);
})();
})();