app
Adom Screensaver
Public Made by Adomby adom
Vibe-coding screensaver, fades up a live billboard of the latest Adom Wiki drops (truly cached to disk; plays webm silently, renders SVG) with a power-aware nerd-stats splash, while signed native keep
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
<!doctype html>
<!-- Adom Screensaver billboard, rendered in a fullscreen WebView2 hosted by AdomSaver.scr.
Plays webm (muted, looped), renders SVG + raster, cross-fades, shows the power + display-off
countdown footer and the Wiki v1 "migrate to v2" kicker. Data comes from manifest.json
(written by the C# host) served from the virtual host https://adom-screensaver.local/. -->
<html lang="en">
<head>
<meta charset="utf-8">
<style>
:root { --teal:#00b8b0; --tealbri:#00e6dc; --hi:#e6edf3; --lo:#8b949e; --dim:#6b7280; --line:#1e2733; --bg:#0d1117; }
* { margin:0; padding:0; box-sizing:border-box; }
html,body { width:100%; height:100%; overflow:hidden; background:var(--bg);
font-family:"Segoe UI","Segoe UI Semibold",system-ui,sans-serif; cursor:none; user-select:none; }
/* The whole billboard rests at 50% (configurable). The host fades the WINDOW out on wake.
Transparent over the dark <body> + a layer ABOVE #bbmark so the giant brand mark reads as a
full-bleed watermark BEHIND the slide content (magazine-ad style). */
#stage { position:fixed; inset:0; background:transparent; z-index:1; filter:brightness(var(--sdim,0.7));
opacity:1; transition:opacity .55s ease; animation:bbdrift 80s ease-in-out infinite; will-change:transform; }
/* anti burn-in: slowly drift ALL billboard content (incl. the lid note) so it never sits on the same
pixels. We first scale to 97% (a ~1.5% margin on every side, visually imperceptible), then drift only
WITHIN that margin (<=1.0vw/vh) so content can never run off the screen. Display also powers off after rest. */
/* Content is scaled to 88% -> a ~6% margin on every edge; the drift stays within ±1% so the content
never comes closer than ~5% to any edge. Humans need margins. */
@keyframes bbdrift {
0% { transform: scale(.83) translate(0, 0); }
20% { transform: scale(.83) translate(3.3vw, -2.9vh); }
40% { transform: scale(.83) translate(-3.1vw, 3.0vh); }
60% { transform: scale(.83) translate(2.8vw, 2.9vh); }
80% { transform: scale(.83) translate(-3.2vw, -2.7vh); }
100% { transform: scale(.83) translate(0, 0); }
}
/* Slide = a TWO-COLUMN editorial split: text on the left, the hero shot on the right. Fills a 16:9
widescreen far better than a centered column, and gives the description room to breathe. */
.slide { position:absolute; inset:0; display:grid; grid-template-columns:43% 57%;
align-items:center; column-gap:4.5vw; padding:9vh 6vw 11vh; text-align:left; }
.slide .coltext { display:flex; flex-direction:column; align-items:flex-start; min-width:0; }
.slide .colmedia { display:flex; align-items:center; justify-content:center; min-width:0; height:100%; }
.kicker { font-size:1.25vh; font-weight:600; letter-spacing:.66em; margin-left:.66em;
text-transform:uppercase; color:var(--teal); margin-bottom:3.4vh; }
/* visibility badge: the billboards double as a privacy QA surface, so PUBLIC vs ADOM-ONLY must be loud */
.vbadge { display:inline-flex; align-items:center; gap:.65vw; font-size:1.2vh; font-weight:700;
letter-spacing:.16em; text-transform:uppercase; padding:.6vh 1.2vw; border-radius:999px; }
.vbadge::before { content:""; width:.85vh; height:.85vh; border-radius:50%; flex:none; }
.vbadge .sub { font-weight:600; letter-spacing:.03em; text-transform:none; opacity:.82; }
.vbadge.pub { color:#5dd879; background:rgba(46,160,67,.13); border:1px solid rgba(46,160,67,.5); }
.vbadge.pub::before { background:#2ea043; box-shadow:0 0 .9vh rgba(46,160,67,.75); }
.vbadge.priv { color:#f0c548; background:rgba(240,197,72,.13); border:1px solid rgba(240,197,72,.55); }
.vbadge.priv::before { background:#f0c548; box-shadow:0 0 .9vh rgba(240,197,72,.75); }
/* wiki identity block: owner-qualified URL + org/personal + author (the kicker's replacement) */
.wikiid { margin:.6vh 0 0; }
.wurl { font-family:"Cascadia Code","Consolas","Courier New",monospace; font-size:1.65vh; letter-spacing:.005em; color:var(--hi); }
.wurl .wdim { color:var(--dim); }
.wurl .worg { color:var(--tealbri); font-weight:700; }
.wurl .wuser { color:var(--lo); font-weight:600; }
.wmeta { margin-top:.9vh; font-size:1.3vh; color:var(--lo); letter-spacing:.02em; }
.wmeta b { font-weight:700; } .wmeta .worg { color:var(--tealbri); }
/* meta row: visibility badge + freshness chip grouped on one line (design-pass: one compact source
line instead of stacked blocks). The freshness chip leads the eye to NEW content. */
.metarow { display:flex; align-items:center; gap:1vw; flex-wrap:wrap; margin-bottom:.5vh; }
.freshchip { font-size:1.2vh; font-weight:700; letter-spacing:.03em; padding:.42vh 1.15vw; border-radius:999px;
display:inline-flex; align-items:center; gap:.5vw; line-height:1; }
.freshchip::before { content:"\2726"; font-size:1.05vh; }
.freshchip.fresh-new { color:#04211f; background:linear-gradient(100deg,#00e6dc,#39b8ff); border:1px solid rgba(0,230,220,.6); box-shadow:0 0 1.4vh rgba(0,184,176,.32); }
.freshchip.fresh-recent { color:#aeb8c4; background:rgba(255,255,255,.05); border:1px solid var(--line); }
.freshchip.fresh-old { color:var(--dim); background:transparent; border:1px solid var(--line); }
.freshchip.fresh-old::before { opacity:.5; }
/* TYPE capsule (App / Skill / Component) so you can tell at a glance WHAT KIND of wiki repo a billboard is.
The wiki's `type` field is first-class (the Apps/Skills/Components/Bootstraps header tabs filter on it);
each kind gets a distinct color + leading dot. */
.typecap { font-size:1.2vh; font-weight:800; letter-spacing:.08em; text-transform:uppercase;
padding:.42vh 1.1vw; border-radius:999px; display:inline-flex; align-items:center; gap:.55vw; line-height:1; }
.typecap::before { content:""; width:.85vh; height:.85vh; flex:none; }
.typecap.t-app { color:#8cc0ff; background:rgba(0,97,239,.16); border:1px solid rgba(100,171,255,.55); }
.typecap.t-app::before { background:#64ABFF; border-radius:2px; box-shadow:0 0 .9vh rgba(100,171,255,.8); }
.typecap.t-skill { color:#c2acff; background:rgba(140,107,247,.16); border:1px solid rgba(140,107,247,.55); }
.typecap.t-skill::before { background:#8C6BF7; transform:rotate(45deg); box-shadow:0 0 .9vh rgba(140,107,247,.8); }
.typecap.t-component { color:#ff9ccb; background:rgba(255,126,182,.14); border:1px solid rgba(255,126,182,.5); }
.typecap.t-component::before { background:#ff7eb6; border-radius:50%; box-shadow:0 0 .9vh rgba(255,126,182,.8); }
/* Preview wait screen: never leave a preview on a blank screen while a billboard (esp. a component, whose
hero must download) is still loading. Tell the user exactly what's happening so nothing feels stuck. */
#previewwait { position:fixed; inset:0; z-index:6; display:none; flex-direction:column;
align-items:center; justify-content:center; gap:2.4vh; text-align:center; background:#0d1117; }
#previewwait .pw-logo { width:9vw; max-width:180px; opacity:.92;
filter:drop-shadow(0 0 28px rgba(0,184,176,.16)); }
#previewwait .pw-spin { width:3vh; height:3vh; border:.34vh solid rgba(0,184,176,.22);
border-top-color:var(--tealbri); border-radius:50%; animation:pwspin .8s linear infinite; }
#previewwait .pw-text { color:var(--hi); font-size:2.1vh; font-weight:500; max-width:62ch;
line-height:1.55; padding:0 6vw; }
#previewwait .pw-text b { color:var(--tealbri); font-weight:600; }
#previewwait .pw-sub { color:var(--lo); font-size:1.5vh; max-width:62ch; padding:0 6vw; min-height:1.5vh; }
@keyframes pwspin { to { transform:rotate(360deg); } }
.kicker.v1 { color:var(--lo); letter-spacing:.5em; margin-left:.5em; }
.herobox { width:100%; max-height:66vh; display:flex; align-items:center; justify-content:center; }
.herobox > * { max-width:100%; max-height:66vh; object-fit:contain;
border:1px solid rgba(255,255,255,.07); border-radius:14px; background:#000;
box-shadow:0 3.5vh 8vh rgba(0,0,0,.62), 0 0 0 1px rgba(0,184,176,.12), 0 0 14vh rgba(0,184,176,.06); }
.title { margin-top:1.8vh; font-size:4.6vh; font-weight:600; letter-spacing:-.022em;
line-height:1.06; color:var(--hi); }
/* description: left-aligned, given room (up to 6 lines) so it isn't cut off at line 3 anymore */
.brief { margin-top:1.6vh; max-width:52ch; font-size:1.85vh; line-height:1.55; color:var(--lo);
font-weight:300; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:6; overflow:hidden; }
.label { margin-top:3vh; font-size:1.05vh; font-weight:600; letter-spacing:.5em;
text-transform:uppercase; color:var(--lo); }
/* CTA pill, wraps to at most 2 lines (no mid-word cut); left-aligned in the text column. */
.promptbox { margin-top:1.1vh; padding:1.1vh 1.7vw; border:1px solid rgba(0,184,176,.42);
border-radius:16px; background:rgba(0,184,176,.1); color:var(--tealbri); font-size:1.75vh;
font-weight:500; letter-spacing:.005em; max-width:46ch; line-height:1.4; text-align:left;
display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; }
.foot { position:absolute; left:0; right:0; bottom:3.4vh; font-size:1.35vh; letter-spacing:.04em;
color:#aeb8c2; text-align:center; }
/* Intro splash, a LEFT-ALIGNED editorial layout (logo → eyebrow → headline → sub → live status,
on a teal accent rail) over a soft off-center glow, with a big faded brand mark anchoring the
right and a bottom control bar of real keycaps. Held until data is ready, then cross-faded. */
#intro { position:fixed; inset:0; z-index:5; opacity:1; overflow:hidden;
background:radial-gradient(70% 90% at 26% 42%, #17212f 0%, #0d1117 58%); }
/* A solid full-screen cover sits OVER the splash while its text + logo render underneath, then
fades off (800ms) to reveal them already-drawn, a simple rectangle composites perfectly smooth. */
/* Plain dark cover, the branded grey logo lives ONLY on the GDI+ fade-to-black phase (showing it
here too made it visibly jump, since GDI+ and the browser viewport scale it differently). */
#blackcover { position:fixed; inset:0; z-index:30; background:#0d1117; opacity:1;
transition:opacity .8s ease; pointer-events:none; will-change:opacity; }
#blackcover.gone { opacity:0; }
/* Rest layer: pure black covering everything once the display-off time arrives, so nothing burns in. */
#restlayer { position:fixed; inset:0; z-index:50; background:#000; display:none; }
/* FULL-DIM screensaver: the Adom logo + "AI Electronics Prototyping", barely visible, drifting slowly
(DVD-style bounce). During the full dim the billboards don't rotate, just this logo moves so it can't
burn in. Backlight is already at ~0 (host), so this faint white logo reads as a barely-there ghost. */
#dimsaver { position:fixed; left:0; top:0; z-index:55; display:none; text-align:center;
opacity:.14; will-change:transform; pointer-events:none; }
#dimsaver #dimlogo { width:15vw; max-width:280px; display:block; margin:0 auto;
filter:drop-shadow(0 0 1.4vh rgba(0,184,176,.12)); }
#dimsaver #dimtag { margin-top:1.6vh; color:#e6edf3; font-size:1.9vh; font-weight:500;
letter-spacing:.34em; text-transform:uppercase; white-space:nowrap; }
/* Fast dismiss fade, when the user moves the mouse on a billboard, the PAGE fades to black in 200ms
(GPU-composited CSS; fading the host window over a live WebView2 is slow + renders black), then the
host hides. z-index above everything so it covers content + the rest layer. */
#fadeout { position:fixed; inset:0; z-index:60; background:#000; opacity:0; pointer-events:none;
transition:opacity .5s ease; display:none; }
/* Billboard brand mark, ONE faint Adom "wing", crisp inline SVG (no blurry PNG scaling), bleeding
off the top-right corner like the wash of brand on a high-end glossy magazine ad (Drew/John's ask).
The 118vh square is anchored partly off-screen (top:-9vh, right:-22vh) so the single wing stays
full-bleed on the right/top/bottom while its left curve fades toward center; the gentle float keeps
well inside that overscan so it's ALWAYS full-bleed. opacity .035 = ~half as visible (closer to the
near-black bg). z-index:0 = behind #stage (content). Shown only during the rotation. */
/* Visibility-tinted FULL background: PUBLIC billboards stay the clean dark screensaver (no wash/frame);
PRIVATE / org-only billboards get an Adom TEAL wash + a teal screen frame (brand color), so a private
(-adom / -dev) page is unmistakable next to its public twin. Set per slide. */
#bgwash { position:fixed; inset:0; z-index:0; pointer-events:none; opacity:0; transition:opacity .55s ease; }
body.viz-pub #bgwash { opacity:0; }
body.viz-priv #bgwash { opacity:1;
background:radial-gradient(95% 115% at 50% 22%, rgba(0,184,176,.17), transparent 60%), linear-gradient(160deg, rgba(0,97,239,.10), transparent 52%);
box-shadow:inset 0 0 0 .5vh rgba(0,184,176,.5), inset 0 0 16vh rgba(0,184,176,.16); }
#bbmark { position:fixed; inset:0; margin:auto; width:220vh; height:220vh; opacity:.01; z-index:0;
pointer-events:none; display:none; will-change:transform; animation:bbmarkfloat 270s ease-in-out infinite; }
#bbmark svg { width:100%; height:100%; display:block; }
#bbmark path { fill:#ffffff; }
/* Very slow random-ish drift (270s loop): the big mark wanders smoothly so a different part bleeds
off each edge over time, barely-visible (97% transparent) brand wash that exercises the pixels. */
@keyframes bbmarkfloat {
0% { transform: translate(30vh, -10vh); }
20% { transform: translate(8vh, 30vh); }
40% { transform: translate(-30vh, 12vh); }
60% { transform: translate(-14vh, -30vh); }
80% { transform: translate(28vh, 22vh); }
100% { transform: translate(30vh, -10vh); }
}
.intro-mark { position:absolute; right:-7vw; top:50%; transform:translateY(-50%);
width:74vh; height:74vh; opacity:.05; z-index:0; pointer-events:none; }
.intro-grid { position:absolute; left:0; right:0; top:0; bottom:8.8vh; display:flex; align-items:center; z-index:1; }
.intro-content { margin-left:9vw; max-width:60vw; padding-left:2.7vw;
border-left:2px solid rgba(0,184,176,.5); }
#intrologo { width:10.5vw; max-width:220px; display:block; filter:drop-shadow(0 0 28px rgba(0,184,176,.15)); }
.intro-tagline { margin-top:1.7vh; color:var(--tealbri); font-size:2.15vh; font-weight:500;
font-style:italic; letter-spacing:.01em; }
.intro-eyebrow { margin-top:2.4vh; color:var(--teal); letter-spacing:.44em; font-size:1.2vh;
font-weight:600; text-transform:uppercase; }
.intro-head { margin-top:1.4vh; max-width:17ch; color:var(--hi); font-size:4.3vh; font-weight:600;
letter-spacing:-.015em; line-height:1.12; }
.intro-head #introdev { color:var(--tealbri); }
.intro-sub { margin-top:1.8vh; max-width:46ch; color:var(--lo); font-size:1.65vh; line-height:1.6; }
.intro-sub b { color:var(--hi); font-weight:600; }
/* Live "nerd stats", power, screen-off + sleep countdowns, keep-awake internals, cache growth. */
/* A real 2-column grid so every label and every value lines up in rigid columns (a table). */
.intro-stats { margin-top:2.7vh; display:grid; grid-template-columns:max-content 1fr;
column-gap:1.9vw; row-gap:1.15vh; align-items:baseline; max-width:68ch; font-size:1.6vh; line-height:1.34; }
.stat { display:contents; }
.stat-k { white-space:nowrap; color:var(--lo); letter-spacing:.06em; text-transform:uppercase;
font-size:1.18vh; font-weight:600; }
.stat-k::before { content:""; display:inline-block; width:.62vh; height:.62vh; border-radius:50%;
background:var(--teal); margin-right:.8vw; vertical-align:middle; box-shadow:0 0 .7vh rgba(0,184,176,.55); }
.stat-v { color:var(--hi); font-weight:500; font-variant-numeric:tabular-nums; }
.stat-v b { color:var(--tealbri); font-weight:700; }
.stat-v .mono { font-family:"Cascadia Code","Consolas","Courier New",monospace; font-size:.94em; color:var(--tealbri); letter-spacing:-.01em; }
.nowtag { color:var(--tealbri); font-weight:700; font-size:.82em; letter-spacing:.04em; }
/* dual-mode (plugged-in vs battery) value chips: MODE label first, value bold, current mode highlighted.
Replaces the old "<b>Never</b> plugged in" string that read as "never plugged in". */
.mcwrap { display:inline-flex; flex-wrap:wrap; gap:.65vw; }
.mc { display:inline-flex; align-items:baseline; gap:.55vw; padding:.32vh 1.05vw; border-radius:999px;
border:1px solid var(--line); background:rgba(255,255,255,.018); }
.mc .mcl { color:var(--lo); font-size:.84em; letter-spacing:.01em; }
.mc .mcv { color:var(--hi); font-weight:700; font-variant-numeric:tabular-nums; }
.mc.on { border-color:rgba(0,184,176,.55); background:rgba(0,184,176,.1); }
.mc.on .mcv { color:var(--tealbri); }
.mc .mcn { color:var(--tealbri); font-size:.7em; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
/* the lid-behaviour rows are reassurances about how the screensaver manages the PC, set apart in amber */
.lidrow .stat-k { color:#f0c548; }
.lidrow .stat-k::before { background:#f0c548; box-shadow:0 0 .7vh rgba(240,197,72,.5); }
.lidrow .stat-v b { color:#f3d27a; }
/* persistent lid reassurance under each billboard's footer */
.lidnote { position:absolute; left:0; right:0; bottom:1.2vh; font-size:1.3vh; letter-spacing:.02em;
color:#c6cfd9; text-align:center; }
.lidnote b { color:#f3d27a; font-weight:700; }
.lidnote .rb { color:#f3d27a; font-weight:600; }
.rebootnudge { margin-top:1.9vh; display:inline-block; padding:.7vh 1.4vw; border-radius:8px;
background:rgba(240,197,72,.1); border:1px solid rgba(240,197,72,.42); color:#f3d27a;
font-size:1.42vh; font-weight:600; letter-spacing:.01em; }
.intro-status { margin-top:3.1vh; display:inline-flex; align-items:center; gap:.7vw;
padding:.85vh 1.5vw; border:1px solid rgba(0,184,176,.28); border-radius:999px;
background:rgba(0,184,176,.07); color:var(--tealbri); font-size:1.4vh; }
.pulse { width:.9vh; height:.9vh; border-radius:50%; background:var(--tealbri); flex:none;
animation:pulse 1.7s ease-out infinite; }
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(0,230,220,.45)} 70%{box-shadow:0 0 0 1.1vh rgba(0,230,220,0)} 100%{box-shadow:0 0 0 0 rgba(0,230,220,0)} }
.intro-bar { position:absolute; left:0; right:0; bottom:0; display:flex; align-items:center;
justify-content:space-between; gap:3vw; padding:2.3vh 4vw; border-top:1px solid var(--line);
background:linear-gradient(0deg, rgba(8,11,16,.72), transparent); font-size:1.32vh; color:var(--lo); }
.bar-left, .bar-right { display:flex; align-items:center; gap:1vw; flex-wrap:wrap; }
.kgrp { display:inline-flex; align-items:center; gap:.35vw; }
.intro-bar kbd { font-family:inherit; font-size:1.28vh; font-weight:600; color:var(--hi);
background:#1b2330; border:1px solid #2c3645; border-bottom-width:2px; border-radius:6px;
padding:.4vh .85vw; box-shadow:0 1px 0 rgba(0,0,0,.45); }
.intro-bar kbd.soft { background:transparent; border-style:dashed; border-bottom-width:1px;
color:var(--lo); font-weight:500; }
.kgrp i { color:var(--dim); font-style:normal; font-size:1.1vh; }
.cap { color:var(--lo); }
.sep { color:var(--line); }
.bar-right b { color:var(--teal); font-weight:600; }
</style>
</head>
<body>
<div id="intro">
<img class="intro-mark" src="data:image/png;base64,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" alt="">
<div class="intro-grid">
<div class="intro-content">
<img id="intrologo" src="adom-wordmark.png" alt="Adom">
<div class="intro-tagline">Your life in creative mode</div>
<div class="intro-eyebrow">Vibe-coding screensaver</div>
<h1 class="intro-head">Keeps your <span id="introdev">laptop</span> working while you step away.</h1>
<p class="intro-sub">Your <b>/remote-control</b> sessions keep running. Here's what your machine is doing:</p>
<div class="intro-stats" id="introstats">
<div class="stat"><span class="stat-k">Logged in</span><span class="stat-v" id="st-login">checking the Adom Wiki…</span></div>
<div class="stat"><span class="stat-k">Power</span><span class="stat-v" id="st-power">…</span></div>
<div class="stat"><span class="stat-k">Display off</span><span class="stat-v" id="st-screen">…</span></div>
<div class="stat"><span class="stat-k">Sleep</span><span class="stat-v" id="st-sleep">…</span></div>
<div class="stat lidrow"><span class="stat-k" id="lk-open">Lid open</span><span class="stat-v" id="st-lidopen">…</span></div>
<div class="stat lidrow" id="row-lidclosed"><span class="stat-k" id="lk-closed">Lid closed</span><span class="stat-v" id="st-lidclosed">…</span></div>
</div>
<div id="rebootnudge" class="rebootnudge" style="display:none">⟳ One restart needed to switch the lid & stay-awake settings above on</div>
<div class="intro-status"><span class="pulse"></span><span id="introstatus">Fetching the latest drops from the Adom Wiki…</span></div>
</div>
</div>
<div class="intro-bar">
<div class="bar-left">
<span class="kgrp"><kbd>Win</kbd><i>+</i><kbd>Shift</kbd><i>+</i><kbd>L</kbd></span><span class="cap">launch anytime</span>
<span class="sep">·</span>
<kbd class="soft">move mouse / any key</kbd><span class="cap">dismiss</span>
</div>
<div class="bar-right">
<span class="cap">turn it off, ask Claude <b>“turn off the Adom screensaver”</b></span>
</div>
</div>
</div>
<div id="bgwash" aria-hidden="true"></div>
<div id="bbmark" aria-hidden="true"><svg viewBox="0 0 1061.33 1061.38" preserveAspectRatio="xMidYMid meet"><path d="M957.65,103.68c-116.2-116.2-282.77-138.02-372.05-48.73-36.23,36.23-54.16,85.18-54.92,138.34h0s0,.07,0,.07c0,.17,0,.35,0,.52v329.6c0,3.99,3.24,7.23,7.23,7.23h326.06c54.75.14,105.25-17.79,142.42-54.97,89.28-89.28,67.46-255.85-48.74-372.05Z"/><path d="M103.68,957.7c116.2,116.2,282.77,138.02,372.05,48.73,36.23-36.23,54.16-85.18,54.92-138.34h0s0-.07,0-.07c0-.17,0-.35,0-.52v-329.6c0-3.99-3.24-7.23-7.23-7.23H197.37c-54.75-.14-105.25,17.79-142.42,54.97-89.28,89.28-67.46,255.85,48.74,372.05Z"/></svg></div>
<div id="stage"></div>
<div id="previewwait">
<img class="pw-logo" src="adom-wordmark.png" alt="Adom">
<div class="pw-spin"></div>
<div class="pw-text" id="pwtext">Loading a billboard from the Adom Wiki…</div>
<div class="pw-sub" id="pwsub"></div>
</div>
<div id="blackcover"></div>
<div id="restlayer"></div>
<div id="dimsaver" aria-hidden="true"><img id="dimlogo" src="adom-wordmark.png" alt="Adom"><div id="dimtag">AI Electronics Prototyping</div></div>
<div id="fadeout"></div>
<script>
const HOST = "https://adom-screensaver.local/";
let HOLD = 4200; const FADE = 650; let PER = HOLD + FADE; // ms per slide (HOLD is host-configurable)
let INTRO_MS = 5000; // hold the WebView2 splash this long after it appears, THEN billboards (host-configurable)
// The .scr pushes the user's cascade timing (config dialog) right before revealing the splash.
window.adomTiming = function(t){
if (!t) return;
if (typeof t.splashMs === "number" && t.splashMs > 0) INTRO_MS = t.splashMs;
if (typeof t.slideMs === "number" && t.slideMs > 0) { HOLD = t.slideMs; PER = HOLD + FADE; }
};
let slides = [], idx = -1, started = 0, status = { power:"", off:-1 }, dimSet = false;
var me = null, authed = false; // logged-in identity (from the host's manifest) so we can show whose filtered view this is
// Live machine state (from adomStats) that the lid/idle copy depends on.
var onAc = true, sleepLeft = -1, deviceWord = "laptop";
function mmssShort(s){ if(s==null||s<0) return ""; var m=Math.floor(s/60), x=s%60; return m+":"+(x<10?"0":"")+x; }
// Single source of truth for the "what happens to /remote-control" story. Adapts to device (laptop vs
// PC), power (AC vs battery), and the live battery sleep countdown. The goal is to keep remote-control
// alive, while honestly conveying when the machine WILL sleep anyway.
function lidInfo(){
var dev = deviceWord || "laptop";
if (dev !== "laptop"){ // desktop / no battery / no lid
return { showClosed:false, openLabel:"When idle",
openText:"<b>"+dev+" stays awake</b>, screen just dims, so <b>/remote-control</b> stays up." };
}
var closed = { closedLabel:"Lid closed",
closedText:"<b>Sleeps</b> instantly (instant resume); hibernates after 30 min if left." };
if (onAc){
return Object.assign({ showClosed:true, openLabel:"Lid open",
openText:"<b>Stays awake</b>, screen dims to ~10% (not off), so <b>/remote-control</b> stays up." }, closed);
}
var cd = mmssShort(sleepLeft);
return Object.assign({ showClosed:true, openLabel:"Lid open",
openText:"Screen off; <b>/remote-control</b> up "+(cd?("~<b>"+cd+"</b> more"):"a while")+", then sleeps to save battery." }, closed);
}
function applyLid(){
var info = lidInfo();
var kO=document.getElementById("lk-open"), vO=document.getElementById("st-lidopen");
var rowC=document.getElementById("row-lidclosed"), kC=document.getElementById("lk-closed"), vC=document.getElementById("st-lidclosed");
if(kO) kO.textContent = info.openLabel;
if(vO) vO.innerHTML = info.openText;
if(rowC) rowC.style.display = info.showClosed ? "" : "none";
if(info.showClosed){ if(kC) kC.textContent = info.closedLabel; if(vC) vC.innerHTML = info.closedText; }
}
function esc(s){ return (s==null?"":String(s)).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
function orgList(){ try { return (me && me.orgs && me.orgs.length) ? me.orgs.join(", ") : ""; } catch(e){ return ""; } }
function applyMe(){
var el = document.getElementById("st-login"); if(!el) return;
if (!authed){ el.innerHTML = "not logged in, <b>public</b> entries only"; return; }
var who = me && me.name ? esc(me.name) : "you";
var orgs = orgList();
el.innerHTML = "<b>" + who + "</b>" + (orgs ? (" · <b>" + esc(orgs) + "</b> org") : "") + " · your filtered view";
}
let revealedAt = 0;
// Fade the splash up from black. WebView2 defers rendering AND image decode while it's hidden, so if
// we fade the instant it appears, the logo/text aren't painted yet and pop in late (the "blip"). So:
// once the page is actually visible, hold the dark (opacity:0) frame ~420ms to let it render + decode
// the logo/text/watermark, THEN flip to .in so everything rises out of black together, already drawn.
var introFadeScheduled = false;
function startIntroFade(){
if (introFadeScheduled) return; introFadeScheduled = true;
// Hold the black cover while the now-visible page renders + decodes the logo/text underneath,
// THEN fade the cover off over 800ms to reveal the already-drawn splash (no content pop).
setTimeout(function(){
var cov = document.getElementById("blackcover");
if (!cov) return;
requestAnimationFrame(function(){ requestAnimationFrame(function(){
cov.classList.add("gone");
setTimeout(function(){ cov.style.display = "none"; }, 850); // remove after the 800ms fade so it can't cover the billboards
}); });
}, 420);
}
// visibilitychange is the reliable "rendering has resumed" signal when the host un-hides the WebView2.
document.addEventListener("visibilitychange", function(){ if (document.visibilityState === "visible") startIntroFade(); });
// The host calls this when it reveals the WebView2 (live) or when a capture nav completes, a backup
// trigger in case this WebView2 config doesn't emit visibility events.
window.adomRevealed = function(){
if (revealedAt) return;
revealedAt = Date.now();
startIntroFade();
};
// The host calls this every second with live power + seconds-to-display-off.
window.adomStatus = function(power, off){ status.power = power || ""; status.off = (off==null?-1:off); paintFoot(); };
// The host streams what the loader is doing right now (so the intro always explains itself).
window.adomLoading = function(text, device){
const dv = document.getElementById("introdev"); if(dv && device) dv.textContent = device;
// While a preview is still waiting for its first billboard, stream the live load status into the sub-line
// so the user can see it's actively working (downloading, reusing cache, etc.), not hung.
try {
var pw = document.getElementById("previewwait");
if (previewMode && pw && pw.style.display !== "none"){ var s = document.getElementById("pwsub"); if (s && text) s.textContent = text; }
} catch(e){}
};
function mmss(s){ if(s<0) return ""; const m=Math.floor(s/60), x=s%60; return m+":"+(x<10?"0":"")+x; }
function fmtBytes(b){ b=b||0; if(b<1024) return b+" B"; if(b<1048576) return (b/1024).toFixed(0)+" KB"; return (b/1048576).toFixed(1)+" MB"; }
// Reboot nudge: the saver reports rebootPending=true when Modern Standby is set to disabled but a restart
// is still needed to make it (and the lid/stay-awake config) actually take effect. Surface it gently on
// the splash and on every billboard so the saver never claims a state the PC isn't actually in yet.
var rebootPending = false;
function lidnoteHTML(){
var dev = deviceWord || "laptop", base;
if (dev !== "laptop"){
base = "<b>When idle</b> → "+dev+" awake, screen dims, <b>/remote-control stays up</b>";
} else if (onAc){
base = "<b>Lid open</b> → awake, screen dims to ~10%, <b>/remote-control stays up</b> · <b>Lid closed</b> → sleeps, resumes instantly";
} else {
var cd = mmssShort(sleepLeft);
base = "<b>Lid open · battery</b> → screen off, <b>/remote-control up "+(cd?("~"+cd+" more"):"")+"</b> then sleeps to save battery · <b>Lid closed</b> → sleeps";
}
return rebootPending ? ("<span class='rb'>⟳ Restart once to switch these on</span> · " + base) : base;
}
function applyRebootNudge(){
var n = document.getElementById("rebootnudge"); if(n) n.style.display = rebootPending ? "inline-block" : "none";
var cur = document.querySelector("#stage .lidnote"); if(cur) cur.innerHTML = lidnoteHTML();
}
// The host pushes a full live snapshot every second; we render the splash's nerd-stat panel from it.
window.adomStats = function(s){
try {
rebootPending = !!s.rebootPending; onAc = !!s.ac;
sleepLeft = (typeof s.sleepLeft === "number") ? s.sleepLeft : -1;
deviceWord = s.device || "laptop";
applyLid(); applyRebootNudge();
var set = function(id,html){ var el=document.getElementById(id); if(el) el.innerHTML=html; };
// duration formatter: 0 = Never, <0/absent = "?", else minutes (or m+s)
var dur = function(x){ if(x==null||x<0) return "?"; if(x===0) return "Never"; if(x<60) return x+" sec"; if(x%60===0) return (x/60)+" min"; return Math.floor(x/60)+" min "+(x%60)+"s"; };
// Two clean chips, MODE label first so the value can't read as the mode ("Plugged in: Never",
// not "Never plugged in"); the mode you're in right now is highlighted and tagged.
var chip = function(label,val,active){
return "<span class='mc"+(active?" on":"")+"'><span class='mcl'>"+label+"</span>"
+ "<span class='mcv'>"+dur(val)+"</span>" + (active?"<span class='mcn'>now</span>":"") + "</span>";
};
var pair = function(ac,dc){
return "<span class='mcwrap'>"+chip("Plugged in",ac,s.ac)+chip("On battery",dc,!s.ac)+"</span>";
};
set("st-power", s.ac ? "<b>Plugged in</b>" : ("On battery · <b>"+(s.pct<=100?s.pct+"%":"?")+"</b>"));
set("st-screen", pair(s.displayOffAc, s.displayOffDc));
set("st-sleep", pair(s.sleepAc, s.sleepDc));
} catch(e){}
};
function footText(s, i, n){
// Power-aware: keep-awake actively holds the machine only when plugged in; on
// battery it stays passive and Windows' own idle-sleep timer is the give-up clock.
const onAc = /plug/i.test(status.power||"");
const ka = status.power ? (onAc ? " · keep-awake on" : " · sleeps when idle") : "";
const right = (status.power||"") + (status.off>=0 ? " · display turns off in "+mmss(status.off) : "") + ka;
const seen = " · shown " + getCount(s.slug) + "×"; // lifetime view count (proves the deprioritizer is working)
// the priority score that ordered this slide (higher = shown sooner), with its breakdown so it's transparent
var fS = freshScore(s.updated), nS = noveltyScore(s.slug);
const score = " · priority " + Math.round(s._score != null ? s._score : (fS + nS)) + " (fresh " + fS + " + new " + nS + ")";
const as = (me && me.name) ? " · viewing as " + me.name : "";
return s.type + " " + (i+1) + " / " + n + seen + score + as + " " + right;
}
function paintFoot(){
const cur = document.querySelector("#stage .foot");
if (cur && cur._slide) cur.textContent = footText(cur._slide, cur._i, slides.length);
}
// Drop a slide whose hero failed to load (e.g. a stale cached pointer that now 404s) so it
// never shows a broken icon.
function dropSlide(slug){ const i = slides.findIndex(s=>s.slug===slug); if(i>=0) slides.splice(i,1); }
function mediaEl(s){
const src = /^https?:/i.test(s.media) ? s.media : HOST + encodeURIComponent(s.media);
// SELF-HEAL: the manifest carries `s.src` = the remote wiki hero URL. If the LOCAL cached file is missing or
// broken (pruned, never finished downloading, disk wipe), don't show a blank card -- swap to the remote URL
// and stream the hero straight from the wiki. Only if BOTH fail do we drop the slide. (The next fetch also
// re-downloads the missing file, so this is belt-and-suspenders: stream now, repopulate the cache later.)
const remote = s.src && /^https?:/i.test(s.src) ? s.src : "";
function fallback(el){ if (remote && el.src !== remote){ el.src = remote; return true; } return false; }
// wiki.adom.inc/blob has Cloudflare HOTLINK PROTECTION: a cross-origin <img>/<video> request carrying a
// Referer of this page's origin gets a 403 challenge (curl/.NET with no Referer get 200). So when we stream
// a hero straight from the wiki (the self-heal path, or any un-cached remote media) we MUST send no Referer,
// exactly like curl does -- otherwise the fallback silently fails and the card still goes blank.
if (s.kind === "video"){
const v = document.createElement("video"); v.referrerPolicy = "no-referrer";
v.src = src; v.muted = true; v.defaultMuted = true; v.volume = 0; v.loop = true;
v.autoplay = true; v.setAttribute("playsinline",""); v.preload = "auto";
v.addEventListener("canplay", ()=>{ try{ v.play(); }catch(e){} });
v.onerror = ()=>{ if (!fallback(v)) dropSlide(s.slug); };
return v;
}
const img = document.createElement("img"); img.referrerPolicy = "no-referrer"; img.src = src; // <img> renders svg + raster
img.onerror = ()=>{ if (!fallback(img)){ img.style.display="none"; dropSlide(s.slug); } };
return img;
}
function buildSlide(s, i){
const d = document.createElement("div"); d.className = "slide";
const v1 = s.source === "v1";
// Visibility badge (privacy QA + teaches the wiki's 3 audiences, confirmed in git-wiki auth.js):
// public -> anyone
// private + an org -> only that org's members
// private + no org -> the author ALONE (personal-private)
// With your token the saver sees private pages too, so this is the page's REAL audience.
const vis = (s.vis || "public").toLowerCase();
const pub = vis === "public";
const orgPriv = !pub && !!(s.org && (""+s.org).length);
const vb = document.createElement("div"); vb.className = "vbadge " + (pub ? "pub" : "priv");
if (pub)
vb.innerHTML = "Public<span class='sub'>· anyone can see & install it</span>";
else if (orgPriv)
vb.innerHTML = "Private · " + esc(s.org) + " org<span class='sub'>· only members of the " + esc(s.org) + " org can see it</span>";
else
vb.innerHTML = "Private · personal<span class='sub'>· only " + esc(s.author || s.owner || "the owner") + " can see it (not shared with any org)</span>";
// Freshness chip (from the page's last-updated date), grouped with the badge in one meta row so the
// eye catches NEW content. Teal for recent, dim for old.
var flabel = freshLabel(s.updated);
const mr = document.createElement("div"); mr.className = "metarow";
// Type capsule FIRST (App / Skill / Component) so the KIND of repo reads instantly, before visibility.
var tp = (s.type || "").toLowerCase();
if (tp === "app" || tp === "skill" || tp === "component"){
const tc = document.createElement("div"); tc.className = "typecap t-" + tp;
tc.textContent = tp.charAt(0).toUpperCase() + tp.slice(1);
mr.appendChild(tc);
}
mr.appendChild(vb);
if (flabel){ const fr = document.createElement("div"); fr.className = "freshchip " + freshClass(s.updated); fr.textContent = flabel; mr.appendChild(fr); }
// Wiki identity block (replaces the old kicker): the owner-qualified URL + org/personal + author, so you
// can verify the entry is posted right. kyle/x = personal; adom/x = upgraded to Adom-org-owned.
var owner = s.owner || s.org || "";
var orgOwned = !!(s.org && (""+s.org).length);
const idb = document.createElement("div"); idb.className = "wikiid";
idb.innerHTML =
'<div class="wurl"><span class="wdim">wiki.adom.inc/</span>' +
'<span class="' + (orgOwned ? "worg" : "wuser") + '">' + esc(owner) + '</span>' +
'<span class="wdim">/</span>' + esc(s.slug) + '</div>' +
'<div class="wmeta">' +
(orgOwned ? ('<b class="worg">' + esc(s.org) + '</b> org-owned') : 'personal page') +
(s.author ? (' · by ' + esc(s.author)) : '') + '</div>';
const hb = document.createElement("div"); hb.className = "herobox"; hb.appendChild(mediaEl(s));
const t = document.createElement("div"); t.className = "title"; t.textContent = s.title || s.slug;
const b = document.createElement("div"); b.className = "brief"; b.textContent = s.brief || "";
const lab = document.createElement("div"); lab.className = "label"; lab.textContent = "Just ask Claude";
// Install method is a Claude prompt, never a command line. Use the author's prompt only if it's already
// natural language; otherwise (empty, or a CLI line like "install x"/"adompkg ...") synthesize one.
var raw = (s.prompt || "").trim();
var natural = raw && !/^(install\s|adompkg\b|npm\b|sudo\b|cargo\b|pip\b|curl\b)/i.test(raw);
// trim a trailing tagline (a "Name, then tagline" or "Name: tagline" title) so the prompt uses the clean name
var name = (s.title || s.slug).split(/\s+[\u2014\u2013\u00b7:|]\s+/)[0].trim();
var ip = natural ? raw : ("Install " + name);
const pb = document.createElement("div"); pb.className = "promptbox"; pb.textContent = "“" + ip + "”";
const f = document.createElement("div"); f.className = "foot"; f._slide = s; f._i = i; f.textContent = footText(s, i, slides.length);
const ln = document.createElement("div"); ln.className = "lidnote";
ln.innerHTML = lidnoteHTML();
// Two columns: text on the left, the hero shot on the right (foot + lidnote stay absolute at the bottom).
const colText = document.createElement("div"); colText.className = "coltext";
colText.append(mr, idb, t, b, lab, pb);
const colMedia = document.createElement("div"); colMedia.className = "colmedia";
colMedia.appendChild(hb);
d.append(colText, colMedia, f, ln);
return d;
}
function showSlide(i){
const stage = document.getElementById("stage");
const s = slides[i]; if(!s) return;
try { showPreviewWait(false); } catch(e){} // a billboard is rendering now -> drop any preview wait screen
// visibility-tinted background: PUBLIC -> teal wash, PRIVATE/org -> amber wash + frame
var pub = ((s.vis || "public").toLowerCase() === "public");
document.body.classList.toggle("viz-priv", !pub);
document.body.classList.toggle("viz-pub", pub);
bumpCount(s.slug); // count this view so the entry is deprioritized next time
// fade the stage OUT, swap content, fade IN, so text never overlaps mid-transition.
stage.style.opacity = "0";
setTimeout(()=>{
stage.innerHTML = "";
const el = buildSlide(s, i);
stage.appendChild(el);
const vid = el.querySelector("video"); if(vid){ try{ vid.currentTime=0; vid.play(); }catch(e){} }
stage.style.opacity = "1";
}, 460);
}
let introHidden = false;
function hideIntro(){
if (location.hash.indexOf("holdintro") >= 0) return; // capture mode: keep the explanation splash up
if (introHidden) return; introHidden = true;
const el = document.getElementById("intro");
// The 3s transition is the intro fade-IN; for the hand-off to billboards use a quick fade-OUT.
el.style.transition = "opacity .7s ease";
el.style.opacity = "0"; setTimeout(()=>{ el.style.display = "none"; }, 760);
}
// Reveal the faint full-bleed Adom wing behind the billboards (inline SVG, crisp at any scale).
// Start it at a RANDOM point along its existing 270s drift loop (and a random direction) so the wing
// sits in a different spot every launch and never feels repetitive. A negative animation-delay just
// fast-forwards into the SAME keyframe path, so it stays inside the full-bleed constraints already
// defined by bbmarkfloat, we only change WHERE on that path it begins, never the path itself.
function showMark(){
var bbm = document.getElementById("bbmark");
if (!bbm) return;
bbm.style.animationDelay = (-Math.random() * 270).toFixed(2) + "s"; // random phase of the loop
bbm.style.animationDirection = Math.random() < 0.5 ? "reverse" : "normal"; // same path, either way
bbm.style.display = "block";
}
// --- per-entry show-count tracking so the rotation DEPRIORITIZES entries you've already seen a lot. A
// short saver session only ever shows the first chunk of the order, so a pure random shuffle kept
// surfacing the same handful; ordering by lifetime show-count instead leads with the least-seen each run.
// Counts come from the C# HOST (it writes showcounts.json synchronously on each show) rather than
// localStorage, which a short-lived, sometimes hard-killed screensaver can't flush reliably. We seed the
// in-memory map from that file at load, order by it, and report each show back so the host persists it.
var counts = {};
function getCount(slug){ return (slug && counts[slug]) ? counts[slug] : 0; }
function bumpCount(slug){ if(!slug) return; counts[slug] = (counts[slug]||0) + 1; try { window.chrome.webview.postMessage("shown:" + slug); } catch(e){} }
async function loadCounts(){ try { const r = await fetch(HOST + "showcounts.json?_=" + Date.now(), {cache:"no-store"}); counts = (await r.json()) || {}; } catch(e){} }
// --- Freshness (from the page's last-updated timestamp, carried in the manifest) ---
function ageDays(updated){
if(!updated) return 1e9;
var t = Date.parse(updated); if(isNaN(t)) return 1e9;
return (Date.now() - t) / 86400000;
}
function freshLabel(updated){
var d = ageDays(updated);
if(d >= 1e9) return "";
if(d < 1) return "New today";
if(d < 2) return "Updated yesterday";
if(d < 7) return "Updated " + Math.round(d) + " days ago";
if(d < 14) return "Updated last week";
if(d < 60) return "Updated " + Math.max(2, Math.round(d/7)) + " weeks ago";
if(d < 730) return "Updated " + Math.max(2, Math.round(d/30)) + " months ago";
return "Updated " + Math.round(d/365) + (Math.round(d/365)===1?" year ago":" years ago");
}
function freshClass(updated){ var d = ageDays(updated); return d < 7 ? "fresh-new" : (d < 30 ? "fresh-recent" : "fresh-old"); }
// --- Scoring: lead with NEW + UNSEEN, sink OLD + over-seen. Documented in the wiki README. ---
// freshness: <7d +100, <30d +60, <90d +35, <365d +15, older/unknown +5
// novelty: max(0, 60 - 20*timesShown) (never-seen +60; gone after ~3 views)
// jitter: +0..12 so order varies run-to-run and ties never lock
function freshScore(updated){
var d = ageDays(updated);
if(d < 7) return 100; if(d < 30) return 60; if(d < 90) return 35; if(d < 365) return 15; return 5;
}
function noveltyScore(slug){ return Math.max(0, 60 - 20*(counts[slug]||0)); }
function slideScore(s){ return freshScore(s.updated) + noveltyScore(s.slug) + Math.random()*12; }
// Order the rotation by score, highest first, so new + never-seen drops lead and stale repeats fall back.
function shuffleSlides(){
slides.forEach(function(s){ s._score = slideScore(s); });
slides.sort(function(a,b){ return b._score - a._score; });
}
// The host calls this when the display-off time arrives: pause ALL video (releases Chromium's
// display wake-lock so the panel can finally power off) and blank to pure black so nothing burns in.
// The rotation stops (tick() bails on `resting`), so no new slide ever restarts a video.
var resting = false;
// Host calls this on a billboard dismiss (mouse move / key): fade the page to black in 200ms, then it
// hides the window. CSS opacity transition = smooth + fast (no host-window-over-WebView2 slowness).
window.adomFadeOut = function(){
var f = document.getElementById("fadeout");
if (!f) return;
f.style.display = "block";
void f.offsetWidth; // force reflow so the .2s transition runs from opacity 0
f.style.opacity = "1";
};
// The full-dim screensaver: the faint Adom logo + tagline drifting slowly around the screen, bouncing off
// the edges (so it covers the whole panel over time and can't burn in). Deliberately slow (~9 px/sec).
var dimDriftOn = false, dimRAF = 0, dimX = 0, dimY = 0, dimVX = 0, dimVY = 0, dimLast = 0;
function startDimDrift(){
if (dimDriftOn) return; dimDriftOn = true;
var el = document.getElementById("dimsaver"); if (!el) return;
el.style.display = "block";
var W = window.innerWidth, H = window.innerHeight, w = el.offsetWidth, h = el.offsetHeight;
dimX = (W - w) / 2; dimY = (H - h) / 2;
var SPEED = 9; // px/sec, deliberately slow ("really slowly")
var ang = 0.55 + 0.6 * ((Date.now() % 997) / 997); // vary the heading run-to-run so it isn't identical
dimVX = SPEED * Math.cos(ang); dimVY = SPEED * Math.sin(ang);
if (Math.abs(dimVY) < 2) dimVY = (dimVY < 0 ? -2 : 2); // guarantee real 2D travel, never near-horizontal
dimLast = 0;
var step = function(ts){
if (!dimLast) dimLast = ts;
var dt = Math.min(0.1, (ts - dimLast) / 1000); dimLast = ts; // time-based so speed is fps-independent
var W = window.innerWidth, H = window.innerHeight, w = el.offsetWidth, h = el.offsetHeight;
dimX += dimVX * dt; dimY += dimVY * dt;
if (dimX <= 0) { dimX = 0; dimVX = Math.abs(dimVX); } else if (dimX + w >= W) { dimX = W - w; dimVX = -Math.abs(dimVX); }
if (dimY <= 0) { dimY = 0; dimVY = Math.abs(dimVY); } else if (dimY + h >= H) { dimY = H - h; dimVY = -Math.abs(dimVY); }
el.style.transform = "translate(" + Math.round(dimX) + "px," + Math.round(dimY) + "px)";
dimRAF = requestAnimationFrame(step);
};
dimRAF = requestAnimationFrame(step);
}
window.adomRest = function(dim){
if (resting) return; resting = true;
// Drop any PRIVATE-repo tint left over from the last billboard (the teal #bgwash wash + inset screen frame).
// The rest/dim state is always the clean dark screensaver, so the frame must never linger behind the dimmed
// Adom logo. Removing viz-priv fades #bgwash out over its .55s transition.
try { document.body.classList.remove("viz-priv"); document.body.classList.add("viz-pub"); } catch(e){}
try { var vs = document.querySelectorAll("video"); for (var i=0;i<vs.length;i++){ try{ vs[i].pause(); }catch(e){} } } catch(e){} // pause video to save power
if (dim){
// PLUGGED IN: stop the billboards (they don't make sense dimmed) and show the barely-visible Adom logo
// + tagline drifting slowly. The .scr also drops the hardware BACKLIGHT to ~0 (the real "nearly black"
// on an LCD). Screen stays ON so the machine never dozes; the slow drift prevents burn-in.
try { hideIntro(); } catch(e){}
var st = document.getElementById("stage"); if (st) st.style.display = "none";
var bm = document.getElementById("bbmark"); if (bm) bm.style.display = "none";
document.documentElement.style.setProperty("--sdim", "0.04");
startDimDrift();
} else {
// ON BATTERY: blank to black; the host then powers the panel off to save battery.
var bb = document.getElementById("bbmark"); if (bb) bb.style.display = "none";
var r = document.getElementById("restlayer"); if (r) r.style.display = "block";
}
};
// Drew's ask #1, arrow-key navigation. The C# host's low-level keyboard hook swallows Left/Right
// and calls this (a normal keypress would instead dismiss the saver). dir = +1 next / -1 prev. We
// re-anchor `started` so the auto-rotation simply continues from the slide you landed on, and the
// chosen slide still gets a full dwell before it advances on its own.
window.adomNav = function(dir){
if (resting || slides.length === 0) return;
if (started === 0){ shuffleSlides(); started = Date.now(); idx = 0; hideIntro(); showMark(); } // first arrow also enters the rotation
idx = ((idx + dir) % slides.length + slides.length) % slides.length;
started = Date.now() - idx * PER; // align the clock to the manual pick (full dwell, then auto-advance resumes)
showSlide(idx);
};
// PREVIEW (config dialog "test a billboard" buttons): skip the logo + splash entirely and jump straight
// to a billboard. Drop the splash hold to 0 and hide the intro now; the first tick shows slide 0 the
// instant slides are loaded (the page bg stays plain-dark in the meantime, never the splash).
let previewMode = false, previewFilter = "";
window.adomPreview = function(filter){
previewMode = true; INTRO_MS = 0; previewFilter = filter || "";
// The page may have already loaded cached app/skill slides BEFORE this filter arrived. Strip them NOW so
// a "test a component billboard" can never flash an app/skill while the real component is still loading.
try { if (previewFilter) slides = slides.filter(previewKeep); } catch(e){}
try { hideIntro(); } catch(e){}
try { showPreviewWait(true); } catch(e){}
};
// A preview must never sit on a blank screen while it loads (a component's hero may take a few seconds to
// download). Tell the user what's happening; the host's live load status streams into the sub-line so it's
// visibly working, not stuck. Hidden the instant the first billboard renders (showSlide).
function previewBaseMsg(){
if (previewFilter === "component") return "Fetching a <b>component</b> billboard from the Adom Wiki. The first one has to download its hero image, so this can take a few seconds…";
if (previewFilter === "app") return "Fetching an <b>app</b> billboard from the Adom Wiki…";
if (previewFilter === "skill") return "Fetching a <b>skill</b> billboard from the Adom Wiki…";
if (previewFilter === "private") return "Fetching a <b>private</b> billboard from the Adom Wiki…";
return "Loading a billboard from the Adom Wiki…";
}
function showPreviewWait(on){
var el = document.getElementById("previewwait"); if (!el) return;
if (on){ var t = document.getElementById("pwtext"); if (t) t.innerHTML = previewBaseMsg(); el.style.display = "flex"; }
else el.style.display = "none";
}
// previewFilter narrows the rotation for the config "Test a ___ billboard" buttons:
// "private" -> private/org pages; "app"/"skill"/"component" -> that wiki type; "" -> no filter.
function previewKeep(s){
if (!previewFilter) return true;
if (previewFilter === "private") return (s.vis || "public").toLowerCase() !== "public";
return (s.type || "").toLowerCase() === previewFilter;
}
function tick(){
if (resting) return; // display is resting, do nothing (no video, no repaint)
if (!revealedAt) return; // wait until the WebView2 is actually on screen
const now = Date.now();
if (now - revealedAt < INTRO_MS) return; // hold the WebView2 splash (0 in preview)
if (slides.length === 0) return; // not ready yet (preview: plain-dark page bg until slides land)
if (started === 0){
// preview mode: only EVER show the requested kind. If none of that kind has loaded yet, keep the wait
// screen up and return -- NEVER fall back to showing a different kind (that was the "component -> app" bug).
if (previewFilter){
slides = slides.filter(previewKeep);
if (!slides.length){ showPreviewWait(true); return; }
}
shuffleSlides(); started = now; idx = 0; showSlide(0); hideIntro(); showMark(); return;
}
const want = Math.floor((now - started)/PER) % slides.length;
if (want !== idx){ idx = want; showSlide(idx); hideIntro(); }
}
async function loadManifest(){
try{
const r = await fetch(HOST + "manifest.json?_=" + Date.now(), {cache:"no-store"});
const j = await r.json();
if (j && typeof j.dimPct === "number" && !dimSet){ document.documentElement.style.setProperty("--sdim", (j.dimPct/100).toFixed(2)); dimSet = true; }
if (j){ if (j.me) me = j.me; authed = !!j.authed; applyMe(); }
let incoming = j && j.slides ? j.slides : [];
if (location.hash.indexOf("onlyvideo") >= 0) incoming = incoming.filter(s=>s.kind==="video"); // verification mode
// preview: only let the requested kind into the rotation (private, or a wiki type)
if (previewFilter) incoming = incoming.filter(previewKeep);
// Append NEW slugs, and REFRESH IN PLACE any slug whose hero/content changed since we loaded it.
// Why in-place refresh matters: at launch the page instantly shows last run's CACHED slides, then the
// fresh wiki fetch downloads updated heroes into the manifest. Without this, a slide already on screen
// kept its OLD hero until the NEXT launch (the one-launch lag that made a hero updated "this morning"
// not appear). Now an updated hero shows within ~3s, same session, WITHOUT reordering the rotation.
const idxBySlug = {}; slides.forEach((s,i)=>{ idxBySlug[s.slug]=i; });
incoming.forEach(s=>{
const at = idxBySlug[s.slug];
if (at === undefined){ idxBySlug[s.slug]=slides.length; slides.push(s); } // new -> append
else {
const cur = slides[at];
if (cur && (cur.media !== s.media || cur.updated !== s.updated)){ // changed -> refresh
slides[at] = s;
if (at === idx) showSlide(at); // it's the card on screen right now -> repaint with the new hero
}
}
});
// and harden: strip anything that slipped in earlier, so the rotation can NEVER drift off the filter
if (previewFilter) slides = slides.filter(previewKeep);
}catch(e){}
}
(async function(){
await loadCounts(); // seed show-counts from the host's file BEFORE the first ordering
await loadManifest();
setInterval(loadManifest, 3000); // pick up slides as the host downloads more
setInterval(tick, 200);
setTimeout(function(){ window.adomRevealed(); }, 9000); // safety: never hang on the fallback splash
// Tell the host about input so it can run the adaptive wake (it fades the window out + exits).
const wake = ()=>{ try{ window.chrome.webview.postMessage("input"); }catch(e){} };
let armed = false; setTimeout(()=>{ armed = true; }, 1200); // grace so launch doesn't self-dismiss
// Arrow keys NAVIGATE the billboards (and jump straight in from the splash) and NEVER dismiss.
// Normally the C# low-level hook swallows arrows before they reach the page; this is the backup
// path (and what makes arrows work even if the hook isn't active).
window.addEventListener("keydown", function(e){
if (e.key === "ArrowRight" || e.key === "ArrowLeft"){
if (window.adomNav) window.adomNav(e.key === "ArrowRight" ? 1 : -1);
return; // never wake/dismiss on arrows
}
if (armed) wake();
});
["mousemove","mousedown","wheel","touchstart"].forEach(ev=>
window.addEventListener(ev, ()=>{ if(armed) wake(); }, {passive:true}));
})();
</script>
</body>
</html>