'use strict'; // ============================================================================ // CSS Tweaks for Twitch Twilight // ============================================================================ import Module from 'utilities/module'; import {ManagedStyle} from 'utilities/dom'; import {has} from 'utilities/object'; import { getFontsList, useFont } from 'utilities/fonts'; const STYLE_VALIDATOR = document.createElement('span'); const CLASSES = { //'unfollow': '.follow-btn__follow-btn--following,.follow-btn--following', //'top-discover': '.navigation-link[data-a-target="discover-link"]', 'side-nav': '.side-nav,#sideNav', 'side-nav-viewers': '.side-nav-card__live-status', 'side-rec-channels': '.side-nav .recommended-channels,.side-nav .side-nav-section + .side-nav-section:not(.online-friends):not(.bd--shelf)', //'side-rec-friends': '.side-nav .recommended-friends', 'side-friends': '.side-nav .online-friends', 'side-closed-friends': '.side-nav--collapsed .online-friends', 'side-closed-rec-channels': '.side-nav--collapsed .recommended-channels,.side-nav--collapsed .side-nav-section + .side-nav-section:not(.online-friends):not(.bd--shelf)', 'side-offline-channels': '.ffz--side-nav-card-offline', 'side-rerun-channels': '.side-nav .ffz--side-nav-card-rerun', 'modview-hide-info': '.modview-player-widget__hide-stream-info', 'community-highlights': '.community-highlight-stack__card', 'elevate-your-message': '.chat-input__input-icons button[aria-label="ElevatedMessage"]', 'prime-offers': '.top-nav__prime', 'discover-luna': '.top-nav__external-link[data-a-target="try-presto-link"]', 'subtember': '.subtember-gradient', 'player-gain-volume': '.video-player__container[data-compressed="true"] .volume-slider__slider-container:not(.ffz--player-gain)', 'player-ext': '.video-player .extension-taskbar,.video-player .extension-container,.video-player .extensions-dock__layout,.video-player .extensions-notifications,.video-player .extensions-video-overlay-size-container,.video-player .extensions-dock__layout', 'player-ext-hover': '.video-player__container[data-controls="false"] .extension-taskbar,.video-player__container[data-controls="false"] .extension-container,.video-player__container[data-controls="false"] .extensions-dock__layout,.video-player__container[data-controls="false"] .extensions-notifications,.video-player__container[data-controls="false"] .extensions-video-overlay-size-container', 'player-event-bar': '.channel-root .live-event-banner-ui__header', 'player-rerun-bar': '.channel-root__player-container div.tw-c-text-overlay:not([data-a-target="hosting-ui-header"])', 'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2,.channel-leaderboard,.channel-leaderboard-marquee,div[data-test-selector="channel-leaderboard-container"]', 'whispers': 'body .whispers-open-threads,.tw-core-button[data-a-target="whisper-box-button"],.whispers__pill', 'dir-live-ind': '.live-channel-card[data-ffz-type="live"] .tw-channel-status-text-indicator, article[data-ffz-type="live"] .tw-channel-status-text-indicator', 'profile-hover': '.preview-card .tw-relative:hover .ffz-channel-avatar', 'not-live-bar': 'div[data-test-selector="non-live-video-banner-layout"]', 'channel-live-ind': '.channel-header__user .tw-channel-status-text-indicator,.channel-info-content .tw-halo__indicator', 'celebration': 'body .celebration__overlay', 'last-x-events': '.last-x-events_container', 'pinned-hype-chat': '.paid-pinned-chat-message-list', 'ci-mod-view': '.chat-input__buttons-container a[href*="/moderator"]', 'ci-highlight-settings': '.chat-input__buttons-container button[data-highlight-selector="chat-highlights-shortcut"]', 'ci-shield-mode': '.chat-input__buttons-container > div:last-child button[class|="ScCoreButton"]:not([data-highlight-selector]):not([data-a-target])' }; export default class CSSTweaks extends Module { constructor(...args) { super(...args); this.should_enable = true; this.inject('settings'); this.style = new ManagedStyle; this.chunks = {}; this.chunks_loaded = false; this._state = {}; // Layout this.settings.add('metadata.modview.hide-info', { default: false, ui: { path: 'Channel > Metadata >> Mod View', title: 'Hide "Hide Stream Info Stripe" button.', component: 'setting-check-box' }, changed: val => this.toggleHide('modview-hide-info', val) }); this.settings.add('metadata.viewers.no-native', { requires: ['metadata.viewers'], default: null, process(ctx, val) { return val == null ? ctx.get('metadata.viewers') : val }, changed: val => this.toggle('hide-native-viewers', val), ui: { path: 'Channel > Metadata >> Player', title: "Hide Twitch's native Viewer Count.", description: "By default, this is enabled whenever FFZ's own Viewer Count display is enabled to avoid redundant information.", component: 'setting-check-box' } }); this.settings.add('metadata.uptime.no-native', { requires: ['metadata.uptime'], default: null, process(ctx, val) { return val == null ? ctx.get('metadata.uptime') !== 0 : val }, changed: val => this.toggle('hide-native-uptime', val), ui: { path: 'Channel > Metadata >> Player', title: "Hide Twitch's native Stream Uptime.", description: "By default, this is enabled whenever FFZ's own Stream Uptime display is enabled to avoid redundant information.", component: 'setting-check-box' } }); this.settings.add('layout.use-chat-fix', { requires: ['context.force_chat_fix', 'layout.swap-sidebars', 'layout.use-portrait', 'chat.use-width', 'context.isWatchParty'], process(ctx) { if ( ctx.get('context.isWatchParty') ) return false; return ctx.get('context.force_chat_fix') || ctx.get('layout.swap-sidebars') || ctx.get('layout.use-portrait') || ctx.get('chat.use-width') }, changed: val => { if ( val ) this.toggle('chat-no-animate', true); else if ( ! val && ! this._no_anim_timer ) this._no_anim_timer = requestAnimationFrame(() => { this._no_anim_timer = null; if ( ! this.settings.get('layout.use-chat-fix') ) this.toggle('chat-no-animate', false); }); this.toggle('chat-fix', val); this.emit('site.player:fix-player'); this.emit('site.layout:resize'); } }); this.settings.add('layout.side-nav.show', { default: 1, requires: ['layout.use-portrait'], process(ctx, val) { if ( val === 2 ) return ! ctx.get('layout.use-portrait'); return val; }, ui: { sort: -1, path: 'Appearance > Layout >> Side Navigation', title: 'Display Side Navigation', component: 'setting-select-box', data: [ {value: 0, title: 'Never'}, {value: 1, title: 'Always'}, {value: 2, title: 'Hide in Portrait'} ] }, changed: val => { this.toggle('hide-side-nav', !val); this.emit('site.layout:resize'); } }); this.settings.add('layout.side-nav.hide-viewers', { default: false, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Hide Channel View Counts', component: 'setting-check-box' }, changed: val => this.toggleHide('side-nav-viewers', val) }); this.settings.add('layout.side-nav.show-avatars', { default: true, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Display Channel Avatars', component: 'setting-check-box' }, changed: val => this.toggle('hide-side-nav-avatars', !val) }); this.settings.add('layout.side-nav.show-rec-channels', { default: 1, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Display Recommended / Popular Channels', component: 'setting-select-box', simple: true, simple_path: 'Twitch Appearance >> Side Navigation', data: [ {value: 0, title: 'Never'}, {value: 1, title: 'Always'}, {value: 2, title: 'When Side Navigation is Open'} ] }, changed: val => { this.toggleHide('side-rec-channels', val === 0); this.toggleHide('side-closed-rec-channels', val === 2); } }); this.settings.add('layout.side-nav.show-friends', { default: 1, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Display Online Friends', component: 'setting-select-box', simple: true, simple_path: 'Twitch Appearance >> Side Navigation', data: [ {value: 0, title: 'Never'}, {value: 1, title: 'Always'}, {value: 2, title: 'When Side Navigation is Open'} ] }, changed: val => { this.toggleHide('side-friends', val === 0); this.toggleHide('side-closed-friends', val === 2); } }); /*this.settings.add('layout.side-nav.show-rec-friends', { default: true, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Display Recommended Friends', component: 'setting-check-box' }, changed: val => this.toggleHide('side-rec-friends', !val) });*/ this.settings.add('layout.side-nav.hide-offline', { default: false, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Hide Offline Channels', component: 'setting-check-box', simple: true, simple_path: 'Twitch Appearance >> Side Navigation', }, changed: val => this.toggleHide('side-offline-channels', val) }); this.settings.add('layout.side-nav.rerun-style', { default: 1, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Display Reruns', component: 'setting-select-box', simple: true, simple_path: 'Twitch Appearance >> Side Navigation', data: [ {value: 0, title: 'Do Not Display'}, {value: 1, title: 'Normally'}, {value: 2, title: 'Faded (33% Opacity)'} ] }, changed: val => { this.toggleHide('side-rerun-channels', val === 0); this.toggle('side-rerun-opacity', val === 2); } }); this.settings.add('layout.swap-sidebars', { default: false, requires: ['context.isWatchParty'], process(ctx, val) { return ctx.get('context.isWatchParty') ? false : val; }, ui: { path: 'Appearance > Layout >> Side Navigation', title: 'Swap Sidebars', description: 'Swap navigation and chat to the opposite sides of the window.', component: 'setting-check-box' }, changed: val => { this.toggle('swap-sidebars', val); this.emit('site.layout:resize'); } }); this.settings.add('layout.minimal-navigation', { requires: ['layout.theatre-navigation', 'context.isWatchParty'], default: false, process(ctx, val) { if ( ctx.get('context.isWatchParty') ) return false; return ctx.get('layout.theatre-navigation') ? true : val; }, ui: { path: 'Appearance > Layout >> Top Navigation', title: 'Minimize Navigation', description: "Slide the site navigation bar up out of view when it isn't in use.", component: 'setting-check-box' }, changed: val => { this.toggle('minimal-navigation', val); this.emit('site.layout:resize'); } }); this.settings.add('layout.theatre-navigation', { requires: ['layout.is-theater-mode'], default: false, process(ctx, val) { return ctx.get('layout.is-theater-mode') ? val : false }, ui: { path: 'Appearance > Layout >> Top Navigation', title: 'Show the minimized navigation bar when in theatre mode.', component: 'setting-check-box' }, changed: val => { this.toggle('theatre-nav', val); this.emit('site.layout:resize'); } }); this.settings.add('layout.subtember', { default: true, ui: { path: 'Appearance > Layout >> Channel', title: 'Allow the Subtember upsell banner to appear.', component: 'setting-check-box' } }); /*this.settings.add('layout.discover', { default: true, ui: { path: 'Appearance > Layout >> Top Navigation', title: 'Show Discover link.', component: 'setting-check-box' }, changed: val => { this.toggleHide('top-discover', !val); this.updateTopNav(); } });*/ this.settings.add('layout.turbo-cta', { default: true, ui: { path: 'Appearance > Layout >> Top Navigation', title: 'Allow the Twitch Turbo button to appear.', component: 'setting-check-box', simple: true, simple_path: 'Twitch Appearance >> Top Navigation', } }); this.settings.add('layout.prime-offers', { default: true, ui: { path: 'Appearance > Layout >> Top Navigation', title: 'Show Prime Gaming Loot.', component: 'setting-check-box', simple: true, simple_path: 'Twitch Appearance >> Top Navigation', }, changed: val => this.toggleHide('prime-offers', !val) }); this.settings.add('layout.hide-discover-luna', { default: false, ui: { path: 'Appearance > Layout >> Top Navigation', title: 'Hide the "Discover Luna" link.', component: 'setting-check-box' }, changed: val => this.toggleHide('discover-luna', val) }); // Chat this.settings.add('whispers.show', { default: true, ui: { path: 'Chat > Whispers >> General', title: 'Display Whispers', component: 'setting-check-box', simple: true, simple_path: 'Chat > Hide Features' }, changed: val => { this.toggleHide('whispers', !val); this.emit('site.layout:resize'); } }); this.settings.add('chat.hype.show-pinned', { default: true, ui: { path: 'Chat > Hype Chat >> Appearance', title: 'Allow Hype Chat messages to appear pinned at the top of chat.', component: 'setting-check-box', simple: true, simple_path: 'Chat > Hide Features' }, changed: val => this.toggleHide('pinned-hype-chat', ! val) }); this.settings.add('chat.bits.show', { default: true, ui: { order: -1, path: 'Chat > Bits and Cheering >> Appearance', title: 'Display Bits', description: 'Display UI associated with bits. Note: This will not hide cheering in chat messages.', component: 'setting-check-box', simple: true, simple_path: 'Chat > Hide Features' }, changed: val => this.toggle('hide-bits', !val) }); // Other? this.settings.add('channel.show-celebrations', { default: true, ui: { path: 'Channel > Appearance >> General', title: 'Allow Celebrations to appear.', description: 'Celebrations are animations that cover the entire Twitch page.', component: 'setting-check-box' }, changed: val => this.toggleHide('celebration', ! val) }); this.settings.add('layout.theme.global-font', { default: '', ui: { path: 'Appearance > Theme >> Fonts', title: 'Font Family', description: 'Override the font used for the entire Twitch website. The old default font was: `"Helvetica Neue",Helvetica,Arial,sans-serif`\n\nAny font available via [Google Fonts](https://fonts.google.com/) can be loaded by prefixing the font name with `google:`.', component: 'setting-combo-box', data: () => getFontsList() }, changed: () => this.updateFont() }); this.settings.add('channel.hide-unfollow', { default: false, ui: { path: 'Channel > Appearance >> General', title: 'Hide the Unfollow button.', component: 'setting-check-box' }, changed: val => this.toggle('hide-unfollow-button', val) }); this.settings.add('channel.hide-live-indicator', { requires: ['context.route.name'], process(ctx, val) { return (ctx.get('context.route.name') === 'user' || ctx.get('context.route.name') === 'user-home') ? val : false }, default: false, ui: { path: 'Channel > Appearance >> General', title: 'Hide the "LIVE" indicator on live channel pages.', component: 'setting-check-box' }, changed: val => this.toggleHide('channel-live-ind', val) }); this.settings.add('channel.round-avatars', { default: true, ui: { path: 'Channel > Appearance >> General', title: 'Allow avatar images to be rounded.', component: 'setting-check-box' }, changed: val => this.toggle('square-avatars', !val) }); /*this.settings.add('channel.hide-not-live-bar', { default: false, ui: { path: 'Channel > Appearance >> General', title: 'Hide the "Now Live" bar.', description: 'Hide the bar which appears beneath clips and videos when the streamer is live, telling you they are live.', component: 'setting-check-box' }, changed: val => this.toggleHide('not-live-bar', val) });*/ } onEnable() { this.toggleHide('modview-hide-info', this.settings.get('metadata.modview.hide-info')); this.toggleHide('side-nav-viewers', this.settings.get('layout.side-nav.hide-viewers')); this.toggle('hide-native-uptime', this.settings.get('metadata.uptime.no-native')); this.toggle('hide-native-viewers', this.settings.get('metadata.viewers.no-native')); this.toggle('chat-fix', this.settings.get('layout.use-chat-fix')); this.toggle('swap-sidebars', this.settings.get('layout.swap-sidebars')); this.toggle('minimal-navigation', this.settings.get('layout.minimal-navigation')); this.toggle('theatre-nav', this.settings.get('layout.theatre-navigation')); this.toggle('hide-side-nav-avatars', ! this.settings.get('layout.side-nav.show-avatars')); this.toggle('hide-side-nav', !this.settings.get('layout.side-nav.show')); //this.toggleHide('side-rec-friends', !this.settings.get('layout.side-nav.show-rec-friends')); this.toggleHide('side-offline-channels', this.settings.get('layout.side-nav.hide-offline')); this.toggleHide('discover-luna', this.settings.get('layout.hide-discover-luna')); this.toggleHide('prime-offers', !this.settings.get('layout.prime-offers')); //this.toggleHide('top-discover', !this.settings.get('layout.discover')); this.toggle('hide-unfollow-button', this.settings.get('channel.hide-unfollow')); this.toggleHide('pinned-hype-chat', ! this.settings.get('chat.hype.show-pinned')); this.toggle('square-avatars', ! this.settings.get('channel.round-avatars')); //this.toggleHide('not-live-bar', this.settings.get('channel.hide-not-live-bar')); this.toggleHide('channel-live-ind', this.settings.get('channel.hide-live-indicator')); const reruns = this.settings.get('layout.side-nav.rerun-style'); this.toggleHide('side-rerun-channels', reruns === 0); this.toggle('side-rerun-opacity', reruns === 2); const recs = this.settings.get('layout.side-nav.show-rec-channels'); this.toggleHide('side-rec-channels', recs === 0); this.toggleHide('side-closed-rec-channels', recs === 2); const friends = this.settings.get('layout.side-nav.show-friends'); this.toggleHide('side-friends', friends === 0); this.toggleHide('side-closed-friends', friends === 2); this.toggleHide('whispers', !this.settings.get('whispers.show')); this.toggleHide('celebration', ! this.settings.get('channel.show-celebrations')); this.settings.getChanges('layout.subtember', val => this.toggleHide('subtember', !val)); this.updateFont(); this.updateTopNav(); this.emit('site.player:fix-player'); this.emit('site.layout:resize'); } updateTopNav() { requestAnimationFrame(() => this.emit('site.layout:update-nav')); } updateFont() { let font = this.settings.get('layout.theme.global-font'); if ( font && font.length ) { const [processed, unloader] = useFont(font); font = processed; if ( this._font_unloader ) this._font_unloader(); this._font_unloader = unloader; if ( font.indexOf(' ') !== -1 && font.indexOf(',') === -1 && font.indexOf('"') === -1 && font.indexOf("'") === -1 ) font = `"${font}"`; STYLE_VALIDATOR.style.fontFamily = ''; STYLE_VALIDATOR.style.fontFamily = font; if ( STYLE_VALIDATOR.style.fontFamily ) { this.setVariable('global-font', font); this.toggle('global-font', true); return; } } this.toggle('global-font', false); this.deleteVariable('global-font'); } toggleHide(key, val) { const k = `hide--${key}`; if ( ! val ) { this.style.delete(k); return; } if ( ! has(CLASSES, key) ) throw new Error(`cannot find class for "${key}"`); this.style.set(k, `${CLASSES[key]} { display: none !important }`); } toggle(key, val) { val = !! val; if ( (this._state[key] ?? false) === val ) return; this._state[key] = val; this._apply(key); } _apply(key) { const val = this._state[key]; if ( ! val ) { if ( this.style ) this.style.delete(key); return; } if ( this.style.has(key) ) return; if ( ! this.chunks_loaded ) return this.loadFromContext().then(() => this._apply(key)); if ( ! has(this.chunks, key) ) { this.log.warn(`Unknown chunk name "${key}" for toggle()`); return; } this.style.set(key, this.chunks[key]); } set(key, val) { return this.style.set(key, val) } delete(key) { return this.style.delete(key) } setVariable(key, val, scope = 'body') { this.style.set(`var--${key}`, `${scope} { --ffz-${key}: ${val}; }`); } deleteVariable(key) { this.style.delete(`var--${key}`) } loadFromContext() { if ( this.chunks_loaded ) return; return new Promise(async r => { const raw = (await import(/* webpackChunkName: "site-css-tweaks" */ './styles.js')).default; for(const key of raw.keys()) { const k = key.slice(2, key.length - (key.endsWith('.scss') ? 5 : 4)); this.chunks[k] = raw(key).default; } this.emit('site.layout:resize'); this.chunks_loaded = true; r(); }) } }