Add desktop notification support and settings

This commit is contained in:
Staninna committed 2025-08-23 00:01:17 +02:00
1 parent f023191ade
commit b6058225a4
1 file changed
+195 -1
+195 -1
View File
@@ -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 @@
</label>
</div>
<!-- Notifications Section -->
<div style="margin-bottom: 25px;">
<label style="display: block; margin-bottom: 12px; color: white; font-weight: 500; font-size: 16px; display: flex; align-items: center; gap: 8px;">
<i class="fas fa-bell" style="color: #ffd166; font-size: 16px;"></i>
Desktop Notifications
</label>
<div style="background: rgba(255,255,255,0.1); border-radius: 12px; padding: 18px; border: 1px solid rgba(255,255,255,0.1); display:flex; flex-direction:column; gap:10px;">
<label style="display:flex; align-items:center; justify-content:space-between;">
<span>Enable notifications</span>
<input type="checkbox" id="notifEnabledToggle" ${state.notificationsEnabled ? 'checked' : ''} style="width:18px; height:18px; cursor:pointer;" />
</label>
<label style="display:flex; align-items:center; justify-content:space-between;">
<span>Notify when charges reach threshold</span>
<input type="checkbox" id="notifOnChargesToggle" ${state.notifyOnChargesReached ? 'checked' : ''} style="width:18px; height:18px; cursor:pointer;" />
</label>
<label style="display:flex; align-items:center; justify-content:space-between;">
<span>Only when tab is not focused</span>
<input type="checkbox" id="notifOnlyUnfocusedToggle" ${state.notifyOnlyWhenUnfocused ? 'checked' : ''} style="width:18px; height:18px; cursor:pointer;" />
</label>
<div style="display:flex; align-items:center; gap:10px;">
<span>Repeat every</span>
<input type="number" id="notifIntervalInput" min="1" max="60" value="${state.notificationIntervalMinutes}" style="width:70px; padding:6px 8px; border-radius:6px; border:1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.08); color:#fff;" />
<span>minute(s)</span>
</div>
<div style="display:flex; gap:10px;">
<button id="notifRequestPermBtn" class="wplace-btn wplace-btn-secondary" style="flex:1;"><i class="fas fa-unlock"></i><span>Grant Permission</span></button>
<button id="notifTestBtn" class="wplace-btn" style="flex:1;"><i class="fas fa-bell"></i><span>Test</span></button>
</div>
</div>
</div>
<!-- Theme Selection Section -->
<div style="margin-bottom: 25px;">
@@ -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);