// ==UserScript==
// @name Better Theater Mode for YouTube
// @name:zh-TW 更佳 YouTube 劇場模式
// @name:zh-CN 更佳 YouTube 剧场模式
// @name:ja より良いYouTubeシアターモード
// @icon https://www.youtube.com/img/favicon_48.png
// @author ElectroKnight22
// @namespace electroknight22_youtube_better_theater_mode_namespace
// @version 1.11.2
// @match *://www.youtube.com/*
// @match *://www.youtube-nocookie.com/*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM.notification
// @noframes
// @license MIT
// @description Improves YouTube's theater mode with a Twitch.tv-like design, enhancing video and chat layouts while maintaining performance and compatibility. Also adds an optional, customized floating chat for fullscreen mode, seamlessly integrated with YouTube's design.
// @description:zh-TW 改善 YouTube 劇場模式,參考 Twitch.tv 的設計,增強影片與聊天室佈局,同時維持效能與相容性。另新增可選的、自製風格的浮動聊天室功能(僅限全螢幕模式),與 YouTube 原有的設計語言相融合。
// @description:zh-CN 改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性。同时新增可选的、自制风格的浮动聊天室功能(仅限全屏模式),融入了 YouTube 原有的设计语言。
// @description:ja YouTubeのシアターモードを改善し、Twitch.tvのデザインを参考にして、動画とチャットのレイアウトを強化しつつ、パフォーマンスと互換性を維持します。また、全画面モード専用のオプションとして、カスタマイズ済みフローティングチャット機能を、YouTubeのデザイン言語に沿って統合しています。
// ==/UserScript==
/*jshint esversion: 11 */
(function () {
"use strict";
// CONFIG AND CONSTANTS
const CONFIG = {
// UI Constants
MIN_CHAT_SIZE: { width: '300px', height: '355px' },
DRAG_BAR_HEIGHT: '35px',
// Default settings
DEFAULT_SETTINGS: {
isScriptActive: true,
isSimpleMode: true,
enableOnlyForLiveStreams: false,
modifyVideoPlayer: true,
modifyChat: true,
setLowHeadmast: false,
useCustomPlayerHeight: false,
floatingChat: false,
chatOpacity: '0.95',
chatOffset: { left: '0px', top: '-500px' },
chatSize: { width: '300px', height: '355px' },
debug: false
},
// Default empty blacklist
DEFAULT_BLACKLIST: [],
// Version requirements
REQUIRED_VERSIONS: {
Tampermonkey: '5.4.624'
}
};
// TRANSLATIONS
const BROWSER_LANGUAGE = navigator.language || navigator.userLanguage;
function getPreferredLanguage() {
if (BROWSER_LANGUAGE.startsWith('zh') && BROWSER_LANGUAGE !== 'zh-TW') {
return 'zh-CN';
}
// Check if language is supported, otherwise fall back to English
return ['en-US', 'zh-TW', 'zh-CN', 'ja'].includes(BROWSER_LANGUAGE)
? BROWSER_LANGUAGE
: 'en-US';
}
const TRANSLATIONS = {
'en-US': {
tampermonkeyOutdatedAlert: "It looks like you're using an older version of Tampermonkey that might cause menu issues. For the best experience, please update to version 5.4.6224 or later.",
turnOn: 'Turn On',
turnOff: 'Turn Off',
livestreamOnlyMode: 'Livestream Only Mode',
applyChatStyles: 'Apply Chat Styles',
applyVideoPlayerStyles: 'Apply Video Player Styles',
moveHeadmastBelowVideoPlayer: 'Move Headmast Below Video Player',
useCustomPlayerHeight: 'Use Custom Player Height',
playerHeightText: 'Player Height',
floatingChat: 'Floating Chat',
blacklistVideo: 'Blacklist Video',
unblacklistVideo: 'Unblacklist Video',
simpleMode: 'Simple Mode',
advancedMode: 'Advanced Mode',
debug: 'DEBUG'
},
'zh-TW': {
tampermonkeyOutdatedAlert: "看起來您正在使用較舊版本的篡改猴,可能會導致選單問題。為了獲得最佳體驗,請更新至 5.4.6224 或更高版本。",
turnOn: '開啟',
turnOff: '關閉',
livestreamOnlyMode: '僅限直播模式',
applyChatStyles: '套用聊天樣式',
applyVideoPlayerStyles: '套用影片播放器樣式',
moveHeadmastBelowVideoPlayer: '將頁首橫幅移到影片播放器下方',
useCustomPlayerHeight: '使用自訂播放器高度',
playerHeightText: '播放器高度',
floatingChat: '浮動聊天室',
blacklistVideo: '將影片加入黑名單',
unblacklistVideo: '從黑名單中移除影片',
simpleMode: '簡易模式',
advancedMode: '進階模式',
debug: '偵錯'
},
'zh-CN': {
tampermonkeyOutdatedAlert: "看起来您正在使用旧版本的篡改猴,这可能会导致菜单问题。为了获得最佳体验,请更新到 5.4.6224 或更高版本。",
turnOn: '开启',
turnOff: '关闭',
livestreamOnlyMode: '仅限直播模式',
applyChatStyles: '应用聊天样式',
applyVideoPlayerStyles: '应用视频播放器样式',
moveHeadmastBelowVideoPlayer: '将页首横幅移动到视频播放器下方',
useCustomPlayerHeight: '使用自定义播放器高度',
playerHeightText: '播放器高度',
floatingChat: '浮动聊天室',
blacklistVideo: '将视频加入黑名单',
unblacklistVideo: '从黑名单中移除视频',
simpleMode: '简易模式',
advancedMode: '高级模式',
debug: '调试'
},
'ja': {
tampermonkeyOutdatedAlert: "ご利用のTampermonkeyのバージョンが古いため、メニューに問題が発生する可能性があります。より良い体験のため、バージョン5.4.6224以上に更新してください。",
turnOn: "オンにする",
turnOff: "オフにする",
livestreamOnlyMode: "ライブ配信専用モード",
applyChatStyles: "チャットスタイルを適用",
applyVideoPlayerStyles: "ビデオプレイヤースタイルを適用",
moveHeadmastBelowVideoPlayer: "ヘッドマストをビデオプレイヤーの下に移動",
useCustomPlayerHeight: "カスタムプレイヤーの高さを使用",
playerHeightText: "プレイヤーの高さ",
floatingChat: "フローティングチャット",
blacklistVideo: "動画をブラックリストに追加",
unblacklistVideo: "ブラックリストから動画を解除",
simpleMode: "シンプルモード",
advancedMode: "高度モード",
debug: "デバッグ"
}
};
function getLocalizedText() {
return TRANSLATIONS[getPreferredLanguage()] || TRANSLATIONS['en-US'];
}
// STATE VARIABLES
const state = {
userSettings: { ...CONFIG.DEFAULT_SETTINGS },
advancedSettingsBackup: null,
blacklist: new Set(),
useCompatibilityMode: false,
menuItems: new Set(),
activeStyles: new Map(),
resizeObserver: null,
moviePlayer: null,
videoId: null,
chatFrame: null,
currentPageType: '',
isFullscreen: false,
isTheaterMode: false,
chatCollapsed: true,
isLiveStream: false,
chatWidth: 0,
moviePlayerHeight: 0,
isOldTampermonkey: false,
isScriptRecentlyUpdated: false
};
// GM API COMPATIBILITY
const GM = {
registerMenuCommand: state.useCompatibilityMode ? GM_registerMenuCommand : window.GM?.registerMenuCommand,
unregisterMenuCommand: state.useCompatibilityMode ? GM_unregisterMenuCommand : window.GM?.unregisterMenuCommand,
getValue: state.useCompatibilityMode ? GM_getValue : window.GM?.getValue,
setValue: state.useCompatibilityMode ? GM_setValue : window.GM?.setValue,
listValues: state.useCompatibilityMode ? GM_listValues : window.GM?.listValues,
deleteValue: state.useCompatibilityMode ? GM_deleteValue : window.GM?.deleteValue,
notification: state.useCompatibilityMode ? GM_notification : window.GM?.notification
};
// STYLE DEFINITIONS
const styleRules = {
chatStyle: {
id: "betterTheater-chatStyle",
getRule: () => `
ytd-live-chat-frame[theater-watch-while][rounded-container] {
border-radius: 0 !important;
border-top: 0 !important;
}
ytd-watch-flexy[fixed-panels] #chat.ytd-watch-flexy {
top: 0 !important;
border-top: 0 !important;
border-bottom: 0 !important;
}
`,
},
videoPlayerStyle: {
id: "betterTheater-videoPlayerStyle",
getRule: () => {
if (state.userSettings.useCustomPlayerHeight) {
return `
ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
min-height: 0px !important;
height: ${state.userSettings.playerHeightPx}px !important;
}
`;
} else {
return `
ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
max-height: calc(100vh - var(--ytd-watch-flexy-masthead-height)) !important;
}
`;
}
}
},
headmastStyle: {
id: "betterTheater-headmastStyle",
getRule: () => `
#masthead-container.ytd-app {
max-width: calc(100% - ${state.chatWidth}px) !important;
}
`,
},
lowHeadmastStyle: {
id: "betterTheater-lowHeadmastStyle",
getRule: () => `
#page-manager.ytd-app {
margin-top: 0 !important;
top: calc(-1 * var(--ytd-toolbar-offset)) !important;
position: relative !important;
}
ytd-watch-flexy[flexy]:not([full-bleed-player][full-bleed-no-max-width-columns]) #columns.ytd-watch-flexy {
margin-top: var(--ytd-toolbar-offset) !important;
}
${state.userSettings.modifyVideoPlayer ? `
ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
max-height: 100vh !important;
}
` : ''}
#masthead-container.ytd-app {
z-index: 599 !important;
top: ${state.moviePlayerHeight}px !important;
position: relative !important;
}
`,
},
videoPlayerFixStyle: {
id: "betterTheater-videoPlayerFixStyle",
getRule: () => `
.html5-video-container {
top: -1px !important;
}
#skip-navigation.ytd-masthead {
left: -500px;
}
`,
},
chatFrameFixStyle: {
id: "betterTheater-chatFrameFixStyle",
getRule: () => {
const chatInputContainer = document.querySelector(
"tp-yt-iron-pages#panel-pages.style-scope.yt-live-chat-renderer"
);
const shouldHideChatInputContainerTopBorder =
chatInputContainer?.clientHeight === 0;
const borderTopStyle = shouldHideChatInputContainerTopBorder
? 'border-top: 0 !important;'
: '';
return `
#panel-pages.yt-live-chat-renderer {
${borderTopStyle}
border-bottom: 0 !important;
}
`;
},
},
chatRendererFixStyle: {
id: "betterTheater-chatRendererFixStyle",
getRule: () => `
ytd-live-chat-frame[theater-watch-while][rounded-container] {
border-bottom: 0 !important;
}
`,
},
floatingChatStyle: {
id: "betterTheater-floatingChatStyle",
getRule: () => `
#chat-container {
min-width: ${CONFIG.MIN_CHAT_SIZE.width} !important;
max-width: 100vw !important;
max-height: 100vh !important;
position: absolute;
border-radius: 0 0 10px 10px !important;
}
#chat {
top: ${CONFIG.DRAG_BAR_HEIGHT} !important;
height: calc(100% - ${CONFIG.DRAG_BAR_HEIGHT}) !important;
width: inherit !important;
min-width: inherit !important;
max-width: inherit !important;
min-height: ${parseInt(CONFIG.MIN_CHAT_SIZE.height) - parseInt(CONFIG.DRAG_BAR_HEIGHT)}px !important;
max-height: 100vh !important;
}
`,
},
floatingChatStyleExpanded: {
id: "betterTheater-floatingChatStyleExpanded",
getRule: () => `
#chat-container {
min-height: ${CONFIG.MIN_CHAT_SIZE.height} !important;
}
ytd-live-chat-frame:not([theater-watch-while])[rounded-container] {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
border-top: 0 !important;
}
ytd-live-chat-frame:not([theater-watch-while])[rounded-container] iframe.ytd-live-chat-frame {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
`,
},
floatingChatStyleCollapsed: {
id: "betterTheater-floatingChatStyleCollapsed",
getRule: () => `
ytd-live-chat-frame[round-background] #show-hide-button.ytd-live-chat-frame>ytd-toggle-button-renderer.ytd-live-chat-frame,
ytd-live-chat-frame[round-background] #show-hide-button.ytd-live-chat-frame>ytd-button-renderer.ytd-live-chat-frame {
margin: 0 !important;
border-radius: 0 0 12px 12px !important;
border-left: 1px solid var(--yt-spec-10-percent-layer) !important;
border-right: 1px solid var(--yt-spec-10-percent-layer) !important;
border-bottom: 1px solid var(--yt-spec-10-percent-layer) !important;
background-clip: padding-box !important;
}
ytd-live-chat-frame[modern-buttons][collapsed] {
border-radius: 0 0 12px 12px !important;
}
button.yt-spec-button-shape-next.yt-spec-button-shape-next--outline.yt-spec-button-shape-next--mono.yt-spec-button-shape-next--size-m {
border-radius: 0 0 12px 12px !important;
border: none !important;
}
.chat-resize-handle {
visibility: hidden !important;
}
`,
},
debugResizeHandleStyle: {
id: "betterTheater-debugResizeHandleStyle",
getRule: () => `
/* Default state for resize handles */
#chat-container .chat-resize-handle {
background: transparent;
opacity: 0;
}
/* Debug state for resize handles when #chat-container has [debug] attribute */
#chat-container[debug] .chat-resize-handle {
opacity: 0.5;
}
#chat-container[debug] .chat-resize-handle.rs-right {
background: rgba(255, 0, 0, 0.5);
}
#chat-container[debug] .chat-resize-handle.rs-left {
background: rgba(0, 255, 0, 0.5);
}
#chat-container[debug] .chat-resize-handle.rs-bottom {
background: rgba(0, 0, 255, 0.5);
}
#chat-container[debug] .chat-resize-handle.rs-top {
background: rgba(255, 255, 0, 0.5);
}
#chat-container[debug] .chat-resize-handle.rs-bottom-left {
background: rgba(0, 255, 255, 0.5);
}
#chat-container[debug] .chat-resize-handle.rs-top-left {
background: rgba(255, 255, 0, 0.5);
}
#chat-container[debug] .chat-resize-handle.rs-top-right {
background: rgba(255, 0, 0, 0.5);
}
#chat-container[debug] .chat-resize-handle.rs-bottom-right {
background: rgba(255, 0, 255, 0.5);
}
`,
},
chatSliderStyle: {
id: "betterTheater-chatSliderStyle",
getRule: () => `
.chat-drag-bar input[type=range] {
-webkit-appearance: none;
width: 100px;
height: 4px;
background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
border-radius: 2px;
outline: none;
}
.chat-drag-bar input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
cursor: pointer;
}
.chat-drag-bar input[type=range]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
cursor: pointer;
}
.chat-drag-bar input[type=range]::-moz-range-track {
background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
height: 4px;
border-radius: 2px;
}
.chat-drag-bar input[type=range]::-ms-thumb {
background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
}
.chat-drag-bar input[type=range]::-ms-track {
background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
height: 4px;
border-radius: 2px;
}
`
}
};
// STYLE MANAGEMENT
function applyStyle(style, setPersistent = false) {
if (typeof style.getRule !== 'function') return;
if (state.activeStyles.has(style.id)) {
removeStyle(style);
}
const styleElement = document.createElement('style');
styleElement.id = style.id;
styleElement.type = 'text/css';
styleElement.textContent = style.getRule();
(document.head || document.documentElement).appendChild(styleElement);
state.activeStyles.set(style.id, {
element: styleElement,
persistent: setPersistent
});
}
function removeStyle(style) {
if (!state.activeStyles.has(style.id)) return;
const { element: styleElement } = state.activeStyles.get(style.id);
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
state.activeStyles.delete(style.id);
}
function removeAllStyles() {
state.activeStyles.forEach((styleData, styleId) => {
if (!styleData.persistent) {
removeStyle({ id: styleId });
}
});
}
function setStyleState(style, on = true) {
on ? applyStyle(style) : removeStyle(style);
}
// RESIZE HANDLES
function isAtMinSize(element) {
const style = window.getComputedStyle(element);
const width = parseFloat(style.width);
const height = parseFloat(style.height);
const minWidth = parseFloat(style.minWidth);
const minHeight = parseFloat(style.minHeight);
return {
isAtMinWidth: width <= minWidth,
isAtMinHeight: height <= minHeight
};
}
function addResizeHandles(chatContainer) {
const handleConfigs = {
right: {
width: "6px", top: "0", right: "0", bottom: "0",
cursor: "ew-resize", horizontal: true, vertical: false
},
left: {
width: "6px", top: "0", left: "0", bottom: "0",
cursor: "ew-resize", horizontal: true, vertical: false
},
bottom: {
height: "6px", left: "0", bottom: "0", right: "0",
cursor: "ns-resize", horizontal: false, vertical: true
},
top: {
height: "6px", left: "0", top: "0", right: "0",
cursor: "ns-resize", horizontal: false, vertical: true
},
bottomLeft: {
width: "12px", height: "12px", left: "0", bottom: "0",
cursor: "nesw-resize", horizontal: true, vertical: true
},
topLeft: {
width: "12px", height: "12px", left: "0", top: "0",
cursor: "nwse-resize", horizontal: true, vertical: true
},
topRight: {
width: "12px", height: "12px", right: "0", top: "0",
cursor: "nesw-resize", horizontal: true, vertical: true
},
bottomRight: {
width: "12px", height: "12px", right: "0", bottom: "0",
cursor: "nwse-resize", horizontal: true, vertical: true
}
};
const handles = {};
for (const [position, config] of Object.entries(handleConfigs)) {
const handle = document.createElement("div");
handle.className = `chat-resize-handle rs-${position}`;
handle.style.position = "absolute";
handle.style.zIndex = "10001";
Object.assign(handle.style, config);
chatContainer.appendChild(handle);
handles[position] = handle;
initResizeHandler(handle, config);
}
return handles;
function initResizeHandler(handle, config) {
let startX, startY, startWidth, startHeight, startLeft, startTop;
async function saveChatSize() {
state.userSettings.chatSize = {
width: chatContainer.style.width,
height: chatContainer.style.height
};
state.userSettings.chatOffset = {
left: chatContainer.style.left,
top: chatContainer.style.top
};
await updateSetting('chatSize', state.userSettings.chatSize);
await updateSetting('chatOffset', state.userSettings.chatOffset);
}
handle.addEventListener("pointerdown", function (e) {
if (e.pointerType === "mouse" && e.button !== 0) return;
e.preventDefault();
startX = e.clientX;
startY = e.clientY;
startWidth = chatContainer.offsetWidth;
startHeight = chatContainer.offsetHeight;
startLeft = parseFloat(getComputedStyle(chatContainer).left) || 0;
startTop = parseFloat(getComputedStyle(chatContainer).top) || 0;
handle.setPointerCapture(e.pointerId);
});
handle.addEventListener("pointermove", function (e) {
if (!handle.hasPointerCapture(e.pointerId)) return;
e.preventDefault();
const movieRect = state.moviePlayer.getBoundingClientRect();
const chatParentRect = chatContainer.parentElement.getBoundingClientRect();
const chatRect = chatContainer.getBoundingClientRect();
const minWidth = parseInt(CONFIG.MIN_CHAT_SIZE.width);
const minHeight = parseInt(CONFIG.MIN_CHAT_SIZE.height);
let dx = e.clientX - startX;
let dy = e.clientY - startY;
if (dx === 0 && dy === 0) return;
dx = Math.max(-startX, Math.min(dx, movieRect.right - startX));
dy = Math.max(-startY, Math.min(dy, movieRect.bottom - startY));
const tooSmall = isAtMinSize(chatContainer);
if (config.horizontal) {
console.log('horizontal');
const isRightSide = handle.className.toLowerCase().includes('right');
const isLeftSide = handle.className.toLowerCase().includes('left');
if (!isRightSide && !isLeftSide) {
console.log(handle.className);
}
if (isRightSide) {
if (!tooSmall.isAtMinWidth || dx > 0) {
if (startWidth + dx < minWidth) dx = minWidth - startWidth;
if (chatRect.left + startWidth + dx > movieRect.right) {
dx = movieRect.right - chatRect.left - startWidth;
}
chatContainer.style.width = Math.max(minWidth, startWidth + dx) + "px";
}
} else if (isLeftSide) {
if (!tooSmall.isAtMinWidth || dx < 0) {
if (startWidth - dx < minWidth) dx = startWidth - minWidth;
if (startLeft + chatParentRect.left + dx < movieRect.left) {
dx = movieRect.left - startLeft - chatParentRect.left;
}
chatContainer.style.width = Math.max(minWidth, startWidth - dx) + "px";
chatContainer.style.left = (startLeft + dx) + "px";
}
}
}
if (config.vertical) {
console.log('vertical');
const isBottomSide = handle.className.toLowerCase().includes('bottom');
const isTopSide = handle.className.toLowerCase().includes('top');
if (isBottomSide) {
if (!tooSmall.isAtMinHeight || dy > 0) {
if (startHeight + dy < minHeight) dy = minHeight - startHeight;
if (chatRect.top + startHeight + dy > movieRect.bottom) {
dy = movieRect.bottom - chatRect.top - startHeight;
}
chatContainer.style.height = Math.max(minHeight, startHeight + dy) + "px";
}
} else if (isTopSide) {
if (!tooSmall.isAtMinHeight || dy < 0) {
if (startHeight - dy < minHeight) dy = startHeight - minHeight;
if (startTop + chatParentRect.top + dy < movieRect.top) {
dy = movieRect.top - startTop - chatParentRect.top;
}
chatContainer.style.height = Math.max(minHeight, startHeight - dy) + "px";
chatContainer.style.top = (startTop + dy) + "px";
}
}
}
});
handle.addEventListener("pointerup", function (e) {
handle.releasePointerCapture(e.pointerId);
saveChatSize();
});
}
}
// Removes all resize handles from a container
function removeResizeHandles(chatContainer) {
if (!chatContainer) return;
const handles = chatContainer.querySelectorAll(".chat-resize-handle");
handles.forEach(handle => handle.remove());
}
// DRAG BAR & CHAT CONTROLS
function addDragBarWithOpacitySlider(chatContainer) {
let existingBar = chatContainer.querySelector('.chat-drag-bar');
if (existingBar) return existingBar;
// Add chat slider style if not present
applyStyle(styleRules.chatSliderStyle, true);
const dragBar = document.createElement("div");
dragBar.className = "chat-drag-bar";
dragBar.style.position = "absolute";
dragBar.style.top = "0";
dragBar.style.left = "0";
dragBar.style.right = "0";
dragBar.style.height = "15px";
dragBar.style.background = "var(--yt-live-chat-background-color)";
dragBar.style.color = "var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color))";
dragBar.style.border = "1px solid var(--yt-spec-10-percent-layer)";
dragBar.style.backgroundClip = "padding-box";
dragBar.style.display = "flex";
dragBar.style.alignItems = "center";
dragBar.style.justifyContent = "space-between";
dragBar.style.padding = (parseInt(CONFIG.DRAG_BAR_HEIGHT) - 15) / 2 + "px";
dragBar.style.zIndex = "10000";
dragBar.style.borderRadius = "12px 12px 0 0";
const dragLabel = document.createElement("div");
dragLabel.innerText = "⋮⋮";
dragLabel.style.fontSize = "var(--yt-live-chat-header-font-size, 18px)";
dragLabel.style.userSelect = "none";
const opacitySlider = document.createElement("input");
opacitySlider.type = "range";
opacitySlider.min = "20";
opacitySlider.max = "100";
opacitySlider.value = Math.round(parseFloat(state.userSettings.chatOpacity) * 100).toString();
opacitySlider.style.marginLeft = "10px";
opacitySlider.addEventListener("input", () => {
const newOpacity = opacitySlider.value / 100;
chatContainer.style.opacity = newOpacity;
});
opacitySlider.addEventListener("mouseup", () => {
updateSetting('chatOpacity', chatContainer.style.opacity);
});
["pointerdown", "pointermove", "pointerup"].forEach(eventType => {
opacitySlider.addEventListener(eventType, (e) => {
e.stopPropagation();
});
});
dragBar.appendChild(dragLabel);
dragBar.appendChild(opacitySlider);
chatContainer.insertBefore(dragBar, chatContainer.firstChild);
// Add drag functionality
setupDragBehavior(dragBar, chatContainer);
return dragBar;
}
function setupDragBehavior(dragBar, chatContainer) {
let dragging = false;
let startX = 0, startY = 0;
let containerStartLeft = 0;
let containerStartTop = 0;
const dragThreshold = 5;
async function saveChatPosition() {
state.userSettings.chatOffset = {
left: chatContainer.style.left,
top: chatContainer.style.top
};
await updateSetting('chatOffset', state.userSettings.chatOffset);
}
// Handle pointer down event
dragBar.addEventListener("pointerdown", function (e) {
if (e.pointerType === "mouse" && e.button !== 0) return;
dragging = false;
startX = e.clientX;
startY = e.clientY;
containerStartLeft = parseFloat(getComputedStyle(chatContainer).left) || 0;
containerStartTop = parseFloat(getComputedStyle(chatContainer).top) || 0;
dragBar.setPointerCapture(e.pointerId);
e.preventDefault();
});
// Handle pointer move event
dragBar.addEventListener("pointermove", function (e) {
if (!dragBar.hasPointerCapture(e.pointerId)) return;
let dx = e.clientX - startX;
let dy = e.clientY - startY;
if (dx === 0 && dy === 0) return;
// Start dragging after threshold is crossed
if (!dragging && Math.hypot(dx, dy) > dragThreshold) {
dragging = true;
}
if (dragging) {
// Calculate new position
let newLeft = containerStartLeft + dx;
let newTop = containerStartTop + dy;
// Get boundaries
const movieRect = state.moviePlayer.getBoundingClientRect();
const chatParentRect = chatContainer.parentElement.getBoundingClientRect();
// Constrain to left edge
if (newLeft + chatParentRect.left < 0) {
newLeft = -chatParentRect.left;
}
// Constrain to top edge
if (newTop + chatParentRect.top < 0) {
newTop = -chatParentRect.top;
}
// Constrain to right edge
if (newLeft > movieRect.right - (chatParentRect.left + chatContainer.offsetWidth)) {
newLeft = movieRect.right - (chatParentRect.left + chatContainer.offsetWidth);
}
// Constrain to bottom edge, accounting for chat collapsed state
if (state.chatCollapsed) {
const showHideButton = chatContainer.querySelector('#show-hide-button');
if (showHideButton && newTop > movieRect.bottom - (chatParentRect.top + showHideButton.offsetHeight + dragBar.offsetHeight)) {
newTop = movieRect.bottom - (chatParentRect.top + showHideButton.offsetHeight + dragBar.offsetHeight);
}
} else {
if (newTop > movieRect.bottom - (chatParentRect.top + chatContainer.offsetHeight)) {
newTop = movieRect.bottom - (chatParentRect.top + chatContainer.offsetHeight);
}
}
// Apply new position
chatContainer.style.left = newLeft + "px";
chatContainer.style.top = newTop + "px";
e.preventDefault();
}
});
// Handle pointer up event
dragBar.addEventListener("pointerup", function (e) {
dragBar.releasePointerCapture(e.pointerId);
dragging = false;
saveChatPosition();
});
}
// Removes drag bar from chat container
function removeDragBarWithOpacitySlider(chatContainer) {
if (!chatContainer) return;
const dragBar = chatContainer.querySelector('.chat-drag-bar');
if (dragBar) dragBar.remove();
}
// Removes all chat-related styles from container
function removeAllChatStyles(chatContainer) {
removeStyle(styleRules.floatingChatStyleCollapsed);
removeStyle(styleRules.floatingChatStyleExpanded);
removeStyle(styleRules.floatingChatStyle);
if (chatContainer) chatContainer.style = '';
}
/**
* Applies saved chat style to container
* @param {HTMLElement} chatContainer - The chat container
*/
function applySavedChatStyle(chatContainer) {
if (!chatContainer) return;
const chatPrison = chatContainer.parentElement.getBoundingClientRect();
// Apply width
if (state.userSettings.chatSize?.width) {
chatContainer.style.width = Math.min(
window.innerWidth,
parseFloat(state.userSettings.chatSize.width)
) + 'px';
}
// Apply height
if (state.userSettings.chatSize?.height) {
chatContainer.style.height = Math.min(
window.innerHeight,
parseFloat(state.userSettings.chatSize.height)
) + 'px';
}
// Apply left position
if (state.userSettings.chatOffset?.left !== undefined) {
const leftPos = parseFloat(state.userSettings.chatOffset.left);
chatContainer.style.left = Math.min(
Math.max(leftPos, chatPrison.left),
window.innerWidth - chatPrison.width
) + 'px';
}
// Apply top position
if (state.userSettings.chatOffset?.top !== undefined) {
const topPos = parseFloat(state.userSettings.chatOffset.top);
chatContainer.style.top = Math.min(
Math.max(topPos, chatPrison.top),
window.innerHeight - chatPrison.height
) + 'px';
}
// Apply opacity
chatContainer.style.opacity = parseFloat(state.userSettings.chatOpacity);
}
// STYLE UPDATE FUNCTIONS
/**
* Updates styles based on current settings and state
*/
function updateStyles() {
try {
// Enforce dependency: custom player height requires modifying player
if (state.userSettings.useCustomPlayerHeight) {
state.userSettings.modifyVideoPlayer = true;
}
// Check if script should be disabled
const shouldNotActivate =
!state.userSettings.isScriptActive ||
(state.blacklist && state.blacklist.has(state.videoId)) ||
(state.userSettings.enableOnlyForLiveStreams && !state.isLiveStream);
if (shouldNotActivate) {
removeAllStyles();
if (state.moviePlayer && state.moviePlayer.setCenterCrop) {
state.moviePlayer.setCenterCrop();
}
return;
}
// Apply main styles based on settings
setStyleState(styleRules.chatStyle, state.userSettings.modifyChat);
setStyleState(styleRules.videoPlayerStyle, state.userSettings.modifyVideoPlayer);
// Update header styles
updateHeadmastStyle();
// Update floating chat styles
updateFullscreenFloatingChatStyle();
// Reset player crop if needed
if (state.moviePlayer && state.moviePlayer.setCenterCrop) {
state.moviePlayer.setCenterCrop();
}
} catch (error) {
logDebug(`Error when updating styles: ${error}`, 'error');
}
}
/**
* Updates the headmast styles based on current state
*/
function updateHeadmastStyle() {
updateLowHeadmastStyle();
// Determine if headmast should be shrunk to account for chat
const shouldShrinkHeadmast =
state.isTheaterMode &&
state.chatFrame?.getAttribute('theater-watch-while') === '' &&
(state.userSettings.setLowHeadmast || state.userSettings.modifyChat);
// Update chat width for style calculation
state.chatWidth = state.chatFrame?.offsetWidth || 0;
// Apply or remove headmast style
setStyleState(styleRules.headmastStyle, shouldShrinkHeadmast);
}
/**
* Updates low headmast style based on current state
*/
function updateLowHeadmastStyle() {
if (!state.moviePlayer) return;
const shouldApplyLowHeadmast =
state.userSettings.setLowHeadmast &&
state.isTheaterMode &&
!state.isFullscreen &&
state.currentPageType === 'watch';
setStyleState(styleRules.lowHeadmastStyle, shouldApplyLowHeadmast);
}
/**
* Updates floating chat styles based on fullscreen state
*/
function updateFullscreenFloatingChatStyle() {
try {
const chatContainer = document.querySelector('#chat-container');
// Update collapsed/expanded styles based on chat state
setStyleState(styleRules.floatingChatStyleCollapsed,
state.chatCollapsed && state.isFullscreen);
setStyleState(styleRules.floatingChatStyleExpanded,
!state.chatCollapsed && state.isFullscreen);
setStyleState(styleRules.floatingChatStyle, state.isFullscreen);
// Configure floating chat if needed
if (state.userSettings.isScriptActive &&
state.userSettings.floatingChat &&
chatContainer.querySelector('#chat') &&
chatContainer &&
state.isFullscreen) {
applySavedChatStyle(chatContainer);
removeDragBarWithOpacitySlider(chatContainer);
addDragBarWithOpacitySlider(chatContainer);
addResizeHandles(chatContainer);
// Apply saved settings
chatContainer.style.opacity = parseFloat(state.userSettings.chatOpacity);
chatContainer.style.width = Math.min(
window.innerWidth,
parseFloat(state.userSettings.chatSize.width)
) + 'px';
chatContainer.style.height = Math.min(
window.innerHeight,
parseFloat(state.userSettings.chatSize.height)
) + 'px';
chatContainer.style.left = parseFloat(state.userSettings.chatOffset.left) + 'px';
chatContainer.style.top = parseFloat(state.userSettings.chatOffset.top) + 'px';
} else if (chatContainer) {
// Remove chat modifications if not needed
removeAllChatStyles(chatContainer);
removeDragBarWithOpacitySlider(chatContainer);
removeResizeHandles(chatContainer);
}
} catch (error) {
logDebug(`Error when updating fullscreen chat styles: ${error}`, 'error');
}
}
/**
* Updates debug visual indicators
*/
function updateDebugStyles() {
const chatContainer = document.querySelector('#chat-container');
if (chatContainer) {
if (state.userSettings.debug) {
chatContainer.setAttribute("debug", "");
} else {
chatContainer.removeAttribute("debug");
}
}
}
// EVENT HANDLERS
/**
* Updates fullscreen status and related styles
*/
function updateFullscreenStatus() {
state.isFullscreen = !!document.fullscreenElement;
updateStyles();
}
/**
* Handles theater mode toggle events
* @param {Event} event - The theater mode event
*/
function updateTheaterStatus(event) {
state.isTheaterMode = !!event?.detail?.enabled;
updateStyles();
}
/**
* Handles chat status change events
* @param {Event} event - The chat status event
*/
function updateChatStatus(event) {
state.chatFrame = event.target;
state.chatCollapsed = event.detail !== false;
// Wait for player API to be ready before updating styles
window.addEventListener('player-api-ready', updateStyles, { once: true });
}
/**
* Updates movie player reference and sets up resize observer
*/
function updateMoviePlayer() {
const newMoviePlayer = document.querySelector('#movie_player');
// Create resize observer if needed
if (!state.resizeObserver) {
state.resizeObserver = new ResizeObserver(() => {
state.moviePlayerHeight = state.moviePlayer?.offsetHeight || 0;
updateStyles();
});
}
// Stop observing old player
if (state.moviePlayer) {
state.resizeObserver.unobserve(state.moviePlayer);
}
// Start observing new player
state.moviePlayer = newMoviePlayer;
if (state.moviePlayer) {
state.resizeObserver.observe(state.moviePlayer);
}
}
/**
* Handles video status update events
* @param {Event} event - The video status event
*/
function updateVideoStatus(event) {
try {
state.currentPageType = event.detail.pageData.page;
state.videoId = event.detail.pageData.playerResponse.videoDetails.videoId;
state.isLiveStream = event.detail.pageData.playerResponse.videoDetails.isLiveContent;
updateMoviePlayer();
refreshMenuOptions();
} catch (error) {
logDebug(`Failed to update video status: ${error}`, 'error');
}
}
// SETTINGS MANAGEMENT
/**
* Updates a setting in storage
* @param {string} key - The setting key
* @param {any} value - The setting value
*/
async function updateSetting(key, value) {
try {
let currentSettings = await GM.getValue('settings', CONFIG.DEFAULT_SETTINGS);
currentSettings[key] = value;
await GM.setValue('settings', currentSettings);
state.userSettings[key] = value;
} catch (error) {
logDebug(`Error updating setting: ${error}`, 'error');
}
}
/**
* Loads user settings from storage
*/
async function loadUserSettings() {
try {
const storedSettings = await GM.getValue('settings', CONFIG.DEFAULT_SETTINGS);
const newSettings = {};
let needsSave = false;
// Use stored settings or defaults
for (const key in CONFIG.DEFAULT_SETTINGS) {
if (key in storedSettings) {
newSettings[key] = storedSettings[key];
} else {
newSettings[key] = CONFIG.DEFAULT_SETTINGS[key];
needsSave = true;
}
}
// Check for obsolete settings
for (const key in storedSettings) {
if (!(key in CONFIG.DEFAULT_SETTINGS)) {
needsSave = true;
}
}
// Save settings if needed
state.userSettings = newSettings;
if (needsSave) {
await GM.setValue('settings', state.userSettings);
}
updateMode();
} catch (error) {
logDebug(`Error loading user settings: ${error}`, 'error');
throw new Error(`Error loading user settings: ${error}. Aborting script.`);
}
}
/**
* Updates the mode (simple/advanced) based on user settings
*/
function updateMode() {
if (state.userSettings.isSimpleMode === true) {
// Backup advanced settings before switching to simple mode
state.advancedSettingsBackup = {
...state.userSettings,
isSimpleMode: false
};
// Apply simple mode settings
state.userSettings = {
...CONFIG.DEFAULT_SETTINGS,
isScriptActive: state.userSettings.isScriptActive,
isSimpleMode: true
};
logDebug('Using simple mode');
} else if (state.advancedSettingsBackup) {
// Restore advanced settings
state.userSettings = {
...state.advancedSettingsBackup,
isSimpleMode: false
};
logDebug('Using advanced mode');
logDebug('Advanced settings backup:', state.advancedSettingsBackup);
}
logDebug(`Loaded settings: ${JSON.stringify(state.userSettings)}`);
}
/**
* Loads video blacklist from storage
*/
async function loadBlacklist() {
try {
let storedBlacklist = await GM.getValue('blacklist', CONFIG.DEFAULT_BLACKLIST);
state.blacklist = new Set(
Array.isArray(storedBlacklist) ? storedBlacklist : []
);
logDebug(`Loaded blacklist: ${JSON.stringify(Array.from(state.blacklist))}`);
} catch (error) {
logDebug(`Error loading blacklist: ${error}`, 'error');
throw new Error(`Error loading blacklist: ${error}. Aborting script.`);
}
}
/**
* Updates the blacklist in storage
*/
async function updateBlacklist() {
try {
await GM.setValue('blacklist', Array.from(state.blacklist));
} catch (error) {
logDebug(`Error updating blacklist: ${error}`, 'error');
}
}
/**
* Updates script info in storage and checks for updates
*/
async function updateScriptInfo() {
try {
const oldScriptInfo = await GM.getValue('scriptInfo', null);
const newScriptInfo = {
version: getScriptVersionFromMeta(),
};
await GM.setValue('scriptInfo', newScriptInfo);
// Check if script was updated
if (!oldScriptInfo || compareVersions(newScriptInfo.version, oldScriptInfo?.version) !== 0) {
state.isScriptRecentlyUpdated = true;
}
logDebug(`Previous script info: ${JSON.stringify(oldScriptInfo)}`);
logDebug(`Updated script info: ${JSON.stringify(newScriptInfo)}`);
} catch (error) {
logDebug(`Error updating script info: ${error}`, 'error');
}
}
/**
* Cleans up old storage keys
*/
async function cleanupOldStorage() {
try {
const allowedKeys = ['settings', 'scriptInfo', 'blacklist'];
const keys = await GM.listValues();
for (const key of keys) {
if (!allowedKeys.includes(key)) {
await GM.deleteValue(key);
logDebug(`Deleted leftover key: ${key}`);
}
}
} catch (error) {
logDebug(`Error cleaning up old storage keys: ${error}`, 'error');
}
}
// MENU MANAGEMENT
/**
* Removes all menu options
*/
function removeMenuOptions() {
state.menuItems.forEach((menuItem) => {
GM.unregisterMenuCommand(menuItem);
});
state.menuItems.clear();
}
/**
* Updates and shows menu options based on current state
*/
async function refreshMenuOptions() {
const shouldAutoClose = state.isOldTampermonkey;
removeMenuOptions();
// Advanced mode menu options
const advancedMenuOptions = state.userSettings.isSimpleMode ? {} : {
toggleOnlyLiveStreamMode: {
alwaysShow: true,
label: () => `${state.userSettings.enableOnlyForLiveStreams ? "✅" : "❌"} ${getLocalizedText().livestreamOnlyMode}`,
menuId: "toggleOnlyLiveStreamMode",
handleClick: async function () {
state.userSettings.enableOnlyForLiveStreams = !state.userSettings.enableOnlyForLiveStreams;
await updateSetting('enableOnlyForLiveStreams', state.userSettings.enableOnlyForLiveStreams);
updateStyles();
refreshMenuOptions();
},
},
toggleChatStyle: {
alwaysShow: true,
label: () => `${state.userSettings.modifyChat ? "✅" : "❌"} ${getLocalizedText().applyChatStyles}`,
menuId: "toggleChatStyle",
handleClick: async function () {
state.userSettings.modifyChat = !state.userSettings.modifyChat;
await updateSetting('modifyChat', state.userSettings.modifyChat);
updateStyles();
refreshMenuOptions();
},
},
...(!state.userSettings.useCustomPlayerHeight ? {
toggleVideoPlayerStyle: {
alwaysShow: true,
label: () => `${state.userSettings.modifyVideoPlayer ? "✅" : "❌"} ${getLocalizedText().applyVideoPlayerStyles}`,
menuId: "toggleVideoPlayerStyle",
handleClick: async function () {
state.userSettings.modifyVideoPlayer = !state.userSettings.modifyVideoPlayer;
await updateSetting('modifyVideoPlayer', state.userSettings.modifyVideoPlayer);
updateStyles();
refreshMenuOptions();
},
},
} : {}),
toggleLowHeadmast: {
alwaysShow: true,
label: () => `${state.userSettings.setLowHeadmast ? "✅" : "❌"} ${getLocalizedText().moveHeadmastBelowVideoPlayer}`,
menuId: "toggleLowHeadmast",
handleClick: async function () {
state.userSettings.setLowHeadmast = !state.userSettings.setLowHeadmast;
await updateSetting('setLowHeadmast', state.userSettings.setLowHeadmast);
updateStyles();
refreshMenuOptions();
},
},
toggleCustomPlayerHeight: {
alwaysShow: true,
label: () => `${state.userSettings.useCustomPlayerHeight ? "✅" : "❌"} ${getLocalizedText().useCustomPlayerHeight}`,
menuId: "toggleCustomPlayerHeight",
handleClick: async function () {
state.userSettings.useCustomPlayerHeight = !state.userSettings.useCustomPlayerHeight;
await updateSetting('useCustomPlayerHeight', state.userSettings.useCustomPlayerHeight);
updateStyles();
refreshMenuOptions();
},
},
...(state.userSettings.useCustomPlayerHeight ? {
customHeightInputSelector: {
alwaysShow: true,
label: () => `🔢 ${getLocalizedText().playerHeightText} (${state.userSettings.playerHeightPx}px)`,
menuId: "customHeightInputSelector",
handleClick: async function () {
const playerHeightInputValue = await promptForNumber();
if (playerHeightInputValue === null) return;
state.userSettings.playerHeightPx = playerHeightInputValue;
await updateSetting('playerHeightPx', playerHeightInputValue);
updateStyles();
refreshMenuOptions();
},
},
} : {}),
toggleFloatingChat: {
alwaysShow: true,
label: () => `${state.userSettings.floatingChat ? "✅" : "❌"} ${getLocalizedText().floatingChat}`,
menuId: "toggleFloatingChat",
handleClick: async function () {
state.userSettings.floatingChat = !state.userSettings.floatingChat;
await updateSetting('floatingChat', state.userSettings.floatingChat);
refreshMenuOptions();
},
},
toggleDebug: {
alwaysShow: true,
label: () => `${state.userSettings.debug ? "✅" : "❌"} ${getLocalizedText().debug}`,
menuId: "toggleDebug",
handleClick: async function () {
state.userSettings.debug = !state.userSettings.debug;
await updateSetting('debug', state.userSettings.debug);
updateDebugStyles();
refreshMenuOptions();
}
}
};
// Common menu options for both simple and advanced modes
const commonMenuOptions = {
toggleScript: {
alwaysShow: true,
label: () => `🔄 ${state.userSettings.isScriptActive ? getLocalizedText().turnOff : getLocalizedText().turnOn}`,
menuId: "toggleScript",
handleClick: async function () {
state.userSettings.isScriptActive = !state.userSettings.isScriptActive;
await updateSetting('isScriptActive', state.userSettings.isScriptActive);
updateStyles();
refreshMenuOptions();
},
},
addVideoToBlacklist: {
alwaysShow: true,
label: () => `🚫 ${state.blacklist.has(state.videoId) ? getLocalizedText().unblacklistVideo : getLocalizedText().blacklistVideo} [id: ${state.videoId}]`,
menuId: "addVideoToBlacklist",
handleClick: async function () {
if (state.blacklist.has(state.videoId)) {
state.blacklist.delete(state.videoId);
} else {
state.blacklist.add(state.videoId);
}
await updateBlacklist();
updateStyles();
refreshMenuOptions();
},
},
toggleSimpleMode: {
alwaysShow: true,
label: () => `${state.userSettings.isSimpleMode ? "🚀 " + getLocalizedText().simpleMode : "🔧 " + getLocalizedText().advancedMode}`,
menuId: "toggleSimpleMode",
handleClick: async function () {
state.userSettings.isSimpleMode = !state.userSettings.isSimpleMode;
await updateSetting('isSimpleMode', state.userSettings.isSimpleMode);
updateMode();
updateStyles();
refreshMenuOptions();
},
},
};
// Combine menu options
const menuOptions = {
...commonMenuOptions,
...advancedMenuOptions
};
// Process and register all menu options
for (const [_, item] of Object.entries(menuOptions)) {
if (!item.alwaysShow && !state.userSettings.expandMenu) continue;
const menuId = GM.registerMenuCommand(item.label(), item.handleClick, {
id: item.menuId,
autoClose: shouldAutoClose,
});
state.menuItems.add(item.menuId);
}
}
/**
* Shows a number input prompt with validation
* @param {string} message - The prompt message
* @param {Function} validator - Optional validation function
* @returns {number|null} The entered number or null if cancelled
*/
async function promptForNumber(message = "Enter a number:", validator = null) {
while (true) {
const input = prompt(message);
if (input === null) return null;
const value = Number(input.trim());
const isValidNumber = input.trim() !== "" && !isNaN(value);
const passesCustomValidator = typeof validator === "function" ? validator(value) : true;
if (isValidNumber && passesCustomValidator) {
return value;
} else {
alert("⚠️ Please enter a valid number.");
}
}
}
// UTILITY FUNCTIONS
/**
* Logs debug information if debug mode is enabled
* @param {string} message - The message to log
* @param {string} [level='log'] - The log level ('log', 'warn', 'error')
* @param {*} [data] - Optional data to log
*/
function logDebug(message, level = 'log', data) {
if (!state.userSettings.debug) return;
const consoleMethod = console[level] || console.log;
if (data !== undefined) {
consoleMethod('[Better Theater] ' + message, data);
} else {
consoleMethod('[Better Theater] ' + message);
}
}
/**
* Compares two version strings
* @param {string} v1 - First version string
* @param {string} v2 - Second version string
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
*/
function compareVersions(v1, v2) {
if (!v1 || !v2) return 0;
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const len = Math.max(parts1.length, parts2.length);
for (let i = 0; i < len; i++) {
const num1 = parts1[i] || 0;
const num2 = parts2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
/**
* Gets the script version from metadata
* @returns {string} The script version
*/
function getScriptVersionFromMeta() {
const versionMatch = GM_info.scriptMetaStr.match(/@version\s+([^\r\n]+)/);
return versionMatch ? versionMatch[1].trim() : null;
}
/**
* Checks if we have the required Greasemonkey API
* @returns {boolean} Whether the required API is available
*/
function detectGreasemonkeyAPI() {
if (typeof GM !== 'undefined') return true;
if (typeof GM_info !== 'undefined') {
state.useCompatibilityMode = true;
logDebug("Running in compatibility mode", 'warn');
return true;
}
return false;
}
/**
* Checks if Tampermonkey is up to date
*/
function checkTampermonkeyVersion() {
if (GM_info.scriptHandler === "Tampermonkey" &&
compareVersions(GM_info.version, CONFIG.REQUIRED_VERSIONS.Tampermonkey) !== 1) {
state.isOldTampermonkey = true;
if (state.isScriptRecentlyUpdated) {
GM.notification({
text: getLocalizedText().tampermonkeyOutdatedAlert,
timeout: 15000
});
}
}
}
/**
* Checks if the current page is a live chat iframe
* @returns {boolean} Whether this is a live chat iframe
*/
function isLiveChatIFrame() {
return /^https?:\/\/.*youtube\.com\/live_chat.*$/.test(window.location.href);
}
// EVENT LISTENERS
/**
* Attaches all necessary event listeners
*/
function attachEventListeners() {
// YouTube-specific events
window.addEventListener('yt-set-theater-mode-enabled', updateTheaterStatus, true);
window.addEventListener('yt-chat-collapsed-changed', updateChatStatus, true);
window.addEventListener('yt-page-data-fetched', updateVideoStatus, true);
window.addEventListener('yt-page-data-updated', updateStyles, true);
// Standard events
window.addEventListener('fullscreenchange', updateFullscreenStatus, true);
window.addEventListener('yt-navigate-finish', updateDebugStyles, { once: true });
}
// INITIALIZATION
/**
* Initializes the script
*/
async function initialize() {
try {
// Check for Greasemonkey API
if (!detectGreasemonkeyAPI()) {
throw new Error("Did not detect valid Greasemonkey API");
}
// Initialize static styles
applyStyle(styleRules.debugResizeHandleStyle, true);
// Clean up and load settings
await cleanupOldStorage();
await loadUserSettings();
await loadBlacklist();
await updateScriptInfo();
// Check Tampermonkey version
checkTampermonkeyVersion();
// Handle iframe case
if (isLiveChatIFrame()) {
applyStyle(styleRules.chatFrameFixStyle, true);
return;
}
// Apply fixes and initialize
applyStyle(styleRules.chatRendererFixStyle, true);
applyStyle(styleRules.videoPlayerFixStyle, true);
updateStyles();
attachEventListeners();
refreshMenuOptions();
} catch (error) {
logDebug(`Error when initializing script: ${error}. Aborting script.`, 'error');
}
}
// Start the script
initialize();
})();