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 @@
+
+
+
+
+
@@ -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);