Last active
March 5, 2026 21:04
-
-
Save CompewterTutor/c658402f5594c5be5c7c47e48da4a932 to your computer and use it in GitHub Desktop.
Greasemonkey (firefox) script to autosync arctracker.io and have better formatted needed items overlay
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 ArcTracker — Auto Sync & Needs Overlay | |
| // @namespace https://arctracker.io/ | |
| // @version 2.0.2 | |
| // @description Auto-sync stash/quests/hideout/blueprints/projects on a timer, plus a compact Needed Items overlay. | |
| // @author CompewterTutor | |
| // @match https://arctracker.io/* | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @connect arctracker.io | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // ─── CONFIG ──────────────────────────────────────────────────────────────── | |
| const DEFAULT_INTERVAL = 5; // minutes | |
| // ─── STATE ────────────────────────────────────────────────────────────────── | |
| let autoTimer = null; | |
| let cdTimer = null; | |
| let interval = DEFAULT_INTERVAL; // loaded async in init() | |
| let autoEnabled = false; | |
| let nextSecs = 0; | |
| // ─── SYNC ─────────────────────────────────────────────────────────────────── | |
| function getSyncButtons() { | |
| return Array.from(document.querySelectorAll('button')).filter(b => { | |
| const t = b.textContent.trim(); | |
| return !b.disabled && ( | |
| /^sync\s*now$/i.test(t) || | |
| /^sync\s*stash$/i.test(t) || | |
| /^sync\s*inventory$/i.test(t) | |
| ); | |
| }); | |
| } | |
| function clickAll() { | |
| const btns = getSyncButtons(); | |
| btns.forEach(b => { try { b.click(); } catch(e){} }); | |
| if (btns.length) setTimeout(closeSyncDialogs, 1500); | |
| return btns.length; | |
| } | |
| function closeSyncDialogs() { | |
| document.querySelectorAll('[role="dialog"] button').forEach(btn => { | |
| if (btn.textContent.trim() === 'Close') { | |
| try { btn.click(); } catch(e) {} | |
| } | |
| }); | |
| } | |
| function flashStatus(msg, color='#4ade80') { | |
| const el = document.getElementById('arc-status'); | |
| if (!el) return; | |
| el.textContent = msg; | |
| el.style.color = color; | |
| clearTimeout(el._t); | |
| el._t = setTimeout(() => { | |
| el.textContent = autoEnabled ? 'Auto ON' : 'Ready'; | |
| el.style.color = autoEnabled ? '#4ade80' : '#475569'; | |
| }, 3000); | |
| } | |
| function fmtMin(m) { | |
| if (m >= 60) { const h=Math.floor(m/60),r=m%60; return r?`${h}h${r}m`:`${h}h`; } | |
| return `${m}m`; | |
| } | |
| function startAuto() { | |
| stopAuto(true); | |
| autoEnabled = true; | |
| nextSecs = interval * 60; | |
| scheduleNext(); | |
| startCd(); | |
| updateAutoBtn(); | |
| flashStatus(`Auto · ${fmtMin(interval)}`); | |
| } | |
| function stopAuto(silent) { | |
| clearTimeout(autoTimer); autoTimer = null; | |
| clearInterval(cdTimer); cdTimer = null; | |
| autoEnabled = false; | |
| updateAutoBtn(); | |
| if (!silent) flashStatus('Auto off', '#f87171'); | |
| } | |
| function scheduleNext() { | |
| autoTimer = setTimeout(() => { | |
| const n = clickAll(); | |
| flashStatus(n ? `Synced ${n} ✓` : 'No btns', n?'#4ade80':'#fbbf24'); | |
| nextSecs = interval * 60; | |
| scheduleNext(); | |
| }, interval * 60 * 1000); | |
| } | |
| function startCd() { | |
| clearInterval(cdTimer); | |
| cdTimer = setInterval(() => { | |
| nextSecs = Math.max(0, nextSecs - 1); | |
| const el = document.getElementById('arc-cd'); | |
| if (el) { const m=Math.floor(nextSecs/60),s=nextSecs%60; el.textContent=`${m}:${String(s).padStart(2,'0')}`; } | |
| }, 1000); | |
| } | |
| function updateAutoBtn() { | |
| const btn = document.getElementById('arc-auto-btn'); | |
| const cdw = document.getElementById('arc-cdw'); | |
| if (!btn) return; | |
| if (autoEnabled) { | |
| btn.textContent = '⏹ Stop'; | |
| btn.style.cssText = bCSS('#7f1d1d','#991b1b','#ef4444','#fca5a5'); | |
| } else { | |
| btn.textContent = '▶ Auto'; | |
| btn.style.cssText = bCSS('#14532d','#166534','#4ade80','#bbf7d0'); | |
| } | |
| if (cdw) cdw.style.display = autoEnabled ? 'flex' : 'none'; | |
| } | |
| // ─── NEEDED ITEMS OVERLAY ─────────────────────────────────────────────────── | |
| function openOverlay() { | |
| let ov = document.getElementById('arc-ov'); | |
| if (ov && !document.documentElement.contains(ov)) { | |
| ov.remove(); | |
| ov = null; | |
| } | |
| if (ov) { ov.style.display='flex'; doLoad(); return; } | |
| ov = document.createElement('div'); | |
| ov.id = 'arc-ov'; | |
| ov.style.cssText = ` | |
| position:fixed;inset:0;z-index:999998; | |
| background:rgba(4,7,14,0.95);backdrop-filter:blur(10px); | |
| display:flex;flex-direction:column; | |
| font-family:'JetBrains Mono','Fira Mono',monospace;color:#e2e8f0; | |
| `; | |
| const hdr = document.createElement('div'); | |
| hdr.style.cssText = 'display:flex;align-items:center;gap:10px;padding:9px 16px;background:#06090f;border-bottom:2px solid #1e3a5f;flex-shrink:0;'; | |
| hdr.innerHTML = ` | |
| <span style="color:#38bdf8;font-weight:700;font-size:14px;letter-spacing:2px;">◈ NEEDED ITEMS</span> | |
| <span id="arc-ni-ts" style="color:#334155;font-size:10px;"></span> | |
| <div style="flex:1"></div> | |
| <button id="arc-ni-ref" style="${bCSS('#0c4a6e','#075985','#38bdf8','#bae6fd')}font-size:11px;padding:3px 10px;">⟳ Refresh</button> | |
| <button id="arc-ni-cls" style="background:transparent;border:1px solid #1e293b;color:#475569;padding:3px 8px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:13px;margin-left:4px;">✕</button> | |
| `; | |
| ov.appendChild(hdr); | |
| const content = document.createElement('div'); | |
| content.id = 'arc-ni-body'; | |
| content.style.cssText = 'flex:1;overflow:hidden;display:flex;'; | |
| content.innerHTML = loading(); | |
| ov.appendChild(content); | |
| document.documentElement.appendChild(ov); | |
| document.getElementById('arc-ni-cls').onclick = () => { ov.style.display='none'; }; | |
| document.getElementById('arc-ni-ref').onclick = doLoad; | |
| doLoad(); | |
| } | |
| function loading() { | |
| return `<div style="flex:1;display:flex;align-items:center;justify-content:center;color:#334155;font-size:13px;">Loading…</div>`; | |
| } | |
| function doLoad() { | |
| const body = document.getElementById('arc-ni-body'); | |
| const ts = document.getElementById('arc-ni-ts'); | |
| if (!body) return; | |
| body.innerHTML = loading(); | |
| if (ts) ts.textContent = 'loading…'; | |
| if (location.pathname.replace(/^\/\w{2}(?=\/)/,'').includes('/needed-items')) { | |
| if (ts) ts.textContent = 'scraping…'; | |
| // Disable pointer events so accordion clicks pass through the overlay to the page | |
| const ov = document.getElementById('arc-ov'); | |
| if (ov) ov.style.pointerEvents = 'none'; | |
| expandAccordions(() => { | |
| if (ov) ov.style.pointerEvents = ''; | |
| renderLive(body, ts); | |
| }); | |
| } else { | |
| GM_xmlhttpRequest({ | |
| method:'GET', url:location.origin+'/needed-items', headers:{'Accept':'text/html'}, | |
| onload(r) { renderFromSSR(r.responseText, body, ts); }, | |
| onerror() { body.innerHTML=errMsg('Fetch failed. Are you logged in?'); if(ts) ts.textContent='error'; } | |
| }); | |
| } | |
| } | |
| // Only expand Quests / Hideout / Projects accordions, not sidebar nav ones | |
| function expandAccordions(cb) { | |
| document.querySelectorAll('button[aria-expanded="false"][aria-controls]').forEach(btn => { | |
| const text = btn.textContent.trim(); | |
| if (/quests|hideout|projects/i.test(text) && /\d+ items/i.test(text)) { | |
| try { btn.click(); } catch(e) {} | |
| } | |
| }); | |
| setTimeout(cb, 1000); | |
| } | |
| // ── Live render (on /needed-items page after expanding accordions) ────────── | |
| function renderLive(container, tsEl) { | |
| const COLS = colDefs(); | |
| const sections = {}; | |
| document.querySelectorAll('button[aria-expanded][aria-controls]').forEach(btn => { | |
| const rawLabel = btn.textContent.trim().toLowerCase(); | |
| const col = COLS.find(c => rawLabel.includes(c.key)); | |
| if (!col) return; | |
| const panelId = btn.getAttribute('aria-controls'); | |
| const panel = document.getElementById(panelId); | |
| const countMatch = rawLabel.match(/(\d+)\s*items/); | |
| const count = countMatch ? countMatch[1] : '?'; | |
| const isActive = btn.getAttribute('aria-expanded') === 'true'; | |
| if (!sections[col.key] || isActive) { | |
| sections[col.key] = { | |
| col, | |
| count, | |
| items: (panel && isActive) ? extractItems(panel) : [], | |
| active: isActive | |
| }; | |
| } | |
| }); | |
| renderGrid(container, COLS, sections); | |
| if (tsEl) tsEl.textContent = `Live · ${new Date().toLocaleTimeString()}`; | |
| } | |
| // ── SSR parse (fetched HTML — items not present but counts are) ───────────── | |
| function renderFromSSR(html, container, tsEl) { | |
| const doc = new DOMParser().parseFromString(html, 'text/html'); | |
| const COLS = colDefs(); | |
| const sections = {}; | |
| doc.querySelectorAll('button[aria-expanded][aria-controls]').forEach(btn => { | |
| const rawLabel = btn.textContent.trim().toLowerCase(); | |
| const col = COLS.find(c => rawLabel.includes(c.key)); | |
| if (!col) return; | |
| const countMatch = rawLabel.match(/(\d+)\s*items/); | |
| const count = countMatch ? countMatch[1] : '?'; | |
| const isActive = !btn.closest('[class*="opacity-70"]'); | |
| if (!sections[col.key] || isActive) { | |
| sections[col.key] = { col, count, items: [], active: isActive, ssrOnly: true }; | |
| } | |
| }); | |
| renderGrid(container, COLS, sections, true); | |
| if (tsEl) tsEl.textContent = `Counts only · ${new Date().toLocaleTimeString()}`; | |
| } | |
| // ── Shared grid renderer ──────────────────────────────────────────────────── | |
| function renderGrid(container, COLS, sections, ssrMode) { | |
| container.style.cssText = 'flex:1;overflow:hidden;display:flex;flex-direction:column;'; | |
| let html = ''; | |
| if (ssrMode) { | |
| html += `<div style="padding:6px 14px;background:#0c1525;border-bottom:1px solid #1e3a5f;font-size:10px;color:#475569;flex-shrink:0;"> | |
| <span style="color:#38bdf8;">ℹ</span> | |
| Item lists load client-side. Showing live counts. | |
| Navigate to <a href="/needed-items" style="color:#38bdf8;">/needed-items</a> | |
| and re-open this panel for full item data. | |
| </div>`; | |
| } | |
| html += `<div style="flex:1;overflow-y:auto;display:grid;grid-template-columns:1fr 1fr 1fr;gap:0;">`; | |
| COLS.forEach((col, idx) => { | |
| const sec = sections[col.key] || { count:'—', items:[], ssrOnly: true }; | |
| const borderR = idx < 2 ? `border-right:3px solid ${col.border};` : ''; | |
| const padL = idx === 0 ? '0' : '12px'; | |
| const padR = idx === 2 ? '0' : '12px'; | |
| html += `<div style="${borderR}padding:12px ${padR} 12px ${padL};display:flex;flex-direction:column;gap:6px;overflow-y:auto;">`; | |
| html += `<div style="display:flex;align-items:center;gap:6px;padding-bottom:8px;border-bottom:2px solid ${col.border};flex-shrink:0;"> | |
| <span style="font-size:15px;">${col.icon}</span> | |
| <span style="color:${col.color};font-weight:700;font-size:12px;letter-spacing:2px;text-transform:uppercase;">${col.label}</span> | |
| <span style="color:#475569;font-size:10px;margin-left:auto;">${sec.count}</span> | |
| </div>`; | |
| if (ssrMode || sec.ssrOnly || sec.items.length === 0) { | |
| html += `<div style="color:#334155;font-size:11px;padding:4px 0;"> | |
| ${ssrMode | |
| ? `<a href="/needed-items" style="color:${col.color};" target="_blank">Open page for items →</a>` | |
| : 'No items found.'} | |
| </div>`; | |
| } else { | |
| const half = Math.ceil(sec.items.length / 2); | |
| const left = sec.items.slice(0, half); | |
| const right = sec.items.slice(half); | |
| html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;flex:1;overflow-y:auto;min-height:0;">`; | |
| [left, right].forEach((group, si) => { | |
| const br = si === 0 ? `border-right:1px solid ${col.border}40;` : ''; | |
| html += `<div style="${br}padding:4px;background:rgba(10,18,34,0.5);border-radius:4px;overflow-y:auto;">`; | |
| group.forEach(item => { | |
| const qColor = item.qty ? (item.have >= item.need ? '#4ade80' : '#f87171') : ''; | |
| html += `<div style="display:flex;align-items:center;padding:2px 0;border-bottom:1px solid #0a1222;gap:4px;"> | |
| <span style="color:#94a3b8;font-size:10px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${item.name}">${item.name}</span> | |
| ${item.qty ? `<span style="color:${qColor};font-size:10px;font-weight:700;flex-shrink:0;">${item.have}/${item.need}</span>` : ''} | |
| </div>`; | |
| }); | |
| html += `</div>`; | |
| }); | |
| html += `</div>`; | |
| } | |
| html += `</div>`; | |
| }); | |
| html += `</div>`; | |
| container.innerHTML = html; | |
| } | |
| // Extract item cards from an open accordion panel | |
| function extractItems(panel) { | |
| const items = []; | |
| const seen = new Set(); | |
| panel.querySelectorAll('[class*="bg-card"]').forEach(card => { | |
| const nameEl = card.querySelector('h3'); | |
| const qtyEl = card.querySelector('span.whitespace-nowrap'); | |
| if (!nameEl) return; | |
| const name = nameEl.textContent.trim(); | |
| if (!name || seen.has(name)) return; | |
| seen.add(name); | |
| let have = 0, need = 0, hasQty = false; | |
| if (qtyEl) { | |
| const m = qtyEl.textContent.trim().match(/^(\d+)\s*\/\s*(\d+)$/); | |
| if (m) { have = parseInt(m[1]); need = parseInt(m[2]); hasQty = true; } | |
| } | |
| items.push({ name, have, need, qty: hasQty }); | |
| }); | |
| return items; | |
| } | |
| // ─── HELPERS ──────────────────────────────────────────────────────────────── | |
| function colDefs() { | |
| return [ | |
| { key:'quests', label:'Quests', icon:'⚔', color:'#f59e0b', border:'#92400e' }, | |
| { key:'hideout', label:'Hideout', icon:'🏠', color:'#34d399', border:'#065f46' }, | |
| { key:'projects', label:'Projects', icon:'🔬', color:'#818cf8', border:'#312e81' }, | |
| ]; | |
| } | |
| function errMsg(msg) { | |
| return `<div style="flex:1;display:flex;align-items:center;justify-content:center;color:#f87171;font-size:12px;padding:20px;">${msg}</div>`; | |
| } | |
| function bCSS(b1, b2, border, color) { | |
| return `background:linear-gradient(135deg,${b1},${b2});border:1px solid ${border};color:${color};padding:4px 10px;border-radius:5px;cursor:pointer;font-family:inherit;font-size:11px;white-space:nowrap;`; | |
| } | |
| function sep() { | |
| const d = document.createElement('div'); | |
| d.style.cssText = 'width:1px;height:20px;background:#1e3a5f;flex-shrink:0;'; | |
| return d; | |
| } | |
| function mk(tag, text, css) { | |
| const e = document.createElement(tag); | |
| if (text !== undefined) e.textContent = text; | |
| if (css) e.style.cssText = css; | |
| return e; | |
| } | |
| function makeDraggable(bar, handle) { | |
| let ox=0,oy=0,sx=0,sy=0; | |
| handle.addEventListener('mousedown', e => { | |
| e.preventDefault(); | |
| const r=bar.getBoundingClientRect(); ox=r.left; oy=r.top; sx=e.clientX; sy=e.clientY; | |
| bar.style.bottom='auto'; bar.style.right='auto'; | |
| bar.style.left=ox+'px'; bar.style.top=oy+'px'; | |
| const mv=e2=>{bar.style.left=(ox+e2.clientX-sx)+'px';bar.style.top=(oy+e2.clientY-sy)+'px';}; | |
| const up=()=>{document.removeEventListener('mousemove',mv);document.removeEventListener('mouseup',up);}; | |
| document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up); | |
| }); | |
| } | |
| // ─── BUILD CONTROL BAR ────────────────────────────────────────────────────── | |
| function buildBar() { | |
| if (document.getElementById('arc-bar')) return; | |
| const bar = document.createElement('div'); | |
| bar.id = 'arc-bar'; | |
| bar.style.cssText = ` | |
| position:fixed;bottom:20px;right:20px;z-index:99999; | |
| display:flex;align-items:center;gap:6px;flex-wrap:nowrap; | |
| background:linear-gradient(135deg,#060a12,#0d1520); | |
| border:1px solid #1e3a5f;border-radius:10px; | |
| padding:7px 12px; | |
| box-shadow:0 8px 32px rgba(0,0,0,.8),0 0 0 1px rgba(56,189,248,.05); | |
| font-family:'JetBrains Mono','Fira Mono',monospace;font-size:11px; | |
| user-select:none; | |
| `; | |
| const logo = mk('span','◈ ARC','color:#38bdf8;font-weight:700;font-size:13px;letter-spacing:1px;cursor:move;'); | |
| logo.id = 'arc-drag'; | |
| bar.appendChild(logo); | |
| bar.appendChild(sep()); | |
| const syncBtn = mk('button','⚡ Sync Now', bCSS('#0c4a6e','#075985','#38bdf8','#bae6fd')); | |
| syncBtn.onclick = () => { | |
| const n = clickAll(); | |
| flashStatus(n ? `Synced ${n} ✓` : 'No sync btns found', n?'#4ade80':'#fbbf24'); | |
| }; | |
| bar.appendChild(syncBtn); | |
| bar.appendChild(sep()); | |
| const autoBtn = mk('button','▶ Auto', bCSS('#14532d','#166534','#4ade80','#bbf7d0')); | |
| autoBtn.id = 'arc-auto-btn'; | |
| autoBtn.onclick = () => autoEnabled ? stopAuto() : startAuto(); | |
| bar.appendChild(autoBtn); | |
| const ivSel = document.createElement('select'); | |
| ivSel.id = 'arc-iv'; | |
| ivSel.style.cssText = 'background:#0a1222;color:#64748b;border:1px solid #1e293b;border-radius:4px;padding:3px 4px;font-family:inherit;font-size:10px;cursor:pointer;'; | |
| [1,2,3,5,10,15,20,30,60].forEach(m => { | |
| const o = document.createElement('option'); | |
| o.value=m; o.textContent=fmtMin(m); if(m===interval) o.selected=true; | |
| ivSel.appendChild(o); | |
| }); | |
| ivSel.onchange = async () => { | |
| interval = parseInt(ivSel.value); | |
| await GM_setValue('syncInterval', interval); | |
| if (autoEnabled) startAuto(); | |
| flashStatus(`Interval: ${fmtMin(interval)}`); | |
| }; | |
| bar.appendChild(ivSel); | |
| const cdw = mk('span',undefined,'display:none;align-items:center;gap:3px;color:#475569;font-size:10px;'); | |
| cdw.id = 'arc-cdw'; | |
| cdw.innerHTML = `<span style="color:#334155;">→</span><span id="arc-cd">-:--</span>`; | |
| bar.appendChild(cdw); | |
| bar.appendChild(sep()); | |
| const niBtn = mk('button','📋 Needed Items', bCSS('#2d1b69','#3730a3','#818cf8','#c7d2fe')); | |
| niBtn.onclick = openOverlay; | |
| bar.appendChild(niBtn); | |
| bar.appendChild(sep()); | |
| const status = mk('span','Ready','color:#475569;font-size:10px;min-width:48px;text-align:right;'); | |
| status.id = 'arc-status'; | |
| bar.appendChild(status); | |
| const minBtn = mk('button','–','background:transparent;border:1px solid #1e293b;color:#334155;padding:2px 6px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:12px;margin-left:2px;'); | |
| let minimized = false; | |
| const hideable = [syncBtn, autoBtn, ivSel, cdw, niBtn, status]; | |
| minBtn.onclick = () => { | |
| minimized = !minimized; | |
| hideable.forEach(e => e.style.display = minimized ? 'none' : ''); | |
| bar.querySelectorAll('div[style*="1px"]').forEach(d => d.style.display = minimized ? 'none' : ''); | |
| minBtn.textContent = minimized ? '+' : '–'; | |
| }; | |
| bar.appendChild(minBtn); | |
| document.body.appendChild(bar); | |
| makeDraggable(bar, logo); | |
| } | |
| // ─── INIT ─────────────────────────────────────────────────────────────────── | |
| async function init() { | |
| interval = await GM_getValue('syncInterval', DEFAULT_INTERVAL); | |
| setTimeout(buildBar, 1000); | |
| } | |
| // Re-init on SPA navigation | |
| let lastHref = location.href; | |
| new MutationObserver(() => { | |
| if (location.href !== lastHref) { | |
| lastHref = location.href; | |
| setTimeout(() => { if (!document.getElementById('arc-bar')) buildBar(); }, 1200); | |
| } | |
| }).observe(document.body, { childList:true, subtree:true }); | |
| document.readyState === 'loading' | |
| ? document.addEventListener('DOMContentLoaded', init) | |
| : init(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment