Feed Thomas Reminder

Feed reminder overlay with 3 snooze modes (1hr @ hh:02, 10s, 24hr), sync, Open Sans, Snooze button, and visual mode indicators. Updated 1hr mode to resume 2 minutes past the hour. 🐾🐈🕒⏱️📅🧭🔕

// ==UserScript==
// @name         Feed Thomas Reminder
// @namespace    https://farmrpg.com/
// @version      2.8
// @description  Feed reminder overlay with 3 snooze modes (1hr @ hh:02, 10s, 24hr), sync, Open Sans, Snooze button, and visual mode indicators. Updated 1hr mode to resume 2 minutes past the hour. 🐾🐈🕒⏱️📅🧭🔕
// @author       Clientcoin
// @match        *://*/*
// @icon         https://farmrpg.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @license      Unlicense
// @homepage     https://farmrpg.com/
// @supportURL   https://farmrpg.com/
// @run-at       document-end
// ==/UserScript==

(async function () {
    'use strict';

    const FORCE_SHOW_NEXT_LOAD = false;

    const ID = 'feed-thomas-box';
    const SCALE_KEY = 'feedThomasScale';
    const STORAGE_KEY = 'feedThomasHiddenUntil';
    const MODE_KEY = 'feedThomasMode';
    const FORCE_KEY = 'feedThomasForceFlag';

    let scale = parseFloat(localStorage.getItem(SCALE_KEY)) || 1.0;
    let mode = parseInt(await GM_getValue(MODE_KEY, 0), 10);
    let hiddenUntil = parseInt(await GM_getValue(STORAGE_KEY, 0), 10) || 0;
    let clockInterval = null;

    const fontLink = document.createElement('link');
    fontLink.href = 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap';
    fontLink.rel = 'stylesheet';
    document.head.appendChild(fontLink);

    if (FORCE_SHOW_NEXT_LOAD) {
        await GM_setValue(FORCE_KEY, true);
    }
    const forceOverride = await GM_getValue(FORCE_KEY, false);
    if (forceOverride) {
        hiddenUntil = 0;
        await GM_setValue(FORCE_KEY, false);
    }

    function shouldShowBox() {
        return Date.now() >= hiddenUntil;
    }

    async function handleSnooze() {
        const now = new Date();
        let next;
        if (mode === 1) {
            next = new Date(now);
            const remainder = 10 - (now.getSeconds() % 10);
            next.setSeconds(now.getSeconds() + remainder, 0);
        } else if (mode === 2) {
            next = new Date(now);
            next.setDate(now.getDate() + 1);
        } else {
            next = new Date(now);
            next.setMinutes(2, 0, 0);  // set to next hour @ 02:00
            next.setHours(next.getHours() + 1);
        }
        await GM_setValue(STORAGE_KEY, next.getTime());
    }

    function createBox() {
        if (document.getElementById(ID)) return;

        const box = document.createElement('div');
        box.id = ID;
        box.title = 'Click to snooze until next hour +2min, 10s or 24h depending on mode';
        Object.assign(box.style, {
            position: 'fixed',
            top: '10px',
            right: '10px',
            zIndex: '999999',
            backgroundColor: 'blue',
            color: 'yellow',
            fontSize: `${scale}em`,
            padding: '10px',
            borderRadius: '5px',
            cursor: 'pointer',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            textAlign: 'center',
            fontFamily: "'Open Sans', sans-serif",
            lineHeight: '1.1'
        });

        const text = document.createElement('div');
        text.textContent = 'Feed Thomas';
        text.style.fontSize = '1em';
        box.appendChild(text);

        const clock = document.createElement('div');
        clock.style.fontSize = '0.8em';
        clock.style.marginTop = '4px';
        box.appendChild(clock);

        const date = document.createElement('div');
        date.style.fontSize = '0.75em';
        box.appendChild(date);

        updateClockAndDate(clock, date);
        clockInterval = setInterval(() => updateClockAndDate(clock, date), 1000);

        const controls = document.createElement('div');
        controls.style.display = 'flex';
        controls.style.gap = '6px';
        controls.style.marginTop = '6px';

        const minusBtn = document.createElement('button');
        minusBtn.textContent = '-';
        minusBtn.title = 'Decrease size';
        styleControlButton(minusBtn);
        minusBtn.style.width = '28px';
        minusBtn.onclick = (e) => {
            e.stopPropagation();
            scale = Math.max(0.5, scale - 0.1);
            localStorage.setItem(SCALE_KEY, scale.toFixed(2));
            document.getElementById(ID).style.fontSize = `${scale}em`;
        };

        const plusBtn = document.createElement('button');
        plusBtn.textContent = '+';
        plusBtn.title = 'Increase size';
        styleControlButton(plusBtn);
        plusBtn.style.width = '28px';
        plusBtn.onclick = (e) => {
            e.stopPropagation();
            scale = Math.min(3.0, scale + 0.1);
            localStorage.setItem(SCALE_KEY, scale.toFixed(2));
            document.getElementById(ID).style.fontSize = `${scale}em`;
        };

        controls.appendChild(minusBtn);
        controls.appendChild(plusBtn);
        box.appendChild(controls);

        const modeBtn = document.createElement('button');
        updateModeButtonStyle(modeBtn, mode);
        modeBtn.title = 'Click to cycle reminder interval (1hr, 10s, 24hr)';
        modeBtn.onclick = async (e) => {
            e.stopPropagation();
            mode = (mode + 1) % 3;
            await GM_setValue(MODE_KEY, mode);
        };
        box.appendChild(modeBtn);

        const snoozeBtn = document.createElement('button');
        snoozeBtn.textContent = 'Snooze';
        snoozeBtn.title = 'Manually snooze this reminder';
        Object.assign(snoozeBtn.style, {
            marginTop: '4px',
            fontSize: '11px'
        });
        styleControlButton(snoozeBtn);
        snoozeBtn.onclick = async (e) => {
            e.stopPropagation();
            await handleSnooze();
        };
        box.appendChild(snoozeBtn);

        box.onclick = async () => {
            await handleSnooze();
        };

        document.body.appendChild(box);
    }

    function styleControlButton(btn) {
        btn.style.background = 'yellow';
        btn.style.color = 'blue';
        btn.style.border = 'none';
        btn.style.cursor = 'pointer';
        btn.style.padding = '2px 6px';
        btn.style.fontWeight = 'bold';
        btn.style.borderRadius = '4px';
        btn.style.fontFamily = "'Open Sans', sans-serif";
    }

    function updateModeButtonStyle(btn, modeValue) {
        const labels = ['Mode: 1hr', 'Mode: 10s', 'Mode: 24hr'];
        btn.textContent = labels[modeValue];
        btn.style.marginTop = '6px';
        btn.style.fontSize = '10px';

        styleControlButton(btn);

        if (modeValue === 1) {
            btn.style.opacity = '0.6';
        } else if (modeValue === 2) {
            btn.style.background = 'blue';
            btn.style.color = 'yellow';
            btn.style.opacity = '1.0';
        } else {
            btn.style.opacity = '1.0';
            btn.style.background = 'yellow';
            btn.style.color = 'blue';
        }
    }

    function updateClockAndDate(clockEl, dateEl) {
        const now = new Date();
        const timeStr = now.toLocaleTimeString([], {
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        });

        const year = now.getFullYear();
        const month = now.toLocaleString('default', { month: 'short' });
        const day = String(now.getDate()).padStart(2, '0');
        const weekday = now.toLocaleString('default', { weekday: 'short' });
        const dateStr = `${year}-${month}-${day} (${weekday})`;

        clockEl.textContent = timeStr;
        dateEl.textContent = dateStr;
    }

    function checkAndShowBox() {
        const box = document.getElementById(ID);
        if (shouldShowBox()) {
            if (!box) createBox();
        } else if (box) {
            clearInterval(clockInterval);
            box.remove();
        }
    }

    GM_addValueChangeListener(STORAGE_KEY, (_, __, newValue) => {
        hiddenUntil = parseInt(newValue, 10);
        checkAndShowBox();
    });

    GM_addValueChangeListener(MODE_KEY, (_, __, newValue) => {
        mode = parseInt(newValue, 10);
        const box = document.getElementById(ID);
        if (box) {
            const btn = [...box.querySelectorAll('button')]
                .find(b => b.textContent.startsWith('Mode:'));
            if (btn) {
                updateModeButtonStyle(btn, mode);
            }
        }
    });

    function init() {
        checkAndShowBox();
        setInterval(checkAndShowBox, 2000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();