// ==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 = ` ◈ NEEDED ITEMS
`; 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 `
Loading…
`; } 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 += `
Item lists load client-side. Showing live counts. Navigate to /needed-items and re-open this panel for full item data.
`; } html += `
`; 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 += `
`; html += `
${col.icon} ${col.label} ${sec.count}
`; if (ssrMode || sec.ssrOnly || sec.items.length === 0) { html += `
${ssrMode ? `Open page for items →` : 'No items found.'}
`; } else { const half = Math.ceil(sec.items.length / 2); const left = sec.items.slice(0, half); const right = sec.items.slice(half); html += `
`; [left, right].forEach((group, si) => { const br = si === 0 ? `border-right:1px solid ${col.border}40;` : ''; html += `
`; group.forEach(item => { const qColor = item.qty ? (item.have >= item.need ? '#4ade80' : '#f87171') : ''; html += `
${item.name} ${item.qty ? `${item.have}/${item.need}` : ''}
`; }); html += `
`; }); html += `
`; } html += `
`; }); html += `
`; 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 `
${msg}
`; } 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 = `-:--`; 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(); })();