475 lines
16 KiB
HTML
475 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
<meta name="theme-color" content="#0d0d0f">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="apple-mobile-web-app-title" content="NotifyPulse">
|
|
<link rel="apple-touch-icon" href="icon-192.png">
|
|
<link rel="manifest" href="/pwa/manifest.json">
|
|
<title>NotifyPulse</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d0d0f;
|
|
--card: #16161a;
|
|
--border: #2a2a32;
|
|
--accent: #4f8ef7;
|
|
--accent2: #7c5cfc;
|
|
--text: #e2e2e8;
|
|
--muted: #6b6b7b;
|
|
--green: #22c55e;
|
|
--red: #ef4444;
|
|
--yellow: #f59e0b;
|
|
--mono: 'Cascadia Code','Consolas',monospace;
|
|
--safe-top: env(safe-area-inset-top, 0px);
|
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
|
html { height: 100%; background: var(--bg); }
|
|
body {
|
|
min-height: 100%; min-height: -webkit-fill-available;
|
|
background: var(--bg); color: var(--text);
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
padding: calc(var(--safe-top) + 18px) 16px calc(var(--safe-bottom) + 20px);
|
|
display: flex; flex-direction: column; gap: 14px;
|
|
}
|
|
|
|
/* Header */
|
|
header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 4px 0 8px;
|
|
}
|
|
header h1 { font-size: 1.2rem; font-weight: 700; letter-spacing: .03em; color: var(--accent); }
|
|
.conn-badge {
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 5px 12px; border-radius: 999px; font-size: .72rem; font-weight: 600;
|
|
border: 1px solid var(--border); background: var(--card); color: var(--muted);
|
|
transition: .3s;
|
|
}
|
|
.conn-badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); transition: .3s; }
|
|
.conn-badge.ok { border-color: rgba(34,197,94,.35); color: var(--green); }
|
|
.conn-badge.ok .dot { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
.conn-badge.err { border-color: rgba(239,68,68,.35); color: var(--red); }
|
|
.conn-badge.err .dot { background: var(--red); }
|
|
|
|
/* Cards */
|
|
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
|
|
.card h2 {
|
|
font-size: .68rem; text-transform: uppercase; letter-spacing: .1em;
|
|
color: var(--muted); margin-bottom: 12px;
|
|
}
|
|
|
|
/* Stat grid */
|
|
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
.stat { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 11px; }
|
|
.stat-label { font-size: 9px; color: var(--muted); font-family: var(--mono); letter-spacing: .07em; text-transform: uppercase; margin-bottom: 4px; }
|
|
.stat-val { font-size: 1rem; font-weight: 700; color: var(--text); }
|
|
.stat-val.green { color: var(--green); }
|
|
.stat-val.red { color: var(--red); }
|
|
.stat-val.yellow { color: var(--yellow); }
|
|
.stat-val.accent { color: var(--accent); }
|
|
|
|
/* Wallpaper tabs */
|
|
.wp-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
|
|
.wp-tab {
|
|
flex: 1; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border);
|
|
background: transparent; color: var(--muted); font-size: .78rem; font-weight: 600;
|
|
cursor: pointer; transition: .15s; text-align: center; font-family: inherit;
|
|
}
|
|
.wp-tab.active { background: rgba(79,142,247,.12); border-color: var(--accent); color: var(--accent); }
|
|
|
|
/* Wallpaper preview */
|
|
.wp-frame {
|
|
border-radius: 10px; overflow: hidden; background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
aspect-ratio: 9/16; max-height: 300px;
|
|
display: flex; align-items: center; justify-content: center; position: relative;
|
|
}
|
|
.wp-frame img { width: 100%; height: 100%; object-fit: cover; transition: opacity .4s; }
|
|
.wp-placeholder {
|
|
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
color: var(--muted); font-size: .75rem;
|
|
}
|
|
.wp-placeholder svg { opacity: .4; }
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
width: 100%; padding: 11px 14px; border: none; border-radius: 9px; cursor: pointer;
|
|
font-size: .85rem; font-weight: 600; font-family: inherit;
|
|
display: flex; align-items: center; justify-content: center; gap: 7px;
|
|
transition: opacity .15s, transform .1s;
|
|
}
|
|
.btn:active { opacity: .82; transform: scale(.98); }
|
|
.btn-primary { background: var(--accent); color: #fff; }
|
|
.btn-purple { background: var(--accent2); color: #fff; }
|
|
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
|
.btn-green { background: rgba(34,197,94,.12); border: 1px solid rgba(34,197,94,.3); color: var(--green); }
|
|
.btn-gap { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
|
|
.btn-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
|
|
/* Install hint */
|
|
.install-hint {
|
|
background: rgba(79,142,247,.07); border: 1px solid rgba(79,142,247,.2);
|
|
border-radius: 10px; padding: 12px 14px; font-size: .78rem; line-height: 1.6; color: var(--muted);
|
|
}
|
|
.install-hint strong { color: var(--text); }
|
|
|
|
/* Toast */
|
|
.toast {
|
|
position: fixed; bottom: calc(var(--safe-bottom) + 20px); left: 50%;
|
|
transform: translateX(-50%) translateY(70px);
|
|
background: var(--card); border: 1px solid var(--border); border-radius: 9px;
|
|
padding: 9px 16px; font-size: .8rem; white-space: nowrap;
|
|
transition: .3s cubic-bezier(.34,1.56,.64,1); pointer-events: none; z-index: 999;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,.5);
|
|
}
|
|
.toast.show { transform: translateX(-50%) translateY(0); }
|
|
.toast.ok { border-color: rgba(34,197,94,.4); color: var(--green); }
|
|
.toast.err { border-color: rgba(239,68,68,.4); color: var(--red); }
|
|
|
|
::-webkit-scrollbar { width: 4px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
.splash-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, rgba(13,13,15,0.96), rgba(22,22,26,0.95));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: calc(var(--safe-top) + 20px) 16px calc(var(--safe-bottom) + 20px);
|
|
z-index: 9999;
|
|
transition: opacity .75s ease;
|
|
}
|
|
.splash-overlay.hidden {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
.splash-overlay img {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
opacity: 0;
|
|
transition: opacity .6s ease;
|
|
}
|
|
.splash-overlay.loaded img {
|
|
opacity: 1;
|
|
}
|
|
.splash-credit {
|
|
position: relative;
|
|
z-index: 2;
|
|
font-size: .75rem;
|
|
letter-spacing: .25em;
|
|
text-transform: uppercase;
|
|
color: rgba(255,255,255,.85);
|
|
border: 1px solid rgba(255,255,255,.25);
|
|
padding: 6px 12px;
|
|
border-radius: 999px;
|
|
background: rgba(0,0,0,.35);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="splash-overlay" id="splash">
|
|
<img id="splash-img" alt="NotifyPulse splash">
|
|
<div class="splash-credit" id="splash-credit">NotifyPulse</div>
|
|
</div>
|
|
|
|
<header>
|
|
<h1 id="app-title">NotifyPulse</h1>
|
|
<div class="conn-badge" id="conn-badge">
|
|
<div class="dot"></div>
|
|
<span id="conn-label">Connecting…</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Status -->
|
|
<div class="card">
|
|
<h2>Status</h2>
|
|
<div class="stat-grid">
|
|
<div class="stat">
|
|
<div class="stat-label">PC Connection</div>
|
|
<div class="stat-val green" id="stat-conn">—</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-label">Last Heartbeat</div>
|
|
<div class="stat-val accent" id="stat-hb">—</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-label">WPs Received</div>
|
|
<div class="stat-val" id="stat-wpcount">0</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-label">App Mode</div>
|
|
<div class="stat-val yellow" id="stat-mode">Browser</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Wallpapers -->
|
|
<div class="card">
|
|
<h2>Wallpapers</h2>
|
|
<div class="wp-tabs">
|
|
<button class="wp-tab active" id="tab-ls" onclick="switchTab('lockscreen')">🔒 Lock Screen</button>
|
|
<button class="wp-tab" id="tab-bg" onclick="switchTab('background')">🏠 Home Screen</button>
|
|
</div>
|
|
<div class="wp-frame" id="wp-frame">
|
|
<div class="wp-placeholder" id="wp-ph">
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
|
<span>Waiting for wallpaper…</span>
|
|
</div>
|
|
<img id="wp-img" style="display:none;opacity:0" alt="Wallpaper">
|
|
</div>
|
|
<div class="btn-gap">
|
|
<button class="btn btn-primary" id="save-btn" onclick="saveWallpaper()" disabled>
|
|
↓ Save to Photos
|
|
</button>
|
|
<div class="btn-row2">
|
|
<button class="btn btn-outline" onclick="requestBothWallpapers()">↺ Request Both</button>
|
|
<button class="btn btn-outline" onclick="requestActiveWallpaper()">↺ Request This</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- iOS install hint -->
|
|
<div class="install-hint" id="install-hint" style="display:none">
|
|
<strong>Install as App (iOS)</strong><br>
|
|
Tap <strong>Share</strong> → <strong>"Add to Home Screen"</strong> for the best experience and background wallpaper saving.
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
const CLIENT_ID = (() => {
|
|
let id = localStorage.getItem('np_cid');
|
|
if (!id) { id = 'pwa_' + Math.random().toString(36).slice(2,10); localStorage.setItem('np_cid', id); }
|
|
return id;
|
|
})();
|
|
|
|
const BASE = window.location.origin;
|
|
const APP_NAME_KEY = 'np_app_name';
|
|
const splashEl = document.getElementById('splash');
|
|
const splashImg = document.getElementById('splash-img');
|
|
let splashTimeout;
|
|
|
|
function applyAppName(name) {
|
|
if (!name || !name.trim()) return;
|
|
const appName = name.trim();
|
|
document.getElementById('app-title').textContent = appName;
|
|
document.title = appName;
|
|
const splashCredit = document.getElementById('splash-credit');
|
|
if (splashCredit) splashCredit.textContent = appName;
|
|
if (splashImg) splashImg.alt = `${appName} splash`;
|
|
const appleTitle = document.querySelector('meta[name="apple-mobile-web-app-title"]');
|
|
if (appleTitle) appleTitle.setAttribute('content', appName);
|
|
try { localStorage.setItem(APP_NAME_KEY, appName); } catch(e) {}
|
|
}
|
|
|
|
function applyCachedAppName() {
|
|
try {
|
|
const cachedName = localStorage.getItem(APP_NAME_KEY);
|
|
if (cachedName) applyAppName(cachedName);
|
|
} catch(e) {}
|
|
}
|
|
|
|
function hideSplash(delay = 2000) {
|
|
if (!splashEl) return;
|
|
clearTimeout(splashTimeout);
|
|
splashTimeout = setTimeout(() => {
|
|
splashEl.classList.add('hidden');
|
|
}, delay);
|
|
}
|
|
|
|
async function loadSplashImage() {
|
|
if (!splashEl || !splashImg) { hideSplash(0); return; }
|
|
try {
|
|
const resp = await fetch(`${BASE}/api/pwa/splash_image`);
|
|
const data = await resp.json();
|
|
if (data.image) {
|
|
splashImg.src = data.image;
|
|
splashEl.classList.add('loaded');
|
|
}
|
|
} catch (e) {
|
|
console.warn('Splash fetch failed', e);
|
|
}
|
|
hideSplash();
|
|
}
|
|
|
|
if (splashEl) {
|
|
splashEl.addEventListener('transitionend', () => {
|
|
if (splashEl.classList.contains('hidden')) {
|
|
splashEl.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
let connected = false;
|
|
let activeTab = 'lockscreen';
|
|
let wallpapers = { lockscreen: null, background: null };
|
|
let wpCount = parseInt(localStorage.getItem('np_wpc') || '0');
|
|
|
|
// Restore saved wallpapers
|
|
try {
|
|
const saved = JSON.parse(localStorage.getItem('np_wps') || '{}');
|
|
if (saved.lockscreen) wallpapers.lockscreen = saved.lockscreen;
|
|
if (saved.background) wallpapers.background = saved.background;
|
|
} catch(e) {}
|
|
|
|
// Service worker
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('/pwa/sw.js', { scope: '/pwa/' }).catch(e => console.warn('SW:', e));
|
|
}
|
|
|
|
// iOS / standalone detection
|
|
const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent);
|
|
const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches;
|
|
|
|
document.getElementById('stat-mode').textContent = isStandalone ? 'Installed PWA' : 'Browser';
|
|
document.getElementById('stat-mode').className = 'stat-val ' + (isStandalone ? 'green' : 'yellow');
|
|
if (isIOS && !isStandalone) document.getElementById('install-hint').style.display = 'block';
|
|
|
|
// Fetch app name and sync
|
|
async function fetchAppName() {
|
|
try {
|
|
const r = await fetch(BASE + '/api/pwa/app_name', { cache: 'no-store' });
|
|
const d = await r.json();
|
|
if (d.app_name) {
|
|
applyAppName(d.app_name);
|
|
return;
|
|
}
|
|
} catch(e) {}
|
|
|
|
try {
|
|
const r = await fetch(BASE + '/api/state', { cache: 'no-store' });
|
|
const d = await r.json();
|
|
if (d.app_name) {
|
|
applyAppName(d.app_name);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
// Ping / heartbeat
|
|
async function ping() {
|
|
try {
|
|
const r = await fetch(BASE + '/api/pwa/ping', {
|
|
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
});
|
|
const d = await r.json();
|
|
if (d.ok) {
|
|
setConn(true);
|
|
const t = new Date().toLocaleTimeString();
|
|
document.getElementById('stat-hb').textContent = t;
|
|
}
|
|
} catch(e) { setConn(false); }
|
|
}
|
|
|
|
function setConn(v) {
|
|
connected = v;
|
|
const badge = document.getElementById('conn-badge');
|
|
badge.className = 'conn-badge ' + (v ? 'ok' : 'err');
|
|
document.getElementById('conn-label').textContent = v ? 'Connected' : 'Offline';
|
|
document.getElementById('stat-conn').textContent = v ? 'Connected ✓' : 'Disconnected';
|
|
document.getElementById('stat-conn').className = 'stat-val ' + (v ? 'green' : 'red');
|
|
}
|
|
|
|
// Wallpaper poll
|
|
async function pollWallpaper() {
|
|
if (!connected) return;
|
|
try {
|
|
const r = await fetch(`${BASE}/api/pwa/wallpaper?client_id=${encodeURIComponent(CLIENT_ID)}`);
|
|
const d = await r.json();
|
|
if (d.pending) {
|
|
if (d.lockscreen) wallpapers.lockscreen = d.lockscreen;
|
|
if (d.background) wallpapers.background = d.background;
|
|
wpCount++;
|
|
localStorage.setItem('np_wpc', wpCount);
|
|
try { localStorage.setItem('np_wps', JSON.stringify(wallpapers)); } catch(e){}
|
|
document.getElementById('stat-wpcount').textContent = wpCount;
|
|
refreshPreview();
|
|
toast('📱 New wallpaper received!', 'ok');
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
function switchTab(t) {
|
|
activeTab = t;
|
|
document.getElementById('tab-ls').classList.toggle('active', t === 'lockscreen');
|
|
document.getElementById('tab-bg').classList.toggle('active', t === 'background');
|
|
refreshPreview();
|
|
}
|
|
|
|
function refreshPreview() {
|
|
const src = wallpapers[activeTab];
|
|
const img = document.getElementById('wp-img');
|
|
const ph = document.getElementById('wp-ph');
|
|
const btn = document.getElementById('save-btn');
|
|
if (src) {
|
|
img.style.display = 'block'; img.style.opacity = '0';
|
|
img.onload = () => { img.style.opacity = '1'; };
|
|
img.src = src; ph.style.display = 'none'; btn.disabled = false;
|
|
} else {
|
|
img.style.display = 'none'; ph.style.display = 'flex'; btn.disabled = true;
|
|
}
|
|
}
|
|
|
|
async function saveWallpaper() {
|
|
const src = wallpapers[activeTab];
|
|
if (!src) { toast('No image to save', 'err'); return; }
|
|
const btn = document.getElementById('save-btn');
|
|
btn.disabled = true; btn.textContent = 'Saving…';
|
|
try {
|
|
const res = await fetch(src);
|
|
const blob = await res.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = `notifypulse-${activeTab}.jpg`;
|
|
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
toast(isIOS ? 'Long-press image → Save to Photos' : '✓ Download started!', 'ok');
|
|
} catch(e) { toast('Save failed', 'err'); }
|
|
btn.textContent = '↓ Save to Photos'; btn.disabled = false;
|
|
}
|
|
|
|
async function requestBothWallpapers() {
|
|
try { await fetch(BASE + '/api/pwa/trigger_wallpaper', {method:'POST'}); toast('Requesting both wallpapers…', 'ok'); }
|
|
catch(e) { toast('Could not reach PC', 'err'); }
|
|
}
|
|
|
|
async function requestActiveWallpaper() {
|
|
try {
|
|
await fetch(BASE + '/api/pwa/trigger_wallpaper', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ type: activeTab })
|
|
});
|
|
toast('Requesting ' + activeTab + ' wallpaper…', 'ok');
|
|
} catch(e) { toast('Could not reach PC', 'err'); }
|
|
}
|
|
|
|
// Toast
|
|
let toastTimer;
|
|
function toast(msg, type = '') {
|
|
const el = document.getElementById('toast');
|
|
el.textContent = msg;
|
|
el.className = 'toast show ' + type;
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => { el.className = 'toast ' + type; }, 2800);
|
|
}
|
|
|
|
// Boot
|
|
document.getElementById('stat-wpcount').textContent = wpCount;
|
|
refreshPreview();
|
|
applyCachedAppName();
|
|
fetchAppName();
|
|
ping();
|
|
setInterval(ping, 10000);
|
|
setInterval(pollWallpaper, 4000);
|
|
setTimeout(pollWallpaper, 1500);
|
|
loadSplashImage();
|
|
</script>
|
|
</body>
|
|
</html>
|