Last active
February 14, 2026 18:43
-
-
Save PsychoSmiley/91588d448cb573509833c7685cee96a0 to your computer and use it in GitHub Desktop.
Userscript to track and get notified in F95zone of game updates - inspired by LenAnderson
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 F95zone Game Watcher | |
| // @namespace f95-watcher | |
| // @version 1.1 | |
| // @description Track F95zone game updates - adds a Tracked tab in the account dropdown | |
| // @match https://f95zone.to/* | |
| // @author PsychoSmiley | |
| // @downloadURL https://gist.github.com/PsychoSmiley/raw/f95-watcher.user.js | |
| // @updateURL https://gist.github.com/PsychoSmiley/raw/f95-watcher.user.js | |
| // @grant GM.getValue | |
| // @grant GM.setValue | |
| // @grant GM_addStyle | |
| // @run-at document-end | |
| // @noframes | |
| // ==/UserScript== | |
| (async () => { | |
| 'use strict'; | |
| // ==================== CONFIG ==================== | |
| // NOTIF_TYPE controls badge aggressiveness: | |
| // 0 = no badge anywhere (Tracked tab in dropdown only) | |
| // 1 = original only (account avatar on non-latest_alpha pages) | |
| // 2 = max (account avatar on normal pages + Forums nav on latest_alpha pages) | |
| const NOTIF_TYPE = 2; | |
| const K = 'f95_watchlist', | |
| BATCH = 50, DELAY = 600, CHECK_H = 12; | |
| const esc = s => s ? s.replace(/[&<>"']/g, c => | |
| ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])) : ''; | |
| const ago = ts => { | |
| const d = Date.now() - ts; | |
| return d < 6e4 ? 'just now' : d < 36e5 ? ~~(d/6e4)+'m ago' | |
| : d < 864e5 ? ~~(d/36e5)+'h ago' : ~~(d/864e5)+'d ago'; | |
| }; | |
| const wait = ms => new Promise(r => setTimeout(r, ms)); | |
| const stop = e => { e.preventDefault(); e.stopPropagation(); }; | |
| const isLatestAlpha = () => /\/sam\/latest_alpha\//.test(location.pathname); | |
| // ==================== STYLES ==================== | |
| GM_addStyle(` | |
| .f95w-spin{display:inline-block;animation:f95w-s .6s linear infinite} | |
| @keyframes f95w-s{to{transform:rotate(360deg)}} | |
| .f95w-badge{background:#b71c1c;color:#fff;font-size:10px;font-weight:600; | |
| padding:1px 5px;border-radius:8px;position:absolute;top:-2px;right:-6px} | |
| .f95w-badge-wrap{position:relative;display:inline-block} | |
| .f95w-new{background:#b71c1c;color:#fff;font-size:10px;font-weight:600; | |
| padding:1px 5px;border-radius:2px;margin-left:6px;vertical-align:middle; | |
| cursor:help} | |
| .f95w-dim{opacity:.5}.f95w-dim:hover{opacity:.8} | |
| .f95w-grp{color:#888!important;font-size:11px!important;text-transform:uppercase; | |
| letter-spacing:.3px;padding:8px 12px 4px!important;background:0 0!important; | |
| border-bottom:1px solid #2a2a2a!important} | |
| .f95w-dd-search{width:100%;box-sizing:border-box} | |
| .f95w-dd-empty{padding:24px;text-align:center;color:#555;font-size:13px} | |
| .f95w-dd-footer{display:flex;justify-content:space-between;align-items:center} | |
| .f95w-dd-footer a{color:#9398A0!important;text-decoration:none;font-size:13px} | |
| .f95w-dd-footer a:hover{color:#eee!important} | |
| .f95w-dd-status{color:#666;font-size:12px} | |
| .f95w-cover{width:72px;height:72px;object-fit:cover;border-radius:3px;background:#2a2a2a;display:block;flex-shrink:0} | |
| .f95w-fig{align-self:stretch;display:flex;align-items:center;margin-right:0!important;padding:0!important} | |
| .f95w-acts{display:flex;gap:6px;align-items:center;font-size:13px;font-family:Lato,Roboto,-apple-system,sans-serif} | |
| .f95w-acts a{color:#9398A0;text-decoration:none;font-size:13px} | |
| .f95w-acts a:hover{color:#eee} | |
| .f95w-acts a.act-played{color:#4caf50} | |
| .f95w-acts a.act-played:hover{color:#66bb6a} | |
| .f95w-acts a.act-remove:hover{color:#b71c1c} | |
| .f95w-acts-sep{color:#555} | |
| .f95w-title{font-size:15px;color:#eee;font-family:Lato,Roboto,-apple-system,sans-serif} | |
| .f95w-title a{color:#eee;text-decoration:none} | |
| .f95w-title a:hover{color:#b71c1c} | |
| .f95w-minor{font-size:13px;color:#9398A0;margin-top:2px;font-family:Lato,Roboto,-apple-system,sans-serif} | |
| .f95w-minor .ver{color:#4caf50}.f95w-minor s{color:#666} | |
| `); | |
| // ==================== STORE ==================== | |
| const Store = { | |
| _d: null, | |
| async _load() { | |
| if (this._d) return this._d; | |
| let d = await GM.getValue(K, null); | |
| if (!d) { const l = localStorage.getItem(K); d = l ? JSON.parse(l) : null; } | |
| d = d || { games: [], lastCheck: 0 }; | |
| const renames = { userVersion:'uv', userVer:'uv', developer:'dev', addedAt:'added' }; | |
| let dirty = false; | |
| for (const g of d.games) { | |
| for (const [old, nw] of Object.entries(renames)) | |
| if (old in g) { g[nw] = g[old]; delete g[old]; dirty = true; } | |
| if ('status' in g) { delete g.status; dirty = true; } | |
| if (Array.isArray(g.cl)) { g.cl = null; dirty = true; } | |
| if (g.cl && g.version === g.uv) { g.cl = null; dirty = true; } | |
| } | |
| if (dirty) await this._flush(d); | |
| return this._d = d; | |
| }, | |
| async _flush(d) { | |
| this._d = d; | |
| await GM.setValue(K, d); | |
| localStorage.setItem(K, JSON.stringify(d)); | |
| }, | |
| async games() { return (await this._load()).games; }, | |
| async add(g) { | |
| const d = await this._load(); | |
| if (d.games.find(x => x.id === g.id)) return false; | |
| d.games.push({ id:g.id, title:g.title, dev:g.dev, version:g.version, | |
| uv:g.version, url:g.url, cover:g.cover||null, added:Date.now() }); | |
| await this._flush(d); return true; | |
| }, | |
| async rm(id) { const d = await this._load(); d.games = d.games.filter(g=>g.id!==id); await this._flush(d); }, | |
| async up(id, u) { | |
| const d = await this._load(); | |
| const g = d.games.find(x=>x.id===id); | |
| if (g) { Object.assign(g, u); await this._flush(d); } | |
| }, | |
| async stamp() { const d = await this._load(); d.lastCheck = Date.now(); await this._flush(d); }, | |
| async last() { return (await this._load()).lastCheck; } | |
| }; | |
| // ==================== API ==================== | |
| const API = { | |
| async versions(ids) { | |
| const out = {}; | |
| for (let i = 0; i < ids.length; i += BATCH) { | |
| const b = ids.slice(i, i + BATCH); | |
| try { | |
| const r = await fetch(`/sam/checker.php?threads=${b.join(',')}`); | |
| if (r.ok) { const d = await r.json(); if (d.status==='ok'&&d.msg) Object.assign(out,d.msg); } | |
| } catch(e) { console.warn('[F95W]', e); } | |
| if (i + BATCH < ids.length) await wait(500); | |
| } | |
| return out; | |
| }, | |
| async search(q) { | |
| if (!q||q.length<2) return []; | |
| try { | |
| const r = await fetch(`/sam/latest_alpha/latest_data.php?cmd=list&cat=games&search=${encodeURIComponent(q)}&rows=15&sort=likes&_=${Date.now()}`); | |
| if (!r.ok) return []; | |
| const d = await r.json(); | |
| return d.status==='ok'&&d.msg?.data | |
| ? d.msg.data.map(g=>({ id:g.thread_id, title:g.title, dev:g.creator, | |
| version:g.version==='Unknown'?null:g.version, | |
| url:`/threads/${g.thread_id}/` })) | |
| : []; | |
| } catch(e) { console.warn('[F95W]', e); return []; } | |
| } | |
| }; | |
| // ==================== SCRAPER ==================== | |
| const RE_CL = /^(Changelog|Change-?logs?|Changelog history):?$/i; | |
| const Scraper = { | |
| tid: () => location.href.match(/threads\/[^.]*\.(\d+)/)?.[1], | |
| isThread: () => /threads\/[^.]*\.\d+/.test(location.href), | |
| _cl(post) { | |
| if (!post) return null; | |
| let hdr = null; | |
| for (const b of post.querySelectorAll('b')) { | |
| if (RE_CL.test(b.textContent.trim())) { hdr = b; break; } | |
| } | |
| if (!hdr) return null; | |
| let sib = hdr.nextElementSibling; | |
| while (sib && !sib.classList.contains('bbCodeSpoiler')) sib = sib.nextElementSibling; | |
| if (!sib) return null; | |
| const outer = sib.querySelector('.bbCodeBlock-content'); | |
| if (!outer) return null; | |
| for (const node of outer.children) { | |
| if (node.classList?.contains('bbCodeSpoiler')) { | |
| const inner = node.querySelector('.bbCodeBlock-content'); | |
| const txt = inner ? (inner.innerText||inner.textContent||'').trim() : ''; | |
| return txt ? txt.substring(0, 1000) : null; | |
| } | |
| } | |
| return null; | |
| }, | |
| _parse(doc, url) { | |
| const el = doc.querySelector('.p-title-value'); | |
| const post = doc.querySelector('.message-threadStarterPost article .bbWrapper'); | |
| if (!el) return null; | |
| const full = el.innerText?.trim()||'', txt = post?.innerText||''; | |
| const tid = url.match(/\.(\d+)/)?.[1]; | |
| const vT = full.match(/\[([^\]]*?v?[\d.]+[^\]]*?)\]/i)?.[1]; | |
| const vP = txt.match(/Version\s*:\s*([^\r\n]+)/i)?.[1]?.trim(); | |
| const title = full.replace(/^\s*\[[^\]]+\]\s*/g,'') | |
| .replace(/\s*\[[^\]]*v?[\d.]+[^\]]*\]\s*/gi,'') | |
| .replace(/\s*\[[^\]]+\]\s*$/g,'').trim(); | |
| const img = post?.querySelector('img.bbImage'); | |
| return { | |
| id: parseInt(tid), | |
| title: title || `Thread #${tid}`, | |
| dev: txt.match(/Developer(?:\/Publisher)?\s*:\s*([^\r\n]+)/i)?.[1]?.trim()?.substring(0,50)||null, | |
| version: vT||(vP&&vP.length<50?vP:null), | |
| cover: img ? (img.getAttribute('data-src')||img.getAttribute('src')||null) : null, | |
| cl: this._cl(post), | |
| url: url.split('?')[0].split('#')[0] | |
| }; | |
| }, | |
| current() { | |
| return this.tid() | |
| ? this._parse(document, location.href.replace(/(threads\/[^.]*\.\d+\/).*$/,'$1')) | |
| : null; | |
| }, | |
| async fetch(url) { | |
| try { | |
| const r = await fetch(url); | |
| if (!r.ok) return null; | |
| return this._parse(new DOMParser().parseFromString(await r.text(),'text/html'), url); | |
| } catch(e) { console.warn('[F95W]', e); return null; } | |
| } | |
| }; | |
| // ==================== CHECKER ==================== | |
| function scrapeFields(s, g) { | |
| const u = {}; | |
| if (s?.cover && !g.cover) u.cover = s.cover; | |
| if (s?.title) u.title = s.title; | |
| if (s?.dev) u.dev = s.dev; | |
| return u; | |
| } | |
| async function checkAll() { | |
| const games = await Store.games(); | |
| if (!games.length) return 0; | |
| let n = 0; | |
| const ver = await API.versions(games.map(g => g.id)); | |
| for (const g of games) { | |
| const v = ver[g.id]; | |
| const unplayed = g.version !== g.uv; | |
| if (v && v !== 'Unknown') { | |
| if (v !== g.version) { | |
| const u = { version: v }; | |
| await wait(DELAY); | |
| const s = await Scraper.fetch(g.url); | |
| Object.assign(u, scrapeFields(s, g)); | |
| if (s?.cl) u.cl = s.cl; | |
| await Store.up(g.id, u); | |
| n++; | |
| } else if (!g.cover || (unplayed && !g.cl)) { | |
| await wait(DELAY); | |
| const s = await Scraper.fetch(g.url); | |
| const u = scrapeFields(s, g); | |
| if (s?.cl && unplayed) u.cl = s.cl; | |
| if (Object.keys(u).length) await Store.up(g.id, u); | |
| } | |
| } else if (g.url) { | |
| await wait(DELAY); | |
| const s = await Scraper.fetch(g.url); | |
| if (!s) continue; | |
| const u = scrapeFields(s, g); | |
| if (s.version && s.version !== g.version) { u.version = s.version; u.cl = s.cl; n++; } | |
| if (Object.keys(u).length) await Store.up(g.id, u); | |
| } | |
| } | |
| await Store.stamp(); | |
| return n; | |
| } | |
| // ==================== BADGE ==================== | |
| async function badge() { | |
| if (NOTIF_TYPE === 0) return; | |
| const n = (await Store.games()).filter(g => g.version && g.version !== g.uv).length; | |
| if (isLatestAlpha()) { | |
| if (NOTIF_TYPE < 2) return; | |
| const forums = document.querySelector('a.p-navEl-link[href="/"]'); | |
| if (!forums) return; | |
| forums.querySelector('.f95w-badge')?.remove(); | |
| if (!n) return; | |
| const b = document.createElement('span'); | |
| b.className = 'f95w-badge'; | |
| b.style.cssText = 'position:static;display:inline-block;margin-left:4px;vertical-align:middle'; | |
| b.textContent = n; | |
| const txt = forums.querySelector('span'); | |
| if (txt) txt.after(b); | |
| else forums.appendChild(b); | |
| } else { | |
| // --- normal pages: badge on account avatar (NOTIF_TYPE 1 or 2) --- | |
| const av = document.querySelector('.p-navgroup--member .p-navgroup-link--user'); | |
| if (!av) return; | |
| av.querySelector('.f95w-badge')?.remove(); | |
| if (!n) return; | |
| let w = av.querySelector('.f95w-badge-wrap'); | |
| if (!w) { | |
| const a = av.querySelector('.avatar'); | |
| if (!a) return; | |
| w = document.createElement('span'); | |
| w.className = 'f95w-badge-wrap'; | |
| a.before(w); w.appendChild(a); | |
| } | |
| const b = document.createElement('span'); | |
| b.className = 'f95w-badge'; b.textContent = n; | |
| w.appendChild(b); | |
| } | |
| } | |
| // ==================== TRACK BUTTON ==================== | |
| function trackBtn() { | |
| if (!Scraper.isThread()) return; | |
| const wb = document.querySelector('[data-sk-watch]'); | |
| if (!wb || wb.parentElement.querySelector('.f95w-track')) return; | |
| const btn = document.createElement('a'); | |
| btn.className = 'button--link button rippleButton f95w-track'; | |
| btn.href = 'javascript:void(0)'; | |
| const sp = document.createElement('span'); | |
| sp.className = 'button-text'; | |
| btn.appendChild(sp); | |
| btn.appendChild(Object.assign(document.createElement('div'), { className:'ripple-container' })); | |
| wb.insertAdjacentElement('afterend', btn); | |
| const sync = async () => { | |
| const tid = parseInt(Scraper.tid()); | |
| const on = (await Store.games()).some(g => g.id === tid); | |
| sp.textContent = on ? 'Untrack' : 'Track'; | |
| btn.onclick = async e => { | |
| e.preventDefault(); sp.textContent = '...'; | |
| if (on) await Store.rm(tid); | |
| else { const g = Scraper.current(); if (g) await Store.add(g); } | |
| sync(); badge(); | |
| }; | |
| }; | |
| sync(); | |
| } | |
| // ==================== DROPDOWN ==================== | |
| let _tab, _panel; | |
| function watchDropdown() { | |
| new MutationObserver(() => { | |
| const s = document.querySelector('.menu--account .hScroller-scroll'); | |
| if (s && !s.querySelector('.f95w-tab')) inject(); | |
| }).observe(document.body, { childList:true, subtree:true }); | |
| } | |
| function inject() { | |
| const mc = document.querySelector('.menu--account .menu-content'); | |
| if (!mc) return; | |
| const scroll = mc.querySelector('.hScroller-scroll'); | |
| const panes = mc.querySelector('.tabPanes'); | |
| if (!scroll || !panes) return; | |
| _tab = Object.assign(document.createElement('a'), | |
| { className:'tabs-tab f95w-tab', href:'#', textContent:'Tracked' }); | |
| _tab.setAttribute('role','tab'); | |
| _tab.setAttribute('tabindex','0'); | |
| scroll.appendChild(_tab); | |
| _panel = document.createElement('li'); | |
| _panel.className = 'f95w-panel'; | |
| _panel.setAttribute('role','tabpanel'); | |
| _panel.setAttribute('aria-expanded','false'); | |
| panes.appendChild(_panel); | |
| mc.querySelector('.menu-tabHeader').addEventListener('click', e => { | |
| const t = e.target.closest('.tabs-tab'); | |
| if (!t) return; | |
| if (t.classList.contains('f95w-tab')) { stop(e); activate(mc); } | |
| else deactivate(); | |
| }, true); | |
| window.dispatchEvent(new Event('resize')); | |
| } | |
| function setTab(el, on) { | |
| el.classList.toggle('is-active', on); | |
| el.setAttribute(el.tagName === 'A' ? 'aria-selected' : 'aria-expanded', String(on)); | |
| } | |
| function activate(mc) { | |
| mc.querySelectorAll('.tabs-tab').forEach(t => setTab(t, false)); | |
| mc.querySelector('.tabPanes').querySelectorAll(':scope>li').forEach(p => setTab(p, false)); | |
| setTab(_tab, true); setTab(_panel, true); | |
| renderPanel(); | |
| } | |
| function deactivate() { | |
| if (!_tab) return; | |
| setTab(_tab, false); setTab(_panel, false); | |
| } | |
| // ==================== PANEL ==================== | |
| async function renderPanel() { | |
| const lc = await Store.last(); | |
| _panel.innerHTML = ` | |
| <div class="menu-row menu-row--alt menu-row--close"> | |
| <input type="text" class="input f95w-dd-search" placeholder="Search F95 to add..."> | |
| </div> | |
| <div class="menu-scroller"><ol class="listPlain f95w-dd-list"></ol></div> | |
| <div class="menu-footer menu-footer--close f95w-dd-footer"> | |
| <a href="#" class="f95w-dd-reload">↻ Check updates</a> | |
| <span class="f95w-dd-status">${lc ? ago(lc) : 'Never'}</span> | |
| </div>`; | |
| await renderGames(); | |
| let st; | |
| _panel.querySelector('.f95w-dd-search').oninput = function () { | |
| clearTimeout(st); | |
| const q = this.value.trim(); | |
| if (q.length < 2) { renderGames(); return; } | |
| st = setTimeout(() => searchResults(q), 300); | |
| }; | |
| _panel.querySelector('.f95w-dd-reload').onclick = async e => { | |
| stop(e); | |
| const el = _panel.querySelector('.f95w-dd-reload'); | |
| el.innerHTML = '<span class="f95w-spin">↻</span> Checking...'; | |
| const n = await checkAll(); | |
| el.innerHTML = `↻ Done (${n})`; | |
| setTimeout(() => { el.innerHTML = '↻ Check updates'; }, 2000); | |
| _panel.querySelector('.f95w-dd-status').textContent = 'just now'; | |
| await renderGames(); badge(); | |
| }; | |
| } | |
| async function searchResults(q) { | |
| const list = _panel.querySelector('.f95w-dd-list'); | |
| list.innerHTML = '<li class="f95w-dd-empty">Searching...</li>'; | |
| const res = await API.search(q); | |
| const ids = (await Store.games()).map(g => g.id); | |
| if (!res.length) { list.innerHTML = '<li class="f95w-dd-empty">No results</li>'; return; } | |
| list.innerHTML = res.map(g => ` | |
| <li class="menu-row menu-row--close menu-row--separated" data-sid="${g.id}"> | |
| <div class="contentRow"><div class="contentRow-main"> | |
| <div class="contentRow-extra"> | |
| <button class="button--link button--smaller button f95w-add" | |
| ${ids.includes(g.id)?'disabled':''}> | |
| <span class="button-text">${ids.includes(g.id)?'Added':'Add'}</span> | |
| </button> | |
| </div> | |
| <div class="contentRow-title">${esc(g.title)}</div> | |
| <div class="contentRow-minor contentRow-minor--smaller"> | |
| ${g.dev?esc(g.dev)+' · ':''}${g.version||'?'} | |
| </div> | |
| </div></div> | |
| </li>`).join(''); | |
| list.querySelectorAll('.f95w-add:not([disabled])').forEach(btn => { | |
| btn.onclick = async e => { | |
| stop(e); | |
| const game = res.find(g => g.id === parseInt(btn.closest('[data-sid]').dataset.sid)); | |
| if (!game) return; | |
| btn.querySelector('.button-text').textContent = '...'; btn.disabled = true; | |
| try { | |
| const s = await Scraper.fetch(game.url); | |
| if (s) Object.assign(game, scrapeFields(s, game), | |
| { title:s.title||game.title, dev:s.dev||game.dev, version:s.version||game.version }); | |
| } catch(e) { console.warn('[F95W]', e); } | |
| await Store.add(game); | |
| btn.querySelector('.button-text').textContent = 'Added'; | |
| badge(); | |
| }; | |
| }); | |
| } | |
| async function renderGames() { | |
| const list = _panel.querySelector('.f95w-dd-list'); | |
| const games = await Store.games(); | |
| if (!games.length) { | |
| list.innerHTML = '<li class="f95w-dd-empty">No games tracked.<br>Visit a thread and click Track, or search above.</li>'; | |
| return; | |
| } | |
| const by = a => [...a].sort((x,y) => (y.added||0) - (x.added||0)); | |
| const hasUp = g => g.version && g.version !== g.uv; | |
| const up = by(games.filter(hasUp)); | |
| const ok = by(games.filter(g => !hasUp(g))); | |
| let h = ''; | |
| if (up.length) h += `<li class="f95w-grp menu-row">Updates · ${up.length}</li>` + up.map(g => card(g,true)).join(''); | |
| if (ok.length) h += `<li class="f95w-grp menu-row">Up to date · ${ok.length}</li>` + ok.map(g => card(g,false)).join(''); | |
| list.innerHTML = h; | |
| list.querySelectorAll('[data-gid]').forEach(el => { | |
| const id = parseInt(el.dataset.gid); | |
| el.querySelector('.act-played')?.addEventListener('click', async e => { | |
| stop(e); | |
| const g = (await Store.games()).find(x => x.id === id); | |
| if (g) { await Store.up(id, { uv:g.version, cl:null }); renderGames(); badge(); } | |
| }); | |
| el.querySelector('.act-remove')?.addEventListener('click', async e => { | |
| stop(e); | |
| el.style.cssText = 'transition:opacity .15s;opacity:0'; | |
| setTimeout(async () => { await Store.rm(id); renderGames(); badge(); }, 150); | |
| }); | |
| }); | |
| } | |
| function card(g, up) { | |
| const url = g.url || `/threads/${g.id}/`; | |
| const fig = g.cover | |
| ? `<div class="contentRow-figure f95w-fig"><img src="${esc(g.cover)}" class="f95w-cover" loading="lazy" onerror="this.style.display='none'"></div>` | |
| : ''; | |
| const acts = (up ? '<a href="#" class="act-played">Played</a><span class="f95w-acts-sep">·</span>' : '') | |
| + '<a href="#" class="act-remove">Untrack</a>'; | |
| let tag = ''; | |
| if (up) { | |
| tag = g.cl | |
| ? `<span class="f95w-new" title="${esc(g.cl)}">NEW</span>` | |
| : '<span class="f95w-new">NEW</span>'; | |
| } | |
| return `<li class="menu-row menu-row--close menu-row--separated ${up?'':'f95w-dim'}" data-gid="${g.id}"> | |
| <div class="contentRow">${fig}<div class="contentRow-main"> | |
| <div class="contentRow-extra"><div class="f95w-acts">${acts}</div></div> | |
| <div class="f95w-title"><a href="${url}">${esc(g.title)}</a>${tag}</div> | |
| <div class="f95w-minor">${g.dev?esc(g.dev)+' · ':''}<span class="ver">${g.version||'?'}</span>${up&&g.uv?' <s>'+g.uv+'</s>':''}</div> | |
| </div></div></li>`; | |
| } | |
| // ==================== INIT ==================== | |
| trackBtn(); | |
| watchDropdown(); | |
| badge(); | |
| if ((await Store.games()).length && Date.now() - await Store.last() > CHECK_H * 36e5) { | |
| await Store.stamp(); | |
| setTimeout(async () => { await checkAll(); badge(); }, 2000); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment