// Vanilla JS frontend for the Notify Tauri desktop app
(function () {
"use strict";
// Elements
const toastContainer = document.getElementById("toast-container");
const profileBadge = document.getElementById("profile-badge");
const serverStatusLabel = document.getElementById("server-status-label");
const settingsModal = document.getElementById("settings-modal");
const openSettingsBtn = document.getElementById("open-settings-btn");
const closeModalX = document.getElementById("close-modal-x");
const profilesList = document.getElementById("profiles-list");
const btnHeart = document.getElementById("btn-heart");
const btnCat = document.getElementById("btn-cat");
const btnEmergency = document.getElementById("btn-emergency");
const customForm = document.getElementById("custom-message-form");
const customInput = document.getElementById("custom-message-input");
const btnCustomSend = document.getElementById("btn-custom-send");
const configForm = document.getElementById("config-form");
const cfgProfileName = document.getElementById("cfg-profile-name");
const cfgServerUrl = document.getElementById("cfg-server-url");
const cfgServerPin = document.getElementById("cfg-server-pin");
const btnTestConn = document.getElementById("btn-test-conn");
const connTestResult = document.getElementById("conn-test-result");
let currentConfig = null;
let isSending = false;
const sendFeedbackTimeouts = new WeakMap();
// Invoke helper for Tauri v2
async function invokeTauri(cmd, args = {}) {
if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
return window.__TAURI__.core.invoke(cmd, args);
}
throw new Error("Tauri core API is unavailable in this environment");
}
// Toast notification
function showToast(message, type = "info") {
const alert = document.createElement("div");
const alertTypeClass =
type === "success"
? "alert-success"
: type === "error"
? "alert-error"
: "alert-info";
alert.className = `alert ${alertTypeClass} shadow-md text-xs py-2 px-3 flex items-center gap-2 pointer-events-auto transition-all`;
const icon =
type === "success"
? `<svg xmlns="http://www.w3.org/2000/svg" class="size-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>`
: type === "error"
? `<svg xmlns="http://www.w3.org/2000/svg" class="size-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>`
: `<svg xmlns="http://www.w3.org/2000/svg" class="size-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>`;
alert.innerHTML = `${icon}<span>${escapeHtml(message)}</span>`;
toastContainer.appendChild(alert);
setTimeout(() => {
alert.style.opacity = "0";
setTimeout(() => alert.remove(), 200);
}, 3200);
}
function escapeHtml(str) {
if (!str) return "";
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function setButtonLoading(btn, loading) {
if (!btn) return;
const spinner = btn.querySelector(".loading");
const label = btn.querySelector(".btn-label");
if (loading) {
btn.disabled = true;
if (spinner) spinner.classList.remove("hidden");
if (label) label.classList.add("opacity-50");
} else {
btn.disabled = false;
if (spinner) spinner.classList.add("hidden");
if (label) label.classList.remove("opacity-50");
}
}
function showSendFeedback(kind) {
const feedback = document.getElementById(
kind === "notification" ? "custom-send-feedback" : "quick-send-feedback"
);
if (!feedback) return;
clearTimeout(sendFeedbackTimeouts.get(feedback));
feedback.textContent = "Sent ✓";
feedback.classList.remove("send-feedback-visible", "send-feedback-error");
void feedback.offsetWidth;
feedback.classList.add("send-feedback-visible");
sendFeedbackTimeouts.set(feedback, setTimeout(() => {
feedback.textContent = "";
feedback.classList.remove("send-feedback-visible");
}, 1800));
}
// Render configuration & profiles list
function renderConfig(state) {
currentConfig = state;
const activeName = state.active_profile_name || (state.active_server && state.active_server.name) || "default";
profileBadge.textContent = activeName;
const srv = state.active_server || {};
const urlDisplay = srv.url || "No server configured";
serverStatusLabel.textContent = urlDisplay;
serverStatusLabel.title = `Server: ${urlDisplay}\nProfile: ${activeName}\nConfig Dir: ${state.config_dir}`;
// Fill Modal form
cfgProfileName.value = activeName;
cfgServerUrl.value = srv.url || "";
cfgServerPin.value = srv.pin || "";
// Render Profiles list (from Omarchy ~/.config/notify)
profilesList.innerHTML = "";
if (!state.profiles || state.profiles.length === 0) {
const emptyItem = document.createElement("div");
emptyItem.className = "text-xs text-base-content/60 p-2 text-center";
emptyItem.textContent = "No profiles found in ~/.config/notify. Create one below.";
profilesList.appendChild(emptyItem);
} else {
state.profiles.forEach((p) => {
const item = document.createElement("button");
item.type = "button";
const isActive = p.name === state.active_profile_name;
item.className = `btn btn-xs justify-between w-full font-normal ${
isActive ? "btn-neutral" : "btn-ghost"
}`;
const srvInfo = p.servers && p.servers[0] ? p.servers[0].url : "unspecified";
item.innerHTML = `
<span class="font-medium">${escapeHtml(p.name)}</span>
<span class="text-xs opacity-60 truncate max-w-xs">${escapeHtml(srvInfo)}</span>
${isActive ? '<span class="badge badge-xs badge-success">active</span>' : ""}
`;
item.addEventListener("click", async () => {
try {
const updated = await invokeTauri("set_active_profile", { name: p.name });
renderConfig(updated);
showToast(`Switched to profile: ${p.name}`, "success");
testCurrentConnection(false);
} catch (e) {
showToast(`Failed to switch profile: ${e}`, "error");
}
});
profilesList.appendChild(item);
});
}
}
async function loadConfig() {
try {
const state = await invokeTauri("get_config");
renderConfig(state);
testCurrentConnection(false);
} catch (e) {
console.error("Failed to load config:", e);
serverStatusLabel.textContent = "Failed to load config";
showToast("Could not load ~/.config/notify config", "error");
}
}
async function testCurrentConnection(silentOnSuccess = true) {
try {
const res = await invokeTauri("test_connection", { targetUrl: null, targetPin: null });
if (res && res.ok) {
serverStatusLabel.innerHTML = `<span class="text-success font-semibold">●</span> ${escapeHtml(currentConfig.active_server.url)}`;
if (!silentOnSuccess) {
showToast("Connected to server successfully", "success");
}
} else {
const errMsg = res ? res.message : "Connection failed";
serverStatusLabel.innerHTML = `<span class="text-error font-semibold">●</span> ${escapeHtml(currentConfig.active_server.url)} (${escapeHtml(errMsg)})`;
if (!silentOnSuccess) {
showToast(errMsg, "error");
}
}
} catch (e) {
serverStatusLabel.innerHTML = `<span class="text-error font-semibold">●</span> Offline / unreachable`;
if (!silentOnSuccess) {
showToast(String(e), "error");
}
}
}
// Send notification action
async function handleSend(kind, message, btnEl) {
if (isSending) return;
isSending = true;
setButtonLoading(btnEl, true);
try {
const result = await invokeTauri("send_notification", {
kind: kind || null,
message: message || null,
targetUrl: null,
targetPin: null,
});
if (result && result.success) {
showSendFeedback(kind);
if (customInput && kind === "notification") {
customInput.value = "";
}
} else {
showToast("Unexpected response from server", "error");
}
} catch (err) {
console.error("Failed to send notification:", err);
showToast(String(err || "Failed to send notification"), "error");
} finally {
setButtonLoading(btnEl, false);
isSending = false;
}
}
// Event Listeners
btnHeart.addEventListener("click", () => {
handleSend("heart", "Look at your phone", btnHeart);
});
btnCat.addEventListener("click", () => {
handleSend("cat", "Missing you", btnCat);
});
btnEmergency.addEventListener("click", () => {
handleSend("emergency", "Emergency", btnEmergency);
});
customForm.addEventListener("submit", (e) => {
e.preventDefault();
const val = customInput.value.trim();
if (!val) {
customInput.focus();
return;
}
handleSend("notification", val, btnCustomSend);
});
// Modal open / close
function openModal() {
connTestResult.classList.add("hidden");
settingsModal.showModal();
}
function closeModal() {
settingsModal.close();
}
openSettingsBtn.addEventListener("click", openModal);
profileBadge.addEventListener("click", openModal);
serverStatusLabel.addEventListener("click", openModal);
closeModalX.addEventListener("click", closeModal);
// Test connection button inside modal
btnTestConn.addEventListener("click", async () => {
setButtonLoading(btnTestConn, true);
connTestResult.className = "text-xs p-2 rounded hidden";
const url = cfgServerUrl.value.trim();
const pin = cfgServerPin.value.trim();
try {
const res = await invokeTauri("test_connection", { targetUrl: url, targetPin: pin });
connTestResult.classList.remove("hidden");
if (res && res.ok) {
connTestResult.className = "text-xs p-2 rounded bg-success/20 text-success border border-success/30";
connTestResult.textContent = `✓ ${res.message}`;
} else {
connTestResult.className = "text-xs p-2 rounded bg-error/20 text-error border border-error/30";
connTestResult.textContent = `✕ ${res ? res.message : "Connection failed"}`;
}
} catch (e) {
connTestResult.classList.remove("hidden");
connTestResult.className = "text-xs p-2 rounded bg-error/20 text-error border border-error/30";
connTestResult.textContent = `✕ ${String(e)}`;
} finally {
setButtonLoading(btnTestConn, false);
}
});
// Save config form inside modal
configForm.addEventListener("submit", async (e) => {
e.preventDefault();
const name = cfgProfileName.value.trim() || "notify";
const url = cfgServerUrl.value.trim();
const pin = cfgServerPin.value.trim();
if (!url) {
showToast("Server URL is required", "error");
cfgServerUrl.focus();
return;
}
try {
const updated = await invokeTauri("save_server_config", {
name,
url,
pin,
makeActive: true,
});
renderConfig(updated);
showToast(`Profile '${name}' saved and activated!`, "success");
closeModal();
testCurrentConnection(false);
} catch (err) {
showToast(`Failed to save config: ${err}`, "error");
}
});
// Initialize on page load
window.addEventListener("DOMContentLoaded", loadConfig);
})();