344 lines
12 KiB
JavaScript
344 lines
12 KiB
JavaScript
// ── State ────────────────────────────────────────────────────────────────────
|
|
let emoji = "💬";
|
|
let settings = { username: "", apiKey: "", theme: "auto", mastodonPost: false, showMastodon: true };
|
|
|
|
// ── DOM ──────────────────────────────────────────────────────────────────────
|
|
const $ = (id) => document.getElementById(id);
|
|
const emojiDisplay = $("emoji-display");
|
|
const emojiShow = $("emoji-show");
|
|
const emojiSection = emojiDisplay.closest(".emoji-section");
|
|
const statusText = $("status-text");
|
|
const statusUrl = $("status-url");
|
|
const charCount = $("char-count");
|
|
const postBtn = $("post-btn");
|
|
const toast = $("toast");
|
|
const settingsBtn = $("settings-btn");
|
|
const overlay = $("overlay");
|
|
const modalClose = $("modal-close");
|
|
const sUsername = $("s-username");
|
|
const sApikey = $("s-apikey");
|
|
const eyeBtn = $("eye-btn");
|
|
const saveBtn = $("save-btn");
|
|
const themeToggle = $("theme-toggle");
|
|
const mastodonVisToggle = $("mastodon-vis-toggle");
|
|
const mastodonToggle = $("mastodon-toggle");
|
|
const mastodonRow = mastodonToggle.closest(".field");
|
|
|
|
// ── Settings ─────────────────────────────────────────────────────────────────
|
|
function applyTheme(theme) {
|
|
if (theme === "auto") {
|
|
delete document.documentElement.dataset.theme;
|
|
} else {
|
|
document.documentElement.dataset.theme = theme;
|
|
}
|
|
const picker = document.getElementById("emoji-picker");
|
|
if (picker) {
|
|
picker.classList.remove("light", "dark");
|
|
if (theme !== "auto") picker.classList.add(theme);
|
|
}
|
|
}
|
|
|
|
function updateThemeToggle(theme) {
|
|
themeToggle.querySelectorAll(".theme-btn").forEach((btn) => {
|
|
const active = btn.dataset.theme === theme;
|
|
btn.classList.toggle("active", active);
|
|
btn.setAttribute("aria-pressed", active ? "true" : "false");
|
|
});
|
|
}
|
|
|
|
function applyMastodonVisibility(show) {
|
|
mastodonRow.style.display = show ? "" : "none";
|
|
}
|
|
|
|
function updateMastodonVisToggle(show) {
|
|
mastodonVisToggle.querySelectorAll(".theme-btn").forEach((btn) => {
|
|
const active = (btn.dataset.mastodonVis === "show") === show;
|
|
btn.classList.toggle("active", active);
|
|
btn.setAttribute("aria-pressed", active ? "true" : "false");
|
|
});
|
|
}
|
|
|
|
function loadSettings() {
|
|
try {
|
|
const raw = localStorage.getItem("spo-settings");
|
|
if (raw) settings = { ...settings, ...JSON.parse(raw) };
|
|
} catch {}
|
|
applyTheme(settings.theme);
|
|
applyMastodonVisibility(settings.showMastodon ?? true);
|
|
updateBadge();
|
|
mastodonToggle.checked = settings.mastodonPost ?? false;
|
|
}
|
|
|
|
function saveSettings() {
|
|
settings.username = sUsername.value.trim().replace(/^@/, "");
|
|
settings.apiKey = sApikey.value.trim();
|
|
try {
|
|
localStorage.setItem("spo-settings", JSON.stringify(settings));
|
|
} catch {
|
|
showToast("Could not save — storage unavailable", "error");
|
|
return;
|
|
}
|
|
updateBadge();
|
|
closeModal();
|
|
showToast("Settings saved", "success");
|
|
}
|
|
|
|
function updateBadge() {
|
|
settingsBtn.classList.toggle(
|
|
"needs-setup",
|
|
!settings.username || !settings.apiKey,
|
|
);
|
|
}
|
|
|
|
// ── Modal ────────────────────────────────────────────────────────────────────
|
|
function openModal() {
|
|
sUsername.value = settings.username;
|
|
sApikey.value = settings.apiKey;
|
|
updateThemeToggle(settings.theme);
|
|
updateMastodonVisToggle(settings.showMastodon ?? true);
|
|
overlay.classList.add("open");
|
|
document.body.style.overflow = "hidden";
|
|
// Focus first empty field
|
|
if (!settings.username) sUsername.focus();
|
|
else if (!settings.apiKey) sApikey.focus();
|
|
}
|
|
|
|
function closeModal() {
|
|
overlay.classList.remove("open");
|
|
document.body.style.overflow = "";
|
|
}
|
|
|
|
const privacyOverlay = $("privacy-overlay");
|
|
|
|
function openPrivacy() {
|
|
privacyOverlay.classList.add("open");
|
|
document.body.style.overflow = "hidden";
|
|
}
|
|
|
|
function closePrivacy() {
|
|
privacyOverlay.classList.remove("open");
|
|
document.body.style.overflow = "";
|
|
}
|
|
|
|
settingsBtn.addEventListener("click", openModal);
|
|
$("privacy-link").addEventListener("click", openPrivacy);
|
|
$("privacy-modal-close").addEventListener("click", closePrivacy);
|
|
privacyOverlay.addEventListener("click", (e) => {
|
|
if (e.target === privacyOverlay) closePrivacy();
|
|
});
|
|
modalClose.addEventListener("click", closeModal);
|
|
overlay.addEventListener("click", (e) => {
|
|
if (e.target === overlay) closeModal();
|
|
});
|
|
saveBtn.addEventListener("click", saveSettings);
|
|
|
|
mastodonToggle.addEventListener("change", () => {
|
|
settings.mastodonPost = mastodonToggle.checked;
|
|
try { localStorage.setItem("spo-settings", JSON.stringify(settings)); } catch {}
|
|
});
|
|
|
|
mastodonVisToggle.addEventListener("click", (e) => {
|
|
const btn = e.target.closest(".theme-btn");
|
|
if (!btn) return;
|
|
const show = btn.dataset.mastodonVis === "show";
|
|
settings.showMastodon = show;
|
|
applyMastodonVisibility(show);
|
|
updateMastodonVisToggle(show);
|
|
});
|
|
|
|
themeToggle.addEventListener("click", (e) => {
|
|
const btn = e.target.closest(".theme-btn");
|
|
if (!btn) return;
|
|
settings.theme = btn.dataset.theme;
|
|
applyTheme(settings.theme);
|
|
updateThemeToggle(settings.theme);
|
|
});
|
|
|
|
// Eye toggle for API key
|
|
eyeBtn.addEventListener("click", () => {
|
|
const show = sApikey.type === "password";
|
|
sApikey.type = show ? "text" : "password";
|
|
eyeBtn.textContent = show ? "🙈" : "👁";
|
|
eyeBtn.setAttribute("aria-label", show ? "Hide API key" : "Show API key");
|
|
});
|
|
|
|
// ── Emoji picker ─────────────────────────────────────────────────────────────
|
|
const emojiPicker = $("emoji-picker");
|
|
|
|
function openEmojiPicker() {
|
|
emojiSection.classList.add("picking");
|
|
emojiDisplay.setAttribute("aria-expanded", "true");
|
|
}
|
|
|
|
function closeEmojiPicker() {
|
|
emojiSection.classList.remove("picking");
|
|
emojiDisplay.setAttribute("aria-expanded", "false");
|
|
}
|
|
|
|
emojiDisplay.addEventListener("click", () => {
|
|
if (emojiSection.classList.contains("picking")) closeEmojiPicker();
|
|
else openEmojiPicker();
|
|
});
|
|
|
|
emojiDisplay.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
if (emojiSection.classList.contains("picking")) closeEmojiPicker();
|
|
else openEmojiPicker();
|
|
}
|
|
});
|
|
|
|
emojiPicker.addEventListener("emoji-click", (e) => {
|
|
emoji = e.detail.unicode;
|
|
emojiShow.textContent = emoji;
|
|
closeEmojiPicker();
|
|
});
|
|
|
|
document.addEventListener("click", (e) => {
|
|
if (emojiSection.classList.contains("picking") && !emojiSection.contains(e.target)) {
|
|
closeEmojiPicker();
|
|
}
|
|
});
|
|
|
|
// ── Keyboard shortcuts ────────────────────────────────────────────────────────
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") {
|
|
if (emojiSection.classList.contains("picking")) closeEmojiPicker();
|
|
else if (overlay.classList.contains("open")) closeModal();
|
|
else if (privacyOverlay.classList.contains("open")) closePrivacy();
|
|
}
|
|
});
|
|
|
|
// ── Character count ───────────────────────────────────────────────────────────
|
|
statusText.addEventListener("input", () => {
|
|
const n = statusText.value.length;
|
|
charCount.textContent = `${n} / 5000`;
|
|
charCount.classList.toggle("warn", n > 4800);
|
|
});
|
|
|
|
// ── Post status ───────────────────────────────────────────────────────────────
|
|
async function postStatus() {
|
|
if (!settings.username || !settings.apiKey) {
|
|
showToast("Add your credentials in settings first", "error");
|
|
openModal();
|
|
return;
|
|
}
|
|
|
|
const content = statusText.value.trim();
|
|
if (!content) {
|
|
showToast("Status text is required", "error");
|
|
statusText.focus();
|
|
return;
|
|
}
|
|
|
|
postBtn.classList.add("loading");
|
|
postBtn.disabled = true;
|
|
|
|
const body = { emoji, content };
|
|
const url = statusUrl.value.trim();
|
|
if (url) body.external_url = url;
|
|
if (!mastodonToggle.checked) body.skip_mastodon_post = true;
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`https://api.omg.lol/address/${encodeURIComponent(settings.username)}/statuses/`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${settings.apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(body),
|
|
},
|
|
);
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
if (res.ok || data?.request?.status_code === 200) {
|
|
const statusLink = data?.response?.url ?? null;
|
|
const msg = statusLink ? "Status posted · View ↗" : "Status posted";
|
|
showToast(msg, "success", statusLink);
|
|
statusText.value = "";
|
|
statusUrl.value = "";
|
|
charCount.textContent = "0 / 5000";
|
|
charCount.classList.remove("warn");
|
|
emoji = "📝";
|
|
emojiShow.textContent = "📝";
|
|
} else {
|
|
const msg = data?.response?.message ?? `Error ${res.status}`;
|
|
showToast(msg, "error");
|
|
}
|
|
} catch (err) {
|
|
const msg =
|
|
err?.message === "Failed to fetch"
|
|
? "Network error — check your connection"
|
|
: (err?.message ?? "Something went wrong");
|
|
showToast(msg, "error");
|
|
} finally {
|
|
postBtn.classList.remove("loading");
|
|
postBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
postBtn.addEventListener("click", postStatus);
|
|
|
|
// Ctrl/Cmd + Enter to post from textarea
|
|
statusText.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) postStatus();
|
|
});
|
|
|
|
// ── Toast ────────────────────────────────────────────────────────────────────
|
|
let toastTimer;
|
|
function showToast(msg, type = "", url = null) {
|
|
clearTimeout(toastTimer);
|
|
toast.textContent = msg;
|
|
toast.onclick = url ? () => window.open(url, "_blank", "noopener") : null;
|
|
toast.className = `show ${type}${url ? " clickable" : ""}`.trim();
|
|
toastTimer = setTimeout(() => {
|
|
toast.className = "";
|
|
toast.onclick = null;
|
|
}, 4000);
|
|
}
|
|
|
|
// ── Service worker ────────────────────────────────────────────────────────────
|
|
if ("serviceWorker" in navigator) {
|
|
navigator.serviceWorker.register("sw.js").catch(() => {});
|
|
}
|
|
|
|
// ── Install banner ────────────────────────────────────────────────────────────
|
|
(function () {
|
|
const isPWA =
|
|
window.matchMedia("(display-mode: standalone)").matches ||
|
|
navigator.standalone === true;
|
|
if (isPWA) return;
|
|
|
|
try {
|
|
if (localStorage.getItem("spo-install-dismissed")) return;
|
|
} catch {}
|
|
|
|
const ua = navigator.userAgent;
|
|
let msg = null;
|
|
if (/iPad|iPhone|iPod/.test(ua)) {
|
|
msg = 'Tap Share ↑ at the bottom of Safari, then "Add to Home Screen" to install this app.';
|
|
} else if (/Android/.test(ua)) {
|
|
msg = 'Tap the ⋮ menu in Chrome, then "Add to Home Screen" to install this app.';
|
|
}
|
|
if (!msg) return;
|
|
|
|
const banner = $("install-banner");
|
|
$("install-banner-text").textContent = msg;
|
|
banner.classList.add("visible");
|
|
|
|
$("install-banner-close").addEventListener("click", () => {
|
|
banner.classList.remove("visible");
|
|
try { localStorage.setItem("spo-install-dismissed", "1"); } catch {}
|
|
});
|
|
})();
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
loadSettings();
|
|
|
|
// Auto-open settings if no credentials saved yet
|
|
if (!settings.username || !settings.apiKey) {
|
|
setTimeout(openModal, 500);
|
|
}
|