Zed City notifier with collapsible bottom-left UI, per-stat toggles, sound, and persistent custom thresholds
目前為
// ==UserScript==
// @name Zed City Notifier
// @namespace http://tampermonkey.net/
// @version 2.0.2
// @description Zed City notifier with collapsible bottom-left UI, per-stat toggles, sound, and persistent custom thresholds
// @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;
const STORAGE_KEY = 'zedCityNotifier';
// --- Load saved config or set defaults ---
const defaultConfig = {
thresholds: { energy: 100, rad: 15, morale: 100, life: 100 },
notified: { energy: false, rad: false, morale: false, life: false },
enabled: { energy: true, rad: true, morale: true, life: true },
uiVisible: false
};
let config = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
// Merge with defaults to avoid undefined keys
config = {
thresholds: { ...defaultConfig.thresholds, ...(config.thresholds || {}) },
notified: { ...defaultConfig.notified, ...(config.notified || {}) },
enabled: { ...defaultConfig.enabled, ...(config.enabled || {}) },
uiVisible: config.uiVisible ?? defaultConfig.uiVisible
};
function saveConfig() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
}
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: `⚡ [ZedCityNotifier] ${message}`, // prepend plugin name with emoji
caption: caption || "", // optional subtitle
color,
position: "top-right",
timeout: 3500,
multiLine: true,
// minimal styling to avoid broken layout
classes: "zc-notify",
});
} else {
console.log("[ZedCityNotifier] Quasar notify not ready:", message);
}
}
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") {
new Notification(title, { body: text });
audio.play().catch(e => console.log("[ZedCityNotifier] Audio error:", e));
}
}
function checkStats() {
GM_xmlhttpRequest({
method: "GET",
url: "https://api.zed.city/getStats",
headers: { "Accept": "application/json" },
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
const stats = {
energy: data.energy,
rad: data.rad,
morale: data.morale,
life: data.life
};
console.log("[ZedCityNotifier] Stats:", stats);
for (const stat in stats) {
if (!config.enabled[stat]) continue; // skip disabled stats
const value = stats[stat];
const limit = config.thresholds[stat];
if (value >= limit && !config.notified[stat]) {
sendNotification("Zed City Alert", `${stat} reached ${value}!`);
notifyUser(stat, value);
config.notified[stat] = true;
saveConfig();
} else if (value < limit && config.notified[stat]) {
config.notified[stat] = false;
saveConfig();
}
}
} catch (e) {
console.error("[ZedCityNotifier] Error parsing stats:", e);
}
}
},
onerror: function(err) { console.error("[ZedCityNotifier] Request error:", err); }
});
}
checkStats();
setInterval(checkStats, CHECK_INTERVAL);
// --- PANEL CONTAINER ---
const panel = document.createElement("div");
Object.assign(panel.style, {
position: "fixed",
bottom: "60px",
left: "20px",
width: "210px",
background: "rgba(20,20,20,0.9)",
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 = "⚙️ Zed City 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", value: config.thresholds.energy },
{ id: "rad", label: "Rad", value: config.thresholds.rad },
{ id: "morale", label: "Morale", value: config.thresholds.morale },
{ id: "life", label: "Life", value: config.thresholds.life }
];
fields.forEach(f => {
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 = f.label;
label.style.flex = "1";
const input = document.createElement("input");
input.type = "number";
input.value = f.value;
input.id = "zc_" + f.id;
Object.assign(input.style, {
width: "50px",
marginRight: "4px"
});
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = config.enabled[f.id];
checkbox.id = "zc_enable_" + f.id;
const checkboxLabel = document.createElement("span");
checkboxLabel.textContent = "🔔";
checkboxLabel.title = "Enable/disable notifications";
row.appendChild(label);
row.appendChild(input);
row.appendChild(checkboxLabel);
row.appendChild(checkbox);
content.appendChild(row);
});
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"
});
content.appendChild(saveButton);
panel.appendChild(content);
document.body.appendChild(panel);
// --- TOGGLE BUTTON ---
const toggleButton = document.createElement("button");
toggleButton.id = "zcToggleBtn";
toggleButton.textContent = "⚙️ Notifier";
Object.assign(toggleButton.style, {
position: "fixed",
bottom: "20px",
left: "20px",
zIndex: "9999",
padding: "6px 12px",
borderRadius: "8px",
border: "none",
background: "#2c2c2c",
color: "#fff",
cursor: "pointer",
boxShadow: "0 0 5px rgba(0,0,0,0.3)"
});
document.body.appendChild(toggleButton);
// --- Toggle logic ---
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();
}
toggleButton.addEventListener("click", () => {
visible = !visible;
updatePanelVisibility();
});
// --- Save handler ---
saveButton.addEventListener("click", () => {
fields.forEach(f => {
config.thresholds[f.id] = parseInt(document.getElementById("zc_" + f.id).value) || 100;
config.enabled[f.id] = document.getElementById("zc_enable_" + f.id).checked;
});
saveConfig();
//alert("Settings saved!");
gmNotify("Settings saved!", "positive", "Your custom thresholds are now active!");
});
})();