// ==UserScript== // @name IMDb Enhanced // @namespace https://github.com/SysAdminDoc // @version 2.3.1 // @description Premium IMDb overhaul: cleaner pages, modern themes, refined score widgets, section controls, spoiler protection, quick navigation, richer external links, TV tools, search shortcuts, and polished settings import/export // @author SysAdminDoc // @match https://www.imdb.com/title/* // @match https://www.imdb.com/name/* // @match https://www.imdb.com/*/title/* // @match https://www.imdb.com/*/name/* // @match https://www.imdb.com/user/*/watchlist* // @match https://m.imdb.com/title/* // @match https://m.imdb.com/name/* // @match https://www.cineby.app/search // @match https://www.cineby.gd/search // @match https://www.cineby.sc/search // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM_listValues // @grant GM_deleteValue // @connect www.rottentomatoes.com // @connect backend.metacritic.com // @connect letterboxd.com // @connect www.justwatch.com // @connect www.opensubtitles.org // @connect www.youtube.com // @connect localhost // @connect 127.0.0.1 // @run-at document-start // @noframes // @license MIT // ==/UserScript== (function () { 'use strict'; // ========================================================================= // CONSTANTS & CONFIG // ========================================================================= const VERSION = '2.3.1'; const PREFIX = 'imdb_enh_'; const CINEBY_QUERY_KEY = PREFIX + 'cineby_query'; const CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days const CACHE_UNAVAILABLE_TTL = 24 * 60 * 60 * 1000; // 24 hours const CACHE_MAX_ENTRIES = 120; const TITLE_STACK_ORDER = { quickCopyID: 10, searchButtons: 20, externalLinks: 30, trailerPopover: 32, servarrIntegration: 35, tvShowEnhancements: 40, }; const CINEBY_HOSTS = [ { label:'Cineby SC', url:'https://www.cineby.sc/search' }, { label:'Cineby GD', url:'https://www.cineby.gd/search' }, { label:'Cineby App', url:'https://www.cineby.app/search' }, ]; const DEFAULT_WATCH_SITES = [ { name:'Cineby', color:'#6366f1', url:CINEBY_HOSTS[0].url, storeQuery:true }, { name:'Popcorn', color:'#10b981', url:'https://popcornmovies.org/search/{{TITLE_DASH}}' }, { name:'XPrime', color:'#f59e0b', url:'https://xprime.su/search/{{TITLE_DASH}}' }, { name:'Aether', color:'#8b5cf6', url:'https://aether.mom/browse/{{TITLE}}' }, { name:'Fmovies+', color:'#ef4444', url:'https://fmovies.gd/search/{{TITLE_DASH}}' }, { name:'Rive', color:'#ec4899', url:'https://rivestream.app/search?q={{TITLE}}' }, { name:'67Movies', color:'#06b6d4', url:'https://67movies.net/search/{{TITLE_DASH}}' }, ]; const DEFAULT_EXTERNAL_SITES = [ { name:'Rotten Tomatoes', color:'#fa320a', url:'https://www.rottentomatoes.com/search?search={{TITLE}}' }, { name:'Letterboxd', color:'#00d735', url:'https://letterboxd.com/imdb/{{IMDB_ID}}/' }, { name:'TMDB', color:'#01b4e4', url:'https://www.themoviedb.org/search/movie?query={{TITLE}}' }, { name:'YouTube', color:'#ff0000', url:'https://www.youtube.com/results?search_query={{TITLE}}%20trailer' }, { name:'Wikipedia', color:'#636466', url:'https://en.wikipedia.org/w/index.php?search={{TITLE}}+film' }, { name:'JustWatch', color:'#fbc500', url:'https://www.justwatch.com/us/search?q={{TITLE}}' }, { name:'Trakt', color:'#ed1c24', url:'https://trakt.tv/search/imdb/{{IMDB_ID}}?id_type={{TRAKT_TYPE}}' }, ]; const DEFAULTS = { // Cleanup removeAds: true, removeProUpsell: true, removeNewsSection: true, removeRelatedInterests: true, removeContribution: true, removeSponsoredRecs: true, removeAppBanner: true, // Appearance modernUI: true, compactHeader: true, enhancedRatingDisplay: true, widerLayout: true, ratingColorCoding: true, // Theme themeVariant: 'dark', // dark | oled | midnight | light | highContrast themeAuto: false, // Sections collapsibleSections: true, spoilerBlur: true, quickNav: true, // Scores inlineRTScore: true, inlineLetterboxdScore: true, inlineMetacriticScore: true, streamAvailability: true, // Links searchButtons: true, externalLinks: true, expandedLinkMenu: true, trailerPopover: true, watchSites: DEFAULT_WATCH_SITES, externalSites: DEFAULT_EXTERNAL_SITES, cinebyHost: CINEBY_HOSTS[0].url, watchedMarking: true, userMarks: {}, servarrIntegration: false, radarrUrl: 'http://localhost:7878', radarrApiKey: '', radarrRootFolderPath: '', radarrQualityProfileId: '1', sonarrUrl: 'http://localhost:8989', sonarrApiKey: '', sonarrRootFolderPath: '', sonarrQualityProfileId: '1', sonarrLanguageProfileId: '1', // TV tvEpisodeTools: true, tvShowEnhancements: true, subtitleLinks: true, // Utility quickCopyID: true, watchlistBatch: true, keyboardShortcuts: false, }; const FEATURE_DETAILS = { removeAds: 'Removes ad slots, tracking pixels, sponsored media, and injected ad wrappers.', removeProUpsell: 'Hides IMDbPro prompts and add-to-list upsells from title and name pages.', removeNewsSection: 'Keeps the page focused by removing IMDb news modules.', removeRelatedInterests: 'Hides broad interest recommendations that dilute title and cast pages.', removeContribution: 'Removes contribution calls to action from detail pages.', removeSponsoredRecs: 'Suppresses sponsored recommendation blocks where IMDb inserts them.', removeAppBanner: 'Hides app-install prompts and mobile app banners.', modernUI: 'Applies the cohesive dark surface, typography, focus, and component treatment.', compactHeader: 'Slims the IMDb header while keeping it readable and stable.', enhancedRatingDisplay: 'Elevates IMDb rating and popularity blocks with clearer emphasis.', widerLayout: 'Uses more horizontal room on desktop while preserving mobile readability.', ratingColorCoding: 'Adds a small quality label beside the IMDb score.', collapsibleSections: 'Adds per-section collapse controls and remembers each state.', spoilerBlur: 'Softens long plot text until you intentionally reveal it.', quickNav: 'Adds a right-side section navigator on wide screens.', inlineRTScore: 'Shows Rotten Tomatoes score feedback inline when available.', inlineLetterboxdScore: 'Shows Letterboxd average ratings inline for films when available.', inlineMetacriticScore: 'Shows Metacritic score feedback inline when available.', streamAvailability: 'Shows one-glance JustWatch streaming providers when available.', searchButtons: 'Adds prominent, keyboard-friendly watch-site links near the title.', externalLinks: 'Adds trusted research and trailer links near the title.', expandedLinkMenu: 'Groups additional movie, review, subtitle, and TV lookup links.', trailerPopover: 'Adds an in-page trailer modal backed by a click-to-fetch YouTube lookup.', watchedMarking: 'Adds local Watched and Skip marks to title posters and recommendation cards.', servarrIntegration: 'Adds optional local Radarr/Sonarr quick-add buttons when API settings are configured.', tvEpisodeTools: 'Blurs episode synopses and surfaces the highest-rated episodes where episode data is present.', tvShowEnhancements: 'Adds TV-specific lookup shortcuts on series pages.', subtitleLinks: 'Adds subtitle lookup links in the details section.', quickCopyID: 'Adds a visible IMDb ID copy button beside the title.', watchlistBatch: 'Adds a watchlist-page button that copies all visible IMDb title IDs.', keyboardShortcuts: 'Optional. Enables ? for settings, c to copy, r for rating, and t for top.', }; // ========================================================================= // STORAGE HELPERS // ========================================================================= const get = (k) => GM_getValue(PREFIX + k, DEFAULTS[k]); const set = (k, v) => GM_setValue(PREFIX + k, v); function cacheGet(key) { try { const storageKey = 'cache_' + key; const raw = GM_getValue(storageKey, null); if (!raw) return null; const { data, ts, ttl } = JSON.parse(raw); if (!ts || Date.now() - ts > (ttl || CACHE_TTL)) { if (typeof GM_deleteValue === 'function') GM_deleteValue(storageKey); return null; } return data; } catch { return null; } } function cacheSet(key, data, ttl = CACHE_TTL) { GM_setValue('cache_' + key, JSON.stringify({ data, ts: Date.now(), ttl })); } function cacheSetUnavailable(key) { cacheSet(key, { unavailable: true }, CACHE_UNAVAILABLE_TTL); } function cacheGC() { if (cacheGC._ran) return; cacheGC._ran = true; try { const now = Date.now(); const live = []; GM_listValues().forEach(storageKey => { if (!storageKey.startsWith('cache_')) return; try { const raw = GM_getValue(storageKey, null); const entry = raw ? JSON.parse(raw) : null; const ts = Number(entry?.ts) || 0; const ttl = Number(entry?.ttl) || CACHE_TTL; if (!entry || !ts || now - ts > ttl) { if (typeof GM_deleteValue === 'function') GM_deleteValue(storageKey); return; } live.push({ storageKey, ts }); } catch { if (typeof GM_deleteValue === 'function') GM_deleteValue(storageKey); } }); if (live.length <= CACHE_MAX_ENTRIES) return; live.sort((a, b) => a.ts - b.ts) .slice(0, live.length - CACHE_MAX_ENTRIES) .forEach(entry => GM_deleteValue(entry.storageKey)); } catch (e) { console.warn('[IMDb Enhanced] cache GC failed:', e); } } function normalizeUserMark(record) { if (record === 'watched' || record === 'skip') return { state: record, title: '', ts: 0 }; if (!record || typeof record !== 'object') return null; const state = record.state === 'watched' || record.state === 'skip' ? record.state : ''; if (!state) return null; return { state, title: String(record.title || '').trim().slice(0, 160), ts: Number(record.ts) || 0, }; } function getUserMarks() { const raw = get('userMarks'); if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}; const marks = {}; Object.entries(raw).forEach(([id, record]) => { if (!/^tt\d+$/.test(id)) return; const normalized = normalizeUserMark(record); if (normalized) marks[id] = normalized; }); return marks; } function setUserMarks(marks) { set('userMarks', marks && typeof marks === 'object' && !Array.isArray(marks) ? marks : {}); } function getUserMark(imdbId) { return getUserMarks()[imdbId]?.state || ''; } function setUserMark(imdbId, state, title = '') { if (!/^tt\d+$/.test(imdbId || '')) return; const marks = getUserMarks(); if (state === 'watched' || state === 'skip') { marks[imdbId] = { state, title: String(title || '').trim().slice(0, 160), ts: Date.now() }; } else { delete marks[imdbId]; } setUserMarks(marks); } function getUserMarkEntries() { return Object.entries(getUserMarks()).sort((a, b) => (b[1].ts || 0) - (a[1].ts || 0)); } // ========================================================================= // DOM UTILITIES // ========================================================================= function waitFor(sel, timeout = 8000) { return new Promise((resolve, reject) => { const el = document.querySelector(sel); if (el) return resolve(el); const root = document.body || document.documentElement; if (!root) return reject(); const obs = new MutationObserver(() => { const el = document.querySelector(sel); if (el) { obs.disconnect(); resolve(el); } }); obs.observe(root, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); reject(); }, timeout); }); } function getTitleSurface() { const explicit = document.querySelector('[data-testid="hero__pageTitle"]'); if (explicit) return explicit; const primary = document.querySelector('[data-testid="hero__primary-text"]'); if (primary) return primary.closest('[data-testid="hero__pageTitle"]') || primary.closest('h1') || primary.parentElement || primary; const mainHeading = document.querySelector('main h1, h1'); return mainHeading || null; } function getTitleActionAnchor() { const title = getTitleSurface(); if (!title) return null; const heading = title.matches?.('[data-testid="hero__pageTitle"], h1') ? title : title.closest?.('[data-testid="hero__pageTitle"], h1'); return heading?.parentElement || title.parentElement || title; } function insertAfter(anchor, node) { if (!anchor?.parentElement || !node) return false; anchor.parentElement.insertBefore(node, anchor.nextSibling); return true; } function getOrCreateTitleStack() { const existing = document.getElementById('enh-title-stack'); if (existing) return existing; const anchor = getTitleActionAnchor(); if (!anchor) return null; const stack = makeEl('div', { id:'enh-title-stack' }); return insertAfter(anchor, stack) ? stack : null; } function appendTitleStackItem(node, order) { const stack = getOrCreateTitleStack(); if (!stack || !node) return false; node.dataset.titleStackOrder = String(order); const next = Array.from(stack.children).find(child => Number(child.dataset.titleStackOrder || Number.MAX_SAFE_INTEGER) > order ); stack.insertBefore(node, next || null); return true; } function pruneTitleStack() { const stack = document.getElementById('enh-title-stack'); if (stack && !stack.children.length) stack.remove(); } function waitForTitleSurface(timeout = 20000) { return new Promise((resolve, reject) => { const found = getTitleSurface(); if (found) return resolve(found); const root = document.body || document.documentElement; if (!root) return reject(); const obs = new MutationObserver(() => { const next = getTitleSurface(); if (next) { obs.disconnect(); resolve(next); } }); obs.observe(root, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); reject(); }, timeout); }); } function addCSS(css, id) { let s = document.getElementById(id); if (s) { s.textContent = css; return s; } s = document.createElement('style'); s.id = id; s.textContent = css; (document.head || document.documentElement).appendChild(s); return s; } function removeCSS(id) { document.getElementById(id)?.remove(); } function makeEl(tag, attrs = {}, ...children) { const e = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) { if (v === false || v === null || v === undefined) continue; if (k === 'style' && typeof v === 'object') { Object.entries(v).forEach(([prop, val]) => { if (prop.startsWith('--')) e.style.setProperty(prop, val); else e.style[prop] = val; }); } else if (k === 'className') e.className = v; else if (k === 'innerHTML') e.innerHTML = v; else if (k === 'textContent') e.textContent = v; else if (k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2).toLowerCase(), v); else if (k === 'dataset') Object.assign(e.dataset, v); else e.setAttribute(k, v); } for (const c of children) { if (typeof c === 'string') e.appendChild(document.createTextNode(c)); else if (c) e.appendChild(c); } return e; } function normalizeColor(color, fallback = '#6366f1') { const value = String(color || '').trim(); return /^#[0-9a-f]{6}$/i.test(value) ? value : fallback; } function normalizeUrlTemplate(url) { const value = String(url || '').trim(); return /^https?:\/\//i.test(value) ? value : ''; } function getCinebyHost() { const saved = normalizeUrlTemplate(get('cinebyHost')); return CINEBY_HOSTS.some(host => host.url === saved) ? saved : CINEBY_HOSTS[0].url; } function normalizeSite(site, fallbackColor = '#6366f1') { const name = String(site?.name || '').trim().slice(0, 40); const url = normalizeUrlTemplate(site?.url); if (!name || !url) return null; return { name, url, color: normalizeColor(site?.color, fallbackColor), ...(site?.storeQuery ? { storeQuery:true } : {}), }; } function getSiteList(key, defaults) { const value = get(key); if (Array.isArray(value)) return value.map(site => normalizeSite(site)).filter(Boolean); return defaults.map(site => normalizeSite(site)).filter(Boolean); } function setSiteList(key, sites) { const normalized = sites.map(site => normalizeSite(site)).filter(Boolean); set(key, normalized); } function getLinkContext(title = getTitleText(), imdbId = getIMDbID(), year = getTitleYear()) { const rawTitle = title || ''; return { TITLE: encodeURIComponent(rawTitle), TITLE_RAW: rawTitle, TITLE_DASH: encodeURIComponent(rawTitle.replace(/\s+/g, '-')), TITLE_SLUG: rawTitle.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim().replace(/\s+/g, '-'), IMDB_ID: imdbId || '', IMDB_NUM: (imdbId || '').replace(/^tt/, ''), TRAKT_TYPE: isTVType() ? 'show' : 'movie', YEAR: year || '', }; } function applyLinkTemplate(template, ctx) { return String(template || '').replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => ctx[key] ?? ''); } // ========================================================================= // PAGE DATA EXTRACTION // ========================================================================= function getIMDbID() { return window.location.pathname.match(/\/(tt\d+)/)?.[1] || null; } function getTitleText() { return (document.querySelector('[data-testid="hero__primary-text"]') || document.querySelector('h1'))?.textContent?.trim() || ''; } let _ldData = null; function getLDData() { if (_ldData) return _ldData; try { const s = document.querySelector('script[type="application/ld+json"]'); if (s) _ldData = JSON.parse(s.textContent); } catch { /* ignore */ } return _ldData || {}; } function yearFromText(text) { return String(text || '').match(/\b(18|19|20)\d{2}\b/)?.[0] || ''; } function getTitleYear() { const ld = getLDData(); const releaseEvents = Array.isArray(ld.releasedEvent) ? ld.releasedEvent : [ld.releasedEvent].filter(Boolean); const structuredCandidates = [ ld.datePublished, ld.releaseDate, ld.startDate, ld.dateCreated, ...releaseEvents.flatMap(ev => [ev?.startDate, ev?.endDate]), ]; for (const candidate of structuredCandidates) { const year = yearFromText(candidate); if (year) return year; } const inlines = document.querySelectorAll('[data-testid="hero-subnav-bar-left-block"] a, section[data-testid="hero-parent"] a[href*="releaseinfo"], main h1 ~ ul a'); for (const a of inlines) { const m = a.textContent.match(/\b(19|20)\d{2}\b/); if (m) return m[0]; } const metaTitle = document.querySelector('meta[property="og:title"], meta[name="title"]')?.content; const fallbackSources = [ metaTitle, document.querySelector('[data-testid="hero__pageTitle"]')?.textContent, document.querySelector('h1')?.parentElement?.textContent, document.title, ]; for (const source of fallbackSources) { const year = yearFromText(source); if (year) return year; } return ''; } function getMediaType() { const ld = getLDData(); const types = Array.isArray(ld['@type']) ? ld['@type'] : [ld['@type']]; if (types.includes('TVEpisode') || ld.partOfSeries || ld.partOfSeason) return 'episode'; if (types.includes('TVSeries')) { const text = [ld.name, ld.description, ld.keywords].filter(Boolean).join(' '); return /mini[-\s]?series/i.test(text) ? 'miniseries' : 'series'; } const genres = Array.isArray(ld.genre) ? ld.genre : [ld.genre].filter(Boolean); if (genres.some(genre => /short/i.test(String(genre)))) return 'short'; return 'movie'; } function isTVType(type = getMediaType()) { return type === 'series' || type === 'episode' || type === 'miniseries'; } function getIMDbRating() { const ld = getLDData(); return ld.aggregateRating?.ratingValue || null; } // ========================================================================= // TOAST // ========================================================================= function showToast(msg, duration = 2500) { document.getElementById('enh-toast')?.remove(); const t = makeEl('div', { id: 'enh-toast', role: 'status', 'aria-live': 'polite' }, msg); document.body.appendChild(t); requestAnimationFrame(() => t.classList.add('visible')); setTimeout(() => { t.classList.remove('visible'); setTimeout(() => t.remove(), 350); }, duration); } // ========================================================================= // ASYNC HTTP // ========================================================================= function httpRequest(url, opts = {}) { return new Promise((resolve, reject) => { const hasBody = opts.body !== undefined; const headers = { ...(hasBody ? { 'Content-Type': 'application/json' } : {}), ...(opts.headers || {}), }; GM_xmlhttpRequest({ ...opts, method: opts.method || 'GET', url, timeout: opts.timeout || 10000, headers, data: hasBody ? JSON.stringify(opts.body) : opts.data, onload: (r) => r.status >= 400 ? reject(r) : resolve(r), onerror: reject, ontimeout: reject, }); }); } function httpGet(url, opts = {}) { return httpRequest(url, { ...opts, method: 'GET' }); } function parseJSONResponse(response) { try { return JSON.parse(response.responseText || 'null'); } catch { throw new Error('Response was not valid JSON'); } } function getRequestErrorMessage(error) { if (error?.responseText) { try { const body = JSON.parse(error.responseText); if (Array.isArray(body) && body[0]?.errorMessage) return body[0].errorMessage; if (body.message) return body.message; if (body.errorMessage) return body.errorMessage; if (body.error) return body.error; } catch { /* use status fallback */ } } if (error?.status) return `HTTP ${error.status}`; return error?.message || 'Request failed'; } function normalizeServarrBaseUrl(value) { const raw = String(value || '').trim().replace(/\/+$/, ''); if (!raw) return ''; try { const url = new URL(raw); if (!/^https?:$/i.test(url.protocol)) return ''; return url.href.replace(/\/+$/, ''); } catch { return ''; } } function isLocalServarrUrl(baseUrl) { try { const host = new URL(baseUrl).hostname.toLowerCase(); return host === 'localhost' || host === '127.0.0.1'; } catch { return false; } } function getServarrConfig(kind) { const prefix = kind === 'sonarr' ? 'sonarr' : 'radarr'; const baseUrl = normalizeServarrBaseUrl(get(`${prefix}Url`)); return { kind: prefix, baseUrl, apiKey: String(get(`${prefix}ApiKey`) || '').trim(), rootFolderPath: String(get(`${prefix}RootFolderPath`) || '').trim(), qualityProfileId: parseInt(get(`${prefix}QualityProfileId`), 10) || 1, languageProfileId: parseInt(get('sonarrLanguageProfileId'), 10) || 1, }; } function isServarrConfigured(kind) { const cfg = getServarrConfig(kind); return Boolean(cfg.baseUrl && cfg.apiKey && cfg.rootFolderPath && cfg.qualityProfileId); } function buildServarrUrl(cfg, path, query = {}) { const url = new URL(`${cfg.baseUrl}/api/v3/${path.replace(/^\/+/, '')}`); Object.entries(query).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, value); }); return url.href; } async function servarrRequest(kind, path, opts = {}) { const cfg = getServarrConfig(kind); if (!isLocalServarrUrl(cfg.baseUrl)) { throw new Error('Only localhost and 127.0.0.1 Servarr URLs are allowed by this userscript build.'); } return httpRequest(buildServarrUrl(cfg, path, opts.query), { method: opts.method || 'GET', body: opts.body, timeout: opts.timeout || 15000, headers: { Accept: 'application/json', 'X-Api-Key': cfg.apiKey, ...(opts.headers || {}), }, }); } // ========================================================================= // FEATURE REGISTRY // ========================================================================= const features = []; function reg(f) { features.push(f); } // ######################################################################### // // CLEANUP FEATURES // // ######################################################################### reg({ key: 'removeAds', name: 'Hide ads and tracking', group: 'Cleanup', css: `.nas-slot,.slot_wrapper,[id*="gpt-ad"],[id*="inline20"],[id*="inline50"], [id="sis_pixel_r2"],[id="cookie_sync_pixel"],.inline20-page-background, [class*="AdSlot"],[class*="adslot"],iframe[src*="amazon-adsystem"], .ipc-wrap-background,#ipc-wrap-background-id,.sponsored_label,.sponsored-content, [data-testid="inline-video-playback-container"] {display:none!important;height:0!important;overflow:hidden!important}`, init() { addCSS(this.css, 'enh-removeAds'); }, destroy() { removeCSS('enh-removeAds'); } }); reg({ key: 'removeProUpsell', name: 'Hide IMDbPro upsells', group: 'Cleanup', css: `[data-testid="hero-subnav-bar-imdb-pro-link"],[data-testid="hero-proupsell"], a[href*="pro.imdb.com"],[class*="ProUpsell"],[class*="proupsell"], [data-testid="tm-box-addtolist-button"]{display:none!important}`, init() { addCSS(this.css, 'enh-proUpsell'); }, destroy() { removeCSS('enh-proUpsell'); } }); reg({ key: 'removeNewsSection', name: 'Hide news modules', group: 'Cleanup', css: `section[data-testid="News"]{display:none!important}`, init() { addCSS(this.css, 'enh-news'); }, destroy() { removeCSS('enh-news'); } }); reg({ key: 'removeRelatedInterests', name: 'Hide related interests', group: 'Cleanup', css: `section[data-testid="RelatedInterests"]{display:none!important}`, init() { addCSS(this.css, 'enh-relInt'); }, destroy() { removeCSS('enh-relInt'); } }); reg({ key: 'removeContribution', name: 'Hide contribution prompts', group: 'Cleanup', css: `section[data-testid="contribution"]{display:none!important}`, init() { addCSS(this.css, 'enh-contrib'); }, destroy() { removeCSS('enh-contrib'); } }); reg({ key: 'removeSponsoredRecs', name: 'Hide sponsored recommendations', group: 'Cleanup', css: `[cel_widget_id*="Sponsored"],[class*="Sponsored"]{display:none!important}`, init() { addCSS(this.css, 'enh-sponsRecs'); }, destroy() { removeCSS('enh-sponsRecs'); } }); reg({ key: 'removeAppBanner', name: 'Hide app banners', group: 'Cleanup', css: `.footer__app,.imdb-footer__open-in-app-button,[class*="AppBanner"],#announcement-text{display:none!important}`, init() { addCSS(this.css, 'enh-appBanner'); }, destroy() { removeCSS('enh-appBanner'); } }); // ######################################################################### // // THEME SYSTEM // // ######################################################################### // ===================== DESIGN SYSTEM ===================== // 4px grid, 3-tier elevation, semantic color roles, consistent radius scale const THEMES = { dark: { scheme: 'dark', // Surfaces (elevation layers) bg: '#101014', // base canvas sf0: '#18181c', // card level 0 sf1: '#1e1e24', // card level 1 (hover, nested) sf2: '#26262e', // card level 2 (active, popovers) // Borders bd0: 'rgba(255,255,255,0.05)', // subtle dividers bd1: 'rgba(255,255,255,0.08)', // card borders bd2: 'rgba(255,255,255,0.12)', // hover borders // Shadows sh1: '0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2)', sh2: '0 4px 16px rgba(0,0,0,0.35), 0 1px 4px rgba(0,0,0,0.25)', sh3: '0 12px 40px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3)', // Text hierarchy tx0: '#f0f0f2', // primary headings tx1: '#c8c8d0', // body text tx2: '#8888a0', // secondary / muted tx3: '#55556a', // disabled / tertiary // Accent palette accent: '#f5c518', // IMDb gold accentMuted: 'rgba(245,197,24,0.12)', accentBorder: 'rgba(245,197,24,0.20)', blue: '#4da8f0', // links, info blueHi: '#7dc4ff', // link hover blueMuted: 'rgba(77,168,240,0.10)', red: '#e84057', // ratings, alerts redMuted: 'rgba(232,64,87,0.10)', green: '#3dd68c', // positive // Header / chrome hdr: 'rgba(16,16,20,0.82)', hdrBorder: 'rgba(255,255,255,0.04)', // Scrollbar sT: '#2a2a34', sH: '#3e3e4a', // Quote accent quoteBar: '#4da8f0', }, oled: { scheme: 'dark', bg: '#000000', sf0: '#0c0c0e', sf1: '#141418', sf2: '#1c1c22', bd0: 'rgba(255,255,255,0.04)', bd1: 'rgba(255,255,255,0.06)', bd2: 'rgba(255,255,255,0.10)', sh1: '0 1px 3px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.4)', sh2: '0 4px 16px rgba(0,0,0,0.6), 0 1px 4px rgba(0,0,0,0.4)', sh3: '0 12px 40px rgba(0,0,0,0.7), 0 2px 8px rgba(0,0,0,0.5)', tx0: '#e4e4e8', tx1: '#b0b0bc', tx2: '#6e6e80', tx3: '#444458', accent: '#f5c518', accentMuted: 'rgba(245,197,24,0.10)', accentBorder: 'rgba(245,197,24,0.18)', blue: '#3d98e0', blueHi: '#6cb8ff', blueMuted: 'rgba(61,152,224,0.08)', red: '#d63850', redMuted: 'rgba(214,56,80,0.08)', green: '#30c47c', hdr: 'rgba(0,0,0,0.92)', hdrBorder: 'rgba(255,255,255,0.03)', sT: '#1a1a22', sH: '#2a2a34', quoteBar: '#3d98e0', }, midnight: { scheme: 'dark', bg: '#0a0e1c', sf0: '#10152a', sf1: '#161c34', sf2: '#1e2644', bd0: 'rgba(120,160,255,0.05)', bd1: 'rgba(120,160,255,0.08)', bd2: 'rgba(120,160,255,0.14)', sh1: '0 1px 3px rgba(0,0,20,0.4), 0 1px 2px rgba(0,0,20,0.3)', sh2: '0 4px 16px rgba(0,0,20,0.45), 0 1px 4px rgba(0,0,20,0.3)', sh3: '0 12px 40px rgba(0,0,20,0.6), 0 2px 8px rgba(0,0,20,0.35)', tx0: '#e4e8f4', tx1: '#b4bcda', tx2: '#6c78a8', tx3: '#445080', accent: '#f5c518', accentMuted: 'rgba(245,197,24,0.10)', accentBorder: 'rgba(245,197,24,0.20)', blue: '#5eaaff', blueHi: '#8ec8ff', blueMuted: 'rgba(94,170,255,0.10)', red: '#f06070', redMuted: 'rgba(240,96,112,0.10)', green: '#48e098', hdr: 'rgba(10,14,28,0.88)', hdrBorder: 'rgba(120,160,255,0.05)', sT: '#1c2444', sH: '#283460', quoteBar: '#5eaaff', }, light: { scheme: 'light', bg: '#f6f7f9', sf0: '#ffffff', sf1: '#eef1f5', sf2: '#e2e7ef', bd0: 'rgba(15,23,42,0.08)', bd1: 'rgba(15,23,42,0.12)', bd2: 'rgba(15,23,42,0.18)', sh1: '0 1px 3px rgba(15,23,42,0.10), 0 1px 2px rgba(15,23,42,0.06)', sh2: '0 8px 22px rgba(15,23,42,0.12), 0 2px 8px rgba(15,23,42,0.08)', sh3: '0 16px 46px rgba(15,23,42,0.16), 0 4px 14px rgba(15,23,42,0.10)', tx0: '#101827', tx1: '#334155', tx2: '#64748b', tx3: '#94a3b8', accent: '#a76500', accentMuted: 'rgba(167,101,0,0.12)', accentBorder: 'rgba(167,101,0,0.28)', blue: '#0f6fbf', blueHi: '#07599c', blueMuted: 'rgba(15,111,191,0.10)', red: '#b91c1c', redMuted: 'rgba(185,28,28,0.10)', green: '#047857', hdr: 'rgba(255,255,255,0.92)', hdrBorder: 'rgba(15,23,42,0.10)', sT: '#c7ced8', sH: '#98a2b3', quoteBar: '#0f6fbf', }, highContrast: { scheme: 'dark', bg: '#000000', sf0: '#050505', sf1: '#111111', sf2: '#1f1f1f', bd0: '#ffffff', bd1: '#ffffff', bd2: '#ffd400', sh1: 'none', sh2: '0 0 0 2px #ffffff', sh3: '0 0 0 3px #ffd400', tx0: '#ffffff', tx1: '#ffffff', tx2: '#eeeeee', tx3: '#cfcfcf', accent: '#ffd400', accentMuted: 'rgba(255,212,0,0.22)', accentBorder: '#ffd400', blue: '#6bd5ff', blueHi: '#ffffff', blueMuted: 'rgba(107,213,255,0.20)', red: '#ff5a66', redMuted: 'rgba(255,90,102,0.20)', green: '#00ff87', hdr: 'rgba(0,0,0,0.98)', hdrBorder: '#ffffff', sT: '#ffffff', sH: '#ffd400', quoteBar: '#ffd400', }, }; function getStoredThemeId() { const id = get('themeVariant'); return THEMES[id] ? id : 'dark'; } function prefersLightTheme() { return typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: light)').matches; } function getActiveThemeId() { return get('themeAuto') ? (prefersLightTheme() ? 'light' : 'dark') : getStoredThemeId(); } function getTheme(id = getActiveThemeId()) { return THEMES[id] || THEMES.dark; } function updateThemeControls(activeId = getActiveThemeId()) { const selector = document.querySelector('.enh-theme-selector'); if (selector) { selector.querySelectorAll('.enh-theme-swatch').forEach(swatch => { const isActive = swatch.dataset.theme === activeId; swatch.classList.toggle('active', isActive); swatch.setAttribute('aria-pressed', String(isActive)); }); } const autoInput = document.getElementById('enh-theme-auto'); if (autoInput) autoInput.checked = !!get('themeAuto'); } function refreshThemeDependentFeatures() { ['compactHeader', 'enhancedRatingDisplay', 'watchedMarking', 'servarrIntegration'].forEach(refreshFeature); } function applyThemeStyles(options = {}) { const activeId = getActiveThemeId(); if (get('modernUI')) addCSS(getThemeCSS(activeId), 'enh-modernUI'); injectGlobalStyles(); injectEarlyThemeShell(); if (options.refreshDependent !== false) refreshThemeDependentFeatures(); updateThemeControls(activeId); } function setupThemeAutoSync() { if (setupThemeAutoSync._done || typeof window.matchMedia !== 'function') return; setupThemeAutoSync._done = true; const media = window.matchMedia('(prefers-color-scheme: light)'); const onChange = () => { if (get('themeAuto')) applyThemeStyles(); }; if (typeof media.addEventListener === 'function') media.addEventListener('change', onChange); else if (typeof media.addListener === 'function') media.addListener(onChange); } function getThemeCSS(id) { const t = getTheme(id); return ` /* ════════════════════════════════════════════ BASE CANVAS & TYPOGRAPHY ════════════════════════════════════════════ */ body, .ipc-page-background, .ipc-page-background--base, .ipc-page-background--baseAlt { background: ${t.bg} !important; } html { color-scheme: ${t.scheme}; scroll-behavior: smooth; } body { color: ${t.tx1} !important; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } /* Type scale — tighten the whole page */ [data-testid="hero__primary-text"] { font-weight: 700 !important; letter-spacing: -0.025em !important; line-height: 1.1 !important; color: ${t.tx0} !important; } .ipc-title__text { font-weight: 600 !important; letter-spacing: -0.015em !important; color: ${t.tx0} !important; } h3.ipc-title__text { color: ${t.blue} !important; } a h3 span, a h3 .ipc-title__text { color: ${t.blue} !important; } .ipc-title__description { color: ${t.tx2} !important; margin-top: 2px !important; } /* Body text */ .ipc-html-content-inner-div { color: ${t.tx1} !important; } .ipc-overflowText--children { color: ${t.tx1} !important; } /* Metadata labels & values */ .ipc-metadata-list-item__label { color: ${t.tx2} !important; } span.ipc-metadata-list-item__label.ipc-btn--not-interactable { color: ${t.tx2} !important; } a.ipc-metadata-list-item__label--link { color: ${t.blue} !important; } a.ipc-metadata-list-item__label--link:hover { color: ${t.blueHi} !important; } .ipc-metadata-list-item__list-content-item--link, .ipc-metadata-list-item__list-content-item a { color: ${t.blue} !important; } .ipc-metadata-list-item__list-content-item--link:hover, .ipc-metadata-list-item__list-content-item a:hover { color: ${t.blueHi} !important; } /* Muted / secondary text */ [data-testid="title-cast-item"] .ipc-inline-list__item, .ipc-metadata-list-item__content-container, .ipc-rating-star--voteCount { color: ${t.tx3} !important; } [data-testid="hero-rating-bar__popularity"] { color: ${t.blue} !important; } /* Links — global */ .ipc-link, .ipc-link--base { color: ${t.blue} !important; transition: color .15s ease !important; } .ipc-link:hover, .ipc-link--base:hover { color: ${t.blueHi} !important; } .ipc-md-link--entity { color: ${t.blue} !important; } /* Rating star */ span.ipc-rating-star--rating { color: ${t.accent} !important; font-weight: 700 !important; } span.ipc-rating-star--maxRating { color: ${t.tx3} !important; } /* ════════════════════════════════════════════ ELEVATION SYSTEM — CARDS & SECTIONS ════════════════════════════════════════════ */ /* Title page main sections → elevation 0 cards */ section[data-testid="title-cast"], section[data-testid="UserReviews"], section[data-testid="MoreLikeThis"], section[data-testid="Details"], section[data-testid="BoxOffice"], section[data-testid="TechSpecs"], section[data-testid="DidYouKnow"], section[data-testid="videos-section"], section[data-testid="Photos"], section[data-testid="Filmography"], section[data-testid="PersonalDetails"] { background: ${t.sf0} !important; border: 1px solid ${t.bd1} !important; border-radius: 12px !important; padding: 20px 24px !important; margin-bottom: 12px !important; box-shadow: ${t.sh1} !important; transition: border-color .2s ease !important; } /* Hero section */ section[data-testid="hero-parent"] { background: linear-gradient(180deg, ${t.sf0} 0%, ${t.bg} 100%) !important; border-radius: 0 0 16px 16px !important; padding-bottom: 24px !important; border-bottom: 1px solid ${t.bd0} !important; } /* Transparent base sections (prevent double-backgrounds) */ section.ipc-page-section.ipc-page-section--base { background: transparent !important; } section.ipc-page-section.ipc-page-section--none { background: transparent !important; } /* Generic list cards → transparent or elevation 0 */ .ipc-list-card--border-line { border-color: ${t.bd0} !important; } .ipc-list-card--border-line.ipc-list-card--tp-none.ipc-list-card--bp-none { background: transparent !important; } .ipc-list-card--span.ipc-list-card--border-shadow { background: transparent !important; } .ipc-inline-list--show-dividers .ipc-inline-list__item::after { border-color: ${t.bd0} !important; } /* ════════════════════════════════════════════ CAST CARDS — elevation 1 with hover lift ════════════════════════════════════════════ */ [data-testid="title-cast-item"] { background: ${t.sf1} !important; border: 1px solid ${t.bd1} !important; border-radius: 10px !important; overflow: hidden !important; box-shadow: ${t.sh1} !important; transition: transform .2s cubic-bezier(.4,0,.2,1), border-color .2s ease, box-shadow .2s ease !important; } [data-testid="title-cast-item"]:hover { transform: translateY(-3px) !important; border-color: ${t.accentBorder} !important; box-shadow: ${t.sh2} !important; } /* ════════════════════════════════════════════ POSTER CARDS (More Like This, shovelers) ════════════════════════════════════════════ */ .ipc-poster-card { border-radius: 10px !important; overflow: hidden !important; transition: transform .2s cubic-bezier(.4,0,.2,1), box-shadow .2s ease !important; } .ipc-poster-card:hover { transform: translateY(-4px) !important; box-shadow: ${t.sh2} !important; } /* Hero poster */ [data-testid="hero-media__poster"] img { border-radius: 10px !important; box-shadow: ${t.sh2} !important; transition: transform .25s cubic-bezier(.4,0,.2,1), box-shadow .25s ease !important; } [data-testid="hero-media__poster"]:hover img { transform: scale(1.03) !important; box-shadow: ${t.sh3} !important; } /* ════════════════════════════════════════════ SQUIRCLE SYSTEM — circles → rounded squares ════════════════════════════════════════════ */ .ipc-avatar, .ipc-avatar__avatar-image, [class*="avatar"] img, [class*="Avatar"] img, .ipc-media--circle, .ipc-media--avatar, img[class*="avatar"], img[class*="Avatar"], [class*="ipc-avatar"] { border-radius: 22% !important; } [style*="border-radius: 50%"], [style*="border-radius:50%"] { border-radius: 22% !important; } /* ════════════════════════════════════════════ BUTTONS & CHIPS ════════════════════════════════════════════ */ .ipc-btn--core-accent1 { border-radius: 8px !important; transition: transform .15s ease, box-shadow .15s ease, background .15s ease !important; } .ipc-btn--core-accent1:hover { transform: translateY(-1px) !important; box-shadow: 0 4px 16px ${t.accentMuted} !important; } .ipc-chip, .ipc-chip--on-base, .ipc-chip--on-baseAlt { border-radius: 8px !important; border-color: ${t.bd1} !important; background: ${t.sf0} !important; transition: background .15s ease, border-color .15s ease, color .15s ease !important; } .ipc-chip:hover, .ipc-chip--on-base:hover { background: ${t.sf1} !important; border-color: ${t.bd2} !important; } .ipc-chip--filled { background: ${t.sf1} !important; } /* ════════════════════════════════════════════ REVIEW PAGE ════════════════════════════════════════════ */ [data-testid="review-card-parent"] { background: ${t.sf0} !important; border: 1px solid ${t.bd1} !important; border-radius: 10px !important; padding: 16px 20px !important; margin: 0 0 10px 0 !important; box-shadow: ${t.sh1} !important; transition: border-color .2s ease !important; } [data-testid="review-card-parent"]:hover { border-color: ${t.bd2} !important; } [data-testid="review-summary"] .ipc-title__text { color: ${t.tx0} !important; font-weight: 600 !important; } [data-testid="author-link"], [data-testid="reviews-author"] { color: ${t.blue} !important; } [data-testid="review-overflow"] .ipc-html-content-inner-div { color: ${t.tx1} !important; line-height: 1.65 !important; } .ipc-list-card__content { padding: 8px 0 !important; } /* Review rating stars inline */ .ipc-rating-star--voteCount, [data-testid="review-card-parent"] .ipc-rating-star--voteCount { color: ${t.tx2} !important; } /* ════════════════════════════════════════════ QUOTES PAGE — blockquote style with accent bar ════════════════════════════════════════════ */ [data-testid="sub-section-Quotes"] .ipc-list-card, section[id*="quote" i] .ipc-list-card { background: ${t.sf0} !important; border: 1px solid ${t.bd1} !important; border-left: 3px solid ${t.quoteBar} !important; border-radius: 0 10px 10px 0 !important; padding: 12px 16px !important; margin: 0 0 8px 0 !important; box-shadow: ${t.sh1} !important; } [data-testid="sub-section-Quotes"] .ipc-list-card, section[id*="quote" i] .ipc-list-card { padding: 4px 0 !important; margin: 0 !important; } [data-testid="sub-section-Quotes"] .ipc-html-content-inner-div, section[id*="quote" i] .ipc-html-content-inner-div { color: ${t.tx1} !important; line-height: 1.6 !important; font-style: italic !important; } /* ════════════════════════════════════════════ NAME / PERSON PAGE ════════════════════════════════════════════ */ /* Hero photo → squircle with shadow */ [data-testid="name-overview-widget"] img, .name-overview-widget img { border-radius: 12px !important; box-shadow: ${t.sh2} !important; } /* Bio text */ [data-testid="bio-content"] { color: ${t.tx1} !important; } [data-testid="bio-content"] .ipc-html-content-inner-div { color: ${t.tx1} !important; line-height: 1.65 !important; } /* Filmography accordion */ .ipc-accordion__item { border-color: ${t.bd0} !important; transition: background .15s ease !important; } .ipc-accordion__item:hover { background: ${t.sf1} !important; } .ipc-accordion__item__header { padding: 10px 0 !important; } .ipc-accordion__item__title { color: ${t.tx0} !important; font-weight: 600 !important; } .ipc-accordion__item__content { padding: 0 !important; } /* Personal details */ [data-testid="PersonalDetails"] .ipc-metadata-list-item__label { color: ${t.tx2} !important; } [data-testid="PersonalDetails"] a { color: ${t.blue} !important; } /* ════════════════════════════════════════════ SIDEBAR (all subpages) ════════════════════════════════════════════ */ [data-testid="sidebar-sticky-block"] .ipc-slate-card { border-radius: 10px !important; overflow: hidden !important; box-shadow: ${t.sh1} !important; } [data-testid="sidebar-sticky-block"] .ipc-list-card, [data-testid="sidebar-sticky-block"] .ipc-slate-card { background: ${t.sf0} !important; border-color: ${t.bd0} !important; border-radius: 8px !important; transition: background .15s ease !important; } [data-testid="sidebar-sticky-block"] .ipc-list-card:hover, [data-testid="sidebar-sticky-block"] .ipc-slate-card:hover { background: ${t.sf1} !important; } [data-testid="sidebar-sticky-block"] .ipc-title__text { color: ${t.tx0} !important; } [data-testid="sidebar-sticky-block"] .ipc-inline-list__item { color: ${t.tx3} !important; } /* ════════════════════════════════════════════ SCROLLBAR ════════════════════════════════════════════ */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: ${t.sT}; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: ${t.sH}; } /* ════════════════════════════════════════════ FOOTER & CHROME CLEANUP ════════════════════════════════════════════ */ footer.imdb-footer { display: none !important; } button.FavoritePeopleCTA_favPeopleCTAOnAvatar__ZQ2LQ { display: none !important; } [data-testid="hero-proupsell"], [data-testid="tm-box-addtolist-button"] { display: none !important; } div.nav__userMenu { display: none !important; } /* ════════════════════════════════════════════ SUBTITLE & CUSTOM ROWS ════════════════════════════════════════════ */ #enh-sub-row { color: ${t.blue} !important; } #enh-sub-row a { color: ${t.blue} !important; } #enh-sub-row a:hover { color: ${t.blueHi} !important; } /* ════════════════════════════════════════════ GLOBAL SPACING RHYTHM (4px grid) ════════════════════════════════════════════ */ .ipc-page-section { margin-top: 0 !important; margin-bottom: 0 !important; } .ipc-page-section--tp-none { padding-top: 0 !important; } .ipc-page-section--bp-none { padding-bottom: 0 !important; } .ipc-title { margin-bottom: 8px !important; } .ipc-chip-list__scroller { gap: 6px !important; } .ipc-overflowText--children { margin: 0 !important; } /* ════════════════════════════════════════════ FOCUS STATES (accessibility) ════════════════════════════════════════════ */ a:focus-visible, button:focus-visible, .ipc-chip:focus-visible { outline: 2px solid ${t.accent} !important; outline-offset: 2px !important; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto !important; } *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; } } `; } function injectEarlyThemeShell() { if (!window.location.hostname.includes('imdb.com')) return; const t = getTheme(); document.documentElement.dataset.imdbEnhanced = 'active'; addCSS(` html[data-imdb-enhanced="active"] { color-scheme: ${t.scheme}; background: ${t.bg}; } html[data-imdb-enhanced="active"] body, html[data-imdb-enhanced="active"] .ipc-page-background { background: ${t.bg} !important; } #imdbHeader { background: ${t.hdr} !important; border-bottom: 1px solid ${t.hdrBorder} !important; } `, 'enh-early-shell'); } injectEarlyThemeShell(); setupThemeAutoSync(); reg({ key: 'modernUI', name: 'Modern IMDb skin', group: 'Appearance', init() { applyThemeStyles({ refreshDependent: false }); }, destroy() { removeCSS('enh-modernUI'); } }); reg({ key: 'compactHeader', name: 'Compact header', group: 'Appearance', init() { const t = getTheme(); addCSS(` #imdbHeader { padding: 4px 0 !important; background: ${t.hdr} !important; border-bottom: 1px solid ${t.hdrBorder} !important; transition: background .2s ease !important; } .navbar__inner { min-height: 46px !important; } #imdbHeader .imdb-header__logo-link svg { height: 24px !important; width: auto !important; } `, 'enh-compactHdr'); }, destroy() { removeCSS('enh-compactHdr'); } }); reg({ key: 'enhancedRatingDisplay', name: 'Refined rating display', group: 'Appearance', init() { const t = getTheme(); addCSS(` [data-testid="hero-rating-bar__aggregate-rating"] { background: ${t.accentMuted} !important; border: 1px solid ${t.accentBorder} !important; border-radius: 12px !important; padding: 8px 16px !important; box-shadow: 0 0 24px ${t.accentMuted} !important; transition: background .2s ease, box-shadow .2s ease !important; } [data-testid="hero-rating-bar__aggregate-rating"]:hover { background: rgba(245,197,24,0.16) !important; box-shadow: 0 0 32px rgba(245,197,24,0.12) !important; } [data-testid="hero-rating-bar__aggregate-rating__score"] span:first-child { font-size: 1.6em !important; font-weight: 800 !important; } [data-testid="hero-rating-bar__popularity"] { background: ${t.blueMuted} !important; border: 1px solid rgba(77,168,240,0.12) !important; border-radius: 12px !important; padding: 8px 16px !important; } `, 'enh-enhRating'); }, destroy() { removeCSS('enh-enhRating'); } }); reg({ key: 'widerLayout', name: 'Wider responsive layout', group: 'Appearance', css: ` /* ── Full-width containers ── */ .ipc-page-content-container--center { max-width: 100% !important; padding: 0 32px !important; } .ipc-page-section--base.celwidget { width: 100% !important; max-width: 100% !important; } .bRimta { width: 100% !important; max-width: 100% !important; } .ipc-page-grid { max-width: 100% !important; width: 100% !important; padding: 0 32px !important; } .ipc-page-content-container--full { max-width: 100% !important; width: 100% !important; } .ipc-page-wrapper { max-width: 100% !important; } [data-testid="atf-wrapper-bg"] { max-width: 100% !important; } /* ── Poster card compaction ── */ div.ipc-rating-star-group.ipc-poster-card__rating-star-group { padding: 0 !important; margin: 0 !important; } a.ipc-poster-card__title.ipc-poster-card__title--clamp-2.ipc-poster-card__title--clickable { padding: 0 !important; margin: 0 0 -29px 0 !important; } /* ── Grid & shoveler spacing ── */ div.ipc-sub-grid.ipc-sub-grid--page-span-2.ipc-sub-grid--nowrap.ipc-shoveler__grid { padding: 0 !important; margin: 0 !important; } /* ── Section vertical compression ── */ section.ipc-page-section.ipc-page-section--base.celwidget { padding: 0 !important; margin: 0 !important; } div.ipc-html-content-inner-div { padding: 0 !important; margin: 0 !important; } li.ipc-metadata-list__item.ipc-metadata-list__item--align-end.ipc-metadata-list-item--link { padding: 0 !important; margin: 0 !important; } h3.ipc-title__text.ipc-title__text--reduced { padding: 0 !important; margin: 0 !important; } .ipc-title__wrapper { padding: 0 !important; margin: 0 !important; } /* ── Accordion (filmography) ── */ .ipc-accordion__item__content_inner { padding: 4px 0 !important; } .ipc-accordion__item__header { padding: 8px 0 !important; min-height: auto !important; } /* ── Review / quote specific ── */ [data-testid="review-overflow"] { margin: 4px 0 !important; } [data-testid="sub-section-Quotes"] .ipc-list-card, section[id*="quote" i] .ipc-list-card { padding: 4px 0 !important; margin: 2px 0 !important; } .ipc-chip-list__scroller { padding: 4px 0 !important; } /* ── Sidebar compression ── */ [data-testid="sidebar-sticky-block"] { gap: 0 !important; } .ipc-page-section--none { margin: 0 !important; padding: 4px 0 !important; } /* ── Name page ── */ [data-testid="bio-content"] { padding: 4px 0 !important; } [data-testid="PersonalDetails"] { padding: 4px 0 !important; } [data-testid="Filmography"] { padding: 4px 0 !important; } @media (max-width: 900px) { .ipc-page-content-container--center, .ipc-page-grid { padding-left: 16px !important; padding-right: 16px !important; } } `, init() { addCSS(this.css, 'enh-wider'); }, destroy() { removeCSS('enh-wider'); } }); // ===================== RATING COLOR CODING ===================== function ratingColor(val) { const n = parseFloat(val); if (isNaN(n)) return { bg:'#555', text:'#ccc', label:'N/A' }; if (n >= 8.0) return { bg:'#22c55e', text:'#fff', label:'Great' }; if (n >= 7.0) return { bg:'#84cc16', text:'#000', label:'Good' }; if (n >= 6.0) return { bg:'#eab308', text:'#000', label:'Average' }; if (n >= 5.0) return { bg:'#f97316', text:'#000', label:'Below Avg' }; return { bg:'#ef4444', text:'#fff', label:'Poor' }; } function mcColor(s) { return s >= 75 ? '#6c3' : s >= 50 ? '#ffbd3f' : s >= 25 ? '#ff6874' : '#f00'; } function rtColorFn(s) { return s >= 60 ? '#fa320a' : '#6b7280'; } function lbColor(s) { return s >= 4 ? '#00e054' : s >= 3 ? '#40bcf4' : s >= 2 ? '#ff8000' : '#ff6874'; } function getRTSlugCandidates(title) { const normalized = String(title || '') .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/&/g, ' and ') .toLowerCase() .replace(/[^a-z0-9\s-]/g, ' ') .trim() .replace(/\s+/g, ' '); if (!normalized) return []; const tokens = normalized.split(' ').filter(Boolean); const withoutArticle = tokens[0] === 'the' && tokens.length > 1 ? tokens.slice(1) : tokens; const variants = [tokens, withoutArticle]; return [...new Set(variants.flatMap(parts => [ parts.join('_'), parts.join('-'), parts.join(''), ]).filter(Boolean))]; } function formatScore(n) { return Number(n).toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); } function formatCount(n) { const count = Number(n); if (!Number.isFinite(count) || count <= 0) return ''; if (count >= 1000000) return `${(count / 1000000).toFixed(count >= 10000000 ? 0 : 1)}M`; if (count >= 1000) return `${Math.round(count / 1000)}K`; return String(count); } function decodeHTML(text) { const ta = document.createElement('textarea'); ta.innerHTML = String(text || ''); return ta.value; } function getJustWatchSlug(title = getTitleText()) { return String(title || '') .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/&/g, ' and ') .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .trim() .replace(/\s+/g, '-') .replace(/-+/g, '-'); } function getJustWatchTypePath() { return isTVType() ? 'tv-show' : 'movie'; } function getJustWatchSearchUrl(title = getTitleText()) { return `https://www.justwatch.com/us/search?q=${encodeURIComponent(title || '')}`; } function getJustWatchDetailUrl(title = getTitleText()) { const slug = getJustWatchSlug(title); return slug ? `https://www.justwatch.com/us/${getJustWatchTypePath()}/${slug}` : getJustWatchSearchUrl(title); } function getTrailerSearchUrl(title = getTitleText(), year = getTitleYear()) { const query = [title, year, 'official trailer'].filter(Boolean).join(' '); return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`; } function compactProviders(providers, limit = 2) { const clean = []; providers.forEach(provider => { const name = String(provider || '').trim().replace(/\s+/g, ' '); if (name && !clean.some(existing => existing.toLowerCase() === name.toLowerCase())) clean.push(name); }); if (clean.length <= limit + 1) return { providers: clean, extra: 0 }; return { providers: clean.slice(0, limit), extra: clean.length - limit }; } function formatProviderSummary(providers) { const { providers: shown, extra } = compactProviders(providers); const summary = shown.join(', '); return extra > 0 ? `${summary} +${extra}` : summary; } reg({ key: 'ratingColorCoding', name: 'Rating quality labels', group: 'Appearance', init() { addCSS(` [data-testid="hero-rating-bar__aggregate-rating"].enh-rating-colorized [data-testid="hero-rating-bar__aggregate-rating__score"] span:first-child { color: var(--enh-rating-score-color) !important; text-shadow: var(--enh-rating-score-shadow) !important; } #enh-rating-badge { display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px; margin-left: 6px; vertical-align: middle; background: var(--enh-rating-badge-bg); color: var(--enh-rating-badge-text); letter-spacing: .03em; } `, 'enh-ratingColor'); waitFor('[data-testid="hero-rating-bar__aggregate-rating__score"]').then(el => { const rating = getIMDbRating(); if (!rating) return; const c = ratingColor(rating); const container = el.closest('[data-testid="hero-rating-bar__aggregate-rating"]') || el; container.classList.add('enh-rating-colorized'); container.style.setProperty('--enh-rating-score-color', c.bg); container.style.setProperty('--enh-rating-score-shadow', `0 0 20px ${c.bg}44`); container.style.setProperty('--enh-rating-badge-bg', c.bg); container.style.setProperty('--enh-rating-badge-text', c.text); if (!document.getElementById('enh-rating-badge')) { const badge = document.createElement('span'); badge.id = 'enh-rating-badge'; badge.textContent = c.label; el.appendChild(badge); } }).catch(() => {}); }, destroy() { removeCSS('enh-ratingColor'); document.getElementById('enh-rating-badge')?.remove(); document.querySelectorAll('.enh-rating-colorized').forEach(el => { el.classList.remove('enh-rating-colorized'); ['--enh-rating-score-color', '--enh-rating-score-shadow', '--enh-rating-badge-bg', '--enh-rating-badge-text'] .forEach(prop => el.style.removeProperty(prop)); }); } }); // ######################################################################### // // INLINE SCORES (RT + Metacritic) // // ######################################################################### function findRatingBar() { const agg = document.querySelector('[data-testid="hero-rating-bar__aggregate-rating"]'); if (!agg) return null; // Walk up to find the flex container holding all rating widgets let parent = agg.parentElement; for (let i = 0; i < 3 && parent; i++) { if (parent.children.length >= 2) return parent; parent = parent.parentElement; } return agg.parentElement; } reg({ key: 'inlineRTScore', name: 'Rotten Tomatoes scores', group: 'Scores', async init() { const imdbId = getIMDbID(), title = getTitleText(); if (!imdbId || !title) return; const cacheKey = 'rt_' + imdbId; const cached = cacheGet(cacheKey); if (cached) { if (cached.unavailable) this._renderUnavailable(); else this._render(cached); return; } this._renderLoading(); const type = isTVType() ? 'tv' : 'movie'; const prefix = type === 'tv' ? '/tv/' : '/m/'; for (const slug of getRTSlugCandidates(title)) { try { const res = await httpGet('https://www.rottentomatoes.com' + prefix + slug); const data = this._parse(res.responseText); if (data) { cacheSet(cacheKey, data); this._render(data); return; } } catch { /* try next slug */ } } // Fallback: search page try { const res2 = await httpGet(`https://www.rottentomatoes.com/search?search=${encodeURIComponent(title)}`); const tm = res2.responseText.match(/"tomatoScore"\s*:\s*(\d+)/); const au = res2.responseText.match(/"audienceScore"\s*:\s*(\d+)/); if (tm) { const d = { tomatometer: parseInt(tm[1]), audience: au ? parseInt(au[1]) : null }; cacheSet(cacheKey, d); this._render(d); return; } } catch { /* handled below */ } cacheSetUnavailable(cacheKey); this._renderUnavailable(); }, _parse(html) { try { const ldM = html.match(/