Zed City notifier with toolbar icon, sound alerts, and junk store countdown
当前为
// ==UserScript==
// @name Zed City Notifier
// @namespace http://tampermonkey.net/
// @version 2.5.2
// @description Zed City notifier with toolbar icon, sound alerts, and junk store countdown
// @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';
const defaultConfig = {
thresholds: { energy: 100, rad: 15, morale: 100, life: 100 },
notified: { energy: false, rad: false, morale: false, life: false, reset_time: false },
enabled: { energy: true, rad: true, morale: true, life: true, reset_time: true },
uiVisible: false,
membership: false
};
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
};
let junkTimeSeconds = 0;
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}`,
caption: caption || "",
color,
position: "top-right",
timeout: 3500,
multiLine: true
});
} else {
console.log("[ZedCityNotifier] Notify:", 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));
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => {
if (permission === "granted") {
new Notification(title, { body: text });
audio.play().catch(e => console.log("[ZedCityNotifier] Audio error:", e));
}
});
} else {
audio.play().catch(e => console.log("[ZedCityNotifier] Audio error:", e));
}
}
function notifyUser(stat, value) {
const msg = `${stat} reached ${value}!`;
gmNotify(msg, "warning", "Stat Alert!");
sendNotification("⚡ Zed City Alert", msg);
}
function updateJunkUI() {
const junkTimeEl = document.getElementById("zc_junk_time");
if (!junkTimeEl) return;
if (junkTimeSeconds <= 0) {
junkTimeEl.textContent = "(Ready!)";
junkTimeEl.style.color = "#00ff66";
junkTimeEl.style.fontWeight = "bold";
} else {
const hours = Math.floor(junkTimeSeconds / 3600);
const minutes = Math.floor((junkTimeSeconds % 3600) / 60);
const seconds = junkTimeSeconds % 60;
const timeStr = hours > 0
? `${hours}h ${minutes}m ${seconds}s`
: `${minutes}m ${seconds}s`;
junkTimeEl.textContent = `(${timeStr})`;
junkTimeEl.style.color = "#ffd966";
junkTimeEl.style.fontWeight = "normal";
}
}
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 => console.error("[ZedCityNotifier] Fetch error:", e));
}
}
function checkStats() {
// Stats
xhrGet("https://api.zed.city/getStats", 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,
isMember: data.membership
};
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]) {
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);
}
}
});
// Junk store reset
xhrGet("https://api.zed.city/getStore?store_id=junk", function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
// if server says you're traveling or any error, skip this cycle
if (data.error) {
console.log(`[ZedCityNotifier] Skipping junk check: ${data.error}`);
return; // do not alert, do not modify junkTimeSeconds
}
junkTimeSeconds = Number(data?.limits?.reset_time ?? data?.store?.reset_time ?? 0);
updateJunkUI();
if (config.enabled.reset_time) {
if ((junkTimeSeconds <= 300 && 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!`;
sendNotification("Zed City Alert", msg);
notifyUser("Junk Store Reset", msg);
config.notified.reset_time = true;
saveConfig();
} else if (junkTimeSeconds > 300 && config.notified.reset_time) {
config.notified.reset_time = false;
saveConfig();
}
}
} catch (e) {
console.error("[ZedCityNotifier] Store parse error:", e);
}
}
});
}
checkStats();
setInterval(checkStats, CHECK_INTERVAL);
setInterval(() => {
if (junkTimeSeconds > 0) {
junkTimeSeconds--;
updateJunkUI();
}
}, 1000);
// === Settings Panel ===
const panel = document.createElement("div");
Object.assign(panel.style, {
position: "fixed",
bottom: "60px",
left: "20px",
width: "230px",
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 = "⚙️ 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 bell = document.createElement("span");
bell.textContent = "🔔";
bell.title = "Enable/disable notifications";
row.appendChild(label);
row.appendChild(input);
row.appendChild(bell);
row.appendChild(checkbox);
content.appendChild(row);
});
// Junk row
const junkRow = document.createElement("div");
Object.assign(junkRow.style, {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "6px"
});
const junkLabel = document.createElement("label");
junkLabel.textContent = "Junk Reset";
junkLabel.style.flex = "1";
const junkTimeEl = document.createElement("span");
junkTimeEl.id = "zc_junk_time";
junkTimeEl.textContent = "(—)";
junkTimeEl.style.marginRight = "6px";
junkTimeEl.style.opacity = "0.8";
junkTimeEl.style.fontSize = "11px";
const junkBell = document.createElement("span");
junkBell.textContent = "🔔";
const junkCheck = document.createElement("input");
junkCheck.type = "checkbox";
junkCheck.checked = config.enabled.reset_time;
junkCheck.id = "zc_enable_reset_time";
junkRow.appendChild(junkLabel);
junkRow.appendChild(junkTimeEl);
junkRow.appendChild(junkBell);
junkRow.appendChild(junkCheck);
content.appendChild(junkRow);
// 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("Zed City 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 => {
config.thresholds[f.id] = parseInt(document.getElementById("zc_" + f.id).value) || 100;
config.enabled[f.id] = document.getElementById("zc_enable_" + f.id).checked;
});
config.enabled.reset_time = document.getElementById("zc_enable_reset_time").checked;
saveConfig();
gmNotify("Settings saved!", "positive", "Thresholds updated!");
});
content.appendChild(saveButton);
panel.appendChild(content);
document.body.appendChild(panel);
// === Toolbar Icon ===
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();
}
function insertToolbarIcon() {
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 = "Zed 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"; });
console.log("[ZedCityNotifier] Toolbar icon added (size matched)");
return true;
}
const toolbarCheck = setInterval(() => {
if (insertToolbarIcon()) clearInterval(toolbarCheck);
}, 1000);
})();