Last active
August 12, 2026 21:12
-
-
Save klydeinside/ec546153caba19d37890dd5a0f0f1ade to your computer and use it in GitHub Desktop.
Twitch userscripts - Liquid Glass Suite (merged)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Twitch – Liquid Glass Suite | |
| // @namespace https://cody-tools.local | |
| // @version 1.3 | |
| // @description All-in-one: frosted "Liquid Glass" sidebar treatment (Off/Subtle/More/Full — Full gives each followed-channel card a raised glass-bubble look), a Live Follows shortcut button (mini-player dims via a dark overlay pseudo-element instead of opacity, so the video's own compositing is never touched), and a narrow Following grid — all controlled from one glass-styled settings panel (gear icon, bottom-right). Replaces twitch-liquid-glass-sidebar, twitch-open-chat-button, and twitch-following-narrow-grid. | |
| // @match https://www.twitch.tv/* | |
| // @grant GM_setValue | |
| // @grant GM_getValue | |
| // @updateURL https://gist.githubusercontent.com/klydeinside/ec546153caba19d37890dd5a0f0f1ade/raw/twitch-liquid-glass-suite.user.js | |
| // @downloadURL https://gist.githubusercontent.com/klydeinside/ec546153caba19d37890dd5a0f0f1ade/raw/twitch-liquid-glass-suite.user.js | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // --------------------------------------------------------------------- | |
| // Settings (persisted via GM_setValue/GM_getValue). Keys match the | |
| // original standalone scripts' storage keys where applicable, so | |
| // switching from those scripts to this one keeps your existing | |
| // preferences instead of resetting them. | |
| // --------------------------------------------------------------------- | |
| const GLASS_LEVEL_KEY = 'lgSidebarLevel'; // 0 off / 1 subtle / 2 more / 3 full (bubble) | |
| const COLS_KEY = 'lfDirColumns'; // 2 or 3 | |
| const MINI_DIM_KEY = 'lgMiniPlayerDimEnabled'; // boolean | |
| const GLASS_LEVELS = ['Off', 'Subtle', 'More', 'Full']; | |
| function getGlassLevel() { | |
| const v = GM_getValue(GLASS_LEVEL_KEY, 2); | |
| return (v === 0 || v === 1 || v === 2 || v === 3) ? v : 2; | |
| } | |
| function setGlassLevel(n) { | |
| GM_setValue(GLASS_LEVEL_KEY, n); | |
| applyStyles(); | |
| renderPanel(); | |
| } | |
| function getCols() { | |
| const v = GM_getValue(COLS_KEY, 3); | |
| return v === 2 ? 2 : 3; | |
| } | |
| function setCols(n) { | |
| GM_setValue(COLS_KEY, n); | |
| applyStyles(); | |
| renderPanel(); | |
| } | |
| function getMiniDimEnabled() { | |
| const v = GM_getValue(MINI_DIM_KEY, true); | |
| return v !== false; | |
| } | |
| function setMiniDimEnabled(b) { | |
| GM_setValue(MINI_DIM_KEY, b); | |
| applyStyles(); | |
| renderPanel(); | |
| } | |
| // --------------------------------------------------------------------- | |
| // Selectors — verified against live Twitch DOM. data-a-target hooks are | |
| // Twitch's own stable attributes; the class names below are semantic | |
| // ones, not hashed styled-components classes, so they've held up. | |
| // --------------------------------------------------------------------- | |
| const SIDE_NAV = '[data-a-target="side-nav-bar"]'; | |
| const SCROLL_AREA = '.side-nav__scrollable_content'; | |
| const HEADER = '.followed-side-nav-header, .side-nav-header'; | |
| const CARD = '.side-nav-card'; | |
| const AVATAR_IMG = '.side-nav-card__avatar img'; | |
| const SIDE_ARROW = '[data-a-target="side-nav-arrow"]'; | |
| const FOLLOWING_LINK = '[data-a-target="following-link"]'; | |
| const LIVE_TAB = '[data-a-target="following-live-tab"]'; | |
| function isFollowingDirectory() { | |
| return location.pathname.replace(/\/+$/, '') === '/directory/following/live'; | |
| } | |
| // --------------------------------------------------------------------- | |
| // Combined stylesheet — rebuilt any time a setting changes or Twitch | |
| // re-renders. Sections are conditionally included based on current | |
| // settings / current page so nothing leaks where it shouldn't. | |
| // --------------------------------------------------------------------- | |
| const STYLE_ID = 'lgSuiteStyle'; | |
| function applyStyles() { | |
| let style = document.getElementById(STYLE_ID); | |
| if (!style) { | |
| style = document.createElement('style'); | |
| style.id = STYLE_ID; | |
| document.head.appendChild(style); | |
| } | |
| let css = ''; | |
| // --- Liquid glass sidebar --- | |
| const level = getGlassLevel(); | |
| if (level > 0) { | |
| css += ` | |
| ${SIDE_NAV} { | |
| position: relative; | |
| background: | |
| linear-gradient(180deg, rgba(255,255,255,0.09), rgba(255,255,255,0.01) 35%, rgba(255,255,255,0.04)), | |
| color-mix(in srgb, var(--color-background-body, #0e0e10) 20%, transparent) !important; | |
| -webkit-backdrop-filter: blur(14px) saturate(140%); | |
| backdrop-filter: blur(14px) saturate(140%); | |
| border-right: 1px solid rgba(255,255,255,0.35); | |
| box-shadow: | |
| inset 0 1px 0 rgba(255,255,255,0.5), | |
| inset -1px 0 0 rgba(255,255,255,0.08), | |
| 1px 0 0 rgba(120,180,255,0.12), | |
| 14px 0 34px rgba(0,0,0,0.5); | |
| } | |
| ${SCROLL_AREA} { | |
| background: transparent !important; | |
| scrollbar-width: none; | |
| -ms-overflow-style: none; | |
| } | |
| ${SCROLL_AREA}::-webkit-scrollbar { | |
| display: none; | |
| } | |
| ${HEADER} { | |
| position: relative; | |
| -webkit-backdrop-filter: blur(8px); | |
| backdrop-filter: blur(8px); | |
| background: rgba(255,255,255,0.03) !important; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| } | |
| ${CARD} { | |
| position: relative; | |
| border-radius: 10px; | |
| transition: | |
| background 0.25s ease, | |
| transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), | |
| box-shadow 0.25s ease; | |
| } | |
| ${CARD}:hover { | |
| background: color-mix(in srgb, white 7%, transparent); | |
| box-shadow: | |
| inset 0 0 0 1px rgba(255,255,255,0.4), | |
| 0 6px 16px rgba(0,0,0,0.35); | |
| transform: translateX(3px) scale(1.015); | |
| } | |
| ${AVATAR_IMG} { | |
| box-shadow: 0 0 0 1px rgba(255,255,255,0.2); | |
| } | |
| `; | |
| if (level >= 2) { | |
| css += ` | |
| ${SIDE_NAV}::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| pointer-events: none; | |
| background: linear-gradient( | |
| 115deg, | |
| transparent 15%, | |
| rgba(255,255,255,0.07) 38%, | |
| rgba(255,255,255,0.18) 50%, | |
| rgba(255,255,255,0.07) 62%, | |
| transparent 85% | |
| ); | |
| background-size: 250% 250%; | |
| mix-blend-mode: overlay; | |
| animation: lgSheen 7s ease-in-out infinite; | |
| } | |
| @keyframes lgSheen { | |
| 0% { background-position: 0% 0%; } | |
| 50% { background-position: 100% 100%; } | |
| 100% { background-position: 0% 0%; } | |
| } | |
| ${CARD}::before { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| border-radius: inherit; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| background: radial-gradient( | |
| 160px 160px at var(--lg-mx, 50%) var(--lg-my, 50%), | |
| rgba(255,255,255,0.3), | |
| transparent 65% | |
| ); | |
| } | |
| ${CARD}:hover::before { | |
| opacity: 1; | |
| } | |
| `; | |
| } | |
| if (level === 3) { | |
| // "Full" — each followed-channel card looks like a raised | |
| // dome of liquid glass sitting on top of the sidebar: a | |
| // strong convex highlight up top, a dark rim underneath for | |
| // depth, extra local blur/saturation to suggest refraction, | |
| // and a lift/scale on hover like the bubble is reacting to | |
| // your cursor. Experimental — easy to strip out (this is | |
| // the only `level === 3` block) if it doesn't land right. | |
| css += ` | |
| ${CARD} { | |
| border-radius: 18px; | |
| background: | |
| radial-gradient(130% 150% at 28% 15%, rgba(255,255,255,0.4), rgba(255,255,255,0.06) 45%, rgba(255,255,255,0.14) 100%); | |
| -webkit-backdrop-filter: blur(10px) saturate(180%) brightness(1.12); | |
| backdrop-filter: blur(10px) saturate(180%) brightness(1.12); | |
| box-shadow: | |
| inset 0 2px 1px rgba(255,255,255,0.9), | |
| inset 0 -8px 12px rgba(0,0,0,0.28), | |
| inset 8px 0 14px rgba(255,255,255,0.12), | |
| inset -6px 0 10px rgba(0,0,0,0.12), | |
| 0 12px 26px rgba(0,0,0,0.45), | |
| 0 3px 8px rgba(0,0,0,0.35); | |
| transform: scale(1.02); | |
| } | |
| ${CARD}:hover { | |
| background: | |
| radial-gradient(130% 150% at 28% 15%, rgba(255,255,255,0.5), rgba(255,255,255,0.08) 45%, rgba(255,255,255,0.18) 100%); | |
| box-shadow: | |
| inset 0 2px 1px rgba(255,255,255,1), | |
| inset 0 -8px 14px rgba(0,0,0,0.32), | |
| inset 8px 0 16px rgba(255,255,255,0.18), | |
| inset -6px 0 10px rgba(0,0,0,0.14), | |
| 0 16px 34px rgba(145,71,255,0.4), | |
| 0 0 0 1px rgba(145,71,255,0.45); | |
| transform: translateX(3px) scale(1.06); | |
| } | |
| `; | |
| } | |
| } | |
| // --- Narrow Following grid (only on the Live tab of the directory) --- | |
| if (isFollowingDirectory()) { | |
| const cols = getCols(); | |
| const pct = (100 / cols).toFixed(4); | |
| css += ` | |
| .tw-tower { | |
| justify-content: flex-start !important; | |
| } | |
| .tw-tower > div { | |
| box-sizing: border-box !important; | |
| flex: 0 0 ${pct}% !important; | |
| width: ${pct}% !important; | |
| max-width: ${pct}% !important; | |
| } | |
| .tw-tower article { | |
| border-radius: 8px; | |
| transition: | |
| transform 0.18s cubic-bezier(0.16, 1, 0.3, 1), | |
| box-shadow 0.18s cubic-bezier(0.16, 1, 0.3, 1), | |
| border-radius 0.18s ease; | |
| } | |
| .tw-tower article:hover { | |
| transform: translateY(-4px) scale(1.02); | |
| box-shadow: | |
| 0 12px 32px rgba(145,71,255,0.35), | |
| 0 0 0 1px rgba(145,71,255,0.5); | |
| position: relative; | |
| z-index: 2; | |
| } | |
| `; | |
| } | |
| // --- Mini-player dim-on-rest --- | |
| // IMPORTANT: this used to set `opacity` directly on .persistent-player. | |
| // That forces Chromium to promote the whole subtree (including the | |
| // live <video>) into a translucent compositing layer, and that was | |
| // triggering Twitch's player-error ("?") placeholder intermittently | |
| // — confirmed by the fact hovering (opacity back to 1) always fixed | |
| // it instantly. Using a separate dark overlay pseudo-element instead | |
| // dims the box visually without ever touching the video's own | |
| // opacity/compositing, so the player itself is untouched. | |
| if (getMiniDimEnabled()) { | |
| css += ` | |
| .persistent-player::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(0,0,0,0.55); | |
| opacity: 1; | |
| pointer-events: none; | |
| transition: opacity 0.25s ease; | |
| z-index: 5; | |
| } | |
| .persistent-player:hover::after { | |
| opacity: 0; | |
| } | |
| `; | |
| } | |
| style.textContent = css; | |
| } | |
| // Delegated mousemove: tracks cursor position per hovered sidebar card | |
| // and writes it to CSS custom properties, so the "Full" glass level's | |
| // shine follows the cursor like light catching curved glass. | |
| let shineHandlerBound = false; | |
| function bindShineHandler() { | |
| if (shineHandlerBound) return; | |
| shineHandlerBound = true; | |
| document.addEventListener('mousemove', (e) => { | |
| if (getGlassLevel() < 2) return; | |
| const card = e.target.closest && e.target.closest(CARD); | |
| if (!card) return; | |
| const rect = card.getBoundingClientRect(); | |
| const x = ((e.clientX - rect.left) / rect.width) * 100; | |
| const y = ((e.clientY - rect.top) / rect.height) * 100; | |
| card.style.setProperty('--lg-mx', `${x}%`); | |
| card.style.setProperty('--lg-my', `${y}%`); | |
| }, { passive: true }); | |
| } | |
| // --------------------------------------------------------------------- | |
| // Live Follows shortcut button — inserted next to the side-nav collapse | |
| // arrow. Clicking Twitch's own real anchors (not a synthetic <a> or a | |
| // raw location.href) is what keeps a playing stream alive in the | |
| // floating mini-player instead of forcing a full page reload. | |
| // --------------------------------------------------------------------- | |
| const BTN_ID = 'lgGoLiveFollowsBtn'; | |
| const DEST = 'https://www.twitch.tv/directory/following/live'; | |
| function goToLiveFollows() { | |
| const followingLink = document.querySelector(FOLLOWING_LINK); | |
| if (!followingLink) { | |
| window.location.href = DEST; | |
| return; | |
| } | |
| followingLink.click(); | |
| const start = Date.now(); | |
| const poll = setInterval(() => { | |
| const liveTab = document.querySelector(LIVE_TAB); | |
| if (liveTab) { | |
| clearInterval(poll); | |
| liveTab.click(); | |
| } else if (Date.now() - start > 3000) { | |
| clearInterval(poll); | |
| } | |
| }, 50); | |
| } | |
| function insertGoLiveButton() { | |
| if (document.getElementById(BTN_ID)) return; | |
| const sideArrow = document.querySelector(SIDE_ARROW); | |
| if (!sideArrow) return; | |
| const wrapper = sideArrow.parentElement; | |
| const row = wrapper.parentElement; | |
| if (!wrapper || !row) return; | |
| const btn = document.createElement('button'); | |
| btn.id = BTN_ID; | |
| btn.className = sideArrow.className; | |
| btn.setAttribute('aria-label', 'Go to Live Follows'); | |
| btn.style.marginLeft = '4px'; | |
| btn.innerHTML = `<div class="ButtonIconFigure-sc-1emm8lf-0 jgYFuA"><div class="ScSvgWrapper-sc-wkgzod-0 cnGLHG tw-svg"><svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" role="presentation"><path d="M3 5h2v14H3V5Zm19.414 7-6.707-6.707-1.414 1.414L18.586 11H7v2h11.586l-4.293 4.293 1.414 1.414L22.414 12Z"></path></svg></div></div>`; | |
| btn.addEventListener('click', goToLiveFollows); | |
| const newWrapper = wrapper.cloneNode(false); | |
| newWrapper.removeAttribute('id'); | |
| newWrapper.appendChild(btn); | |
| row.insertBefore(newWrapper, wrapper.nextSibling); | |
| } | |
| // --------------------------------------------------------------------- | |
| // Settings panel — one gear icon (bottom-right), one glass-styled panel | |
| // with everything in it. Replaces the three separate floating pills. | |
| // --------------------------------------------------------------------- | |
| const GEAR_ID = 'lgSuiteGear'; | |
| const PANEL_ID = 'lgSuitePanel'; | |
| let panelOpen = false; | |
| function segButton(label, active, action, value) { | |
| return `<button class="lgSeg${active ? ' lgSegActive' : ''}" data-action="${action}" data-value="${value}">${label}</button>`; | |
| } | |
| function panelHTML() { | |
| const level = getGlassLevel(); | |
| const cols = getCols(); | |
| const dim = getMiniDimEnabled(); | |
| return ` | |
| <div class="lgPanelRow"> | |
| <div class="lgPanelLabel">Liquid Glass</div> | |
| <div class="lgSegGroup"> | |
| ${GLASS_LEVELS.map((l, i) => segButton(l, level === i, 'glass', i)).join('')} | |
| </div> | |
| </div> | |
| <div class="lgPanelRow"> | |
| <div class="lgPanelLabel">Following Grid</div> | |
| <div class="lgSegGroup"> | |
| ${segButton('2 col', cols === 2, 'cols', 2)} | |
| ${segButton('3 col', cols === 3, 'cols', 3)} | |
| </div> | |
| </div> | |
| <div class="lgPanelRow"> | |
| <div class="lgPanelLabel">Dim Mini-Player</div> | |
| <div class="lgSegGroup"> | |
| ${segButton('On', dim, 'dim', 1)} | |
| ${segButton('Off', !dim, 'dim', 0)} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function renderPanel() { | |
| const panel = document.getElementById(PANEL_ID); | |
| if (panel) panel.innerHTML = panelHTML(); | |
| } | |
| function ensurePanelStyle() { | |
| if (document.getElementById('lgSuitePanelStyle')) return; | |
| const style = document.createElement('style'); | |
| style.id = 'lgSuitePanelStyle'; | |
| style.textContent = ` | |
| #${GEAR_ID} { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 999999; | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 9999px; | |
| border: 1px solid rgba(255,255,255,0.35); | |
| background: | |
| linear-gradient(180deg, rgba(255,255,255,0.14), rgba(255,255,255,0.04)), | |
| color-mix(in srgb, #0e0e10 55%, transparent); | |
| -webkit-backdrop-filter: blur(14px) saturate(150%); | |
| backdrop-filter: blur(14px) saturate(150%); | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), 0 8px 20px rgba(0,0,0,0.45); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| color: #fff; | |
| transition: transform 0.15s ease; | |
| } | |
| #${GEAR_ID}:hover { | |
| transform: scale(1.06); | |
| } | |
| #${PANEL_ID} { | |
| position: fixed; | |
| bottom: 76px; | |
| right: 20px; | |
| z-index: 999999; | |
| width: 240px; | |
| padding: 14px; | |
| border-radius: 16px; | |
| border: 1px solid rgba(255,255,255,0.35); | |
| background: | |
| linear-gradient(180deg, rgba(255,255,255,0.1), rgba(255,255,255,0.02)), | |
| color-mix(in srgb, #0e0e10 45%, transparent); | |
| -webkit-backdrop-filter: blur(20px) saturate(150%); | |
| backdrop-filter: blur(20px) saturate(150%); | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), 0 20px 50px rgba(0,0,0,0.5); | |
| color: #fff; | |
| font-family: Arial, sans-serif; | |
| display: none; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| #${PANEL_ID}.lgOpen { | |
| display: flex; | |
| } | |
| .lgPanelRow { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .lgPanelLabel { | |
| font-size: 12px; | |
| font-weight: 700; | |
| opacity: 0.85; | |
| letter-spacing: 0.02em; | |
| } | |
| .lgSegGroup { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .lgSeg { | |
| flex: 1; | |
| padding: 6px 0; | |
| font-size: 12px; | |
| font-weight: 600; | |
| font-family: Arial, sans-serif; | |
| border-radius: 8px; | |
| border: 1px solid rgba(255,255,255,0.2); | |
| background: rgba(255,255,255,0.06); | |
| color: #fff; | |
| cursor: pointer; | |
| transition: background 0.15s ease, border-color 0.15s ease; | |
| } | |
| .lgSeg:hover { | |
| background: rgba(255,255,255,0.14); | |
| } | |
| .lgSegActive { | |
| background: #9147ff; | |
| border-color: #9147ff; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| function createGear() { | |
| if (document.getElementById(GEAR_ID)) return; | |
| ensurePanelStyle(); | |
| const gear = document.createElement('button'); | |
| gear.id = GEAR_ID; | |
| gear.setAttribute('aria-label', 'Liquid Glass Suite settings'); | |
| gear.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`; | |
| gear.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| panelOpen = !panelOpen; | |
| const panel = document.getElementById(PANEL_ID); | |
| if (panel) panel.classList.toggle('lgOpen', panelOpen); | |
| }); | |
| document.body.appendChild(gear); | |
| const panel = document.createElement('div'); | |
| panel.id = PANEL_ID; | |
| panel.innerHTML = panelHTML(); | |
| panel.addEventListener('click', (e) => { | |
| const btn = e.target.closest('.lgSeg'); | |
| if (!btn) return; | |
| const action = btn.dataset.action; | |
| const value = btn.dataset.value; | |
| if (action === 'glass') setGlassLevel(Number(value)); | |
| else if (action === 'cols') setCols(Number(value)); | |
| else if (action === 'dim') setMiniDimEnabled(value === '1'); | |
| }); | |
| document.body.appendChild(panel); | |
| document.addEventListener('click', (e) => { | |
| if (!panelOpen) return; | |
| if (panel.contains(e.target) || gear.contains(e.target)) return; | |
| panelOpen = false; | |
| panel.classList.remove('lgOpen'); | |
| }); | |
| } | |
| // --------------------------------------------------------------------- | |
| // Single shared tick loop for the whole suite. | |
| // --------------------------------------------------------------------- | |
| function tick() { | |
| applyStyles(); | |
| insertGoLiveButton(); | |
| createGear(); | |
| bindShineHandler(); | |
| } | |
| let tickScheduled = false; | |
| function scheduleTick() { | |
| if (tickScheduled) return; | |
| tickScheduled = true; | |
| requestAnimationFrame(() => { | |
| tickScheduled = false; | |
| tick(); | |
| }); | |
| } | |
| const origPushState = history.pushState; | |
| history.pushState = function (...args) { | |
| origPushState.apply(this, args); | |
| scheduleTick(); | |
| }; | |
| window.addEventListener('popstate', scheduleTick); | |
| const observer = new MutationObserver(scheduleTick); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| tick(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment