Add AMOLED theme, fullscreen, settings tab, and NTFY notifications
This commit is contained in:
1 parent
fe13a4d989
commit
bbf92bb478
6 files changed
+429
-4
No files matched your search
@@ -383,3 +383,322 @@ async function renderFreedays(token) {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
settings + ntfy
|
||||
========================================================================== */
|
||||
|
||||
const SETTINGS_KEY = "librustui.settings.v1";
|
||||
const SETTINGS_LOG_KEY = "librustui.settings.log.v1";
|
||||
|
||||
function defaultSettings() {
|
||||
return {
|
||||
enabled: false,
|
||||
server: "https://ntfy.sh",
|
||||
topic: "",
|
||||
token: "",
|
||||
grades: true,
|
||||
messages: true,
|
||||
exams: true,
|
||||
attendance: false,
|
||||
pollMinutes: 5,
|
||||
};
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
try {
|
||||
const raw = localStorage.getItem(SETTINGS_KEY);
|
||||
if (!raw) return defaultSettings();
|
||||
const parsed = JSON.parse(raw);
|
||||
return Object.assign(defaultSettings(), parsed || {});
|
||||
} catch (error) {
|
||||
return defaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(settings) {
|
||||
const next = Object.assign(defaultSettings(), settings || {});
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(next));
|
||||
} catch (error) {
|
||||
/* ignore storage errors */
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function loadSettingsLog() {
|
||||
try {
|
||||
const raw = localStorage.getItem(SETTINGS_LOG_KEY);
|
||||
const parsed = raw ? JSON.parse(raw) : [];
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function appendSettingsLog(message) {
|
||||
const log = loadSettingsLog();
|
||||
log.unshift({ at: new Date().toISOString(), message: String(message || "") });
|
||||
if (log.length > 40) log.length = 40;
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_LOG_KEY, JSON.stringify(log));
|
||||
} catch (error) {
|
||||
/* ignore */
|
||||
}
|
||||
const container = $("#settings-log");
|
||||
if (container) renderSettingsLog(container);
|
||||
}
|
||||
|
||||
function renderSettingsLog(container) {
|
||||
const log = loadSettingsLog();
|
||||
if (!log.length) {
|
||||
container.innerHTML = `<div class="empty">no activity yet</div>`;
|
||||
return;
|
||||
}
|
||||
container.innerHTML = log
|
||||
.map((entry) => {
|
||||
const when = fmtDateShort ? fmtDateShort(entry.at) : entry.at;
|
||||
return `<div class="freeday-line">
|
||||
<span class="name">${esc(entry.message)}</span>
|
||||
<span class="countdown">${esc(when)}</span>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function collectSettingsFromForm() {
|
||||
const get = (id) => $(`#${id}`);
|
||||
const checkbox = (id) => !!(get(id) && get(id).checked);
|
||||
const value = (id) => (get(id)?.value || "").trim();
|
||||
return {
|
||||
enabled: checkbox("set-enabled"),
|
||||
server: value("set-server") || "https://ntfy.sh",
|
||||
topic: value("set-topic"),
|
||||
token: value("set-token"),
|
||||
grades: checkbox("set-grades"),
|
||||
messages: checkbox("set-messages"),
|
||||
exams: checkbox("set-exams"),
|
||||
attendance: checkbox("set-attendance"),
|
||||
pollMinutes: Math.max(1, Number(value("set-poll")) || 5),
|
||||
};
|
||||
}
|
||||
|
||||
function fillSettingsForm(settings) {
|
||||
const set = (id, value) => {
|
||||
const element = $(`#${id}`);
|
||||
if (element) element.value = value;
|
||||
};
|
||||
const check = (id, value) => {
|
||||
const element = $(`#${id}`);
|
||||
if (element) element.checked = !!value;
|
||||
};
|
||||
const s = settings || defaultSettings();
|
||||
check("set-enabled", s.enabled);
|
||||
set("set-server", s.server);
|
||||
set("set-topic", s.topic);
|
||||
set("set-token", s.token);
|
||||
check("set-grades", s.grades);
|
||||
check("set-messages", s.messages);
|
||||
check("set-exams", s.exams);
|
||||
check("set-attendance", s.attendance);
|
||||
set("set-poll", s.pollMinutes);
|
||||
}
|
||||
|
||||
async function sendNtfy(settings, title, message, priority) {
|
||||
const cfg = settings || loadSettings();
|
||||
if (!cfg.enabled) throw new Error("NTFY is disabled in settings.");
|
||||
if (!cfg.topic) throw new Error("NTFY topic is empty.");
|
||||
const response = await api("/api/ntfy", {
|
||||
method: "POST",
|
||||
body: {
|
||||
server: cfg.server,
|
||||
topic: cfg.topic,
|
||||
token: cfg.token,
|
||||
title: title || "librus-tui",
|
||||
message: message || "",
|
||||
priority: priority || null,
|
||||
},
|
||||
});
|
||||
if (!response || !response.ok) throw new Error("NTFY push failed.");
|
||||
return true;
|
||||
}
|
||||
|
||||
async function renderSettings() {
|
||||
const settings = loadSettings();
|
||||
$("#view").innerHTML = `
|
||||
<div class="view-inner">
|
||||
<div class="split-2">
|
||||
<section class="panel">
|
||||
<div class="panel-title"><span>ntfy notifications</span><span class="title-note">push alerts</span></div>
|
||||
<div class="panel-body">
|
||||
<label class="field">
|
||||
<span class="field-label">enabled</span>
|
||||
<span class="field-input-wrap">
|
||||
<input id="set-enabled" type="checkbox">
|
||||
<span class="dim">send pushes when new items appear</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">server</span>
|
||||
<span class="field-input-wrap">
|
||||
<span class="field-prompt">></span>
|
||||
<input id="set-server" type="url" placeholder="https://ntfy.sh" spellcheck="false" autocapitalize="off">
|
||||
</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">topic</span>
|
||||
<span class="field-input-wrap">
|
||||
<span class="field-prompt">></span>
|
||||
<input id="set-topic" type="text" placeholder="librus-yourname" spellcheck="false" autocapitalize="off">
|
||||
</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">access token (optional)</span>
|
||||
<span class="field-input-wrap">
|
||||
<span class="field-prompt">></span>
|
||||
<input id="set-token" type="password" placeholder="tk_..." autocomplete="off">
|
||||
</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">poll interval (minutes)</span>
|
||||
<span class="field-input-wrap">
|
||||
<span class="field-prompt">></span>
|
||||
<input id="set-poll" type="number" min="1" max="60" step="1">
|
||||
</span>
|
||||
</label>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-primary" type="button" data-action="settings-save">[ save ]</button>
|
||||
<button class="btn" type="button" data-action="settings-test">[ test push ]</button>
|
||||
<button class="btn btn-warn" type="button" data-action="settings-clear">[ reset ]</button>
|
||||
</div>
|
||||
<div id="settings-status" class="dim" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-title"><span>notification categories</span><span class="title-note">what to push</span></div>
|
||||
<div class="panel-body">
|
||||
<label class="field"><span class="field-label">new grades</span><span class="field-input-wrap"><input id="set-grades" type="checkbox"><span class="dim">grade added or changed</span></span></label>
|
||||
<label class="field"><span class="field-label">new messages</span><span class="field-input-wrap"><input id="set-messages" type="checkbox"><span class="dim">inbox count increases</span></span></label>
|
||||
<label class="field"><span class="field-label">exams / homework</span><span class="field-input-wrap"><input id="set-exams" type="checkbox"><span class="dim">new tests or assignments</span></span></label>
|
||||
<label class="field"><span class="field-label">absences</span><span class="field-input-wrap"><input id="set-attendance" type="checkbox"><span class="dim">new absence entries</span></span></label>
|
||||
<p class="dim" style="margin-top:10px; font-size:10.5px; line-height:1.5;">
|
||||
Polling runs while this tab is open. The first poll only records the
|
||||
baseline and does not send pushes.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel span-2">
|
||||
<div class="panel-title"><span>activity log</span><span class="title-note">recent pushes</span></div>
|
||||
<div class="panel-body tight" id="settings-log"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
fillSettingsForm(settings);
|
||||
renderSettingsLog($("#settings-log"));
|
||||
const status = $("#settings-status");
|
||||
if (status) {
|
||||
status.textContent = settings.enabled && settings.topic
|
||||
? `enabled · topic ${settings.topic} · poll every ${settings.pollMinutes} min`
|
||||
: "disabled · configure a topic to enable pushes";
|
||||
}
|
||||
}
|
||||
|
||||
function stopNotificationPolling() {
|
||||
if (state.notificationPoller) {
|
||||
clearInterval(state.notificationPoller);
|
||||
state.notificationPoller = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startNotificationPolling() {
|
||||
stopNotificationPolling();
|
||||
const settings = loadSettings();
|
||||
state.lastNotifySnapshot = null;
|
||||
if (!settings.enabled) return;
|
||||
const minutes = Math.max(1, Number(settings.pollMinutes) || 5);
|
||||
pollNotifications();
|
||||
state.notificationPoller = setInterval(pollNotifications, minutes * 60 * 1000);
|
||||
}
|
||||
|
||||
async function pollNotifications() {
|
||||
if (!state.session || !state.session.authenticated) return;
|
||||
const settings = loadSettings();
|
||||
if (!settings.enabled) return;
|
||||
try {
|
||||
const notif = await api("/api/notifications");
|
||||
let messageCount = 0;
|
||||
if (settings.messages) {
|
||||
try {
|
||||
const messages = await api("/api/messages");
|
||||
messageCount = Array.isArray(messages)
|
||||
? messages.length
|
||||
: Array.isArray(messages?.items)
|
||||
? messages.items.length
|
||||
: 0;
|
||||
} catch (error) {
|
||||
messageCount = state.lastNotifySnapshot?.messages || 0;
|
||||
}
|
||||
} else if (state.lastNotifySnapshot) {
|
||||
messageCount = state.lastNotifySnapshot.messages;
|
||||
}
|
||||
|
||||
const snapshot = {
|
||||
grades: Number(notif?.grades) || 0,
|
||||
exams: Number(notif?.exams) || 0,
|
||||
absences: Number(notif?.absences) || 0,
|
||||
conferences: Number(notif?.conferences) || 0,
|
||||
messages: messageCount,
|
||||
};
|
||||
|
||||
if (state.lastNotifySnapshot) {
|
||||
const previous = state.lastNotifySnapshot;
|
||||
const pushes = [];
|
||||
if (settings.grades && snapshot.grades > previous.grades) {
|
||||
pushes.push({
|
||||
title: "New grades",
|
||||
message: `${snapshot.grades - previous.grades} new grade(s). Total: ${snapshot.grades}.`,
|
||||
priority: 3,
|
||||
});
|
||||
}
|
||||
if (settings.exams && snapshot.exams > previous.exams) {
|
||||
pushes.push({
|
||||
title: "New exams / homework",
|
||||
message: `${snapshot.exams - previous.exams} new item(s). Total: ${snapshot.exams}.`,
|
||||
priority: 4,
|
||||
});
|
||||
}
|
||||
if (settings.messages && snapshot.messages > previous.messages) {
|
||||
pushes.push({
|
||||
title: "New messages",
|
||||
message: `${snapshot.messages - previous.messages} new message(s). Total: ${snapshot.messages}.`,
|
||||
priority: 3,
|
||||
});
|
||||
}
|
||||
if (settings.attendance && snapshot.absences > previous.absences) {
|
||||
pushes.push({
|
||||
title: "New absence",
|
||||
message: `${snapshot.absences - previous.absences} new absence entry. Total: ${snapshot.absences}.`,
|
||||
priority: 4,
|
||||
});
|
||||
}
|
||||
|
||||
for (const push of pushes) {
|
||||
toast(`[ntfy] ${push.title}`, "ok", 2200);
|
||||
try {
|
||||
await sendNtfy(settings, push.title, push.message, push.priority);
|
||||
appendSettingsLog(`pushed: ${push.title}`);
|
||||
} catch (error) {
|
||||
appendSettingsLog(`push failed: ${push.title} — ${error.message}`);
|
||||
toast(`[ntfy fail] ${error.message}`, "warn");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.lastNotifySnapshot = snapshot;
|
||||
} catch (error) {
|
||||
/* polling failures are non-fatal */
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user