Torn City Chain Watch Alert (persistent flash + controls)

Flash screen and play sound when chain timer drops below threshold, with UI controls.

// ==UserScript==
// @name         Torn City Chain Watch Alert (persistent flash + controls)
// @namespace    http://tampermonkey.net/
// @version      1.1.3
// @description  Flash screen and play sound when chain timer drops below threshold, with UI controls.
// @author       DarthRevan
// @match        https://www.torn.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const TIMER_SELECTORS = [
        "#react-root > div > div.appHeaderWrapper___uyPti.disableLinksRightMargin___gY7V5 > div.topSection___U7sVi > div.labelsContainer___Oz6Su > div > span.labelTitle___ZtfnD > div > span:nth-child(2) > span",
        "#sidebar > div:nth-child(1) > div > div.user-information___VBSOk > div > div.toggle-content___BJ9Q9 > div > div:nth-child(4) > a.chain-bar___vjdPL.bar-desktop___F8PEF > div.bar-stats___E_LqA > p.bar-timeleft___B9RGV"
    ];

    let enabled = true;
    let flashThreshold = 60;
    let soundThreshold = 60;
    let flashing = false;
    let overlay = null;
    let style = null;

    const AUDIO_DURATION = 45000;
    const AUDIO_URL = 'https://audio.jukehost.co.uk/byq2uIIeKkzqHwFMOST5V3XR8Kx3wWJB';
    let audio = null;
    let audioStartTime = null;

const controlPanel = document.createElement('div');
controlPanel.style.position = 'fixed';
controlPanel.style.top = '10px';
controlPanel.style.right = '10px';
controlPanel.style.width = '220px';
controlPanel.style.height = 'auto';
controlPanel.style.backgroundColor = '#222';
controlPanel.style.color = '#fff';
controlPanel.style.padding = '10px';
controlPanel.style.borderRadius = '8px';
controlPanel.style.zIndex = 10000;
controlPanel.style.fontSize = '14px';
controlPanel.style.fontFamily = 'Arial, sans-serif';
controlPanel.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
controlPanel.style.cursor = 'move';
controlPanel.style.userSelect = 'none'; // prevent accidental text selection

controlPanel.innerHTML = `
    <h1 style="margin: 0 0 8px 0;">Chain Watch Notifier</h1>
    <label><input type="checkbox" id="toggleAlert" checked> Turn on Chain Watch mode</label><br>
    Flash Threshold (sec): <input type="number" id="flashInput" value="${flashThreshold}" style="width:50px"><br>
    Sound Threshold (sec): <input type="number" id="soundInput" value="${soundThreshold}" style="width:50px">
`;
document.body.appendChild(controlPanel);

// --- Make the panel draggable ---
let isDragging = false;
let offsetX, offsetY;

// Start dragging when mouse down (but not on inputs)
controlPanel.addEventListener('mousedown', (e) => {
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'LABEL') return;
  isDragging = true;

  const rect = controlPanel.getBoundingClientRect();
  offsetX = e.clientX - rect.left;
  offsetY = e.clientY - rect.top;

  // Allow moving freely by switching to absolute positioning
  controlPanel.style.position = 'fixed';
  controlPanel.style.right = 'auto'; // disable right anchoring
});

// Move panel as mouse moves
document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;
  controlPanel.style.left = e.clientX - offsetX + 'px';
  controlPanel.style.top = e.clientY - offsetY + 'px';
});

// Stop dragging on mouse up
document.addEventListener('mouseup', () => {
  isDragging = false;
});

    document.body.appendChild(controlPanel);

    document.getElementById('toggleAlert').addEventListener('change', (e) => {
        enabled = e.target.checked;
        if (!enabled) {
            stopFlashing();
            stopSoundLoop();
        }
    });

    document.getElementById('flashInput').addEventListener('change', (e) => {
        const val = parseInt(e.target.value, 10);
        if (!isNaN(val) && val >= 0) flashThreshold = val;
    });

    document.getElementById('soundInput').addEventListener('change', (e) => {
        const val = parseInt(e.target.value, 10);
        if (!isNaN(val) && val >= 0) soundThreshold = val;
    });

    function getTimeInSeconds(timeStr) {
        const parts = timeStr.split(':').map(Number);
        if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
            return parts[0] * 60 + parts[1];
        }
        return null;
    }

    function startFlashing() {
        if (flashing) return;
        flashing = true;
        overlay = document.createElement('div');
        overlay.style.position = 'fixed';
        overlay.style.top = 0;
        overlay.style.left = 0;
        overlay.style.width = '100%';
        overlay.style.height = '100%';
        overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.5)';
        overlay.style.zIndex = 9999;
        overlay.style.pointerEvents = 'none';
        overlay.style.animation = 'flash 0.5s alternate infinite';
        document.body.appendChild(overlay);

        style = document.createElement('style');
        style.textContent = `
            @keyframes flash {
                from { opacity: 0.3; }
                to { opacity: 0.8; }
            }
        `;
        document.head.appendChild(style);
    }

    function stopFlashing() {
        if (!flashing) return;
        flashing = false;
        if (overlay) overlay.remove();
        if (style) style.remove();
        overlay = null;
        style = null;
    }

    function initAudio() {
        if (!audio) {
            audio = new Audio(AUDIO_URL);
            audio.preload = 'auto';
            audio.volume = 0.5;
            audio.loop = true;
            audio.load();
        }
    }

    async function startSoundLoop() {
        if (audioStartTime !== null) return;
        initAudio();
        try {
            await audio.play();
            audioStartTime = Date.now();
            setTimeout(() => {
                if (audioStartTime !== null) stopSoundLoop();
            }, AUDIO_DURATION);
        } catch (e) {
            if (e.name === 'NotAllowedError') {
                const startAudio = async () => {
                    try {
                        await audio.play();
                        document.removeEventListener('click', startAudio);
                    } catch (err) {
                        console.error('Still failed to play audio:', err);
                    }
                };
                document.addEventListener('click', startAudio);
            }
        }
    }

    function stopSoundLoop() {
        if (audioStartTime === null) return;
        audio.pause();
        audio.currentTime = 0;
        audioStartTime = null;
    }

    function checkTimer() {
        if (!enabled) return;

        let timerEl = null;
        for (const selector of TIMER_SELECTORS) {
            timerEl = document.querySelector(selector);
            if (timerEl) break;
        }
        if (!timerEl) return;

        const timeText = timerEl.textContent.trim();
        if (!timeText.includes(':')) return;

        const seconds = getTimeInSeconds(timeText);
        if (seconds === null) return;

        if (seconds < flashThreshold) {
            startFlashing();
        } else {
            stopFlashing();
        }

        if (seconds < soundThreshold) {
            startSoundLoop();
        } else {
            stopSoundLoop();
        }
    }

    const observer = new MutationObserver(checkTimer);
    observer.observe(document.body, { childList: true, subtree: true });
    setInterval(checkTimer, 1000);
})();