ZedTools Notifier

ZedTools notifier with persistent timers for Junk Store, Scrap Expert, and Radio Tower (works during travel) + toolbar icon & UI

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         ZedTools Notifier
// @namespace    http://tampermonkey.net/
// @version      3.4.0
// @description  ZedTools notifier with persistent timers for Junk Store, Scrap Expert, and Radio Tower (works during travel) + toolbar icon & UI
// @author       You
// @match        https://zed.city/*
// @match        https://*.zed.city/*
// @grant        GM_xmlhttpRequest
// @connect      zed.city
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const CHECK_INTERVAL = 60 * 1000; // normal stats
    const XP_CHECK_INTERVAL = 10 * 1000; // XP percentage check
    const STORAGE_KEY = 'ZedToolsNotifier';

    const defaultConfig = {
        thresholds: { energy: 100, rad: 15, morale: 100, life: 100, booster: 300, raid: 300, xp: 80, scrap_expert: 300, radio_tower: 300 },
        notified: { energy: false, rad: false, morale: false, life: false, reset_time: false, booster: false, raid: false, xp: false, relaxed: false, rested: false, traveling: false, scrap_expert: false, radio_tower: false },
        enabled: { energy: true, rad: true, morale: true, life: true, reset_time: true, booster: true, raid: true, xp: true, relaxed: true, rested: true, traveling: true, scrap_expert: true, radio_tower: true },
        uiVisible: true
    };

    let config = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
    config = {
        thresholds: { ...defaultConfig.thresholds, ...(config.thresholds || {}) },
        notified: { ...defaultConfig.notified, ...(config.notified || {}) },
        enabled: { ...defaultConfig.enabled, ...(config.enabled || {}) },
        uiVisible: config.uiVisible ?? defaultConfig.uiVisible,
        isMember: config.membership ?? false
    };

    function saveConfig() { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); }

    // timers (remaining seconds shown in UI)
    let junkTimeSeconds = 0;
    let scrapExpertTime = 0;
    let radioTowerTime = 0;
    let boosterTime = 0;
    let raidTime = 0;
    let relaxedTime = 0;
    let restedTime = 0;
    let currentXP = 0;
    let nextLevelXP = 0;
    let travelingTime = 0;
    let travelingLoc = "";
    let travelingNotified = false;

    // === Persistent Cooldown storage using expiry timestamps (now + seconds) ===
    function saveCooldownExpiry(key, seconds) {
        try {
            const expiry = Date.now() + (Number(seconds) * 1000);
            localStorage.setItem(`ZedCooldown_${key}`, expiry.toString());
        } catch (e) { console.error("[ZedTools] saveCooldownExpiry error:", e); }
    }

    function loadCooldownRemaining(key) {
        try {
            const raw = localStorage.getItem(`ZedCooldown_${key}`);
            if (!raw) return null;
            const expiry = parseInt(raw, 10);
            if (!expiry) return null;
            const remaining = Math.max(0, Math.floor((expiry - Date.now()) / 1000));
            return remaining;
        } catch (e) {
            console.error("[ZedTools] loadCooldownRemaining error:", e);
            return null;
        }
    }

    // audio + notify permission
    const audio = new Audio("https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg");
    if (Notification.permission !== "granted" && Notification.permission !== "denied") Notification.requestPermission();

    function sendNotification(title, text) {
        if (Notification.permission === "granted") {
            try { new Notification(title, { body: text }); } catch (e) { console.log("[ZedTools] Notification error:", e); }
            audio.play().catch(e => console.log("[ZedTools] Audio error:", e));
        } else {
            console.log(`[ZedTools] ${title}: ${text}`);
            audio.play().catch(e => console.log("[ZedTools] Audio error:", e));
        }
    }

    function notifyUser(stat, value) {
        const msg = typeof value === 'number' ? `${stat} reached ${value}!` : value;
        console.log("[ZedTools Notifier] Notify:", msg);
        sendNotification("⚡ ZedTools Notifier", msg);
        gmNotify(msg, "warning", "Stat Alert!");
    }

    function gmNotify(message, color = "positive", caption) {
        const vueApp = window.app || window.vueApp || document.querySelector("#q-app")?._vnode?.appContext?.app;
        const $q = vueApp?.config?.globalProperties?.$q;
        if ($q && typeof $q.notify === "function") {
            $q.notify({
                message: `⚡ [ZedTools Notifier] ${message}`,
                caption: caption || "",
                color,
                position: "top-right",
                timeout: 3500,
                multiLine: true
            });
        } else {
            console.log("[ZedTools Notifier] Notify:", message);
        }
    }

    // UI update helpers
    function updateCountdownUI(elId, timeSeconds) {
        const el = document.getElementById(elId);
        if (!el) return;
        if (timeSeconds <= 0) {
            el.textContent = "(Ready!)";
            el.style.color = "#00ff66";
            el.style.fontWeight = "bold";
        } else {
            const hours = Math.floor(timeSeconds / 3600);
            const minutes = Math.floor((timeSeconds % 3600) / 60);
            const seconds = timeSeconds % 60;
            const timeStr = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : `${minutes}m ${seconds}s`;
            el.textContent = `(${timeStr})`;
            el.style.color = "#ffd966";
            el.style.fontWeight = "normal";
        }
    }
    function updateJunkUI() { updateCountdownUI("zc_junk_time", junkTimeSeconds); }
    function updateScrapExpertUI() { updateCountdownUI("zc_scrapexpert_time", scrapExpertTime); }
    function updateRadioTowerUI() { updateCountdownUI("zc_radiotower_time", radioTowerTime); }
    function updateBoosterUI() { updateCountdownUI("zc_booster_time", boosterTime); }
    function updateRaidUI() { updateCountdownUI("zc_raid_time", raidTime); }
    function updateRelaxedUI() { updateCountdownUI("zc_relaxed_time", relaxedTime); }
    function updateRestedUI() { updateCountdownUI("zc_rested_time", restedTime); }
    function updateTravelingUI() { updateCountdownUI("zc_traveling_time", travelingTime); }

    function updateXPUI(percent) {
        const el = document.getElementById("zc_xp");
        if (!el) return;
        el.textContent = `${percent}%`;
        el.style.color = "#000";
    }

    // portable xhr helper
    function xhrGet(url, onload) {
        if (typeof GM_xmlhttpRequest === 'function') {
            GM_xmlhttpRequest({ method: "GET", url, headers: { "Accept": "application/json" }, onload });
        } else {
            fetch(url, { headers: { "Accept": "application/json" } })
                .then(r => r.text())
                .then(responseText => onload({ status: 200, responseText }))
                .catch(e => onload({ status: 0, error: e }));
        }
    }

    // === Core checks ===
    function checkStats() {
        xhrGet("https://api.zed.city/getStats", function(response) {
            if (response.status === 200) {
                try {
                    const data = JSON.parse(response.responseText);

                    // Basic stats checks
                    const stats = { energy: data.energy, rad: data.rad, morale: data.morale, life: data.life };
                    for (const stat in stats) {
                        if (!config.enabled[stat]) continue;
                        const value = stats[stat];
                        const limit = config.thresholds[stat];
                        if (value >= limit && !config.notified[stat]) {
                            notifyUser(stat, value);
                            config.notified[stat] = true; saveConfig();
                        } else if (value < limit && config.notified[stat]) {
                            config.notified[stat] = false; saveConfig();
                        }
                    }

                    // Booster & Raid (seconds)
                    boosterTime = Number(data.booster_cooldown ?? 0);
                    raidTime = Number(data.raid_cooldown ?? 0);

                    // Notifications for booster/raid
                    if (config.enabled.booster && ((boosterTime <= config.thresholds.booster && boosterTime > 0 && !config.notified.booster) || (boosterTime === 0 && !config.notified.booster))) {
                        const msg = boosterTime === 0 ? "Booster ready!" : `Booster ready in ${Math.ceil(boosterTime/60)} min!`;
                        notifyUser("Booster", msg); config.notified.booster = true; saveConfig();
                    } else if (boosterTime > config.thresholds.booster && config.notified.booster) { config.notified.booster = false; saveConfig(); }

                    if (config.enabled.raid && ((raidTime <= config.thresholds.raid && raidTime > 0 && !config.notified.raid) || (raidTime === 0 && !config.notified.raid))) {
                        const msg = raidTime === 0 ? "Raid ready!" : `Raid ready in ${Math.ceil(raidTime/60)} min!`;
                        notifyUser("Raid", msg); config.notified.raid = true; saveConfig();
                    } else if (raidTime > config.thresholds.raid && config.notified.raid) { config.notified.raid = false; saveConfig(); }

                    updateBoosterUI();
                    updateRaidUI();

                    // Temporary effects
                    const effects = data.effects || [];
                    const relaxed = effects.find(e => e.codename === "player_effect_feeling_relaxed");
                    const rested = effects.find(e => e.codename === "player_effect_recently_rested");

                    relaxedTime = relaxed ? Number(relaxed.expire) : 0;
                    restedTime = rested ? Number(rested.expire) : 0;

                    updateRelaxedUI();
                    updateRestedUI();

                } catch (e) { console.error("[ZedTools Notifier] Error parsing stats:", e); }
            }
        });

        // === Junk Store ===
        xhrGet("https://api.zed.city/getStore?store_id=junk", function(response) {
            try {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    if (data.error) throw new Error(data.error);
                    // API returns seconds until reset
                    junkTimeSeconds = Number(data?.limits?.reset_time ?? data?.store?.reset_time ?? 0);
                    if (junkTimeSeconds > 0) saveCooldownExpiry("junk", junkTimeSeconds);
                } else {
                    const stored = loadCooldownRemaining("junk");
                    if (stored !== null) junkTimeSeconds = stored;
                }
            } catch (e) {
                // parse/other error => try stored expiry
                const stored = loadCooldownRemaining("junk");
                if (stored !== null) junkTimeSeconds = stored;
            }
            updateJunkUI();

            // notify based on config thresholds (reset_time used for junk)
            if (config.enabled.reset_time) {
                if ((junkTimeSeconds <= config.thresholds.reset_time && junkTimeSeconds > 0 && !config.notified.reset_time) || (junkTimeSeconds === 0 && !config.notified.reset_time)) {
                    const msg = junkTimeSeconds === 0 ? "Junk store has just reset!" : `Junk store reset in ${Math.ceil(junkTimeSeconds/60)} minutes!`;
                    notifyUser("Junk Store Reset", msg); config.notified.reset_time = true; saveConfig();
                } else if (junkTimeSeconds > config.thresholds.reset_time && config.notified.reset_time) {
                    config.notified.reset_time = false; saveConfig();
                }
            }
        });
    }

    function checkTraveling() {
        xhrGet("https://api.zed.city/getTraveling", function(response) {
            if (response.status === 200) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.traveling) {
                        travelingTime = Number(data.time_left ?? 0);
                        travelingLoc = data.loc_name ?? data.codename ?? "Unknown location";
                        // store? traveling is ephemeral; keep as-is
                        if (!travelingNotified && travelingTime <= 60) {
                            notifyUser("Traveling", `Arriving soon at ${travelingLoc}!`);
                            travelingNotified = true;
                        }
                    } else {
                        if (travelingTime > 0) notifyUser("Traveling", `You have arrived at ${travelingLoc}!`);
                        travelingTime = 0;
                        travelingNotified = false;
                    }
                    updateTravelingUI();
                } catch(e) { console.error("[ZedTools Notifier] Traveling parse error:", e); }
            } else {
                // if traveling API not reachable, try to keep previous travelingTime as-is (countdown will continue locally)
                const stored = null; // no persistent store for traveling
                // nothing to do
            }
        });
    }

    function checkScrapExpert() {
        xhrGet("https://api.zed.city/getRoom?group=scrap_expert", function(response) {
            try {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    // API returns seconds remaining for scrapnote_time_limit_remaining
                    scrapExpertTime = Number(data?.extra?.scrapnote_time_limit_remaining ?? 0);
                    if (scrapExpertTime > 0) saveCooldownExpiry("scrap_expert", scrapExpertTime);
                } else {
                    const stored = loadCooldownRemaining("scrap_expert");
                    if (stored !== null) scrapExpertTime = stored;
                }
            } catch (e) {
                const stored = loadCooldownRemaining("scrap_expert");
                if (stored !== null) scrapExpertTime = stored;
            }
            updateScrapExpertUI();

            if (config.enabled.scrap_expert) {
                if ((scrapExpertTime <= config.thresholds.scrap_expert && scrapExpertTime > 0 && !config.notified.scrap_expert) || (scrapExpertTime === 0 && !config.notified.scrap_expert)) {
                    const msg = scrapExpertTime === 0 ? "Scrap Expert reset!" : `Scrap Expert reset in ${Math.ceil(scrapExpertTime/60)} minutes!`;
                    notifyUser("Scrap Expert", msg); config.notified.scrap_expert = true; saveConfig();
                } else if (scrapExpertTime > config.thresholds.scrap_expert && config.notified.scrap_expert) {
                    config.notified.scrap_expert = false; saveConfig();
                }
            }
        });
    }

    function checkRadioTower() {
        xhrGet("https://api.zed.city/getRadioTower", function(response) {
            try {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    radioTowerTime = Number(data?.expire ?? 0); // seconds until expire/reset
                    if (radioTowerTime > 0) saveCooldownExpiry("radio_tower", radioTowerTime);
                } else {
                    const stored = loadCooldownRemaining("radio_tower");
                    if (stored !== null) radioTowerTime = stored;
                }
            } catch (e) {
                const stored = loadCooldownRemaining("radio_tower");
                if (stored !== null) radioTowerTime = stored;
            }
            updateRadioTowerUI();

            if (config.enabled.radio_tower) {
                if ((radioTowerTime <= config.thresholds.radio_tower && radioTowerTime > 0 && !config.notified.radio_tower) || (radioTowerTime === 0 && !config.notified.radio_tower)) {
                    const msg = radioTowerTime === 0 ? "Radio Tower reset!" : `Radio Tower reset in ${Math.ceil(radioTowerTime/60)} minutes!`;
                    notifyUser("Radio Tower", msg); config.notified.radio_tower = true; saveConfig();
                } else if (radioTowerTime > config.thresholds.radio_tower && config.notified.radio_tower) {
                    config.notified.radio_tower = false; saveConfig();
                }
            }
        });
    }

    function checkXP() {
        xhrGet("https://api.zed.city/getStats", function(response) {
            if (response.status === 200) {
                try {
                    const data = JSON.parse(response.responseText);
                    currentXP = Number(data.experience);
                    nextLevelXP = Number(data.xp_end);
                    const xpPercent = nextLevelXP > 0 ? Math.floor((currentXP / nextLevelXP) * 100) : 0;
                    updateXPUI(xpPercent);
                    if (config.enabled.xp && xpPercent >= config.thresholds.xp && !config.notified.xp) {
                        notifyUser("XP", `You are ${xpPercent}% towards next level!`);
                        config.notified.xp = true; saveConfig();
                    } else if (xpPercent < config.thresholds.xp && config.notified.xp) {
                        config.notified.xp = false; saveConfig();
                    }
                } catch(e){ console.error("[ZedTools Notifier] XP parse error:", e); }
            }
        });
    }

    // === Initialization calls & intervals ===
    checkStats();
    checkXP();
    checkTraveling();
    checkScrapExpert();
    checkRadioTower();

    setInterval(checkStats, CHECK_INTERVAL);
    setInterval(checkXP, XP_CHECK_INTERVAL);
    setInterval(checkTraveling, CHECK_INTERVAL);
    setInterval(checkScrapExpert, CHECK_INTERVAL);
    setInterval(checkRadioTower, CHECK_INTERVAL);

    // === Local 1-second countdowns (UI smooth countdown) ===
    setInterval(() => {
        if (junkTimeSeconds > 0) junkTimeSeconds--;
        if (boosterTime > 0) boosterTime--;
        if (raidTime > 0) raidTime--;
        if (relaxedTime > 0) relaxedTime--;
        if (restedTime > 0) restedTime--;
        if (travelingTime > 0) travelingTime--;
        if (scrapExpertTime > 0) scrapExpertTime--;
        if (radioTowerTime > 0) radioTowerTime--;

        updateJunkUI();
        updateBoosterUI();
        updateRaidUI();
        updateRelaxedUI();
        updateRestedUI();
        updateTravelingUI();
        updateScrapExpertUI();
        updateRadioTowerUI();
    }, 1000);

    // === Settings Panel & Toolbar Icon (injected into site) ===
    const panel = document.createElement("div");
    Object.assign(panel.style, { position: "fixed", bottom: "60px", left: "20px", width: "260px", background: "rgba(20,20,20,0.95)", color: "#fff", borderRadius: "10px", fontFamily: "Arial,sans-serif", fontSize: "12px", zIndex: "9999", boxShadow: "0 0 10px rgba(0,0,0,0.5)", transition: "all 0.3s ease", opacity: config.uiVisible ? "1" : "0", transform: config.uiVisible ? "translateY(0)" : "translateY(10px)", display: config.uiVisible ? "block" : "none", backdropFilter: "blur(4px)" });

    const header = document.createElement("div");
    header.textContent = "⚙️ ZedTools Notifier";
    Object.assign(header.style, { padding: "6px", fontWeight: "bold", textAlign: "center", background: "#2c2c2c", borderRadius: "10px 10px 0 0" });
    panel.appendChild(header);

    const content = document.createElement("div");
    content.style.padding = "6px";

    const fields = [
        { id: "energy", label: "Energy" },
        { id: "rad", label: "Rad" },
        { id: "morale", label: "Morale" },
        { id: "life", label: "Life" },
        { id: "xp", label: "Experience (%)" }
    ];

    function createRow(labelText, elId, key, isNumberInput = true) {
        const row = document.createElement("div");
        Object.assign(row.style, { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "6px" });
        const label = document.createElement("label");
        label.textContent = labelText; label.style.flex = "1";
        let input;
        if (isNumberInput) {
            input = document.createElement("input");
            input.type = "number";
            input.value = config.thresholds[key];
            input.id = "zc_" + key;
            Object.assign(input.style, { width: "50px", marginRight: "4px" });
        }
        const timeEl = document.createElement("span");
        timeEl.id = elId;
        timeEl.textContent = isNumberInput ? "" : "(—)";
        timeEl.style.marginRight = "6px";
        timeEl.style.opacity = "0.8";
        timeEl.style.fontSize = "11px";
        const bell = document.createElement("span");
        bell.textContent = "🔔";
        const check = document.createElement("input");
        check.type = "checkbox";
        check.checked = config.enabled[key];
        check.id = "zc_enable_" + key;
        row.appendChild(label);
        if (isNumberInput) row.appendChild(input);
        row.appendChild(timeEl);
        row.appendChild(bell);
        row.appendChild(check);
        content.appendChild(row);
    }

    createRow("Energy", "zc_energy_time", "energy");
    createRow("Rad", "zc_rad_time", "rad");
    createRow("Morale", "zc_morale_time", "morale");
    createRow("Life", "zc_life_time", "life");
    createRow("Experience (%)", "zc_xp", "xp", true);
    createRow("Junk Reset", "zc_junk_time", "reset_time", false);
    createRow("Traveling", "zc_traveling_time", "traveling", false);
    createRow("Scrap Expert", "zc_scrapexpert_time", "scrap_expert", false);
    createRow("Radio Tower", "zc_radiotower_time", "radio_tower", false);
    createRow("Booster Ready", "zc_booster_time", "booster", false);
    createRow("Raid Ready", "zc_raid_time", "raid", false);
    createRow("SPA Timer", "zc_relaxed_time", "relaxed", false);
    createRow("Sleeping Timer", "zc_rested_time", "rested", false);

    // Test button
    const testButton = document.createElement("button");
    testButton.textContent = "🔊 Test Alert";
    Object.assign(testButton.style, { marginTop: "5px", width: "100%", borderRadius: "6px", border: "none", background: "#28a745", color: "white", padding: "5px 0", cursor: "pointer" });
    testButton.addEventListener("click", () => { sendNotification("ZedTools Notifier Test", "This is a test alert!"); gmNotify("Test notification sent!", "info"); });
    content.appendChild(testButton);

    // Save button
    const saveButton = document.createElement("button");
    saveButton.id = "zc_save";
    saveButton.textContent = "💾 Save";
    Object.assign(saveButton.style, { marginTop: "5px", width: "100%", borderRadius: "6px", border: "none", background: "#0078d7", color: "white", padding: "5px 0", cursor: "pointer" });
    saveButton.addEventListener("click", () => {
        fields.forEach(f => {
            if (f.id !== "xp") {
                config.thresholds[f.id] = parseInt(document.getElementById("zc_" + f.id).value) || 100;
            } else {
                config.thresholds[f.id] = parseInt(document.getElementById("zc_" + f.id).value) || 80;
            }
            config.enabled[f.id] = document.getElementById("zc_enable_" + f.id).checked;
        });
        // enable checkboxes for non-field items
        ["reset_time","booster","raid","relaxed","rested","traveling","scrap_expert","radio_tower"].forEach(k => {
            const el = document.getElementById("zc_enable_" + k);
            if (el) config.enabled[k] = el.checked;
        });
        saveConfig();
        gmNotify("Settings saved!", "info");
    });
    content.appendChild(saveButton);

    panel.appendChild(content);
    document.body.appendChild(panel);

    let visible = config.uiVisible;
    function updatePanelVisibility() {
        if (visible) {
            panel.style.display = "block";
            setTimeout(() => { panel.style.opacity = "1"; panel.style.transform = "translateY(0)"; }, 10);
        } else {
            panel.style.opacity = "0";
            panel.style.transform = "translateY(10px)";
            setTimeout(() => { panel.style.display = "none"; }, 300);
        }
        config.uiVisible = visible;
        saveConfig();
    }

    // toolbar injection (keeps site look & feel by copying notifications link style)
    function insertToolbarIcon() {
        try {
            const notifIcon = document.querySelector('a[href="/notifications"]');
            if (!notifIcon || document.getElementById('zcToolbarBtn')) return false;
            const iconLink = document.createElement('a'); iconLink.id = 'zcToolbarBtn'; iconLink.className = notifIcon.className; iconLink.href = 'javascript:void(0)'; iconLink.style.display = 'inline-flex'; iconLink.style.alignItems = 'center'; iconLink.style.justifyContent = 'center'; iconLink.style.height = notifIcon.offsetHeight + 'px'; iconLink.style.width = notifIcon.offsetWidth + 'px';
            iconLink.innerHTML = `<span class="q-focus-helper"></span><span class="q-btn__content text-center col items-center q-anchor--skip justify-center row" style="font-size: 1.3em;"><i class="q-icon fal fa-bullhorn" style="font-size: 1em; line-height: 1;" aria-hidden="true" role="img"></i></span>`;
            iconLink.title = "ZedTools Notifier";
            iconLink.addEventListener('click', () => { visible = !visible; updatePanelVisibility(); });
            notifIcon.parentElement.insertBefore(iconLink, notifIcon);
            const computed = window.getComputedStyle(notifIcon); iconLink.style.color = computed.color;
            iconLink.addEventListener("mouseenter", () => { iconLink.style.opacity = "0.8"; });
            iconLink.addEventListener("mouseleave", () => { iconLink.style.opacity = "1"; });
            return true;
        } catch (e) {
            console.error("[ZedTools] insertToolbarIcon error:", e);
            return false;
        }
    }

    const toolbarCheck = setInterval(() => { if (insertToolbarIcon()) clearInterval(toolbarCheck); }, 1000);

})();