From b6058225a42c228cdbafa099e06136127c60e9d9 Mon Sep 17 00:00:00 2001 From: Staninna <26458805+Staninna@users.noreply.github.com> Date: Sat, 23 Aug 2025 00:01:17 +0200 Subject: [PATCH] Add desktop notification support and settings --- Auto-Image.js | 196 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 1 deletion(-) diff --git a/Auto-Image.js b/Auto-Image.js index 960fe30..9ca8973 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -13,6 +13,13 @@ PAINTING_SPEED_ENABLED: false, AUTO_CAPTCHA_ENABLED: true, // Turnstile generator enabled by default COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold + // Desktop Notifications (defaults) + NOTIFICATIONS: { + ENABLED: true, + ON_CHARGES_REACHED: true, + ONLY_WHEN_UNFOCUSED: true, + REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds + }, OVERLAY: { OPACITY_DEFAULT: 0.6, BLUE_MARBLE_DEFAULT: false, @@ -988,6 +995,13 @@ resizeSettings: null, originalImage: null, resizeIgnoreMask: null, + // Notification prefs and runtime bookkeeping + notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, + notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, + notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, + notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, + _lastChargesNotifyAt: 0, + _lastChargesBelow: true, } // Placeholder for the resize preview update function @@ -2459,6 +2473,100 @@ }, } + // Desktop Notification Manager + const NotificationManager = { + pollTimer: null, + pollIntervalMs: 60_000, + icon() { + const link = document.querySelector("link[rel~='icon']"); + return link?.href || (location.origin + "/favicon.ico"); + }, + async requestPermission() { + if (!("Notification" in window)) { + Utils.showAlert("Notifications are not supported in this browser.", "warning"); + return "denied"; + } + if (Notification.permission === "granted") return "granted"; + try { + const perm = await Notification.requestPermission(); + return perm; + } catch { + return Notification.permission; + } + }, + canNotify() { + return state.notificationsEnabled && + typeof Notification !== "undefined" && + Notification.permission === "granted"; + }, + notify(title, body, tag = "wplace-charges", force = false) { + if (!this.canNotify()) return false; + if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false; + try { + new Notification(title, { + body, + tag, + renotify: true, + icon: this.icon(), + badge: this.icon(), + silent: false, + }); + return true; + } catch { + // Graceful fallback + Utils.showAlert(body, "info"); + return false; + } + }, + resetEdgeTracking() { + state._lastChargesBelow = state.currentCharges < state.cooldownChargeThreshold; + state._lastChargesNotifyAt = 0; + }, + maybeNotifyChargesReached(force = false) { + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + const reached = state.currentCharges >= state.cooldownChargeThreshold; + const now = Date.now(); + const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; + if (reached) { + const shouldEdge = state._lastChargesBelow || force; + const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; + if (shouldEdge || shouldRepeat) { + const msg = `Charges ready: ${Math.floor(state.currentCharges)} / ${state.maxCharges}. Threshold: ${state.cooldownChargeThreshold}.`; + this.notify("WPlace — Charges Ready", msg, "wplace-notify-charges"); + state._lastChargesNotifyAt = now; + } + state._lastChargesBelow = false; + } else { + state._lastChargesBelow = true; + } + }, + startPolling() { + this.stopPolling(); + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + // lightweight background polling + this.pollTimer = setInterval(async () => { + try { + const { charges, cooldown, max } = await WPlaceService.getCharges(); + state.currentCharges = Math.floor(charges); + state.cooldown = cooldown; + state.maxCharges = Math.max(1, Math.floor(max)); + this.maybeNotifyChargesReached(); + } catch { /* ignore */ } + }, this.pollIntervalMs); + }, + stopPolling() { + if (this.pollTimer) { + clearInterval(this.pollTimer); + this.pollTimer = null; + } + }, + syncFromState() { + this.resetEdgeTracking(); + if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling(); + else this.stopPolling(); + }, + }; + // COLOR MATCHING FUNCTION - Optimized with caching const colorCache = new Map() @@ -4204,6 +4312,36 @@ + +
+ +
+ + + +
+ Repeat every + + minute(s) +
+
+ + +
+
+
@@ -4739,9 +4877,22 @@ // Update functional thresholds CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold; CONFIG.WHITE_THRESHOLD = state.customWhiteThreshold; + // Notifications + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked; + if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked; + if (notifOnlyUnfocusedToggle) state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.checked; + if (notifIntervalInput) { + const v = parseInt(notifIntervalInput.value, 10); + if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v; + } saveBotSettings(); Utils.showAlert(Utils.t("settingsSaved"), "success"); closeSettingsBtn.click(); + NotificationManager.syncFromState(); }); makeDraggable(settingsContainer) @@ -4792,6 +4943,22 @@ } // (Advanced color listeners moved outside to work with resize dialog) + // (Advanced color listeners moved outside to work with resize dialog) + // Notifications listeners + const notifPermBtn = settingsContainer.querySelector("#notifRequestPermBtn"); + const notifTestBtn = settingsContainer.querySelector("#notifTestBtn"); + if (notifPermBtn) { + notifPermBtn.addEventListener("click", async () => { + const perm = await NotificationManager.requestPermission(); + if (perm === "granted") Utils.showAlert("Notifications enabled.", "success"); + else Utils.showAlert("Notifications permission denied.", "warning"); + }); + } + if (notifTestBtn) { + notifTestBtn.addEventListener("click", () => { + NotificationManager.notify("WPlace — Test", "This is a test notification.", "wplace-notify-test", true); + }); + } } @@ -5004,6 +5171,8 @@ state.currentCharges = Math.floor(charges); state.cooldown = cooldown; state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges; + // Evaluate notifications every time we refresh server-side charges + NotificationManager.maybeNotifyChargesReached(); if (cooldownSlider.max != state.maxCharges) { cooldownSlider.max = state.maxCharges; @@ -6064,10 +6233,13 @@ state.cooldownChargeThreshold = threshold; cooldownValue.textContent = threshold; saveBotSettings(); + NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change }); } loadBotSettings(); + // Ensure notification poller reflects current settings + NotificationManager.syncFromState(); } async function processImage() { @@ -6241,6 +6413,8 @@ state.cooldown = cooldown; if (state.currentCharges >= state.cooldownChargeThreshold) { + // Edge-trigger a notification the instant threshold is crossed + NotificationManager.maybeNotifyChargesReached(true); updateStats(); break; } @@ -6453,6 +6627,11 @@ resizeIgnoreMask: (state.resizeIgnoreMask && state.resizeSettings && state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length) ? { w: state.resizeSettings.width, h: state.resizeSettings.height, data: btoa(String.fromCharCode(...state.resizeIgnoreMask)) } : null, + // Notifications + notificationsEnabled: state.notificationsEnabled, + notifyOnChargesReached: state.notifyOnChargesReached, + notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused, + notificationIntervalMinutes: state.notificationIntervalMinutes, }; CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled; // AUTO_CAPTCHA_ENABLED is always true - no need to save/load @@ -6485,6 +6664,11 @@ state.paintWhitePixels = settings.paintWhitePixels ?? true; state.resizeSettings = settings.resizeSettings ?? null; state.originalImage = settings.originalImage ?? null; + // Notifications + state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED; + state.notifyOnChargesReached = settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED; + state.notifyOnlyWhenUnfocused = settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED; + state.notificationIntervalMinutes = settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES; // Restore ignore mask if dims match current resizeSettings if (settings.resizeIgnoreMask && settings.resizeIgnoreMask.data && state.resizeSettings && settings.resizeIgnoreMask.w === state.resizeSettings.width && settings.resizeIgnoreMask.h === state.resizeSettings.height) { try { @@ -6529,7 +6713,17 @@ const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); if (transparencyThresholdInput) transparencyThresholdInput.value = state.customTransparencyThreshold; const whiteThresholdInput = document.getElementById('whiteThresholdInput'); - if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold; + if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold; + // Notifications UI + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + if (notifEnabledToggle) notifEnabledToggle.checked = state.notificationsEnabled; + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + if (notifOnChargesToggle) notifOnChargesToggle.checked = state.notifyOnChargesReached; + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + if (notifOnlyUnfocusedToggle) notifOnlyUnfocusedToggle.checked = state.notifyOnlyWhenUnfocused; + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifIntervalInput) notifIntervalInput.value = state.notificationIntervalMinutes; + NotificationManager.resetEdgeTracking(); } catch (e) { console.warn("Could not load bot settings:", e);