1395 lines
36 KiB
HTML
1395 lines
36 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="#08080a" />
|
||
<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>
|
||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap');
|
||
:root {
|
||
--bg: #08080a;
|
||
--card: #111115;
|
||
--border: #1e1e28;
|
||
--accent: #4f8ef7;
|
||
--green: #22c55e;
|
||
--red: #ef4444;
|
||
--yellow: #f59e0b;
|
||
--text: #e2e2e8;
|
||
--muted: #6b6b7b;
|
||
--mono: 'Space Mono', monospace;
|
||
--safe-t: env(safe-area-inset-top, 0px);
|
||
--safe-b: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
html,
|
||
body {
|
||
height: 100%;
|
||
background: var(--bg);
|
||
overflow: hidden;
|
||
}
|
||
body {
|
||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||
color: var(--text);
|
||
}
|
||
|
||
/* ── SCREENS ── */
|
||
.screen {
|
||
position: fixed;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition:
|
||
opacity 0.45s ease,
|
||
transform 0.45s ease;
|
||
overflow: hidden;
|
||
}
|
||
.screen.hidden {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transform: scale(0.97);
|
||
}
|
||
.screen.gone {
|
||
display: none;
|
||
}
|
||
|
||
/* ── SHARED BACKGROUND ── */
|
||
#shared-bg {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 0;
|
||
background-size: contain;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
transition: opacity 1s ease;
|
||
opacity: 0;
|
||
}
|
||
#shared-bg.loaded {
|
||
opacity: 1;
|
||
}
|
||
#shared-bg-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1;
|
||
background: rgba(8, 8, 10, 0);
|
||
transition: background 0.6s ease;
|
||
}
|
||
#shared-bg-overlay.for-splash {
|
||
background: rgba(8, 8, 10, 0.45);
|
||
}
|
||
#shared-bg-overlay.for-selector {
|
||
background: rgba(8, 8, 10, 0.72);
|
||
backdrop-filter: blur(18px) saturate(0.7);
|
||
-webkit-backdrop-filter: blur(18px) saturate(0.7);
|
||
}
|
||
#shared-bg-overlay.for-app {
|
||
background: rgba(8, 8, 10, 0.88);
|
||
}
|
||
|
||
/* ── SPLASH ── */
|
||
#splash {
|
||
z-index: 10;
|
||
background: transparent;
|
||
}
|
||
.splash-content {
|
||
position: relative;
|
||
z-index: 2;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding-bottom: 40px;
|
||
animation: fadeUp 0.8s ease both;
|
||
animation-delay: 0.3s;
|
||
}
|
||
@keyframes fadeUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(18px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
.splash-logo {
|
||
font-family: 'Space Mono', monospace;
|
||
font-size: 1.6rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.1em;
|
||
color: #fff;
|
||
text-shadow: 0 2px 40px rgba(79, 142, 247, 0.7);
|
||
}
|
||
.splash-sub {
|
||
font-size: 0.72rem;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
letter-spacing: 0.2em;
|
||
text-transform: uppercase;
|
||
font-family: 'Space Mono', monospace;
|
||
}
|
||
.splash-tap {
|
||
margin-top: 36px;
|
||
font-size: 0.78rem;
|
||
color: rgba(255, 255, 255, 0.3);
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
animation: blink 2.2s infinite;
|
||
}
|
||
@keyframes blink {
|
||
0%,
|
||
100% {
|
||
opacity: 0.3;
|
||
}
|
||
50% {
|
||
opacity: 0.65;
|
||
}
|
||
}
|
||
|
||
/* ── SELECTOR ── */
|
||
#selector {
|
||
z-index: 5;
|
||
background: transparent;
|
||
justify-content: flex-start;
|
||
align-items: stretch;
|
||
}
|
||
.sel-header {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: calc(var(--safe-t) + 22px) 20px 14px;
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
.sel-header h1 {
|
||
font-family: 'Space Mono', monospace;
|
||
font-size: 1.05rem;
|
||
font-weight: 700;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
letter-spacing: 0.1em;
|
||
text-shadow: 0 1px 20px rgba(79, 142, 247, 0.5);
|
||
}
|
||
.sel-header p {
|
||
font-size: 0.76rem;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* ── TREE VIEW ── */
|
||
.uc-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: 12px 14px calc(var(--safe-b) + 16px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.tree-root {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
/* Folder row */
|
||
.tree-folder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.tree-folder-hdr {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
padding: 8px 10px 8px 6px;
|
||
cursor: pointer;
|
||
border-radius: 10px;
|
||
user-select: none;
|
||
transition: background 0.12s;
|
||
background: rgba(17, 17, 21, 0.5);
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
margin-bottom: 2px;
|
||
}
|
||
.tree-folder-hdr:active {
|
||
background: rgba(255, 255, 255, 0.07);
|
||
}
|
||
|
||
/* Indent guides */
|
||
.tree-indent {
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
}
|
||
.tree-guide {
|
||
width: 20px;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
.tree-guide::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
left: 50%;
|
||
width: 1px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
.tree-guide.last::before {
|
||
bottom: 50%;
|
||
}
|
||
|
||
/* Arrow + icon */
|
||
.tree-arrow {
|
||
width: 18px;
|
||
height: 18px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.6rem;
|
||
color: var(--muted);
|
||
transition: transform 0.18s;
|
||
}
|
||
.tree-arrow.open {
|
||
transform: rotate(90deg);
|
||
}
|
||
.tree-folder-icon {
|
||
font-size: 0.95rem;
|
||
margin-right: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
.tree-folder-label {
|
||
font-size: 0.85rem;
|
||
font-weight: 700;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
flex: 1;
|
||
}
|
||
.tree-folder-count {
|
||
font-size: 0.6rem;
|
||
color: var(--muted);
|
||
font-family: var(--mono);
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 999px;
|
||
padding: 1px 7px;
|
||
margin-left: 8px;
|
||
}
|
||
.tree-children {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
padding-left: 10px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.tree-children.collapsed {
|
||
display: none;
|
||
}
|
||
|
||
/* Leaf = usecase item */
|
||
.tree-leaf {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
cursor: pointer;
|
||
border-radius: 10px;
|
||
transition: background 0.12s;
|
||
background: rgba(17, 17, 21, 0.6);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
.tree-leaf::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 3px;
|
||
background: var(--uc-color, var(--accent));
|
||
}
|
||
.tree-leaf:active {
|
||
transform: scale(0.98);
|
||
background: rgba(255, 255, 255, 0.07);
|
||
}
|
||
.tree-leaf.active-uc {
|
||
border-color: rgba(255, 255, 255, 0.18);
|
||
background: rgba(79, 142, 247, 0.1);
|
||
}
|
||
.tree-leaf-inner {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 12px 10px 10px;
|
||
min-width: 0;
|
||
}
|
||
.tree-leaf-color {
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
background: var(--uc-color, var(--accent));
|
||
box-shadow: 0 0 6px var(--uc-color, var(--accent));
|
||
}
|
||
.tree-leaf-name {
|
||
font-size: 0.88rem;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
flex: 1;
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.tree-leaf-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 3px;
|
||
flex-shrink: 0;
|
||
}
|
||
.tree-leaf-chip {
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
font-size: 0.6rem;
|
||
font-family: var(--mono);
|
||
font-weight: 600;
|
||
background: rgba(79, 142, 247, 0.1);
|
||
color: var(--accent);
|
||
border: 1px solid rgba(79, 142, 247, 0.2);
|
||
}
|
||
.tree-leaf-active-tag {
|
||
font-size: 0.58rem;
|
||
font-family: 'Space Mono', monospace;
|
||
color: var(--green);
|
||
background: rgba(34, 197, 94, 0.12);
|
||
border: 1px solid rgba(34, 197, 94, 0.25);
|
||
border-radius: 4px;
|
||
padding: 2px 6px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
flex-shrink: 0;
|
||
}
|
||
.tree-leaf-arrow {
|
||
color: rgba(255, 255, 255, 0.2);
|
||
font-size: 0.85rem;
|
||
flex-shrink: 0;
|
||
padding-right: 2px;
|
||
}
|
||
|
||
/* ── APP ── */
|
||
#app {
|
||
z-index: 1;
|
||
background: transparent;
|
||
justify-content: flex-start;
|
||
align-items: stretch;
|
||
}
|
||
.app-header {
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: calc(var(--safe-t) + 17px) 17px 13px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
.app-header h1 {
|
||
font-family: 'Space Mono', monospace;
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
letter-spacing: 0.06em;
|
||
}
|
||
.conn-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 5px 12px;
|
||
border-radius: 999px;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: rgba(17, 17, 21, 0.6);
|
||
color: var(--muted);
|
||
transition: 0.3s;
|
||
}
|
||
.conn-badge .dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: var(--muted);
|
||
}
|
||
.conn-badge.ok {
|
||
border-color: rgba(34, 197, 94, 0.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, 0.35);
|
||
color: var(--red);
|
||
}
|
||
.conn-badge.err .dot {
|
||
background: var(--red);
|
||
}
|
||
|
||
.app-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
position: relative;
|
||
z-index: 2;
|
||
padding: 13px 15px calc(var(--safe-b) + 18px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 11px;
|
||
}
|
||
|
||
/* Stat row */
|
||
.stat-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
.stat {
|
||
background: rgba(17, 17, 21, 0.7);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 10px;
|
||
padding: 11px 13px;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.sl {
|
||
font-size: 9px;
|
||
color: var(--muted);
|
||
font-family: 'Space Mono', monospace;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
margin-bottom: 3px;
|
||
}
|
||
.sv {
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
}
|
||
.sv.green { color: var(--green); }
|
||
.sv.yellow { color: var(--yellow); }
|
||
.sv.accent { color: var(--accent); }
|
||
|
||
/* Cards */
|
||
.c {
|
||
background: rgba(17, 17, 21, 0.7);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 13px;
|
||
padding: 14px;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.ct {
|
||
font-size: 0.6rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.1em;
|
||
color: var(--muted);
|
||
font-family: 'Space Mono', monospace;
|
||
margin-bottom: 11px;
|
||
}
|
||
|
||
/* Wallpaper section */
|
||
.wp-tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.wp-tab {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: transparent;
|
||
color: var(--muted);
|
||
font-size: 0.74rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: 0.15s;
|
||
}
|
||
.wp-tab.active {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
background: rgba(79, 142, 247, 0.1);
|
||
}
|
||
.wp-frame {
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
aspect-ratio: 9/16;
|
||
max-height: 250px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
}
|
||
.wp-frame img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: opacity 0.4s;
|
||
}
|
||
.wp-ph {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 7px;
|
||
color: var(--muted);
|
||
font-size: 0.72rem;
|
||
}
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.83rem;
|
||
font-weight: 600;
|
||
transition: 0.15s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 7px;
|
||
}
|
||
.btn-accent {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
}
|
||
.btn-accent:active { filter: brightness(0.9); }
|
||
.btn-ghost {
|
||
background: rgba(255, 255, 255, 0.07);
|
||
color: var(--text);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
.btn-ghost:active { background: rgba(255, 255, 255, 0.12); }
|
||
.btn-green {
|
||
background: rgba(34, 197, 94, 0.1);
|
||
color: var(--green);
|
||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||
}
|
||
.btn-green:active { background: rgba(34, 197, 94, 0.2); }
|
||
|
||
/* Usecase chip at top of app */
|
||
.uc-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: rgba(17, 17, 21, 0.7);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 10px;
|
||
padding: 10px 14px;
|
||
cursor: pointer;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.uc-chip-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
.uc-chip-name {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
flex: 1;
|
||
}
|
||
.uc-chip-change {
|
||
font-size: 0.7rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.save-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* ── PWA OVERLAY ── */
|
||
#pwa-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #000;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.35s ease;
|
||
}
|
||
#pwa-overlay.visible {
|
||
opacity: 1;
|
||
pointer-events: all;
|
||
}
|
||
#pwa-overlay img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
display: block;
|
||
}
|
||
|
||
/* ── BG SETTINGS PANEL ── */
|
||
#bg-settings-btn {
|
||
position: fixed;
|
||
bottom: calc(var(--safe-b) + 18px);
|
||
right: 18px;
|
||
z-index: 20;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: rgba(17, 17, 21, 0.7);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
color: rgba(255, 255, 255, 0.4);
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
backdrop-filter: blur(6px);
|
||
transition: 0.2s;
|
||
}
|
||
#bg-settings-btn:hover {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
border-color: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
#bg-settings-panel {
|
||
position: fixed;
|
||
bottom: calc(var(--safe-b) + 62px);
|
||
right: 14px;
|
||
z-index: 20;
|
||
width: 240px;
|
||
background: rgba(17, 17, 21, 0.92);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-radius: 14px;
|
||
padding: 16px;
|
||
backdrop-filter: blur(20px);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transform: translateY(8px);
|
||
transition:
|
||
opacity 0.2s ease,
|
||
transform 0.2s ease;
|
||
}
|
||
#bg-settings-panel.open {
|
||
opacity: 1;
|
||
pointer-events: all;
|
||
transform: translateY(0);
|
||
}
|
||
.bgs-title {
|
||
font-size: 0.65rem;
|
||
font-family: 'Space Mono', monospace;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.1em;
|
||
color: var(--muted);
|
||
margin-bottom: 14px;
|
||
}
|
||
.bgs-row {
|
||
margin-bottom: 12px;
|
||
}
|
||
.bgs-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 0.74rem;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
margin-bottom: 6px;
|
||
}
|
||
.bgs-label span {
|
||
font-family: 'Space Mono', monospace;
|
||
color: var(--accent);
|
||
font-size: 0.7rem;
|
||
}
|
||
.bgs-slider {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 100%;
|
||
height: 4px;
|
||
border-radius: 2px;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
outline: none;
|
||
cursor: pointer;
|
||
}
|
||
.bgs-slider::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background: var(--accent);
|
||
cursor: pointer;
|
||
border: 2px solid #fff;
|
||
box-shadow: 0 1px 6px rgba(79, 142, 247, 0.5);
|
||
}
|
||
.bgs-save {
|
||
margin-top: 14px;
|
||
}
|
||
|
||
/* Toast */
|
||
.toast {
|
||
position: fixed;
|
||
bottom: calc(var(--safe-b) + 80px);
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: var(--green);
|
||
color: #000;
|
||
padding: 9px 20px;
|
||
border-radius: 999px;
|
||
font-size: 0.8rem;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: 0.3s;
|
||
z-index: 99;
|
||
}
|
||
.toast.show {
|
||
opacity: 1;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Persistent blurred background image (behind all screens) -->
|
||
<div id="shared-bg"></div>
|
||
<div id="shared-bg-overlay" class="for-splash"></div>
|
||
|
||
<!-- SPLASH -->
|
||
<div class="screen" id="splash" onclick="goToSelector()">
|
||
<div class="splash-content">
|
||
<div class="splash-logo" id="splashName">NOTIFYPULSE</div>
|
||
<div class="splash-sub" id="splashSub">V3</div>
|
||
<div class="splash-tap">Tap to continue</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SELECTOR -->
|
||
<div class="screen hidden gone" id="selector">
|
||
<div class="sel-header">
|
||
<h1 id="selHeaderName">NOTIFYPULSE</h1>
|
||
<p>Choose a usecase</p>
|
||
</div>
|
||
<div class="uc-list" id="ucList">Loading…</div>
|
||
</div>
|
||
|
||
<!-- APP -->
|
||
<div class="screen hidden gone" id="app">
|
||
<div class="app-header">
|
||
<h1 id="appNameEl">NOTIFYPULSE</h1>
|
||
<div class="conn-badge" id="connBadge">
|
||
<div class="dot"></div>
|
||
<span id="connText">Connecting</span>
|
||
</div>
|
||
</div>
|
||
<div class="app-body">
|
||
<div class="uc-chip" onclick="goToSelector()">
|
||
<div
|
||
class="uc-chip-dot"
|
||
id="ucChipDot"
|
||
style="background: var(--accent)"
|
||
></div>
|
||
<span class="uc-chip-name" id="ucChipName">—</span>
|
||
<span class="uc-chip-change">Switch ↗</span>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<div class="stat">
|
||
<div class="sl">Status</div>
|
||
<div class="sv" id="a-status">—</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="sl">Next Fire</div>
|
||
<div class="sv accent" id="a-next">—</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="c" id="wpCard">
|
||
<div class="ct">Mobile Wallpaper</div>
|
||
<div class="wp-tabs">
|
||
<button class="wp-tab active" onclick="wpTab('lockscreen', this)">
|
||
Lock Screen
|
||
</button>
|
||
<button class="wp-tab" onclick="wpTab('background', this)">
|
||
Home Screen
|
||
</button>
|
||
</div>
|
||
<div class="wp-frame">
|
||
<img id="wpImg" src="" style="opacity: 0" />
|
||
<div class="wp-ph" id="wpPh">
|
||
<svg
|
||
width="28"
|
||
height="28"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.5"
|
||
opacity=".4"
|
||
>
|
||
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||
<path d="m21 15-5-5L5 21" />
|
||
</svg>
|
||
<span>No wallpaper yet</span>
|
||
</div>
|
||
</div>
|
||
<div style="height: 8px"></div>
|
||
<div class="save-row">
|
||
<button class="btn btn-ghost" onclick="saveWp('lockscreen')">
|
||
💾 Save Lock
|
||
</button>
|
||
<button class="btn btn-ghost" onclick="saveWp('background')">
|
||
💾 Save Home
|
||
</button>
|
||
</div>
|
||
<div style="height: 8px"></div>
|
||
<button class="btn btn-green" onclick="requestWallpaper()">
|
||
🔄 Request New Wallpaper
|
||
</button>
|
||
</div>
|
||
|
||
<button class="btn btn-ghost" onclick="goToSelector()">
|
||
↩ Back to Usecases
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BG Settings button (always visible) -->
|
||
<button
|
||
id="bg-settings-btn"
|
||
onclick="toggleBgSettings()"
|
||
title="Background settings"
|
||
>
|
||
⚙
|
||
</button>
|
||
<div id="bg-settings-panel">
|
||
<div class="bgs-title">Background Appearance</div>
|
||
<div class="bgs-row">
|
||
<div class="bgs-label">Blur <span id="blurVal">18px</span></div>
|
||
<input
|
||
class="bgs-slider"
|
||
id="blurSlider"
|
||
type="range"
|
||
min="0"
|
||
max="40"
|
||
step="1"
|
||
value="18"
|
||
oninput="previewBg()"
|
||
/>
|
||
</div>
|
||
<div class="bgs-row">
|
||
<div class="bgs-label">Darkness <span id="opacVal">72%</span></div>
|
||
<input
|
||
class="bgs-slider"
|
||
id="opacSlider"
|
||
type="range"
|
||
min="0"
|
||
max="95"
|
||
step="1"
|
||
value="72"
|
||
oninput="previewBg()"
|
||
/>
|
||
</div>
|
||
<button class="btn btn-accent bgs-save" onclick="saveBgSettings()">
|
||
Save
|
||
</button>
|
||
</div>
|
||
|
||
<!-- PWA Overlay — mirrors the desktop screen overlay -->
|
||
<div id="pwa-overlay"><img id="pwa-overlay-img" src="" alt="" /></div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
const CLIENT_ID = 'pwa_' + Math.random().toString(36).slice(2, 11);
|
||
let appInfo = null,
|
||
activeUCId = '',
|
||
allBlocks = [];
|
||
let wpImages = { lockscreen: null, background: null },
|
||
wpCurTab = 'lockscreen';
|
||
let pingInterval, wallpaperInterval, statInterval;
|
||
let bgImageLoaded = false;
|
||
|
||
// ── Screen transitions ─────────────────────────────────────
|
||
function show(id) {
|
||
const overlay = document.getElementById('shared-bg-overlay');
|
||
overlay.className =
|
||
'for-' +
|
||
(id === 'splash' ? 'splash' : id === 'selector' ? 'selector' : 'app');
|
||
|
||
document.querySelectorAll('.screen').forEach((s) => {
|
||
s.classList.remove('gone');
|
||
s.classList.toggle('hidden', s.id !== id);
|
||
});
|
||
setTimeout(() => {
|
||
document
|
||
.querySelectorAll('.screen.hidden')
|
||
.forEach((s) => s.classList.add('gone'));
|
||
}, 500);
|
||
}
|
||
|
||
function goToSelector() {
|
||
show('selector');
|
||
loadSelectorList();
|
||
}
|
||
|
||
function goToApp(ucId) {
|
||
show('app');
|
||
setActiveUC(ucId);
|
||
}
|
||
|
||
// ── Init ──────────────────────────────────────────────────
|
||
async function init() {
|
||
const splashR = await fetchJSON('/api/pwa/splash_image').catch(
|
||
() => null,
|
||
);
|
||
if (splashR && splashR.image) {
|
||
const bg = document.getElementById('shared-bg');
|
||
bg.style.backgroundImage = `url('${splashR.image}')`;
|
||
requestAnimationFrame(() => {
|
||
bg.classList.add('loaded');
|
||
bgImageLoaded = true;
|
||
});
|
||
}
|
||
|
||
const info = await fetchJSON('/api/pwa/app_name').catch(() => null);
|
||
if (info) {
|
||
appInfo = info;
|
||
activeUCId = info.active || '';
|
||
allBlocks = info.blocks || [];
|
||
const settingsName =
|
||
info.settings_app_name || info.app_name || 'NOTIFYPULSE';
|
||
document.getElementById('splashName').textContent =
|
||
settingsName.toUpperCase();
|
||
document.getElementById('selHeaderName').textContent =
|
||
settingsName.toUpperCase();
|
||
document.getElementById('splashSub').textContent =
|
||
`${(info.usecases || []).length} USECASE(S)`;
|
||
applyBgSettings(
|
||
info.pwa_bg_blur !== undefined ? info.pwa_bg_blur : 18,
|
||
info.pwa_bg_opacity !== undefined ? info.pwa_bg_opacity : 0.72,
|
||
);
|
||
}
|
||
|
||
startPing();
|
||
startOverlayPoll();
|
||
}
|
||
|
||
// ── Tree builder ──────────────────────────────────────────
|
||
const SEP = /\s*(?:\/|>|\u203a)\s*/;
|
||
let _treeGid = 0;
|
||
|
||
function insertIntoTree(node, parts, uc) {
|
||
if (!parts.length) {
|
||
node._items.push(uc);
|
||
return;
|
||
}
|
||
const k = parts[0];
|
||
if (!node[k]) node[k] = { _items: [] };
|
||
insertIntoTree(node[k], parts.slice(1), uc);
|
||
}
|
||
|
||
function countLeaves(node) {
|
||
let n = (node._items || []).length;
|
||
Object.keys(node)
|
||
.filter((k) => k !== '_items')
|
||
.forEach((k) => (n += countLeaves(node[k])));
|
||
return n;
|
||
}
|
||
|
||
function leafHtml(uc, depth) {
|
||
const indent = Array.from(
|
||
{ length: depth },
|
||
() => '<span class="tree-guide"></span>',
|
||
).join('');
|
||
const chips = (uc.blocks || [])
|
||
.slice(0, 3)
|
||
.map((bid) => {
|
||
const b = allBlocks.find((x) => x.id === bid);
|
||
return b ? `<span class="tree-leaf-chip">${b.icon} ${b.name}</span>` : '';
|
||
})
|
||
.join('');
|
||
const isActive = uc.id === activeUCId;
|
||
const rightEl = isActive
|
||
? '<span class="tree-leaf-active-tag">Active</span>'
|
||
: '<span class="tree-leaf-arrow">›</span>';
|
||
return `<div class="tree-leaf${isActive ? ' active-uc' : ''}" style="--uc-color:${uc.color}" onclick="pickUC('${uc.id}','${uc.color}')">
|
||
<div class="tree-indent">${indent}</div>
|
||
<div class="tree-leaf-inner">
|
||
<div class="tree-leaf-color"></div>
|
||
<span class="tree-leaf-name">${escHtml(uc.name)}</span>
|
||
<div class="tree-leaf-chips">${chips}</div>
|
||
${rightEl}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderNode(node, depth) {
|
||
let html = '';
|
||
(node._items || []).forEach((uc) => {
|
||
html += leafHtml(uc, depth);
|
||
});
|
||
Object.keys(node)
|
||
.filter((k) => k !== '_items')
|
||
.forEach((key) => {
|
||
const child = node[key];
|
||
const id = 'tg-' + ++_treeGid;
|
||
const count = countLeaves(child);
|
||
const indent = Array.from(
|
||
{ length: depth },
|
||
() => '<span class="tree-guide"></span>',
|
||
).join('');
|
||
html += `<div class="tree-folder">
|
||
<div class="tree-folder-hdr" onclick="toggleGroup('${id}')">
|
||
<div class="tree-indent">${indent}</div>
|
||
<span class="tree-arrow open" id="arr-${id}">▶</span>
|
||
<span class="tree-folder-icon">📁</span>
|
||
<span class="tree-folder-label">${escHtml(key)}</span>
|
||
<span class="tree-folder-count">${count}</span>
|
||
</div>
|
||
<div class="tree-children" id="${id}">${renderNode(child, depth + 1)}</div>
|
||
</div>`;
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function toggleGroup(id) {
|
||
const body = document.getElementById(id);
|
||
const arr = document.getElementById('arr-' + id);
|
||
if (!body) return;
|
||
const collapsed = body.classList.toggle('collapsed');
|
||
if (arr) arr.classList.toggle('open', !collapsed);
|
||
}
|
||
|
||
// ── Selector list (tree view) ─────────────────────────────
|
||
async function loadSelectorList() {
|
||
const r = await fetchJSON('/api/pwa/app_name').catch(() => null);
|
||
if (!r) {
|
||
document.getElementById('ucList').innerHTML =
|
||
'<p style="color:rgba(255,255,255,.3);padding:20px;text-align:center">Connection error</p>';
|
||
return;
|
||
}
|
||
|
||
const settingsName = r.settings_app_name || r.app_name || 'NOTIFYPULSE';
|
||
document.getElementById('selHeaderName').textContent =
|
||
settingsName.toUpperCase();
|
||
|
||
activeUCId = r.active || '';
|
||
allBlocks = r.blocks || [];
|
||
const usecases = r.usecases || [];
|
||
|
||
if (!usecases.length) {
|
||
document.getElementById('ucList').innerHTML =
|
||
'<p style="color:rgba(255,255,255,.3);padding:20px;text-align:center">No usecases yet.<br>Create them in the desktop UI.</p>';
|
||
return;
|
||
}
|
||
|
||
// Build tree
|
||
_treeGid = 0;
|
||
const tree = { _items: [] };
|
||
usecases.forEach((uc) => {
|
||
const path = (uc.group || '').trim();
|
||
if (!path) {
|
||
tree._items.push(uc);
|
||
return;
|
||
}
|
||
insertIntoTree(tree, path.split(SEP).filter(Boolean), uc);
|
||
});
|
||
|
||
document.getElementById('ucList').innerHTML =
|
||
`<div class="tree-root">${renderNode(tree, 0)}</div>`;
|
||
}
|
||
|
||
async function pickUC(id, color) {
|
||
await postJSON('/api/pwa/activate_usecase', { usecase_id: id }).catch(
|
||
() => null,
|
||
);
|
||
activeUCId = id;
|
||
goToApp(id);
|
||
}
|
||
|
||
// ── App view ──────────────────────────────────────────────
|
||
async function setActiveUC(ucId) {
|
||
const r = await fetchJSON('/api/pwa/app_name').catch(() => null);
|
||
if (!r) return;
|
||
const uc = (r.usecases || []).find((u) => u.id === ucId);
|
||
if (!uc) return;
|
||
|
||
document.getElementById('appNameEl').textContent =
|
||
uc.name.toUpperCase();
|
||
document.getElementById('ucChipName').textContent = uc.name;
|
||
document.getElementById('ucChipDot').style.background = uc.color;
|
||
|
||
const hasMobile = (uc.blocks || []).includes('mobile_wallpaper');
|
||
document.getElementById('wpCard').style.display = hasMobile
|
||
? 'block'
|
||
: 'none';
|
||
|
||
clearInterval(statInterval);
|
||
statInterval = setInterval(updateStats, 3000);
|
||
updateStats();
|
||
|
||
clearInterval(wallpaperInterval);
|
||
if (hasMobile) wallpaperInterval = setInterval(pollWallpaper, 4000);
|
||
}
|
||
|
||
async function updateStats() {
|
||
const s = await fetchJSON('/api/state').catch(() => null);
|
||
if (!s) return;
|
||
const paused = s.paused;
|
||
document.getElementById('a-status').textContent = paused
|
||
? '⏸ Paused'
|
||
: '▶ Running';
|
||
document.getElementById('a-status').className =
|
||
'sv ' + (paused ? 'yellow' : 'green');
|
||
const next = s.next_fire_at
|
||
? Math.max(0, Math.round(s.next_fire_at - Date.now() / 1000))
|
||
: 0;
|
||
document.getElementById('a-next').textContent =
|
||
next > 0 ? `${Math.floor(next / 60)}m ${next % 60}s` : '—';
|
||
if (paused && _overlayActive) hideOverlay();
|
||
}
|
||
|
||
// ── Wallpaper ─────────────────────────────────────────────
|
||
async function pollWallpaper() {
|
||
const r = await fetchJSON(
|
||
`/api/pwa/wallpaper?client_id=${CLIENT_ID}`,
|
||
).catch(() => null);
|
||
if (!r || !r.pending) return;
|
||
if (r.lockscreen) wpImages.lockscreen = r.lockscreen;
|
||
if (r.background) wpImages.background = r.background;
|
||
displayWp(wpCurTab);
|
||
showToast('New wallpaper received!');
|
||
}
|
||
|
||
async function requestWallpaper() {
|
||
await postJSON('/api/pwa/trigger_wallpaper', {});
|
||
showToast('Request sent…');
|
||
}
|
||
|
||
function wpTab(tab, btn) {
|
||
wpCurTab = tab;
|
||
document
|
||
.querySelectorAll('.wp-tab')
|
||
.forEach((b) => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
displayWp(tab);
|
||
}
|
||
|
||
function displayWp(tab) {
|
||
const img = document.getElementById('wpImg');
|
||
const ph = document.getElementById('wpPh');
|
||
const src = wpImages[tab];
|
||
if (src) {
|
||
img.src = src;
|
||
img.style.opacity = '1';
|
||
ph.style.display = 'none';
|
||
} else {
|
||
img.style.opacity = '0';
|
||
ph.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
function saveWp(tab) {
|
||
const src = wpImages[tab];
|
||
if (!src) {
|
||
showToast('No image to save');
|
||
return;
|
||
}
|
||
const a = document.createElement('a');
|
||
a.href = src;
|
||
a.download = tab === 'lockscreen' ? 'Lockscreen.jpg' : 'Background.jpg';
|
||
a.click();
|
||
}
|
||
|
||
// ── Ping ──────────────────────────────────────────────────
|
||
function startPing() {
|
||
clearInterval(pingInterval);
|
||
pingInterval = setInterval(doPing, 4000);
|
||
doPing();
|
||
}
|
||
|
||
async function doPing() {
|
||
try {
|
||
await postJSON('/api/pwa/ping', { client_id: CLIENT_ID });
|
||
document.getElementById('connBadge').className = 'conn-badge ok';
|
||
document.getElementById('connText').textContent = 'Connected';
|
||
} catch {
|
||
document.getElementById('connBadge').className = 'conn-badge err';
|
||
document.getElementById('connText').textContent = 'Offline';
|
||
}
|
||
}
|
||
|
||
// ── Helpers ───────────────────────────────────────────────
|
||
async function fetchJSON(url) {
|
||
const r = await fetch(url);
|
||
if (!r.ok) throw new Error(r.status);
|
||
return r.json();
|
||
}
|
||
async function postJSON(url, body) {
|
||
return fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
}).then((r) => r.json());
|
||
}
|
||
function showToast(msg) {
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg;
|
||
t.classList.add('show');
|
||
setTimeout(() => t.classList.remove('show'), 2500);
|
||
}
|
||
function escHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|
||
// Countdown ticker
|
||
setInterval(() => {
|
||
const el = document.getElementById('a-next');
|
||
if (!el) return;
|
||
const m = el.textContent.match(/(\d+)m (\d+)s/);
|
||
if (!m) return;
|
||
let t = parseInt(m[1]) * 60 + parseInt(m[2]) - 1;
|
||
if (t < 0) t = 0;
|
||
el.textContent = `${Math.floor(t / 60)}m ${t % 60}s`;
|
||
}, 1000);
|
||
|
||
// ── PWA Overlay ───────────────────────────────────────────
|
||
let _overlayInterval = null;
|
||
let _overlayDismissTimer = null;
|
||
let _overlayActive = false;
|
||
|
||
function startOverlayPoll() {
|
||
clearInterval(_overlayInterval);
|
||
_overlayInterval = setInterval(pollOverlay, 1000);
|
||
pollOverlay();
|
||
}
|
||
|
||
async function pollOverlay() {
|
||
const r = await fetchJSON('/api/pwa/overlay').catch(() => null);
|
||
if (!r) return;
|
||
|
||
const el = document.getElementById('pwa-overlay');
|
||
const img = document.getElementById('pwa-overlay-img');
|
||
|
||
if (r.active && r.image) {
|
||
if (!_overlayActive) {
|
||
img.src = r.image;
|
||
el.classList.add('visible');
|
||
_overlayActive = true;
|
||
}
|
||
clearTimeout(_overlayDismissTimer);
|
||
if (r.remaining_ms > 0) {
|
||
_overlayDismissTimer = setTimeout(hideOverlay, r.remaining_ms);
|
||
}
|
||
} else {
|
||
hideOverlay();
|
||
}
|
||
}
|
||
|
||
function hideOverlay() {
|
||
clearTimeout(_overlayDismissTimer);
|
||
_overlayActive = false;
|
||
const el = document.getElementById('pwa-overlay');
|
||
el.classList.remove('visible');
|
||
setTimeout(() => {
|
||
if (!_overlayActive)
|
||
document.getElementById('pwa-overlay-img').src = '';
|
||
}, 400);
|
||
}
|
||
|
||
// ── Background Settings ───────────────────────────────────
|
||
let _bgBlur = 18,
|
||
_bgOpac = 0.72;
|
||
|
||
function applyBgSettings(blur, opac) {
|
||
_bgBlur = blur;
|
||
_bgOpac = opac;
|
||
let styleEl = document.getElementById('bg-dynamic-style');
|
||
if (!styleEl) {
|
||
styleEl = document.createElement('style');
|
||
styleEl.id = 'bg-dynamic-style';
|
||
document.head.appendChild(styleEl);
|
||
}
|
||
styleEl.textContent = `
|
||
#shared-bg-overlay.for-selector {
|
||
background: rgba(8,8,10,${opac}) !important;
|
||
backdrop-filter: blur(${blur}px) saturate(.7) !important;
|
||
-webkit-backdrop-filter: blur(${blur}px) saturate(.7) !important;
|
||
}
|
||
#shared-bg-overlay.for-app {
|
||
background: rgba(8,8,10,${Math.min(opac + 0.16, 0.97)}) !important;
|
||
}
|
||
`;
|
||
document.getElementById('blurSlider').value = blur;
|
||
document.getElementById('opacSlider').value = Math.round(opac * 100);
|
||
document.getElementById('blurVal').textContent = blur + 'px';
|
||
document.getElementById('opacVal').textContent =
|
||
Math.round(opac * 100) + '%';
|
||
}
|
||
|
||
function previewBg() {
|
||
const blur = parseInt(document.getElementById('blurSlider').value);
|
||
const opac =
|
||
parseInt(document.getElementById('opacSlider').value) / 100;
|
||
applyBgSettings(blur, opac);
|
||
}
|
||
|
||
function toggleBgSettings() {
|
||
document.getElementById('bg-settings-panel').classList.toggle('open');
|
||
}
|
||
|
||
async function saveBgSettings() {
|
||
const blur = parseInt(document.getElementById('blurSlider').value);
|
||
const opac =
|
||
parseInt(document.getElementById('opacSlider').value) / 100;
|
||
await postJSON('/api/settings', {
|
||
pwa_bg_blur: blur,
|
||
pwa_bg_opacity: opac,
|
||
}).catch(() => null);
|
||
applyBgSettings(blur, opac);
|
||
document.getElementById('bg-settings-panel').classList.remove('open');
|
||
showToast('Saved!');
|
||
}
|
||
|
||
if ('serviceWorker' in navigator)
|
||
navigator.serviceWorker.register('/pwa/sw.js');
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|