Personaliza o layout, remove elementos indesejados e adiciona um relógio em tela cheia.
// ==UserScript==
// @name YouTube Enhancer
// @namespace Violentmonkey Scripts
// @version 2.5.0
// @description Personaliza o layout, remove elementos indesejados e adiciona um relógio em tela cheia.
// @author John Wiliam & IA
// @match *://*.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_addElement
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const SCRIPT_VERSION = '2.5.0';
const FLAG = `__yt_enhancer_v${SCRIPT_VERSION.replace(/\./g, '_')}__`;
if (window[FLAG]) return;
window[FLAG] = true;
const log = (msg) => console.log(`[YT Enhancer] ${msg}`);
// =======================================================
// EVENT BUS SYSTEM
// =======================================================
const EventBus = {
events: new Map(),
on(event, callback) {
if (!this.events.has(event)) this.events.set(event, []);
this.events.get(event).push(callback);
return () => this.off(event, callback);
},
off(event, callback) {
if (!this.events.has(event)) return;
const callbacks = this.events.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
},
emit(event, data) {
if (!this.events.has(event)) return;
[...this.events.get(event)].forEach(callback => {
try { callback(data); } catch (e) { console.error(`EventBus error [${event}]:`, e); }
});
},
clear() {
this.events.clear();
}
};
// =======================================================
// UTILITÁRIOS
// =======================================================
const Utils = {
clamp(value, min, max, fallback = min) {
const num = Number(value);
return Number.isFinite(num) ? Math.min(max, Math.max(min, num)) : fallback;
},
isHexColor(value) { return typeof value === 'string' && /^#[0-9a-f]{6}$/i.test(value); },
toBoolean(value, fallback = false) {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') return ['true', '1', 'yes', 'on'].includes(value.toLowerCase());
if (typeof value === 'number') return value !== 0;
return fallback;
},
sanitizeConfig(config, defaults) {
const featureValues = Object.fromEntries(Object.keys(defaults.FEATURES).map((key) => [key, config?.FEATURES?.[key] ?? defaults.FEATURES[key]]));
const safe = { ...defaults, ...(config || {}), FEATURES: featureValues, CLOCK_STYLE: { ...defaults.CLOCK_STYLE, ...(config?.CLOCK_STYLE || {}) } };
safe.LANGUAGE = ['pt', 'en'].includes(safe.LANGUAGE) ? safe.LANGUAGE : defaults.LANGUAGE;
safe.VIDEOS_PER_ROW = this.clamp(safe.VIDEOS_PER_ROW, 3, 8, defaults.VIDEOS_PER_ROW);
safe.CLOCK_STYLE.bgOpacity = this.clamp(safe.CLOCK_STYLE.bgOpacity, 0, 1, defaults.CLOCK_STYLE.bgOpacity);
safe.CLOCK_STYLE.fontSize = this.clamp(safe.CLOCK_STYLE.fontSize, 12, 48, defaults.CLOCK_STYLE.fontSize);
safe.CLOCK_STYLE.margin = this.clamp(safe.CLOCK_STYLE.margin, 0, 120, defaults.CLOCK_STYLE.margin);
safe.CLOCK_STYLE.borderRadius = this.clamp(safe.CLOCK_STYLE.borderRadius, 0, 50, defaults.CLOCK_STYLE.borderRadius);
safe.CLOCK_STYLE.color = this.isHexColor(safe.CLOCK_STYLE.color) ? safe.CLOCK_STYLE.color : defaults.CLOCK_STYLE.color;
safe.CLOCK_STYLE.bgColor = this.isHexColor(safe.CLOCK_STYLE.bgColor) ? safe.CLOCK_STYLE.bgColor : defaults.CLOCK_STYLE.bgColor;
safe.FEATURES.LAYOUT_ENHANCEMENT = this.toBoolean(safe.FEATURES.LAYOUT_ENHANCEMENT, defaults.FEATURES.LAYOUT_ENHANCEMENT);
safe.FEATURES.SHORTS_REMOVAL = this.toBoolean(safe.FEATURES.SHORTS_REMOVAL, defaults.FEATURES.SHORTS_REMOVAL);
safe.FEATURES.REMOVE_RELEVANT = this.toBoolean(safe.FEATURES.REMOVE_RELEVANT, defaults.FEATURES.REMOVE_RELEVANT);
safe.FEATURES.FULLSCREEN_CLOCK = this.toBoolean(safe.FEATURES.FULLSCREEN_CLOCK, defaults.FEATURES.FULLSCREEN_CLOCK);
return safe;
},
debounce(func, wait) {
let timeout = null;
const debounced = function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
func.apply(this, args);
}, wait);
};
debounced.cancel = () => {
clearTimeout(timeout);
timeout = null;
};
return debounced;
},
scheduleFrame(callback) {
let frameId = 0;
let pending = false;
const run = () => {
frameId = 0;
pending = false;
callback();
};
const scheduled = () => {
if (pending) return;
pending = true;
frameId = requestAnimationFrame(run);
};
scheduled.cancel = () => {
if (frameId) cancelAnimationFrame(frameId);
frameId = 0;
pending = false;
};
return scheduled;
},
getAppRoot() {
return document.querySelector('ytd-app, ytd-page-manager') || document.body || document.documentElement;
},
queryWithin(root, selector) {
if (!(root instanceof Element || root instanceof Document)) return [];
const matches = root instanceof Element && root.matches(selector) ? [root] : [];
return matches.concat([...root.querySelectorAll(selector)]);
},
DOMCache: {
cache: new Map(),
get(selector, force = false) {
if (!force && this.cache.has(selector)) {
const el = this.cache.get(selector);
if (el && el.isConnected) return el;
this.cache.delete(selector);
}
const el = document.querySelector(selector);
if (el) this.cache.set(selector, el);
return el;
},
refresh() { this.cache.clear(); }
},
safeAddEventListener(element, event, handler, options = {}) {
if (!element) return () => {};
const safeHandler = (e) => { try { return handler(e); } catch (err) { console.error(`[Event] ${event}:`, err); } };
element.addEventListener(event, safeHandler, options);
return () => element.removeEventListener(event, safeHandler, options);
},
migrateConfig(savedConfig, currentVersion) {
if (!savedConfig || typeof savedConfig !== 'object') return null;
const previousVersion = savedConfig.version || '1.0.0';
if (!savedConfig.FEATURES || typeof savedConfig.FEATURES !== 'object') savedConfig.FEATURES = {};
if (!savedConfig.CLOCK_STYLE || typeof savedConfig.CLOCK_STYLE !== 'object') savedConfig.CLOCK_STYLE = {};
if (!savedConfig.version) {
savedConfig.version = '1.0.0';
if (!Object.prototype.hasOwnProperty.call(savedConfig.CLOCK_STYLE, 'borderRadius')) savedConfig.CLOCK_STYLE = { ...savedConfig.CLOCK_STYLE, borderRadius: 12 };
}
if (!Object.prototype.hasOwnProperty.call(savedConfig.FEATURES, 'REMOVE_RELEVANT')) savedConfig.FEATURES.REMOVE_RELEVANT = true;
if (previousVersion !== currentVersion && savedConfig.CLOCK_STYLE.bgColor?.toLowerCase() === '#191919') savedConfig.CLOCK_STYLE.bgColor = '#000000';
savedConfig.version = currentVersion;
return savedConfig;
},
injectCSS(css, id) {
try {
const old = document.getElementById(id);
if (old) old.remove();
let styleEl = null;
if (typeof GM_addStyle === 'function') {
styleEl = GM_addStyle(css) || null;
if (styleEl && id) styleEl.id = id;
} else if (typeof GM_addElement === 'function') {
styleEl = GM_addElement('style', { id, textContent: css }) || null;
} else {
styleEl = document.createElement('style');
styleEl.id = id;
styleEl.textContent = css;
(document.head || document.documentElement).appendChild(styleEl);
}
if (!styleEl && id) styleEl = document.getElementById(id);
return Boolean(styleEl && styleEl.isConnected);
} catch (error) {
console.error('[YT Enhancer] CSS injection failed:', error);
return false;
}
}
};
// =======================================================
// 1. I18N + CONFIG MANAGER
// =======================================================
const I18N = {
pt: {
modal: { title: '⚙️ Configurações', closeTitle: 'Fechar', tabs: { features: '🔧 Funcionalidades', appearance: '🎨 Aparência do relógio' }, features: { layout: { title: 'Layout Grid', description: 'Ajusta vídeos por linha' }, videosPerRow: 'Vídeos por linha', videosPerRowHint: 'Define quantos vídeos aparecem', shorts: { title: 'Remover Shorts', description: 'Limpa Shorts da interface' }, relevant: { title: 'Remover (Mais Relevantes)', description: "Remove o elemento 'Mais relevantes' da página de inscrições." }, clock: { title: 'Relógio Flutuante', description: 'Mostra hora sobre o vídeo' }, language: { title: 'Idioma da Interface', description: 'Troca textos entre PT e EN' } }, clockStyle: { textColor: 'Cor do Texto', backgroundColor: 'Cor do Fundo', backgroundOpacity: 'Opacidade Fundo', fontSize: 'Tamanho Fonte (px)', margin: 'Margem (px)', borderRadius: 'Arredondamento (px)' }, buttons: { apply: 'Aplicar', applyAndReload: 'Aplicar e Recarregar' }, reloadNotice: 'A alteração de idioma exige recarregar a página.' },
menu: { openSettings: '⚙️ Configurações' }
},
en: {
modal: { title: '⚙️ Settings', closeTitle: 'Close', tabs: { features: '🔧 Features', appearance: '🎨 Clock appearance' }, features: { layout: { title: 'Grid Layout', description: 'Adjusts videos per row' }, videosPerRow: 'Videos per row', videosPerRowHint: 'Defines videos in each row', shorts: { title: 'Remove Shorts', description: 'Cleans Shorts from UI' }, relevant: { title: 'Remove (Most Relevant)', description: "Removes the 'Most relevant' element from the Subscriptions page." }, clock: { title: 'Floating Clock', description: 'Shows time over the video' }, language: { title: 'Interface Language', description: 'Switch texts between EN and PT' } }, clockStyle: { textColor: 'Text Color', backgroundColor: 'Background Color', backgroundOpacity: 'Background Opacity', fontSize: 'Font Size (px)', margin: 'Margin (px)', borderRadius: 'Roundness (px)' }, buttons: { apply: 'Apply', applyAndReload: 'Apply and Reload' }, reloadNotice: 'Changing the language requires reloading the page.' },
menu: { openSettings: '⚙️ Settings' }
}
};
const t = (key, lang = null) => {
const resolvedLang = (lang || ConfigManager.load()?.LANGUAGE || 'en').toLowerCase();
const segments = key.split('.');
const getValue = (dictionary) => segments.reduce((acc, segment) => acc?.[segment], dictionary);
return getValue(I18N[resolvedLang]) ?? getValue(I18N.en) ?? getValue(I18N.pt) ?? key;
};
const ConfigManager = {
CONFIG_VERSION: '2.5.0',
STORAGE_KEY: 'YT_ENHANCER_CONFIG',
defaults: {
version: '2.5.0', LANGUAGE: 'pt', VIDEOS_PER_ROW: 5,
FEATURES: { LAYOUT_ENHANCEMENT: true, SHORTS_REMOVAL: true, REMOVE_RELEVANT: true, FULLSCREEN_CLOCK: true },
CLOCK_STYLE: { color: '#ffffff', bgColor: '#000000', bgOpacity: 0.3, fontSize: 22, margin: 30, borderRadius: 25, position: 'bottom-right' }
},
load() {
try {
const saved = GM_getValue(this.STORAGE_KEY);
return Utils.sanitizeConfig(Utils.migrateConfig(saved, this.CONFIG_VERSION) || {}, this.defaults);
} catch (e) { return Utils.sanitizeConfig({}, this.defaults); }
},
save(config) {
try {
const sanitized = Utils.sanitizeConfig(config, this.defaults);
sanitized.version = this.CONFIG_VERSION;
GM_setValue(this.STORAGE_KEY, sanitized);
EventBus.emit('configChanged', sanitized);
return true;
} catch (e) { return false; }
}
};
const SettingsLauncher = {
menuRegistered: false,
apiRegistered: false,
apiCleanup: null,
opening: false,
open(source = 'unknown') {
if (this.opening) return;
this.opening = true;
Promise.resolve(UIManager.openSettings((newConfig) => ConfigManager.save(newConfig)))
.then((opened) => {
if (!opened) console.warn(`[YT Enhancer] Falha ao abrir modal de configurações (source=${source}).`);
})
.catch((error) => console.error(`[YT Enhancer] Exceção ao abrir configurações.`, error))
.finally(() => this.opening = false);
},
registerMenuCommand() {
if (this.menuRegistered || typeof GM_registerMenuCommand !== 'function') return;
this.menuRegistered = true;
const lang = ConfigManager.load().LANGUAGE;
const label = t('menu.openSettings', lang);
const callback = () => this.open('menu');
try { GM_registerMenuCommand(label, callback, { id: 'yt-enhancer-settings-cmd', autoClose: true }); }
catch (error) { try { GM_registerMenuCommand(label, callback); } catch (e) {} }
},
registerSafeApi() {
if (this.apiRegistered) return;
this.apiRegistered = true;
this.apiCleanup = Utils.safeAddEventListener(window, 'yt-enhancer-open-settings', () => this.open('event_api'));
},
cleanup() {
this.apiCleanup?.();
this.apiCleanup = null;
this.apiRegistered = false;
}
};
// =======================================================
// 2. UI MANAGER (Modal Blindado)
// =======================================================
const UIManager = {
cleanupFunctions: [], styleId: 'yt-enhancer-modal-style',
applyModalInlineFallback(modalElement) {
if (!modalElement) return;
modalElement.style.cssText = 'position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; background: #121212 !important; color: #f1f1f1 !important; z-index: 2147483647 !important;';
},
ensureRootReady(maxAttempts = 60, interval = 50) {
return new Promise((resolve) => {
let attempts = 0;
const check = () => {
if (document.documentElement && document.body) { resolve(true); return; }
attempts += 1;
if (attempts >= maxAttempts) { resolve(false); return; }
setTimeout(check, interval);
};
check();
});
},
async openSettings(onSave) {
const config = ConfigManager.load();
const rootReady = await this.ensureRootReady();
if (!rootReady) return false;
const stylesInjected = this.ensureStyles();
this.cleanupFunctions.forEach(fn => fn());
this.cleanupFunctions = [];
document.getElementById('yt-enhancer-settings-modal')?.remove();
document.getElementById('yt-enhancer-overlay')?.remove();
const create = (tag, options = {}) => {
const el = document.createElement(tag);
if (options.id) el.id = options.id;
if (options.className) el.className = options.className;
if (options.text) el.textContent = options.text;
if (options.type) el.type = options.type;
if (options.title) el.title = options.title;
if (options.value !== undefined) el.value = options.value;
if (options.checked !== undefined) el.checked = !!options.checked;
if (options.min !== undefined) el.min = String(options.min);
if (options.max !== undefined) el.max = String(options.max);
if (options.step !== undefined) el.step = String(options.step);
if (options.forId) el.htmlFor = options.forId;
if (options.dataset) Object.assign(el.dataset, options.dataset);
return el;
};
const overlay = create('div', { id: 'yt-enhancer-overlay' });
overlay.style.cssText = 'position: fixed !important; inset: 0 !important; background: rgba(0,0,0,0.75) !important; z-index: 2147483646 !important; pointer-events: auto !important;';
const modal = create('div', { id: 'yt-enhancer-settings-modal', className: 'yt-enhancer-modal' });
if (!stylesInjected) this.applyModalInlineFallback(modal);
const modalHeader = create('div', { className: 'modal-header' });
modalHeader.append(
create('h2', { className: 'modal-title', text: t('modal.title', config.LANGUAGE) }),
create('button', { id: 'yt-enhancer-close', className: 'close-btn', text: '×', title: t('modal.closeTitle', config.LANGUAGE) })
);
const tabsNav = create('div', { className: 'tabs-nav' });
const tabFeaturesBtn = create('button', { className: 'tab-btn active', text: t('modal.tabs.features', config.LANGUAGE), dataset: { target: 'tab-features' } });
const tabAppearanceBtn = create('button', { className: 'tab-btn', text: t('modal.tabs.appearance', config.LANGUAGE), dataset: { target: 'tab-appearance' } });
tabsNav.append(tabFeaturesBtn, tabAppearanceBtn);
const modalContent = create('div', { className: 'modal-content' });
const tabFeatures = create('div', { id: 'tab-features', className: 'tab-pane active' });
const optionsList = create('div', { className: 'options-list' });
const createToggle = (id, title, description, checked) => {
const label = create('label', { className: 'feature-toggle' });
const textWrap = create('div', { className: 'toggle-text' });
textWrap.append(create('strong', { text: title }), create('span', { text: description }));
const switchWrap = create('div', { className: 'toggle-switch' });
switchWrap.append(create('input', { id, type: 'checkbox', checked }), create('span', { className: 'slider' }));
label.append(textWrap, switchWrap);
return label;
};
optionsList.append(
createToggle('cfg-layout', t('modal.features.layout.title', config.LANGUAGE), t('modal.features.layout.description', config.LANGUAGE), config.FEATURES.LAYOUT_ENHANCEMENT)
);
const layoutSettings = create('label', { id: 'layout-settings', className: 'feature-toggle feature-card-input', forId: 'cfg-videos-row' });
layoutSettings.style.display = config.FEATURES.LAYOUT_ENHANCEMENT ? 'flex' : 'none';
const layoutText = create('div', { className: 'toggle-text' });
layoutText.append(create('strong', { text: t('modal.features.videosPerRow', config.LANGUAGE) }), create('span', { text: t('modal.features.videosPerRowHint', config.LANGUAGE) }));
layoutSettings.append(layoutText, create('input', { id: 'cfg-videos-row', className: 'styled-input-small', type: 'number', min: 3, max: 8, value: config.VIDEOS_PER_ROW }));
optionsList.append(layoutSettings);
optionsList.append(
createToggle('cfg-shorts', t('modal.features.shorts.title', config.LANGUAGE), t('modal.features.shorts.description', config.LANGUAGE), config.FEATURES.SHORTS_REMOVAL),
createToggle('cfg-remove-relevant', t('modal.features.relevant.title', config.LANGUAGE), t('modal.features.relevant.description', config.LANGUAGE), config.FEATURES.REMOVE_RELEVANT),
createToggle('cfg-clock-enable', t('modal.features.clock.title', config.LANGUAGE), t('modal.features.clock.description', config.LANGUAGE), config.FEATURES.FULLSCREEN_CLOCK)
);
const languageCard = create('label', { className: 'feature-toggle feature-card-select', forId: 'cfg-language' });
const languageText = create('div', { className: 'toggle-text' });
languageText.append(create('strong', { text: t('modal.features.language.title', config.LANGUAGE) }), create('span', { text: t('modal.features.language.description', config.LANGUAGE) }));
const languageSelect = create('select', { id: 'cfg-language', className: 'styled-select' });
[{ value: 'en', label: 'English' }, { value: 'pt', label: 'Português' }].forEach(({ value, label }) => {
const option = create('option', { value, text: label });
if (config.LANGUAGE === value) option.selected = true;
languageSelect.appendChild(option);
});
languageCard.append(languageText, languageSelect);
optionsList.append(languageCard);
tabFeatures.appendChild(optionsList);
const tabAppearance = create('div', { id: 'tab-appearance', className: 'tab-pane' });
const appearanceGrid = create('div', { className: 'appearance-grid' });
const createControl = (id, labelText, inputEl, valueEl = null) => {
const group = create('div', { className: 'control-group' });
group.append(create('label', { text: labelText }));
if (valueEl) {
const wrap = create('div', { className: 'color-input-wrapper' });
wrap.append(inputEl, valueEl);
group.append(wrap);
} else {
group.append(inputEl);
}
inputEl.id = id;
return group;
};
appearanceGrid.append(
createControl('style-color', t('modal.clockStyle.textColor', config.LANGUAGE), create('input', { type: 'color', value: config.CLOCK_STYLE.color }), create('span', { className: 'color-value', text: config.CLOCK_STYLE.color })),
createControl('style-bg-color', t('modal.clockStyle.backgroundColor', config.LANGUAGE), create('input', { type: 'color', value: config.CLOCK_STYLE.bgColor }), create('span', { className: 'color-value', text: config.CLOCK_STYLE.bgColor })),
createControl('style-bg-opacity', t('modal.clockStyle.backgroundOpacity', config.LANGUAGE), create('input', { className: 'styled-input', type: 'number', min: 0, max: 1, step: 0.1, value: config.CLOCK_STYLE.bgOpacity })),
createControl('style-font-size', t('modal.clockStyle.fontSize', config.LANGUAGE), create('input', { className: 'styled-input', type: 'number', min: 12, max: 48, value: config.CLOCK_STYLE.fontSize })),
createControl('style-margin', t('modal.clockStyle.margin', config.LANGUAGE), create('input', { className: 'styled-input', type: 'number', min: 0, max: 120, value: config.CLOCK_STYLE.margin })),
createControl('style-border-radius', t('modal.clockStyle.borderRadius', config.LANGUAGE), create('input', { className: 'styled-input', type: 'number', min: 0, max: 50, value: config.CLOCK_STYLE.borderRadius || 12 }))
);
tabAppearance.appendChild(appearanceGrid);
modalContent.append(tabFeatures, tabAppearance);
const modalFooter = create('div', { className: 'modal-footer' });
const reloadNotice = create('p', { id: 'yt-enhancer-reload-note', className: 'reload-note', text: t('modal.reloadNotice', config.LANGUAGE) });
reloadNotice.style.display = 'none';
const modalActions = create('div', { className: 'modal-actions' });
const btnApply = create('button', { id: 'yt-enhancer-apply', className: 'btn btn-primary', text: t('modal.buttons.apply', config.LANGUAGE) });
const btnReload = create('button', { id: 'yt-enhancer-reload', className: 'btn btn-primary', text: t('modal.buttons.applyAndReload', config.LANGUAGE) });
btnReload.style.display = 'none';
modalActions.append(btnApply, btnReload);
modalFooter.append(reloadNotice, modalActions);
modal.append(modalHeader, tabsNav, modalContent, modalFooter);
const mountTarget = document.documentElement || document.body;
mountTarget.append(overlay, modal);
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'yt-enhancer-modal-title');
modalHeader.querySelector('.modal-title').id = 'yt-enhancer-modal-title';
const focusable = () => Array.from(modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter(el => !el.closest('.tab-pane') || el.closest('.tab-pane').classList.contains('active'));
let closed = false;
const closeModal = () => {
if (closed) return;
closed = true;
modal.remove();
overlay.remove();
const cleanupFunctions = this.cleanupFunctions;
this.cleanupFunctions = [];
cleanupFunctions.forEach((cleanup) => cleanup());
};
this.cleanupFunctions.push(Utils.safeAddEventListener(overlay, 'click', closeModal));
this.cleanupFunctions.push(Utils.safeAddEventListener(document.getElementById('yt-enhancer-close'), 'click', closeModal));
this.cleanupFunctions.push(Utils.safeAddEventListener(document, 'keydown', (e) => {
if (e.key === 'Escape') closeModal();
if (e.key === 'Tab') {
const nodes = focusable();
if (!nodes.length) return;
const first = nodes[0];
const last = nodes[nodes.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
}));
[tabFeaturesBtn, tabAppearanceBtn].forEach((btn) => {
this.cleanupFunctions.push(Utils.safeAddEventListener(btn, 'click', () => {
[tabFeaturesBtn, tabAppearanceBtn].forEach((b) => b.classList.remove('active'));
[tabFeatures, tabAppearance].forEach((pane) => pane.classList.remove('active'));
btn.classList.add('active');
(btn.dataset.target === 'tab-features' ? tabFeatures : tabAppearance).classList.add('active');
}));
});
this.cleanupFunctions.push(Utils.safeAddEventListener(document.getElementById('cfg-layout'), 'change', (e) => {
layoutSettings.style.display = e.target.checked ? 'flex' : 'none';
}));
['style-color', 'style-bg-color'].forEach(id => {
this.cleanupFunctions.push(Utils.safeAddEventListener(document.getElementById(id), 'input', (e) => {
if (e.target.nextElementSibling) e.target.nextElementSibling.textContent = e.target.value;
}));
});
const getNewConfig = () => Utils.sanitizeConfig({
LANGUAGE: document.getElementById('cfg-language').value,
VIDEOS_PER_ROW: parseInt(document.getElementById('cfg-videos-row').value, 10) || 5,
FEATURES: {
LAYOUT_ENHANCEMENT: document.getElementById('cfg-layout').checked,
SHORTS_REMOVAL: document.getElementById('cfg-shorts').checked,
REMOVE_RELEVANT: document.getElementById('cfg-remove-relevant').checked,
FULLSCREEN_CLOCK: document.getElementById('cfg-clock-enable').checked
},
CLOCK_STYLE: {
color: document.getElementById('style-color').value,
bgColor: document.getElementById('style-bg-color').value,
bgOpacity: parseFloat(document.getElementById('style-bg-opacity').value),
fontSize: parseInt(document.getElementById('style-font-size').value, 10),
margin: parseInt(document.getElementById('style-margin').value, 10),
borderRadius: parseInt(document.getElementById('style-border-radius').value, 10),
position: 'bottom-right'
}
}, ConfigManager.defaults);
const updateSaveButtons = () => {
const newConfig = getNewConfig();
const requiresReload = newConfig.LANGUAGE !== config.LANGUAGE;
btnApply.style.display = requiresReload ? 'none' : 'block';
btnReload.style.display = requiresReload ? 'block' : 'none';
reloadNotice.style.display = requiresReload ? 'block' : 'none';
};
this.cleanupFunctions.push(Utils.safeAddEventListener(document.getElementById('cfg-language'), 'change', updateSaveButtons));
this.cleanupFunctions.push(Utils.safeAddEventListener(btnApply, 'click', () => { onSave(getNewConfig()); closeModal(); }));
this.cleanupFunctions.push(Utils.safeAddEventListener(btnReload, 'click', () => { onSave(getNewConfig()); closeModal(); setTimeout(() => window.location.reload(), 100); }));
const focusTimer = setTimeout(() => document.getElementById('cfg-layout')?.focus(), 0);
this.cleanupFunctions.push(() => clearTimeout(focusTimer));
return true;
},
ensureStyles() {
const css = `
.yt-enhancer-modal { position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; width: min(420px, calc(100vw - 32px)) !important; max-height: 80vh !important; background: #121212 !important; color: #f1f1f1 !important; border: 1px solid #333 !important; border-radius: 12px !important; box-shadow: 0 12px 24px rgba(0,0,0,0.8) !important; font-family: 'Roboto', Arial, sans-serif !important; font-size: 14px !important; display: flex !important; flex-direction: column !important; z-index: 2147483647 !important; isolation: isolate !important; }
.yt-enhancer-modal input::-webkit-outer-spin-button, .yt-enhancer-modal input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.yt-enhancer-modal input[type=number] { -moz-appearance: textfield; }
.modal-header { height: 50px; border-bottom: 1px solid #333; display: flex; align-items: center; justify-content: flex-end; padding: 0 15px; position: relative; }
.modal-title { position: absolute; left: 50%; transform: translateX(-50%); margin: 0; font-size: 16px; font-weight: 500; color: #fff; }
.close-btn { background: none; border: none; color: #aaa; font-size: 24px; cursor: pointer; padding: 0 5px; }
.close-btn:hover { color: #fff; }
.tabs-nav { display: flex; background: #1a1a1a; border-bottom: 1px solid #333; }
.tab-btn { flex: 1; padding: 12px; background: transparent; border: none; color: #888; cursor: pointer; font-weight: 500; border-bottom: 2px solid transparent; }
.tab-btn:hover { color: #ccc; background: #222; }
.tab-btn.active { color: #3ea6ff; border-bottom-color: #3ea6ff; background: #1a1a1a; }
.modal-content { padding: 20px; overflow-y: auto; flex: 1; }
.tab-pane { display: none; }
.tab-pane.active { display: block; animation: fadeEffect 0.2s; }
@keyframes fadeEffect { from {opacity: 0;} to {opacity: 1;} }
.options-list { display: flex; flex-direction: column; gap: 15px; }
.feature-toggle { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: #1e1e1e; border-radius: 8px; cursor: pointer; }
.feature-toggle:hover { background: #252525; }
.feature-card-select, .feature-card-input { gap: 16px; }
.feature-card-select .styled-select { max-width: 140px; }
.toggle-text strong { display: block; font-size: 14px; margin-bottom: 2px; }
.toggle-text span { font-size: 12px; color: #aaa; }
.toggle-switch { position: relative; width: 40px; height: 22px; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; border-radius: 22px; transition: .3s; }
.slider:before { position: absolute; content: ''; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; border-radius: 50%; transition: .3s; }
.yt-enhancer-modal input:checked + .slider { background-color: #3ea6ff; }
.yt-enhancer-modal input:checked + .slider:before { transform: translateX(18px); }
.appearance-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.control-group { display: flex; flex-direction: column; gap: 8px; }
.styled-input, .styled-select { background: #1a1a1a; border: 1px solid #333; color: white; padding: 10px; border-radius: 6px; width: 100%; box-sizing: border-box; }
.styled-input-small { width: 60px; padding: 5px; background: #222; border: 1px solid #444; color: white; border-radius: 4px; text-align: center; }
.color-input-wrapper { display: flex; align-items: center; gap: 10px; background: #1a1a1a; padding: 5px; border: 1px solid #333; border-radius: 6px; }
.yt-enhancer-modal input[type='color'] { border: none; width: 30px; height: 30px; padding: 0; background: none; cursor: pointer; }
.modal-footer { padding: 15px 20px; border-top: 1px solid #333; display: flex; align-items: center; gap: 12px; }
.reload-note { margin: 0; color: #f6cf6a; font-size: 12px; flex: 1; min-width: 0; }
.modal-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-left: auto; }
.btn { min-width: 156px; padding: 8px 20px; border: none; border-radius: 18px; cursor: pointer; font-weight: 500; display: inline-flex; justify-content: center; }
.btn-primary { background: #3ea6ff; color: #000; }
.btn-primary:hover { opacity: 0.9; }
`;
return Utils.injectCSS(css, this.styleId);
},
cleanup() {
const cleanupFunctions = this.cleanupFunctions;
this.cleanupFunctions = [];
cleanupFunctions.forEach((cleanup) => cleanup());
document.getElementById('yt-enhancer-settings-modal')?.remove();
document.getElementById('yt-enhancer-overlay')?.remove();
document.getElementById(this.styleId)?.remove();
}
};
// =======================================================
// 3. STYLE MANAGER
// =======================================================
const StyleManager = {
styleId: 'yt-enhancer-styles',
cleanupFunctions: [],
lastCss: null,
init() {
this.cleanupFunctions.push(
EventBus.on('configChanged', (config) => this.apply(config)),
Utils.safeAddEventListener(document, 'yt-navigate-finish', () => this.apply(ConfigManager.load()))
);
},
apply(config) {
let css = '';
if (config.FEATURES.LAYOUT_ENHANCEMENT) {
css += `ytd-rich-grid-renderer { --ytd-rich-grid-items-per-row: ${config.VIDEOS_PER_ROW} !important; } @media (max-width: 1200px) { ytd-rich-grid-renderer { --ytd-rich-grid-items-per-row: ${Math.min(config.VIDEOS_PER_ROW, 4)} !important; } }`;
}
if (config.FEATURES.SHORTS_REMOVAL) {
css += `ytd-rich-section-renderer:has(ytd-rich-shelf-renderer[is-shorts]), ytd-reel-shelf-renderer, grid-shelf-view-model:has(a[href^="/shorts/"]), shorts-lockup-view-model:has(a[href^="/shorts/"]), ytd-video-renderer:has(ytd-thumbnail-overlay-time-status-renderer[overlay-style="SHORTS"]), ytd-guide-entry-renderer:has(a[href="/shorts"], a[href^="/shorts/"]), ytd-mini-guide-entry-renderer:has(a[href="/shorts"], a[href^="/shorts/"]) { display: none !important; }`;
}
if (css === this.lastCss && document.getElementById(this.styleId)) return;
this.lastCss = css;
Utils.injectCSS(css, this.styleId);
},
cleanup() {
this.cleanupFunctions.forEach((cleanup) => cleanup());
this.cleanupFunctions = [];
this.lastCss = null;
document.getElementById(this.styleId)?.remove();
}
};
// =======================================================
// SHORTS MANAGER
// =======================================================
const ShortsManager = {
observer: null,
observedRoot: null,
listenersCleanup: [],
eventCleanup: null,
hiddenElements: new Set(),
previousDisplay: new WeakMap(),
pendingRoots: new Set(),
enabled: false,
scheduledPrune: null,
init(config) {
this.scheduledPrune = Utils.scheduleFrame(() => this.flushPendingRoots());
this.eventCleanup = EventBus.on('configChanged', (newConfig) => this.updateConfig(newConfig));
this.updateConfig(config);
},
updateConfig(config) {
const shouldEnable = Boolean(config?.FEATURES?.SHORTS_REMOVAL);
if (shouldEnable === this.enabled) return;
this.enabled = shouldEnable;
if (this.enabled) this.start();
else this.stop();
},
start() {
if (!document.documentElement) return;
this.ensureObserverRoot();
this.queueRoot(this.observedRoot || Utils.getAppRoot());
if (this.listenersCleanup.length === 0) {
const rescan = () => {
this.ensureObserverRoot();
this.queueRoot(this.observedRoot || Utils.getAppRoot());
};
this.listenersCleanup.push(
Utils.safeAddEventListener(document, 'yt-navigate-start', rescan),
Utils.safeAddEventListener(document, 'yt-navigate-finish', rescan),
Utils.safeAddEventListener(document, 'yt-page-data-updated', rescan),
Utils.safeAddEventListener(window, 'popstate', rescan)
);
}
},
ensureObserverRoot() {
const root = Utils.getAppRoot();
if (this.observer && this.observedRoot === root && root?.isConnected) return;
this.observer?.disconnect();
this.observer = null;
this.observedRoot = root || null;
if (this.observedRoot) {
this.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) this.queueRoot(node);
}
});
this.observer.observe(this.observedRoot, { childList: true, subtree: true });
}
},
stop() {
this.observer?.disconnect();
this.observer = null;
this.observedRoot = null;
this.scheduledPrune?.cancel();
this.pendingRoots.clear();
this.listenersCleanup.forEach((cleanup) => cleanup());
this.listenersCleanup = [];
this.restoreHiddenElements();
},
queueRoot(node) {
if (!this.enabled) return;
const root = node instanceof Element || node instanceof Document ? node : node?.parentElement;
if (!root) return;
for (const pendingRoot of this.pendingRoots) {
if (pendingRoot.contains(root)) return;
if (root.contains(pendingRoot)) this.pendingRoots.delete(pendingRoot);
}
this.pendingRoots.add(root);
this.scheduledPrune();
},
markHidden(element) {
if (!(element instanceof HTMLElement) || this.hiddenElements.has(element)) return;
this.previousDisplay.set(element, {
value: element.style.getPropertyValue('display'),
priority: element.style.getPropertyPriority('display')
});
element.style.setProperty('display', 'none', 'important');
this.hiddenElements.add(element);
},
restoreHiddenElements() {
for (const element of this.hiddenElements) {
if (!(element instanceof HTMLElement)) continue;
const previous = this.previousDisplay.get(element);
const stillOwned = element.style.getPropertyValue('display') === 'none'
&& element.style.getPropertyPriority('display') === 'important';
if (stillOwned && previous) {
if (previous.value) element.style.setProperty('display', previous.value, previous.priority);
else element.style.removeProperty('display');
}
this.previousDisplay.delete(element);
}
this.hiddenElements.clear();
},
flushPendingRoots() {
for (const element of this.hiddenElements) {
if (!element.isConnected) {
this.hiddenElements.delete(element);
this.previousDisplay.delete(element);
}
}
if (!this.enabled) return;
const roots = [...this.pendingRoots];
this.pendingRoots.clear();
for (const root of roots) this.pruneRoot(root);
},
pruneRoot(root) {
Utils.queryWithin(root, 'ytd-reel-shelf-renderer, ytd-rich-shelf-renderer[is-shorts], grid-shelf-view-model:has(a[href^="/shorts/"])').forEach((shelf) => {
this.markHidden(shelf.closest('ytd-rich-section-renderer') || shelf);
});
Utils.queryWithin(root, 'ytd-thumbnail-overlay-time-status-renderer[overlay-style="SHORTS"]').forEach((marker) => {
this.markHidden(marker.closest('ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer'));
});
Utils.queryWithin(root, 'a[href="/shorts"], a[href^="/shorts/"]').forEach((link) => {
this.markHidden(link.closest('ytd-guide-entry-renderer, ytd-mini-guide-entry-renderer, ytd-compact-link-renderer, tp-yt-paper-item, ytd-reel-item-renderer, ytd-rich-item-renderer, shorts-lockup-view-model'));
});
},
cleanup() {
this.stop();
this.eventCleanup?.();
this.eventCleanup = null;
this.scheduledPrune = null;
}
};
// =======================================================
// RELEVANT SUBSCRIPTIONS SHELF MANAGER
// =======================================================
const RelevantShelfManager = {
observer: null,
observedRoot: null,
listenersCleanup: [],
eventCleanup: null,
hiddenElements: new Set(),
previousDisplay: new WeakMap(),
pendingRoots: new Set(),
enabled: false,
scheduledPrune: null,
relevantTitles: new Set(['most relevant', 'mais relevantes']),
init(config) {
this.scheduledPrune = Utils.scheduleFrame(() => this.flushPendingRoots());
this.eventCleanup = EventBus.on('configChanged', (newConfig) => this.updateConfig(newConfig));
this.updateConfig(config);
},
updateConfig(config) {
const shouldEnable = Boolean(config?.FEATURES?.REMOVE_RELEVANT);
if (shouldEnable === this.enabled) return;
this.enabled = shouldEnable;
if (this.enabled) this.start();
else this.stop();
},
start() {
if (!document.documentElement) return;
this.ensureObserverRoot();
this.queueRoot(this.observedRoot || Utils.getAppRoot());
if (this.listenersCleanup.length === 0) {
const rescan = () => {
this.ensureObserverRoot();
this.queueRoot(this.observedRoot || Utils.getAppRoot());
};
this.listenersCleanup.push(
Utils.safeAddEventListener(document, 'yt-navigate-start', rescan),
Utils.safeAddEventListener(document, 'yt-navigate-finish', rescan),
Utils.safeAddEventListener(document, 'yt-page-data-updated', rescan),
Utils.safeAddEventListener(window, 'popstate', rescan)
);
}
},
ensureObserverRoot() {
const root = Utils.getAppRoot();
if (this.observer && this.observedRoot === root && root?.isConnected) return;
this.observer?.disconnect();
this.observer = null;
this.observedRoot = root || null;
if (this.observedRoot) {
this.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'characterData' || mutation.addedNodes.length > 0) this.queueRoot(mutation.target);
}
});
this.observer.observe(this.observedRoot, { childList: true, characterData: true, subtree: true });
}
},
stop() {
this.observer?.disconnect();
this.observer = null;
this.observedRoot = null;
this.scheduledPrune?.cancel();
this.pendingRoots.clear();
this.listenersCleanup.forEach((cleanup) => cleanup());
this.listenersCleanup = [];
this.restoreHiddenElements();
},
queueRoot(node) {
const root = node instanceof Element || node instanceof Document ? node : node?.parentElement;
if (!root) return;
const scanRoot = root.closest?.('ytd-rich-shelf-renderer, ytd-rich-section-renderer') || root;
for (const pendingRoot of this.pendingRoots) {
if (pendingRoot.contains(scanRoot)) return;
if (scanRoot.contains(pendingRoot)) this.pendingRoots.delete(pendingRoot);
}
this.pendingRoots.add(scanRoot);
this.scheduledPrune();
},
normalizeTitle(value) {
return value.normalize('NFKC').trim().replace(/\s+/g, ' ').toLocaleLowerCase();
},
isRelevantShelf(shelf) {
const titleSelectors = [
':scope #rich-shelf-header #title',
':scope #title-container > #title',
':scope .rich-shelf-header #title',
':scope #title'
];
return titleSelectors.some((selector) => [...shelf.querySelectorAll(selector)].some((title) => (
this.relevantTitles.has(this.normalizeTitle(title.textContent || ''))
)));
},
markHidden(element) {
if (!(element instanceof HTMLElement) || this.hiddenElements.has(element)) return;
this.previousDisplay.set(element, {
value: element.style.getPropertyValue('display'),
priority: element.style.getPropertyPriority('display')
});
element.style.setProperty('display', 'none', 'important');
this.hiddenElements.add(element);
},
restoreHiddenElements() {
for (const element of this.hiddenElements) {
if (!(element instanceof HTMLElement)) continue;
const previous = this.previousDisplay.get(element);
const stillOwned = element.style.getPropertyValue('display') === 'none'
&& element.style.getPropertyPriority('display') === 'important';
if (stillOwned && previous) {
if (previous.value) element.style.setProperty('display', previous.value, previous.priority);
else element.style.removeProperty('display');
}
this.previousDisplay.delete(element);
}
this.hiddenElements.clear();
},
flushPendingRoots() {
for (const element of this.hiddenElements) {
if (!element.isConnected) {
this.hiddenElements.delete(element);
this.previousDisplay.delete(element);
}
}
if (!this.enabled || location.pathname !== '/feed/subscriptions') {
this.restoreHiddenElements();
this.pendingRoots.clear();
return;
}
const roots = [...this.pendingRoots];
this.pendingRoots.clear();
for (const root of roots) this.pruneRoot(root);
},
pruneRoot(root) {
Utils.queryWithin(root, 'ytd-rich-shelf-renderer').forEach((shelf) => {
if (!this.isRelevantShelf(shelf)) return;
this.markHidden(shelf.closest('ytd-rich-section-renderer') || shelf);
});
},
cleanup() {
this.stop();
this.eventCleanup?.();
this.eventCleanup = null;
this.scheduledPrune = null;
}
};
// =======================================================
// CLOCK MANAGER
// =======================================================
const ClockManager = {
clockElement: null,
updateTimer: null,
config: null,
observer: null,
structureObserver: null,
observerCallback: null,
observedMenus: new Set(),
menuSelector: '.ytp-settings-menu, .ytp-contextmenu, .ytp-popup.ytp-contextmenu, .ytp-panel-menu',
playerElement: null,
fullscreenHandler: null,
navigationHandler: null,
visibilityCleanup: null,
eventCleanup: null,
init(config) {
this.config = config;
this.observerCallback = Utils.scheduleFrame(() => {
this.adjustPosition();
this.refreshVisibility();
});
this.eventCleanup = EventBus.on('configChanged', (newConfig) => this.updateConfig(newConfig));
this.fullscreenHandler = () => this.handleFullscreen();
this.navigationHandler = () => {
if (!this.config.FEATURES.FULLSCREEN_CLOCK) return;
this.resolvePlayerElement(true);
this.handleFullscreen();
};
document.addEventListener('fullscreenchange', this.fullscreenHandler);
document.addEventListener('yt-navigate-finish', this.navigationHandler);
this.updateConfig(config);
},
resolvePlayerElement(force = false) {
if (!this.config?.FEATURES?.FULLSCREEN_CLOCK) return null;
const current = this.playerElement;
if (!force && current?.isConnected) return current;
const fullscreenPlayer = document.fullscreenElement?.matches?.('#movie_player, .html5-video-player')
? document.fullscreenElement
: document.fullscreenElement?.querySelector?.('#movie_player, .html5-video-player');
const player = fullscreenPlayer || Utils.DOMCache.get('#movie_player', force) || Utils.DOMCache.get('.html5-video-player', force);
if (player !== current) {
this.disconnectObservers();
this.visibilityCleanup?.();
this.visibilityCleanup = null;
this.playerElement = player || null;
if (this.playerElement && document.fullscreenElement) {
this.setupObserver();
this.setupVisibilityListeners();
}
} else if (!player) {
this.playerElement = null;
}
return this.playerElement;
},
updateConfig(newConfig) {
this.config = newConfig;
if (!newConfig.FEATURES.FULLSCREEN_CLOCK) {
this.stopRuntime();
return;
}
this.resolvePlayerElement();
this.updateStyle();
this.handleFullscreen();
},
createClock() {
const existing = document.getElementById('yt-enhancer-clock');
if (existing) {
this.clockElement = existing;
return;
}
const clock = document.createElement('div');
clock.id = 'yt-enhancer-clock';
clock.style.cssText = `position: fixed !important; pointer-events: none !important; z-index: 2147483647 !important; font-family: "Roboto", sans-serif !important; font-weight: 400 !important; padding: 6px 14px !important; text-shadow: 0 1px 3px rgba(0,0,0,0.8) !important; display: none; box-shadow: 0 2px 10px rgba(0,0,0,0.3) !important; transition: transform 0.18s cubic-bezier(0.4, 0.0, 0.2, 1), opacity 0.2s !important; will-change: transform !important;`;
(document.fullscreenElement || document.documentElement).appendChild(clock);
this.clockElement = clock;
this.updateStyle();
},
ensureClockMount() {
const fullscreenRoot = document.fullscreenElement;
if (fullscreenRoot && this.clockElement && this.clockElement.parentElement !== fullscreenRoot) {
fullscreenRoot.appendChild(this.clockElement);
}
},
setupObserver() {
if (!this.playerElement || !this.config.FEATURES.FULLSCREEN_CLOCK) return;
this.disconnectObservers();
this.observer = new MutationObserver(() => this.observerCallback());
this.structureObserver = new MutationObserver((mutations) => {
let menuStructureChanged = false;
let hasDetachedMenu = false;
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
menuStructureChanged = this.observeMenusWithin(node) || menuStructureChanged;
}
if (mutation.removedNodes.length > 0 && !hasDetachedMenu) {
hasDetachedMenu = [...this.observedMenus].some((menu) => !menu.isConnected || !this.playerElement.contains(menu));
}
}
if (hasDetachedMenu) {
this.refreshMenuObservers();
menuStructureChanged = true;
}
if (menuStructureChanged) this.observerCallback();
});
this.structureObserver.observe(this.playerElement, { childList: true, subtree: true });
this.refreshMenuObservers();
},
observeMenusWithin(root) {
if (!this.observer) return false;
let observedNewMenu = false;
for (const menu of Utils.queryWithin(root, this.menuSelector)) {
if (this.observedMenus.has(menu)) continue;
this.observer.observe(menu, {
attributes: true,
attributeFilter: ['class', 'style', 'aria-hidden']
});
this.observedMenus.add(menu);
observedNewMenu = true;
}
return observedNewMenu;
},
refreshMenuObservers() {
if (!this.observer || !this.playerElement) return;
this.observer.disconnect();
this.observedMenus.clear();
this.observer.observe(this.playerElement, {
attributes: true,
attributeFilter: ['class']
});
this.observeMenusWithin(this.playerElement);
},
disconnectObservers() {
this.observer?.disconnect();
this.structureObserver?.disconnect();
this.observer = null;
this.structureObserver = null;
this.observedMenus.clear();
},
setupVisibilityListeners() {
if (!this.playerElement || this.visibilityCleanup) return;
const refresh = () => this.observerCallback();
const cleanups = [
Utils.safeAddEventListener(this.playerElement, 'mousemove', refresh, { passive: true }),
Utils.safeAddEventListener(this.playerElement, 'pointermove', refresh, { passive: true }),
Utils.safeAddEventListener(this.playerElement, 'touchstart', refresh, { passive: true }),
Utils.safeAddEventListener(this.playerElement, 'transitionend', refresh, { passive: true }),
Utils.safeAddEventListener(document, 'keydown', refresh, { passive: true, capture: true })
];
this.visibilityCleanup = () => {
cleanups.forEach((cleanup) => cleanup());
this.visibilityCleanup = null;
};
},
areControlsVisible() {
if (!this.playerElement) return false;
if (!this.playerElement.classList.contains('ytp-autohide')) return true;
const controls = this.playerElement.querySelector('.ytp-chrome-bottom, .ytp-gradient-bottom');
if (!controls) return false;
const style = getComputedStyle(controls);
return controls.getClientRects().length > 0
&& style.display !== 'none'
&& style.visibility !== 'hidden'
&& Number(style.opacity) > 0.05;
},
getControlsClearance() {
if (!this.areControlsVisible()) return 0;
const controls = this.playerElement?.querySelector('.ytp-chrome-bottom');
const fullscreenRoot = document.fullscreenElement;
if (!controls || !fullscreenRoot) return 64;
const controlsRect = controls.getBoundingClientRect();
const rootRect = fullscreenRoot.getBoundingClientRect();
if (controlsRect.height <= 0 || rootRect.height <= 0) return 64;
return Utils.clamp(rootRect.bottom - controlsRect.top, 48, 180, 64);
},
adjustPosition() {
if (!this.clockElement) return;
if (!this.playerElement?.isConnected) this.resolvePlayerElement(true);
if (!this.playerElement) return;
const margin = this.config.CLOCK_STYLE.margin;
this.clockElement.style.bottom = `${margin + this.getControlsClearance()}px`;
},
isPlayerMenuOpen() {
const fullscreenRoot = document.fullscreenElement;
if (!fullscreenRoot) return false;
return [...fullscreenRoot.querySelectorAll(this.menuSelector)].some((element) => {
const style = getComputedStyle(element);
return element.getClientRects().length > 0
&& style.display !== 'none'
&& style.visibility !== 'hidden'
&& Number(style.opacity) !== 0;
});
},
refreshVisibility() {
if (!this.clockElement) return;
const shouldShow = Boolean(this.config?.FEATURES?.FULLSCREEN_CLOCK
&& document.fullscreenElement
&& !this.isPlayerMenuOpen());
this.clockElement.style.display = shouldShow ? 'block' : 'none';
},
updateStyle() {
if (!this.clockElement) return;
const style = this.config.CLOCK_STYLE;
const hexToRgb = (hex) => {
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return match ? `${parseInt(match[1], 16)},${parseInt(match[2], 16)},${parseInt(match[3], 16)}` : '0,0,0';
};
this.clockElement.style.backgroundColor = `rgba(${hexToRgb(style.bgColor)}, ${style.bgOpacity})`;
this.clockElement.style.color = style.color;
this.clockElement.style.fontSize = `${style.fontSize}px`;
this.clockElement.style.right = '15px';
this.clockElement.style.borderRadius = `${style.borderRadius}px`;
this.adjustPosition();
},
updateTime() {
if (this.clockElement) {
this.clockElement.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
},
scheduleTimeUpdate() {
clearTimeout(this.updateTimer);
const now = new Date();
const delay = ((60 - now.getSeconds()) * 1000) - now.getMilliseconds() + 50;
this.updateTimer = setTimeout(() => {
this.updateTimer = null;
if (!document.fullscreenElement || !this.config.FEATURES.FULLSCREEN_CLOCK) return;
this.updateTime();
this.scheduleTimeUpdate();
}, delay);
},
handleFullscreen() {
if (!this.config.FEATURES.FULLSCREEN_CLOCK) {
this.stopRuntime();
return;
}
if (document.fullscreenElement) {
this.resolvePlayerElement(true);
if (!this.clockElement) this.createClock();
this.ensureClockMount();
if (!this.observer) this.setupObserver();
this.setupVisibilityListeners();
this.updateStyle();
this.updateTime();
this.refreshVisibility();
this.scheduleTimeUpdate();
} else {
this.stopRuntime();
}
},
stopRuntime() {
this.disconnectObservers();
this.observerCallback?.cancel();
this.visibilityCleanup?.();
clearTimeout(this.updateTimer);
this.updateTimer = null;
this.clockElement?.remove();
this.clockElement = null;
this.playerElement = null;
},
cleanup() {
this.stopRuntime();
if (this.fullscreenHandler) document.removeEventListener('fullscreenchange', this.fullscreenHandler);
if (this.navigationHandler) document.removeEventListener('yt-navigate-finish', this.navigationHandler);
this.eventCleanup?.();
this.eventCleanup = null;
this.observerCallback = null;
this.fullscreenHandler = null;
this.navigationHandler = null;
this.config = null;
}
};
// =======================================================
// INITIALIZATION CORE
// =======================================================
const EnhancerCore = {
initialized: false,
cleanupFunctions: [],
init() {
if (this.initialized) return;
this.initialized = true;
try {
this.cleanupFunctions.push(
Utils.safeAddEventListener(document, 'yt-navigate-start', () => Utils.DOMCache.refresh()),
Utils.safeAddEventListener(document, 'yt-page-data-updated', () => Utils.DOMCache.refresh()),
Utils.safeAddEventListener(window, 'keydown', (event) => {
if (event.altKey && event.shiftKey && (event.code === 'KeyS' || event.key?.toLowerCase() === 's')) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
SettingsLauncher.open('shortcut_alt_shift_s');
}
}, { capture: true })
);
const config = ConfigManager.load();
SettingsLauncher.registerSafeApi();
try { StyleManager.init(); StyleManager.apply(config); } catch (error) { console.error('[YT Enhancer] StyleManager init failed:', error); }
try { ShortsManager.init(config); } catch (error) { console.error('[YT Enhancer] ShortsManager init failed:', error); }
try { RelevantShelfManager.init(config); } catch (error) { console.error('[YT Enhancer] RelevantShelfManager init failed:', error); }
try { ClockManager.init(config); } catch (error) { console.error('[YT Enhancer] ClockManager init failed:', error); }
this.cleanupFunctions.push(Utils.safeAddEventListener(window, 'beforeunload', () => this.cleanup(), { once: true }));
log(`v${ConfigManager.CONFIG_VERSION} iniciado com sucesso.`);
} catch (error) {
console.error('[YT Enhancer] Falha na inicialização:', error);
this.cleanup();
}
},
cleanup() {
if (!this.initialized) return;
this.initialized = false;
const cleanupFunctions = this.cleanupFunctions;
this.cleanupFunctions = [];
cleanupFunctions.forEach((cleanup) => cleanup());
UIManager.cleanup();
ClockManager.cleanup();
ShortsManager.cleanup();
RelevantShelfManager.cleanup();
StyleManager.cleanup();
SettingsLauncher.cleanup();
Utils.DOMCache.refresh();
EventBus.clear();
}
};
const BootstrapGate = {
evaluate() {
const hostnameAllowed = location.hostname === 'www.youtube.com';
let isTopFrame = false;
try { isTopFrame = window === window.top; } catch (error) {}
const shouldInit = hostnameAllowed && isTopFrame;
return { shouldInit };
}
};
if (BootstrapGate.evaluate().shouldInit) {
SettingsLauncher.registerMenuCommand();
try { StyleManager.apply(ConfigManager.load()); } catch (error) { console.error('[YT Enhancer] Early style injection failed:', error); }
if (document.readyState === 'loading') {
Utils.safeAddEventListener(document, 'DOMContentLoaded', () => {
SettingsLauncher.registerMenuCommand();
EnhancerCore.init();
});
} else {
SettingsLauncher.registerMenuCommand();
EnhancerCore.init();
}
}
})();