Testing PWA install prompt

This commit is contained in:
Dan Baker 2026-02-21 21:28:33 +00:00
parent aca0d2dc4f
commit c115121aa7
3 changed files with 127 additions and 1 deletions

View file

@ -49,6 +49,13 @@
</header> </header>
<main> <main>
<!-- Install prompt -->
<div class="install-banner" id="install-banner">
<span class="install-banner-icon">📲</span>
<span id="install-banner-text"></span>
<button class="install-banner-close" id="install-banner-close" aria-label="Dismiss"></button>
</div>
<!-- Emoji picker --> <!-- Emoji picker -->
<div class="emoji-section"> <div class="emoji-section">
<div <div
@ -244,6 +251,33 @@
</div> </div>
</div> </div>
<div class="field">
<label>Display Mastodon option</label>
<div
class="theme-toggle"
id="mastodon-vis-toggle"
role="group"
aria-label="Show or hide the Mastodon option"
>
<button
class="theme-btn"
data-mastodon-vis="show"
type="button"
aria-pressed="true"
>
Show
</button>
<button
class="theme-btn"
data-mastodon-vis="hide"
type="button"
aria-pressed="false"
>
Hide
</button>
</div>
</div>
<div class="field"> <div class="field">
<label>Appearance</label> <label>Appearance</label>
<div <div

View file

@ -1,6 +1,6 @@
// ── State ──────────────────────────────────────────────────────────────────── // ── State ────────────────────────────────────────────────────────────────────
let emoji = "💬"; let emoji = "💬";
let settings = { username: "", apiKey: "", theme: "auto", mastodonPost: false }; let settings = { username: "", apiKey: "", theme: "auto", mastodonPost: false, showMastodon: true };
// ── DOM ────────────────────────────────────────────────────────────────────── // ── DOM ──────────────────────────────────────────────────────────────────────
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@ -20,7 +20,9 @@ const sApikey = $("s-apikey");
const eyeBtn = $("eye-btn"); const eyeBtn = $("eye-btn");
const saveBtn = $("save-btn"); const saveBtn = $("save-btn");
const themeToggle = $("theme-toggle"); const themeToggle = $("theme-toggle");
const mastodonVisToggle = $("mastodon-vis-toggle");
const mastodonToggle = $("mastodon-toggle"); const mastodonToggle = $("mastodon-toggle");
const mastodonRow = mastodonToggle.closest(".field");
// ── Settings ───────────────────────────────────────────────────────────────── // ── Settings ─────────────────────────────────────────────────────────────────
function applyTheme(theme) { function applyTheme(theme) {
@ -44,12 +46,25 @@ function updateThemeToggle(theme) {
}); });
} }
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() { function loadSettings() {
try { try {
const raw = localStorage.getItem("spo-settings"); const raw = localStorage.getItem("spo-settings");
if (raw) settings = { ...settings, ...JSON.parse(raw) }; if (raw) settings = { ...settings, ...JSON.parse(raw) };
} catch {} } catch {}
applyTheme(settings.theme); applyTheme(settings.theme);
applyMastodonVisibility(settings.showMastodon ?? true);
updateBadge(); updateBadge();
mastodonToggle.checked = settings.mastodonPost ?? false; mastodonToggle.checked = settings.mastodonPost ?? false;
} }
@ -80,6 +95,7 @@ function openModal() {
sUsername.value = settings.username; sUsername.value = settings.username;
sApikey.value = settings.apiKey; sApikey.value = settings.apiKey;
updateThemeToggle(settings.theme); updateThemeToggle(settings.theme);
updateMastodonVisToggle(settings.showMastodon ?? true);
overlay.classList.add("open"); overlay.classList.add("open");
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
// Focus first empty field // Focus first empty field
@ -121,6 +137,15 @@ mastodonToggle.addEventListener("change", () => {
try { localStorage.setItem("spo-settings", JSON.stringify(settings)); } catch {} 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) => { themeToggle.addEventListener("click", (e) => {
const btn = e.target.closest(".theme-btn"); const btn = e.target.closest(".theme-btn");
if (!btn) return; if (!btn) return;
@ -280,6 +305,36 @@ if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").catch(() => {}); 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 ────────────────────────────────────────────────────────────────────── // ── Init ──────────────────────────────────────────────────────────────────────
loadSettings(); loadSettings();

View file

@ -608,6 +608,43 @@ input::placeholder {
opacity: 0.8; opacity: 0.8;
} }
/* ── Install banner ────────────────────────────────────────────────────── */
.install-banner {
display: none;
gap: 10px;
align-items: flex-start;
background: var(--accent-dim);
border: 1.5px solid color-mix(in srgb, var(--accent) 30%, transparent);
border-radius: var(--r);
padding: 12px 14px;
font-size: 0.875rem;
color: var(--text);
line-height: 1.5;
}
.install-banner.visible {
display: flex;
}
.install-banner-icon {
font-size: 1.1rem;
flex-shrink: 0;
margin-top: 1px;
}
.install-banner-close {
background: none;
border: none;
cursor: pointer;
color: var(--muted);
font-size: 0.8rem;
padding: 2px 0 0;
margin-left: auto;
flex-shrink: 0;
line-height: 1;
-webkit-tap-highlight-color: transparent;
}
/* ── Theme segmented control ───────────────────────────────────────────── */ /* ── Theme segmented control ───────────────────────────────────────────── */
.theme-toggle { .theme-toggle {
display: flex; display: flex;