// ==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 += `
`;
} 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();
})();