Distribution site served by PocketBase itself (pb_public/, same origin): - Landing (/): create-account CTA, TV sign-in steps, login-gated PC downloads via the existing update manifest + file tokens - Wizard (/onboarding.html): invite code -> account -> named default profile -> family template fetched and encrypted IN THE BROWSER under the new user's password (PBKDF2-210k + AES-GCM, byte-compatible with the app's AddonConfigCrypto - a wizard blob decrypts in Dart, verified) -> pushed as their addon_config ciphertext. Adult fields never included. - /get/tv: public direct download of the latest clean Android APK (Downloader-friendly; adult builds stay account-gated) Backend: - onboarding_template collection (admin-only, maintained via admin UI) - AMBER_INVITE_CODE env gates BOTH users creation (X-Amber-Invite header or ?invite=) and the template route (per-IP rate limit 10/5min); fail-closed when unset. Note: this closes the previously-open in-app registration too. - Dockerfile ships pb_public + --publicDir; compose passes the env var Contract + Coolify steps (second domain amber.petruzalekr.cz) in docs/onboarding-contract.md. Verified locally end-to-end against pocketbase 0.39.6 (real-browser wizard run + Dart decrypt interop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
161 lines
7.2 KiB
HTML
161 lines
7.2 KiB
HTML
<!doctype html>
|
|
<html lang="cs">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<meta name="robots" content="noindex">
|
|
<title>Amber — filmy a seriály pro rodinu</title>
|
|
<style>
|
|
:root { color-scheme: dark; --bg:#0e0f13; --card:#191b21; --fg:#f2e9d8;
|
|
--muted:#9aa0aa; --amber:#f0a63c; --amber2:#c9791b; --err:#ff6b6b; --ok:#5fd08a;
|
|
--line:#2a2d36; }
|
|
* { box-sizing:border-box; }
|
|
body { margin:0; background:var(--bg); color:var(--fg);
|
|
font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
|
min-height:100dvh; display:flex; flex-direction:column; align-items:center; }
|
|
.wrap { width:100%; max-width:640px; padding:32px 20px 48px; }
|
|
.brand { display:flex; align-items:center; gap:12px; margin:18px 0 6px; }
|
|
.gem { width:40px; height:40px; }
|
|
h1 { font-size:34px; margin:0; color:var(--amber); letter-spacing:.5px; }
|
|
p.tag { margin:6px 0 30px; color:var(--muted); font-size:16px; line-height:1.5; }
|
|
.card { background:var(--card); border:1px solid var(--line); border-radius:16px;
|
|
padding:22px 20px; margin-bottom:18px; box-shadow:0 10px 40px rgba(0,0,0,.25); }
|
|
.card h2 { margin:0 0 6px; font-size:18px; }
|
|
.card p { margin:0 0 14px; color:var(--muted); font-size:14px; line-height:1.55; }
|
|
a.btn, button.btn { display:block; width:100%; text-align:center; padding:14px;
|
|
border:none; border-radius:10px; font-size:15px; font-weight:700;
|
|
text-decoration:none; cursor:pointer; }
|
|
.primary { background:var(--amber); color:#1b1206; }
|
|
.secondary { background:transparent; color:var(--amber); border:1px solid var(--amber2) !important; border:1px solid var(--amber2); }
|
|
.row { display:flex; gap:10px; }
|
|
.row > * { flex:1; }
|
|
code.url { display:inline-block; background:var(--bg); border:1px solid var(--line);
|
|
border-radius:8px; padding:6px 10px; font-family:ui-monospace,Menlo,Consolas,monospace;
|
|
font-size:14px; color:var(--amber); }
|
|
label { display:block; font-size:13px; color:var(--muted); margin:10px 0 5px; }
|
|
input { width:100%; padding:12px; border-radius:10px; border:1px solid var(--line);
|
|
background:var(--bg); color:var(--fg); font-size:15px; }
|
|
input:focus { outline:2px solid var(--amber); border-color:transparent; }
|
|
.msg { margin-top:12px; font-size:14px; min-height:18px; }
|
|
.msg.err { color:var(--err); }
|
|
.msg.ok { color:var(--ok); }
|
|
.hidden { display:none; }
|
|
.dl { display:flex; gap:10px; margin-top:12px; }
|
|
.dl a { flex:1; }
|
|
footer { color:var(--muted); font-size:12px; margin-top:8px; text-align:center;
|
|
padding-bottom:24px; }
|
|
ol { margin:0; padding-left:20px; color:var(--muted); font-size:14px; line-height:1.7; }
|
|
ol b { color:var(--fg); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<div class="brand">
|
|
<svg class="gem" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
<polygon points="50,4 90,32 76,92 24,92 10,32" fill="#c9791b"/>
|
|
<polygon points="50,4 90,32 50,50" fill="#f0a63c"/>
|
|
<polygon points="50,4 10,32 50,50" fill="#e9932a"/>
|
|
<polygon points="10,32 24,92 50,50" fill="#b06515"/>
|
|
<polygon points="90,32 76,92 50,50" fill="#8f4f0e"/>
|
|
<polygon points="24,92 76,92 50,50" fill="#a05a12"/>
|
|
</svg>
|
|
<h1>Amber</h1>
|
|
</div>
|
|
<p class="tag">Filmy, seriály a anime pro celou rodinu — česky, na televizi i na počítači.</p>
|
|
|
|
<div class="card">
|
|
<h2>Začínáš?</h2>
|
|
<p>Průvodce ti vytvoří účet a rovnou nastaví vše potřebné. Budeš potřebovat
|
|
<b>kód pozvánky</b> od Richarda.</p>
|
|
<a class="btn primary" href="/onboarding.html">Vytvořit účet</a>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Přihlásit televizi</h2>
|
|
<ol>
|
|
<li>Na televizi nainstaluj Amber — v prohlížeči nebo v aplikaci
|
|
<b>Downloader</b> otevři <code class="url" id="tvUrl">/get/tv</code></li>
|
|
<li>Spusť Amber a zvol <b>„Přihlásit z jiného zařízení“</b></li>
|
|
<li>Naskenuj QR kód telefonem, nebo pokračuj tlačítkem níže a opiš kód</li>
|
|
</ol>
|
|
<div style="height:14px"></div>
|
|
<a class="btn secondary" href="/device">Zadat kód z televize</a>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Stáhnout do počítače</h2>
|
|
<p>Přihlas se svým účtem — stažení je vázané na účet.</p>
|
|
<div id="dlform">
|
|
<label for="dlEmail">E-mail</label>
|
|
<input id="dlEmail" type="email" autocomplete="username">
|
|
<label for="dlPass">Heslo</label>
|
|
<input id="dlPass" type="password" autocomplete="current-password">
|
|
<div style="height:12px"></div>
|
|
<button class="btn primary" id="dlGo">Zobrazit stažení</button>
|
|
</div>
|
|
<div id="dlLinks" class="dl hidden"></div>
|
|
<div id="dlMsg" class="msg"></div>
|
|
</div>
|
|
|
|
<footer>Amber je soukromá rodinná aplikace.</footer>
|
|
</div>
|
|
|
|
<script>
|
|
document.getElementById("tvUrl").textContent = location.host + "/get/tv";
|
|
|
|
// Detect the visitor's platform to pre-order the download buttons.
|
|
function platforms(){
|
|
var ua = navigator.userAgent;
|
|
if (/Windows/i.test(ua)) return ["windows", "linux"];
|
|
if (/Linux|X11/i.test(ua) && !/Android/i.test(ua)) return ["linux", "windows"];
|
|
return ["windows", "linux"];
|
|
}
|
|
var NAMES = { windows: "Windows", linux: "Linux" };
|
|
|
|
async function api(method, path, body, token){
|
|
var opt = { method:method, headers:{} };
|
|
if (body){ opt.headers["Content-Type"]="application/json"; opt.body=JSON.stringify(body); }
|
|
if (token){ opt.headers["Authorization"]=token; }
|
|
var r = await fetch(path, opt);
|
|
var data = null; try { data = await r.json(); } catch(_){}
|
|
return { ok:r.ok, status:r.status, data:data };
|
|
}
|
|
|
|
document.getElementById("dlGo").addEventListener("click", async function(){
|
|
var msg = document.getElementById("dlMsg");
|
|
msg.className = "msg"; msg.textContent = "";
|
|
var email = document.getElementById("dlEmail").value.trim();
|
|
var pass = document.getElementById("dlPass").value;
|
|
if (!email || !pass){ msg.className="msg err"; msg.textContent="Zadej e-mail a heslo."; return; }
|
|
var auth = await api("POST", "/api/collections/users/auth-with-password",
|
|
{ identity: email, password: pass });
|
|
if (!auth.ok || !auth.data || !auth.data.token){
|
|
msg.className="msg err"; msg.textContent="Nesprávný e-mail nebo heslo."; return;
|
|
}
|
|
var token = auth.data.token;
|
|
var box = document.getElementById("dlLinks");
|
|
box.innerHTML = ""; box.classList.remove("hidden");
|
|
var found = 0;
|
|
for (const p of platforms()){
|
|
var m = await api("GET", "/api/update/manifest?platform=" + p, null, token);
|
|
if (!m.ok || !m.data || !m.data.available) continue;
|
|
var t = await api("POST", "/api/files/token", null, token);
|
|
if (!t.ok || !t.data || !t.data.token) continue;
|
|
var a = document.createElement("a");
|
|
a.className = "btn secondary";
|
|
a.href = m.data.downloadPath + "?token=" + encodeURIComponent(t.data.token);
|
|
a.textContent = NAMES[p] + " · " + m.data.version;
|
|
box.appendChild(a);
|
|
found++;
|
|
}
|
|
if (!found){
|
|
box.classList.add("hidden");
|
|
msg.className="msg err"; msg.textContent="Zatím není zveřejněná žádná verze.";
|
|
} else {
|
|
document.getElementById("dlform").classList.add("hidden");
|
|
msg.className="msg ok"; msg.textContent="Hotovo — vyber svůj systém.";
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|