From 12fc79022f91beb3241ddea10de6c6e7538281fa Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 1 Jun 2026 17:03:03 -0500 Subject: [PATCH] feat(main): wire Store + Dialog + DataLayer on /gym.php --- src/main.js | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/main.js diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..d1bbdbc --- /dev/null +++ b/src/main.js @@ -0,0 +1,113 @@ +import { Store } from './store.js'; +import { Dialog } from './ui.js'; +import { currentAttribute } from './dom.js'; +import { startRequestInterceptor } from './interceptor.js'; + +function findAnchorElement() { + // Torn's training form is the element containing the Train button. + // Selector is best-effort; the Dialog will fall back if missing. + const btn = document.querySelector('button[name="train"], a[href*="train"]'); + if (!btn) return null; + return btn.closest('form') || btn.parentElement; +} + +function start() { + const store = new Store({ + storage: localStorage, + onWarn: (m) => console.warn(m), + }); + const prefs = store.getPrefs(); + + const dialog = new Dialog({ + onTargetChange: (v) => { + const attr = currentAttribute()?.attr; + if (!attr) return; + store.setTarget(attr, v); + render(); + }, + onModeChange: (m) => { + store.setMode(m); + applyMode(); + }, + onPosChange: (pos) => store.setPos(pos), + onClose: () => dialog.destroy(), + }); + + dialog.mount({ initialMode: prefs.mode, initialPos: prefs.pos }); + applyMode(); + + let lastSnapshot = null; + let lastAttr = null; + let lastDelta = 0; + + function snapshot() { + const a = currentAttribute(); + if (!a) { + return { error: "Couldn't read attribute — Torn may have updated the page." }; + } + lastAttr = a.attr; + const summary = store.getSummary(a.attr); + return { + attr: a.attr, + gym: a.gym, + current: a.current, + target: store.getTarget(a.attr), + perTrain: lastDelta, + summary, + warn: store._saveDisabled ? 'saving disabled this session' : null, + }; + } + + function render() { + lastSnapshot = snapshot(); + dialog.render(lastSnapshot); + } + + function applyMode() { + if (prefs.mode === 'anchored') { + const el = findAnchorElement(); + if (el) { + const rect = el.getBoundingClientRect(); + dialog.setMode('anchored', { canAnchor: true }); + dialog._positionAnchored(rect); + // observe + const ro = new ResizeObserver(() => { + if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect()); + }); + ro.observe(el); + return; + } + } + dialog.setMode('free'); + } + + // initial paint + render(); + + // watch DOM for attribute changes + const observer = new MutationObserver(() => { + const a = currentAttribute(); + if (a && (a.attr !== lastAttr || a.current !== lastSnapshot?.current)) render(); + }); + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + + // intercept train requests + const prev = currentAttribute()?.current ?? 0; + startRequestInterceptor({ + prevValue: prev, + currentAttr: lastAttr, + onTrain: ({ attr, delta, ts }) => { + store.recordTrain(attr, delta, ts); + lastDelta = delta; + render(); + }, + onParseFail: (url) => console.warn('[tat] could not parse train response from', url), + }); +} + +if (location.hash === '#tat-test') { + // Self-test path: the in-browser tests live in the bundled userscript. + // The bundle runs the test block; nothing to do here. +} else if (/\/gym\.php(\?|$)/.test(location.pathname + location.search)) { + start(); +}