diff --git a/src/main.js b/src/main.js index 6f88918..9e50cec 100644 --- a/src/main.js +++ b/src/main.js @@ -35,113 +35,128 @@ function findAnchorElement() { } function start() { - const store = new Store({ - storage: localStorage, - onWarn: (m) => console.warn(m), - }); - const prefs = store.getPrefs(); + try { + 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); - prefs.mode = m; - applyMode(); - }, - onPosChange: (pos) => store.setPos(pos), - onClose: () => dialog.destroy(), - }); + // State that applyMode() and render() may touch on first call. + // Declared up-front to avoid TDZ ReferenceError if applyMode()'s + // anchor-miss branch fires before the natural declaration point. + let lastSnapshot = null; + let lastAttr = null; + let lastDelta = 0; + let anchorError = null; - dialog.mount({ initialMode: prefs.mode, initialPos: prefs.pos }); - applyMode(); - - let lastSnapshot = null; - let lastAttr = null; - let lastDelta = 0; - let anchorError = null; - - function snapshot() { - const a = currentAttribute(); - if (!a) { - return { error: "Couldn't read attribute — Torn may have updated the page." }; + // One-time migration: dialog now defaults to bottom-left, so reset any + // previously-saved position from the bottom-right era. + if (prefs.pos && (prefs.pos.x !== 0 || prefs.pos.y !== 0)) { + console.info('[tat] resetting dialog position to new bottom-left default'); + prefs.pos = { x: 0, y: 0 }; + store.setPos(prefs.pos); } - 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, - anchorError: anchorError, - }; - } - function render() { - lastSnapshot = snapshot(); - dialog.render(lastSnapshot); - } + const dialog = new Dialog({ + onTargetChange: (v) => { + const attr = currentAttribute()?.attr; + if (!attr) return; + store.setTarget(attr, v); + render(); + }, + onModeChange: (m) => { + store.setMode(m); + prefs.mode = m; + applyMode(); + }, + onPosChange: (pos) => store.setPos(pos), + onClose: () => dialog.destroy(), + }); - 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); - anchorError = null; + dialog.mount({ initialMode: prefs.mode, initialPos: prefs.pos }); + applyMode(); + + 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, + anchorError: anchorError, + }; + } + + 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); + anchorError = null; + return; + } + // Anchor selector missed — don't snap to default, just keep current + // position and show a note. + anchorError = "Couldn't find the training form on this page."; + render(); return; } - // Anchor selector missed — don't snap to default, just keep current - // position and show a note. - anchorError = "Couldn't find the training form on this page."; - render(); - return; + anchorError = null; + dialog.setMode('free'); } - anchorError = null; - dialog.setMode('free'); - } - // initial paint - render(); + // initial paint + render(); - // watch DOM for attribute changes - let pending = false; - const observer = new MutationObserver(() => { - if (pending) return; - pending = true; - requestAnimationFrame(() => { - pending = false; - const a = currentAttribute(); - if (a && (a.attr !== lastAttr || a.current !== lastSnapshot?.current)) render(); + // watch DOM for attribute changes + let pending = false; + const observer = new MutationObserver(() => { + if (pending) return; + pending = true; + requestAnimationFrame(() => { + pending = false; + const a = currentAttribute(); + if (a && (a.attr !== lastAttr || a.current !== lastSnapshot?.current)) render(); + }); }); - }); - observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + 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), - }); + // 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), + }); + } catch (e) { + console.error('[tat] failed to start:', e); + } } if (location.hash === '#tat-test') {