您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性,也达到了类似B站的网页全屏功能。同时新增可选的、自制风格的浮动聊天室功能(仅限全fullscreen模式),融入了 YouTube 原有的设计语言。
- // ==UserScript==
- // @name Better YouTube Theater Mode
- // @name:zh-TW 更佳 YouTube 劇場模式
- // @name:zh-CN 更佳 YouTube 剧场模式
- // @name:ja より良いYouTubeシアターモード
- // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
- // @author ElectroKnight22
- // @namespace electroknight22_youtube_better_theater_mode_namespace
- // @version 2.0.0
- // @match *://www.youtube.com/*
- // @match *://www.youtube-nocookie.com/*
- // @noframes
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.deleteValue
- // @grant GM.listValues
- // @grant GM.registerMenuCommand
- // @grant GM.unregisterMenuCommand
- // @grant GM.notification
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_deleteValue
- // @grant GM_listValues
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // @grant GM_notification
- // @run-at document-start
- // @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 的设计,增强视频与聊天室布局,同时保持性能与兼容性,也达到了类似B站的网页全屏功能。同时新增可选的、自制风格的浮动聊天室功能(仅限全fullscreen模式),融入了 YouTube 原有的设计语言。
- // @description:ja YouTubeのシアターモードを改善し、Twitch.tvのデザインを参考にして、動画とチャットのレイアウトを強化しつつ、パフォーマンスと互換性を維持します。また、全画面モード専用のオプションとして、カスタマイズ済みフローティングチャット機能を、YouTubeのデザイン言語に沿って統合しています。
- // ==/UserScript==
- // Note: Both GM.* and GM_.* are granted for compatibility with older script managers.
- /*jshint esversion: 11 */
- (function () {
- 'use strict';
- const CONFIG = {
- DRAG_BAR_HEIGHT: '35px',
- MIN_CHAT_SIZE: { // YouTube chat minimum size is 300px by 320px, going smaller would require a lot of CSS overrides.
- width: 300, // px
- height: 355, // px (320 + DRAG_BAR_HEIGHT)
- },
- DEFAULT_SETTINGS: {
- isSimpleMode: true,
- enableOnlyForLiveStreams: false,
- modifyVideoPlayer: true,
- modifyChat: true,
- setLowHeadmast: false,
- useCustomPlayerHeight: false,
- playerHeightPx: 600,
- floatingChat: false,
- get theaterChatWidth() { return `${CONFIG.MIN_CHAT_SIZE.width}px`; },
- chatStyle: {
- left: '0px',
- top: '-500px',
- get width() { return `${CONFIG.MIN_CHAT_SIZE.width}px`; },
- get height() { return `${CONFIG.MIN_CHAT_SIZE.height}px`; },
- opacity: '0.95',
- },
- debug: false,
- },
- DEFAULT_BLACKLIST: [],
- REQUIRED_VERSIONS: {
- Tampermonkey: '5.4.624',
- },
- };
- const BROWSER_LANGUAGE = navigator.language ?? navigator.userLanguage;
- 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 getPreferredLanguage() {
- if (TRANSLATIONS[BROWSER_LANGUAGE]) {
- return BROWSER_LANGUAGE;
- }
- if (BROWSER_LANGUAGE.startsWith('zh')) {
- return 'zh-CN'; // Default to Simplified Mainland Chinese if Chinese variant is not available or not specified.
- }
- return 'en-US'; // Default to US English if all else fails.
- }
- function getLocalizedText() {
- return TRANSLATIONS[getPreferredLanguage()] ?? TRANSLATIONS['en-US'];
- }
- const state = {
- userSettings: { ...CONFIG.DEFAULT_SETTINGS },
- advancedSettingsBackup: null,
- blacklist: new Set(),
- gmFallback: false,
- menuItems: new Set(),
- activeStyles: new Map(),
- resizeObserver: null,
- videoId: null,
- currentPageType: '',
- isFullscreen: false,
- isTheaterMode: false,
- chatCollapsed: true,
- isLiveStream: false,
- chatWidth: 0,
- moviePlayerHeight: 0,
- isOldTampermonkey: false,
- versionWarningShown: false,
- };
- const DOM = {
- moviePlayer: null,
- chatContainer: null,
- chatFrame: null,
- ytdWatchFlexy: null,
- };
- const createGmApi = () => {
- const isGmFallback = typeof GM === 'undefined' && typeof GM_info !== 'undefined';
- state.gmFallback = isGmFallback;
- if (isGmFallback) {
- return {
- registerMenuCommand: GM_registerMenuCommand,
- unregisterMenuCommand: GM_unregisterMenuCommand,
- getValue: GM_getValue,
- setValue: GM_setValue,
- listValues: GM_listValues,
- deleteValue: GM_deleteValue,
- notification: GM_notification,
- info: () => GM_info,
- };
- }
- return {
- registerMenuCommand: (...args) => window.GM?.registerMenuCommand?.(...args),
- unregisterMenuCommand: (...args) => window.GM?.unregisterMenuCommand?.(...args),
- getValue: (...args) => window.GM?.getValue?.(...args),
- setValue: (...args) => window.GM?.setValue?.(...args),
- listValues: (...args) => window.GM?.listValues?.(...args),
- deleteValue: (...args) => window.GM?.deleteValue?.(...args),
- notification: (...args) => window.GM?.notification?.(...args),
- info: () => window.GM?.info,
- };
- };
- const GM_API = createGmApi();
- const Utils = {
- log(message, level = 'log', data) {
- if (!state.userSettings.debug) return;
- const consoleMethod = console[level] || console.log;
- const prefix = '[Better Theater]';
- data !== undefined ? consoleMethod(prefix, message, data) : consoleMethod(prefix, message);
- },
- 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;
- },
- async 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 passesValidator = typeof validator === 'function' ? validator(value) : true;
- if (isValidNumber && passesValidator) return value;
- alert('⚠️ Please enter a valid number.');
- }
- },
- };
- const StyleManager = {
- styleDefinitions: {
- 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;
- }
- #chat-container { z-index: 2021 !important; }
- `,
- },
- videoPlayerStyle: {
- id: 'betterTheater-videoPlayerStyle',
- getRule: () =>
- state.userSettings.useCustomPlayerHeight
- ? `ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
- min-height: 0px !important;
- height: ${state.userSettings.playerHeightPx}px !important;
- }`
- : `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; }
- `,
- },
- 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}px !important;
- min-height: 0 !important;
- max-width: 100vw !important;
- max-height: 100vh !important;
- position: absolute;
- border-radius: 0 0 12px 12px !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: ${CONFIG.MIN_CHAT_SIZE.height - parseInt(CONFIG.DRAG_BAR_HEIGHT)}px !important;
- max-height: 100vh !important;
- pointer-events: auto !important;
- }
- #chat[collapsed] {
- height: ${CONFIG.DRAG_BAR_HEIGHT} !important;
- min-height: ${CONFIG.DRAG_BAR_HEIGHT} !important;
- }
- .chat-drag-bar {
- cursor: move !important;
- pointer-events: auto !important;
- }
- `,
- },
- floatingChatStyleExpanded: {
- id: 'betterTheater-floatingChatStyleExpanded',
- getRule: () => `
- #chat-container { min-height: ${CONFIG.MIN_CHAT_SIZE.height}px !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: 1px solid var(--yt-spec-10-percent-layer) !important;
- background-clip: padding-box !important;
- border-top: none !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; }
- #chat-container { pointer-events: none !important; }
- `,
- },
- debugResizeHandleStyle: {
- id: 'betterTheater-debugResizeHandleStyle',
- getRule: () => `
- #chat-container .chat-resize-handle { background: transparent; opacity: 0; }
- #chat-container[debug] .chat-resize-handle { opacity: 0.5; }
- #chat-container[debug] .rs-right { background: rgba(255, 0, 0, 0.5); }
- #chat-container[debug] .rs-left { background: rgba(0, 255, 0, 0.5); }
- #chat-container[debug] .rs-bottom { background: rgba(0, 0, 255, 0.5); }
- #chat-container[debug] .rs-top { background: rgba(255, 255, 0, 0.5); }
- #chat-container[debug] .rs-bottom-left { background: rgba(0, 255, 255, 0.5); }
- #chat-container[debug] .rs-top-left { background: rgba(255, 255, 0, 0.5); }
- #chat-container[debug] .rs-top-right { background: rgba(255, 0, 0, 0.5); }
- #chat-container[debug] .rs-bottom-right { background: rgba(255, 0, 255, 0.5); }
- `,
- },
- chatSliderStyle: {
- id: 'betterTheater-chatSliderStyle',
- getRule: () => `
- .chat-drag-bar input[type=range] {
- -webkit-appearance: none; appearance: none;
- width: 100px; height: 4px; border-radius: 2px;
- background: var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color));
- 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;
- }
- `,
- },
- chatClampLimits: {
- id: 'betterTheater-chatClampLimits',
- getRule: () => {
- const hostEl = DOM.ytdWatchFlexy;
- const originalWidth = '402px';
- const originalMinWidth = '402px';
- if (hostEl) {
- const style = window.getComputedStyle(hostEl);
- const fetchedWidth = style.getPropertyValue('--ytd-watch-flexy-sidebar-width')?.trim();
- const fetchedMinWidth = style.getPropertyValue('--ytd-watch-flexy-sidebar-min-width')?.trim();
- return `
- ytd-live-chat-frame[theater-watch-while] {
- min-width: ${CONFIG.MIN_CHAT_SIZE.width}px !important;
- max-width: 33.33vw !important;
- }
- .ytd-watch-flexy {
- --ytd-watch-flexy-sidebar-width: clamp(${
- CONFIG.MIN_CHAT_SIZE.width
- }px, var(--bt-chat-width), 33.33vw) !important;
- --ytd-watch-flexy-sidebar-min-width: clamp(${
- CONFIG.MIN_CHAT_SIZE.width
- }px, var(--bt-chat-width), 33.33vw) !important;
- }
- ytd-watch-flexy[flexy] #secondary.ytd-watch-flexy {
- --ytd-watch-flexy-sidebar-width: ${fetchedWidth ?? originalWidth} !important;
- --ytd-watch-flexy-sidebar-min-width: ${fetchedMinWidth ?? originalMinWidth} !important;
- }
- ytd-watch-next-secondary-results-renderer {
- --ytd-reel-item-compact-layout-width: calc((${
- fetchedWidth ?? originalWidth
- } - 8px) / 3) !important;
- --ytd-reel-item-thumbnail-height: calc((${
- fetchedWidth ?? originalWidth
- } / 3 / 9 * 16)) !important;
- }
- ytd-live-chat-frame[theater-watch-while] yt-live-chat-renderer {
- width: 100% !important; max-width: 100% !important;
- }
- `;
- }
- return ''; // Return empty if host element not found
- },
- },
- },
- apply(styleDef, isPersistent = false) {
- if (typeof styleDef.getRule !== 'function') return;
- this.remove(styleDef); // Ensure no duplicates
- const styleElement = document.createElement('style');
- styleElement.id = styleDef.id;
- styleElement.textContent = styleDef.getRule();
- document.head.appendChild(styleElement);
- state.activeStyles.set(styleDef.id, {
- element: styleElement,
- persistent: isPersistent,
- });
- },
- remove(styleDef) {
- const styleData = state.activeStyles.get(styleDef.id);
- if (styleData) {
- styleData.element?.remove();
- state.activeStyles.delete(styleDef.id);
- }
- },
- removeAll() {
- const styleIdsToRemove = [...state.activeStyles.keys()];
- styleIdsToRemove.forEach((styleId) => {
- const styleData = state.activeStyles.get(styleId);
- if (styleData && !styleData.persistent) {
- this.remove({ id: styleId });
- }
- });
- },
- toggle(styleDef, condition) {
- condition ? this.apply(styleDef) : this.remove(styleDef);
- },
- };
- const SettingsManager = {
- async update(key, value) {
- try {
- const settings = await GM_API.getValue('settings', CONFIG.DEFAULT_SETTINGS);
- settings[key] = value;
- await GM_API.setValue('settings', settings);
- state.userSettings[key] = value;
- } catch (error) {
- Utils.log(`Error updating setting: ${key}`, 'error', error);
- }
- },
- async load() {
- try {
- state.versionWarningShown = await GM_API.getValue('versionWarningShown', false);
- const storedSettings = await GM_API.getValue('settings', CONFIG.DEFAULT_SETTINGS);
- const newSettings = {
- ...CONFIG.DEFAULT_SETTINGS,
- ...storedSettings,
- };
- state.userSettings = newSettings;
- if (Object.keys(storedSettings).length !== Object.keys(newSettings).length) {
- await GM_API.setValue('settings', state.userSettings);
- }
- this.updateMode();
- } catch (error) {
- Utils.log('Error loading settings', 'error', error);
- throw error;
- }
- },
- updateMode() {
- if (state.userSettings.isSimpleMode) {
- if (!state.advancedSettingsBackup) {
- state.advancedSettingsBackup = {
- ...state.userSettings,
- isSimpleMode: false,
- };
- }
- state.userSettings = {
- ...CONFIG.DEFAULT_SETTINGS,
- isSimpleMode: true,
- };
- Utils.log('Switched to Simple Mode');
- } else if (state.advancedSettingsBackup) {
- state.userSettings = {
- ...state.advancedSettingsBackup,
- isSimpleMode: false,
- };
- state.advancedSettingsBackup = null;
- App.warnIfOldTampermonkey();
- Utils.log('Switched to Advanced Mode', 'log', state.userSettings);
- }
- Utils.log('Loaded settings:', 'log', state.userSettings);
- },
- async loadBlacklist() {
- try {
- const stored = await GM_API.getValue('blacklist', CONFIG.DEFAULT_BLACKLIST);
- state.blacklist = new Set(Array.isArray(stored) ? stored : []);
- Utils.log('Loaded blacklist:', 'log', Array.from(state.blacklist));
- } catch (error) {
- Utils.log('Error loading blacklist', 'error', error);
- throw error;
- }
- },
- async updateBlacklist() {
- try {
- await GM_API.setValue('blacklist', Array.from(state.blacklist));
- } catch (error) {
- Utils.log('Error updating blacklist', 'error', error);
- }
- },
- async cleanupStorage() {
- try {
- const allowedKeys = ['settings', 'blacklist', 'versionWarningShown'];
- const keys = await GM_API.listValues();
- for (const key of keys) {
- if (!allowedKeys.includes(key)) {
- await GM_API.deleteValue(key);
- Utils.log(`Deleted leftover key: ${key}`);
- }
- }
- } catch (error) {
- Utils.log('Error cleaning up old storage', 'error', error);
- }
- },
- };
- const MenuManager = {
- clear() {
- state.menuItems.forEach((menuId) => GM_API.unregisterMenuCommand(menuId));
- state.menuItems.clear();
- },
- refresh() {
- this.clear();
- const LABEL = getLocalizedText();
- const shouldAutoClose = state.isOldTampermonkey;
- const menuConfig = [
- // Always visible
- {
- label: () =>
- `🚫 ${
- state.blacklist.has(state.videoId) ? LABEL.unblacklistVideo : LABEL.blacklistVideo
- } [id: ${state.videoId}]`,
- id: 'toggleBlacklist',
- action: async () => {
- state.blacklist.has(state.videoId)
- ? state.blacklist.delete(state.videoId)
- : state.blacklist.add(state.videoId);
- await SettingsManager.updateBlacklist();
- App.updateAllStyles();
- },
- },
- {
- label: () =>
- `${state.userSettings.isSimpleMode ? '🚀 ' + LABEL.simpleMode : '🔧 ' + LABEL.advancedMode}`,
- id: 'toggleMode',
- action: async () => {
- await SettingsManager.update('isSimpleMode', !state.userSettings.isSimpleMode);
- SettingsManager.updateMode();
- App.updateAllStyles();
- },
- },
- // Advanced only
- {
- label: () =>
- `${state.userSettings.enableOnlyForLiveStreams ? '✅' : '❌'} ${LABEL.livestreamOnlyMode}`,
- id: 'toggleLiveOnly',
- action: () =>
- SettingsManager.update(
- 'enableOnlyForLiveStreams',
- !state.userSettings.enableOnlyForLiveStreams
- ).then(App.updateAllStyles),
- advanced: true,
- },
- {
- label: () => `${state.userSettings.modifyChat ? '✅' : '❌'} ${LABEL.applyChatStyles}`,
- id: 'toggleChatStyle',
- action: () =>
- SettingsManager.update('modifyChat', !state.userSettings.modifyChat).then(App.updateAllStyles),
- advanced: true,
- },
- {
- label: () =>
- `${state.userSettings.modifyVideoPlayer ? '✅' : '❌'} ${LABEL.applyVideoPlayerStyles}`,
- id: 'toggleVideoStyle',
- action: () =>
- SettingsManager.update('modifyVideoPlayer', !state.userSettings.modifyVideoPlayer).then(
- App.updateAllStyles
- ),
- advanced: true,
- condition: () => !state.userSettings.useCustomPlayerHeight,
- },
- {
- label: () => `${state.userSettings.setLowHeadmast ? '✅' : '❌'} ${LABEL.moveHeadmastBelowVideoPlayer}`,
- id: 'toggleLowHeadmast',
- action: () =>
- SettingsManager.update('setLowHeadmast', !state.userSettings.setLowHeadmast).then(
- App.updateAllStyles
- ),
- advanced: true,
- },
- {
- label: () =>
- `${state.userSettings.useCustomPlayerHeight ? '✅' : '❌'} ${LABEL.useCustomPlayerHeight}`,
- id: 'toggleCustomHeight',
- action: () =>
- SettingsManager.update('useCustomPlayerHeight', !state.userSettings.useCustomPlayerHeight).then(
- App.updateAllStyles
- ),
- advanced: true,
- },
- {
- label: () => `🔢 ${LABEL.playerHeightText} (${state.userSettings.playerHeightPx}px)`,
- id: 'setCustomHeight',
- action: async () => {
- const newHeight = await Utils.promptForNumber();
- if (newHeight !== null) {
- await SettingsManager.update('playerHeightPx', newHeight);
- App.updateAllStyles();
- }
- },
- advanced: true,
- condition: () => state.userSettings.useCustomPlayerHeight,
- },
- {
- label: () => `${state.userSettings.floatingChat ? '✅' : '❌'} ${LABEL.floatingChat}`,
- id: 'toggleFloatingChat',
- action: () =>
- SettingsManager.update('floatingChat', !state.userSettings.floatingChat).then(
- App.updateAllStyles
- ),
- advanced: true,
- },
- {
- label: () => `${state.userSettings.debug ? '✅' : '❌'} ${LABEL.debug}`,
- id: 'toggleDebug',
- action: async () => {
- await SettingsManager.update('debug', !state.userSettings.debug);
- App.updateDebugStyles();
- },
- advanced: true,
- },
- ];
- menuConfig.forEach((item) => {
- const isAdvancedItem = item.advanced;
- const inAdvancedMode = !state.userSettings.isSimpleMode;
- const conditionMet = item.condition ? item.condition() : true;
- if (conditionMet && (!isAdvancedItem || inAdvancedMode)) {
- const commandId = GM_API.registerMenuCommand(
- item.label(),
- async () => {
- await item.action();
- this.refresh();
- },
- { id: item.id, autoClose: shouldAutoClose }
- );
- state.menuItems.add(commandId ?? item.id);
- }
- });
- },
- };
- const ChatInteractionManager = {
- addTheaterResizeHandle() {
- if (window.innerWidth / 3 <= CONFIG.MIN_CHAT_SIZE.width) return;
- const chat = DOM.chatFrame;
- if (!chat || chat.querySelector('#chat-width-resize-handle')) return;
- const ytdWatchFlexy = DOM.ytdWatchFlexy;
- const storedWidth = state.userSettings.theaterChatWidth ?? `${CONFIG.MIN_CHAT_SIZE.width}px`;
- this._applyTheaterWidth(ytdWatchFlexy, chat, storedWidth);
- const handle = document.createElement('div');
- handle.id = 'chat-width-resize-handle';
- handle.className = 'style-scope ytd-live-chat-frame';
- Object.assign(handle.style, {
- position: 'absolute',
- top: '0',
- left: '0',
- width: '6px',
- height: '100%',
- cursor: 'ew-resize',
- zIndex: '10001',
- });
- chat.appendChild(handle);
- let startX = 0,
- startWidth = 0,
- animationFrame;
- const onPointerMove = (e) => {
- if (!handle.hasPointerCapture(e.pointerId)) return;
- cancelAnimationFrame(animationFrame);
- animationFrame = requestAnimationFrame(() => {
- const dx = startX - e.clientX;
- const newWidth = Math.max(CONFIG.MIN_CHAT_SIZE.width, startWidth + dx);
- this._applyTheaterWidth(ytdWatchFlexy, chat, `${newWidth}px`);
- });
- };
- const onPointerUp = (e) => {
- handle.releasePointerCapture(e.pointerId);
- document.removeEventListener('pointermove', onPointerMove);
- document.removeEventListener('pointerup', onPointerUp);
- SettingsManager.update('theaterChatWidth', ytdWatchFlexy.style.getPropertyValue('--bt-chat-width'));
- };
- handle.addEventListener('pointerdown', (e) => {
- if (e.pointerType === 'mouse' && e.button !== 0) return;
- e.preventDefault();
- document.body.click(); // Deselect any text
- startX = e.clientX;
- startWidth = chat.getBoundingClientRect().width;
- handle.setPointerCapture(e.pointerId);
- document.addEventListener('pointermove', onPointerMove);
- document.addEventListener('pointerup', onPointerUp);
- });
- },
- _applyTheaterWidth(flexy, chat, widthCss) {
- if (flexy) flexy.style.setProperty('--bt-chat-width', widthCss);
- if (chat) {
- chat.style.width = widthCss;
- chat.style.zIndex = '1999';
- }
- },
- removeTheaterResizeHandle() {
- DOM.chatFrame?.querySelector('#chat-width-resize-handle')?.remove();
- const flexy = DOM.ytdWatchFlexy;
- const chat = DOM.chatFrame;
- if (flexy) flexy.style.removeProperty('--bt-chat-width');
- if (chat) {
- chat.style.width = '';
- chat.style.zIndex = '';
- }
- },
- initFullscreenChat(chatContainer) {
- this.applySavedChatStyle(chatContainer, true);
- this.addDragBar(chatContainer);
- this.addResizeHandles(chatContainer);
- },
- cleanupFullscreenChat(chatContainer) {
- this.removeDragBar(chatContainer);
- this.removeResizeHandles(chatContainer);
- chatContainer.style.cssText = '';
- },
- applySavedChatStyle(chatContainer, shouldSave = false) {
- if (!chatContainer || !DOM.moviePlayer) return;
- const movieRect = DOM.moviePlayer.getBoundingClientRect();
- if (movieRect.width === 0 || movieRect.height === 0) return;
- const parentRect = chatContainer.parentElement.getBoundingClientRect();
- const { width, height, top, left, opacity } = state.userSettings.chatStyle;
- const parsedWidth = parseFloat(width) ?? CONFIG.MIN_CHAT_SIZE.width;
- const parsedHeight = parseFloat(height) ?? CONFIG.MIN_CHAT_SIZE.height;
- let newWidth = Math.min(Math.max(CONFIG.MIN_CHAT_SIZE.width, parsedWidth), movieRect.width);
- let newHeight = Math.min(Math.max(CONFIG.MIN_CHAT_SIZE.height, parsedHeight), movieRect.height);
- const parsedTop = parseFloat(top) ?? 0;
- const parsedLeft = parseFloat(left) ?? 0;
- const minTop = movieRect.top - parentRect.top;
- const maxTop = movieRect.bottom - parentRect.top - newHeight;
- const minLeft = movieRect.left - parentRect.left;
- const maxLeft = movieRect.right - parentRect.left - newWidth;
- let newTop = Math.max(minTop, Math.min(parsedTop, maxTop));
- let newLeft = Math.max(minLeft, Math.min(parsedLeft, maxLeft));
- Object.assign(chatContainer.style, {
- width: `${newWidth}px`,
- height: `${newHeight}px`,
- top: `${newTop}px`,
- left: `${newLeft}px`,
- opacity: parseFloat(opacity) ?? 0.95,
- });
- if (shouldSave && (newTop !== parsedTop || newLeft !== parsedLeft)) {
- this.saveChatStyle(chatContainer);
- }
- },
- saveChatStyle(chatContainer) {
- const style = {
- width: chatContainer.style.width,
- height: chatContainer.style.height,
- left: chatContainer.style.left,
- top: chatContainer.style.top,
- opacity: chatContainer.style.opacity,
- };
- return SettingsManager.update('chatStyle', style);
- },
- addDragBar(chatContainer) {
- if (chatContainer.querySelector('.chat-drag-bar')) return;
- StyleManager.apply(StyleManager.styleDefinitions.chatSliderStyle, true);
- const dragBar = document.createElement('div');
- dragBar.className = 'chat-drag-bar';
- Object.assign(dragBar.style, {
- position: 'absolute',
- top: '0',
- left: '0',
- right: '0',
- height: '15px',
- background: 'var(--yt-live-chat-background-color)',
- color: 'var(--yt-live-chat-header-text-color, var(--yt-live-chat-primary-text-color))',
- border: '1px solid var(--yt-spec-10-percent-layer)',
- backgroundClip: 'padding-box',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- padding: `${(parseInt(CONFIG.DRAG_BAR_HEIGHT) - 15) / 2}px`,
- zIndex: '10000',
- borderRadius: '12px 12px 0 0',
- });
- const dragHandleIcon = document.createElement('div');
- dragHandleIcon.textContent = '⋮⋮';
- Object.assign(dragHandleIcon.style, {
- fontSize: '18px',
- userSelect: 'none',
- });
- const opacitySlider = document.createElement('input');
- opacitySlider.type = 'range';
- opacitySlider.min = '20';
- opacitySlider.max = '100';
- opacitySlider.value = String(Math.round((parseFloat(state.userSettings.chatStyle.opacity) ?? 0.95) * 100));
- opacitySlider.addEventListener(
- 'input',
- () => (chatContainer.style.opacity = String(opacitySlider.value / 100))
- );
- opacitySlider.addEventListener('mouseup', () => this.saveChatStyle(chatContainer));
- opacitySlider.addEventListener('pointerdown', (e) => e.stopPropagation());
- dragBar.appendChild(dragHandleIcon);
- dragBar.appendChild(opacitySlider);
- chatContainer.insertBefore(dragBar, chatContainer.firstChild);
- this._initDrag(dragBar, chatContainer);
- },
- _initDrag(dragBar, chatContainer) {
- let start = {},
- parentRect,
- movieRect,
- animationFrame,
- isDragging = false;
- const onPointerMove = (e) => {
- if (!isDragging) return;
- const isOutside =
- e.clientX < movieRect.left ||
- e.clientX > movieRect.right ||
- e.clientY < movieRect.top ||
- e.clientY > movieRect.bottom;
- if (isOutside) return;
- cancelAnimationFrame(animationFrame);
- animationFrame = requestAnimationFrame(() => {
- const newLeft = e.clientX - start.offsetX;
- const newTop = e.clientY - start.offsetY;
- const isChatCollapsed = chatContainer.querySelector('#chat[collapsed]');
- const showHideButtonHeight = chatContainer.querySelector('#show-hide-button')?.offsetHeight ?? 0;
- const chatHeight = isChatCollapsed
- ? parseInt(CONFIG.DRAG_BAR_HEIGHT) + showHideButtonHeight
- : chatContainer.offsetHeight;
- const clampedLeft = Math.max(
- movieRect.left,
- Math.min(newLeft, movieRect.right - chatContainer.offsetWidth)
- );
- const clampedTop = Math.max(movieRect.top, Math.min(newTop, movieRect.bottom - chatHeight));
- chatContainer.style.left = `${clampedLeft - parentRect.left}px`;
- chatContainer.style.top = `${clampedTop - parentRect.top}px`;
- });
- };
- const onPointerUp = (e) => {
- isDragging = false;
- dragBar.releasePointerCapture(e.pointerId);
- document.removeEventListener('pointermove', onPointerMove);
- document.removeEventListener('pointerup', onPointerUp);
- document.removeEventListener('pointercancel', onPointerUp);
- this.saveChatStyle(chatContainer);
- };
- dragBar.addEventListener('pointerdown', (e) => {
- if (e.pointerType === 'mouse' && e.button !== 0) return;
- e.preventDefault();
- isDragging = true;
- dragBar.setPointerCapture(e.pointerId);
- parentRect = chatContainer.parentElement.getBoundingClientRect();
- movieRect = DOM.moviePlayer.getBoundingClientRect();
- const chatRect = chatContainer.getBoundingClientRect();
- start = {
- offsetX: e.clientX - chatRect.left,
- offsetY: e.clientY - chatRect.top,
- };
- document.addEventListener('pointermove', onPointerMove);
- document.addEventListener('pointerup', onPointerUp);
- document.addEventListener('pointercancel', onPointerUp);
- });
- },
- removeDragBar(chatContainer) {
- chatContainer?.querySelector('.chat-drag-bar')?.remove();
- StyleManager.remove(StyleManager.styleDefinitions.chatSliderStyle);
- },
- addResizeHandles(chatContainer) {
- const handleConfigs = {
- right: {
- cursor: 'ew-resize',
- right: '0',
- width: '6px',
- top: '0',
- bottom: '0',
- },
- left: {
- cursor: 'ew-resize',
- left: '0',
- width: '6px',
- top: '0',
- bottom: '0',
- },
- bottom: {
- cursor: 'ns-resize',
- bottom: '0',
- height: '6px',
- left: '0',
- right: '0',
- },
- top: {
- cursor: 'ns-resize',
- top: '0',
- height: '6px',
- left: '0',
- right: '0',
- },
- bottomLeft: {
- cursor: 'nesw-resize',
- left: '0',
- bottom: '0',
- width: '12px',
- height: '12px',
- },
- topLeft: {
- cursor: 'nwse-resize',
- left: '0',
- top: '0',
- width: '12px',
- height: '12px',
- },
- topRight: {
- cursor: 'nesw-resize',
- right: '0',
- top: '0',
- width: '12px',
- height: '12px',
- },
- bottomRight: {
- cursor: 'nwse-resize',
- right: '0',
- bottom: '0',
- width: '12px',
- height: '12px',
- },
- };
- for (const [pos, config] of Object.entries(handleConfigs)) {
- if (chatContainer.querySelector(`.rs-${pos}`)) continue;
- const handle = document.createElement('div');
- handle.className = `chat-resize-handle rs-${pos}`;
- Object.assign(handle.style, {
- position: 'absolute',
- zIndex: '10001',
- ...config,
- });
- chatContainer.appendChild(handle);
- this._initResize(handle, chatContainer);
- }
- },
- _initResize(handle, chatContainer) {
- let start, parentRect, movieRect, animationFrame;
- const onPointerMove = (e) => {
- if (!handle.hasPointerCapture(e.pointerId)) return;
- cancelAnimationFrame(animationFrame);
- animationFrame = requestAnimationFrame(() => {
- const dx = e.clientX - start.x;
- const dy = e.clientY - start.y;
- let newWidth = start.width;
- let newHeight = start.height;
- let newLeft = start.left;
- let newTop = start.top;
- const className = handle.className.toLowerCase();
- const isLeft = className.includes('left');
- const isRight = className.includes('right');
- const isTop = className.includes('top');
- const isBottom = className.includes('bottom');
- if (isRight) newWidth += dx;
- if (isLeft) {
- newWidth -= dx;
- newLeft += dx;
- }
- if (isBottom) newHeight += dy;
- if (isTop) {
- newHeight -= dy;
- newTop += dy;
- }
- newWidth = Math.max(CONFIG.MIN_CHAT_SIZE.width, newWidth);
- newHeight = Math.max(CONFIG.MIN_CHAT_SIZE.height, newHeight);
- if (isLeft)
- newLeft = Math.max(
- movieRect.left,
- Math.min(newLeft, start.left + start.width - CONFIG.MIN_CHAT_SIZE.width)
- );
- if (isTop)
- newTop = Math.max(
- movieRect.top,
- Math.min(newTop, start.top + start.height - CONFIG.MIN_CHAT_SIZE.height)
- );
- newWidth = Math.min(newWidth, movieRect.right - newLeft);
- newHeight = Math.min(newHeight, movieRect.bottom - newTop);
- chatContainer.style.width = `${newWidth}px`;
- chatContainer.style.height = `${newHeight}px`;
- chatContainer.style.left = `${newLeft - parentRect.left}px`;
- chatContainer.style.top = `${newTop - parentRect.top}px`;
- });
- };
- const onPointerUp = (e) => {
- handle.releasePointerCapture(e.pointerId);
- document.removeEventListener('pointermove', onPointerMove);
- document.removeEventListener('pointerup', onPointerUp);
- this.saveChatStyle(chatContainer);
- };
- handle.addEventListener('pointerdown', (e) => {
- if (e.pointerType === 'mouse' && e.button !== 0) return;
- e.preventDefault();
- handle.setPointerCapture(e.pointerId);
- parentRect = chatContainer.parentElement.getBoundingClientRect();
- movieRect = DOM.moviePlayer.getBoundingClientRect();
- const chatRect = chatContainer.getBoundingClientRect();
- start = {
- x: e.clientX,
- y: e.clientY,
- width: chatRect.width,
- height: chatRect.height,
- left: chatRect.left,
- top: chatRect.top,
- };
- document.addEventListener('pointermove', onPointerMove);
- document.addEventListener('pointerup', onPointerUp);
- });
- },
- removeResizeHandles(chatContainer) {
- chatContainer?.querySelectorAll('.chat-resize-handle').forEach((h) => h.remove());
- },
- };
- const App = {
- init() {
- try {
- if (!this.detectGreasemonkey()) throw new Error('Greasemonkey API not detected');
- Utils.log(state.gmFallback ? 'Running in compatibility mode' : 'Running in normal mode', 'warn');
- StyleManager.apply(StyleManager.styleDefinitions.debugResizeHandleStyle, true);
- Promise.all([
- SettingsManager.cleanupStorage(),
- SettingsManager.load(),
- SettingsManager.loadBlacklist(),
- ]).then(() => {
- this.checkTampermonkeyVersion();
- StyleManager.apply(StyleManager.styleDefinitions.chatRendererFixStyle, true);
- StyleManager.apply(StyleManager.styleDefinitions.videoPlayerFixStyle, true);
- this.onPageChange();
- this.attachEventListeners();
- MenuManager.refresh();
- });
- } catch (error) {
- Utils.log('Initialization failed', 'error', error);
- }
- },
- detectGreasemonkey() {
- return typeof window.GM?.info !== 'undefined' || typeof GM_info !== 'undefined';
- },
- checkTampermonkeyVersion() {
- const info = GM_API.info();
- if (info?.scriptHandler === 'Tampermonkey') {
- if (Utils.compareVersions(info.version, CONFIG.REQUIRED_VERSIONS.Tampermonkey) < 0) {
- state.isOldTampermonkey = true;
- this.warnIfOldTampermonkey();
- }
- }
- },
- async warnIfOldTampermonkey() {
- if (state.versionWarningShown || state.userSettings.isSimpleMode || !state.isOldTampermonkey) return;
- GM_API.notification({
- text: getLocalizedText().tampermonkeyOutdatedAlert,
- timeout: 15000,
- });
- state.versionWarningShown = true;
- await GM_API.setValue('versionWarningShown', true);
- },
- updateAllStyles(shouldSaveChatPos = false) {
- try {
- if (state.userSettings.useCustomPlayerHeight) {
- state.userSettings.modifyVideoPlayer = true;
- }
- const isBlacklisted = state.blacklist.has(state.videoId);
- const isLiveOnly = state.userSettings.enableOnlyForLiveStreams && !state.isLiveStream;
- if (isBlacklisted || isLiveOnly) {
- StyleManager.removeAll();
- ChatInteractionManager.removeTheaterResizeHandle();
- DOM.moviePlayer?.setCenterCrop?.();
- return;
- }
- StyleManager.toggle(
- StyleManager.styleDefinitions.videoPlayerStyle,
- state.userSettings.modifyVideoPlayer
- );
- this.updateChatStyles();
- this.updateFullscreenChatStyles(shouldSaveChatPos);
- DOM.moviePlayer?.setCenterCrop?.();
- } catch (error) {
- Utils.log('Error updating styles', 'error', error);
- }
- },
- updateChatStyles() {
- const chatBox = DOM.chatFrame?.getBoundingClientRect();
- const isSecondaryVisible = DOM.ytdWatchFlexy?.querySelector('#secondary')?.style.display !== 'none';
- const shouldApplyChatStyle =
- state.userSettings.modifyChat &&
- state.isTheaterMode &&
- !state.isFullscreen &&
- !state.chatCollapsed &&
- chatBox?.width > 0 &&
- isSecondaryVisible;
- StyleManager.toggle(StyleManager.styleDefinitions.chatStyle, shouldApplyChatStyle);
- StyleManager.toggle(StyleManager.styleDefinitions.chatClampLimits, shouldApplyChatStyle);
- shouldApplyChatStyle
- ? ChatInteractionManager.addTheaterResizeHandle()
- : ChatInteractionManager.removeTheaterResizeHandle();
- this.updateHeadmastStyle(shouldApplyChatStyle);
- },
- updateHeadmastStyle(isChatStyled) {
- this.updateLowHeadmastStyle();
- const shouldShrinkHeadmast =
- isChatStyled &&
- DOM.chatFrame?.getAttribute('theater-watch-while') === '' &&
- (state.userSettings.setLowHeadmast || state.userSettings.modifyChat);
- state.chatWidth = DOM.chatFrame?.offsetWidth ?? 0;
- StyleManager.toggle(StyleManager.styleDefinitions.headmastStyle, shouldShrinkHeadmast);
- },
- updateLowHeadmastStyle() {
- if (!DOM.moviePlayer) return;
- const shouldApply =
- state.userSettings.setLowHeadmast &&
- state.isTheaterMode &&
- !state.isFullscreen &&
- state.currentPageType === 'watch';
- StyleManager.toggle(StyleManager.styleDefinitions.lowHeadmastStyle, shouldApply);
- },
- updateFullscreenChatStyles(shouldSave) {
- const chatContainer = DOM.chatContainer;
- const shouldEnableFloatingChat = state.userSettings.floatingChat && state.isFullscreen;
- if (!chatContainer || !shouldEnableFloatingChat) {
- if (chatContainer) ChatInteractionManager.cleanupFullscreenChat(chatContainer);
- StyleManager.remove(StyleManager.styleDefinitions.floatingChatStyleCollapsed);
- StyleManager.remove(StyleManager.styleDefinitions.floatingChatStyleExpanded);
- StyleManager.remove(StyleManager.styleDefinitions.floatingChatStyle);
- return;
- }
- const isChatAvailable = chatContainer.querySelector('#chat');
- if (isChatAvailable) {
- StyleManager.apply(StyleManager.styleDefinitions.floatingChatStyle);
- StyleManager.toggle(StyleManager.styleDefinitions.floatingChatStyleCollapsed, state.chatCollapsed);
- StyleManager.toggle(StyleManager.styleDefinitions.floatingChatStyleExpanded, !state.chatCollapsed);
- ChatInteractionManager.initFullscreenChat(chatContainer);
- if (shouldSave) {
- ChatInteractionManager.applySavedChatStyle(chatContainer, true);
- }
- } else {
- ChatInteractionManager.cleanupFullscreenChat(chatContainer);
- }
- },
- updateDebugStyles() {
- if (DOM.chatContainer) {
- DOM.chatContainer.toggleAttribute('debug', state.userSettings.debug);
- }
- },
- updateDOMCache() {
- DOM.ytdWatchFlexy = document.querySelector('ytd-watch-flexy');
- DOM.chatContainer = document.querySelector('#chat-container');
- DOM.chatFrame = document.querySelector('ytd-live-chat-frame#chat');
- },
- updateMoviePlayerObserver() {
- const newMoviePlayer = document.querySelector('#movie_player');
- if (DOM.moviePlayer === newMoviePlayer) return;
- if (state.resizeObserver) {
- if (DOM.moviePlayer) {
- state.resizeObserver.unobserve(DOM.moviePlayer);
- }
- } else {
- state.resizeObserver = new ResizeObserver((entries) => {
- for (const entry of entries) {
- state.moviePlayerHeight = entry.contentRect.height;
- this.updateAllStyles();
- }
- });
- }
- DOM.moviePlayer = newMoviePlayer;
- if (DOM.moviePlayer) {
- state.resizeObserver.observe(DOM.moviePlayer);
- }
- },
- onPageChange() {
- this.updateDOMCache();
- this.updateMoviePlayerObserver();
- this.updateAllStyles();
- this.updateDebugStyles();
- MenuManager.refresh();
- },
- handleFullscreenChange() {
- state.isFullscreen = !!document.fullscreenElement;
- this.updateAllStyles(true);
- },
- handleTheaterChange(event) {
- state.isTheaterMode = !!event?.detail?.enabled;
- this.updateAllStyles();
- },
- handleChatCollapse(event) {
- DOM.chatFrame = event.target;
- state.chatCollapsed = event.detail !== false;
- this.updateAllStyles(true);
- },
- handlePageData(event) {
- try {
- const pageData = event.detail.pageData;
- state.currentPageType = pageData.page;
- state.videoId = pageData.playerResponse?.videoDetails?.videoId;
- state.isLiveStream = pageData.playerResponse?.videoDetails?.isLiveContent;
- state.isFullscreen = !!document.fullscreenElement;
- this.onPageChange();
- } catch (error) {
- Utils.log('Failed to process page data', 'error', error);
- }
- },
- attachEventListeners() {
- const events = {
- 'yt-set-theater-mode-enabled': (e) => this.handleTheaterChange(e),
- 'yt-chat-collapsed-changed': (e) => this.handleChatCollapse(e),
- 'yt-page-data-fetched': (e) => this.handlePageData(e),
- 'yt-page-data-updated': () => this.onPageChange(),
- fullscreenchange: () => this.handleFullscreenChange(),
- 'yt-navigate-finish': () => this.onPageChange(),
- };
- for (const [event, handler] of Object.entries(events)) {
- window.addEventListener(event, handler.bind(this), {
- capture: true,
- passive: true,
- });
- }
- let isResizeScheduled = false;
- window.addEventListener('resize', () => {
- if (isResizeScheduled) return;
- isResizeScheduled = true;
- requestAnimationFrame(() => {
- this.updateAllStyles(true);
- isResizeScheduled = false;
- });
- });
- },
- };
- App.init();
- })();