Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
9.0 kB · 311 lines
HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <title>FF1 Pairing · KidLisp</title> <link rel="icon" href="https://assets.aesthetic.computer/kidlisp.com/favicon.ico" /> <style> @font-face { font-family: 'Berkeley Mono'; src: url('https://assets.aesthetic.computer/fonts/BerkeleyMono-Regular.woff2') format('woff2'); font-weight: 400; font-display: swap; } * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0a; font-family: "Berkeley Mono", "Fira Code", ui-monospace, monospace; color: #f5f5f5; } .container { width: 100%; height: 100%; display: flex; flex-direction: column; position: relative; } .header { position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 12px; padding: 16px; padding-top: max(16px, env(safe-area-inset-top)); background: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent); z-index: 10; } .ff1-logo { height: 24px; filter: invert(1); } .header-divider { width: 1px; height: 16px; background: rgba(255,255,255,0.3); } .kidlisp-logo { font-size: 13px; font-weight: bold; color: #a78bfa; } .camera-container { flex: 1; position: relative; overflow: hidden; } #video { width: 100%; height: 100%; object-fit: cover; } #canvas { display: none; } .overlay { position: absolute; inset: 0; pointer-events: none; display: flex; flex-direction: column; align-items: center; justify-content: center; } .scan-frame { width: min(280px, 70vw); height: min(280px, 70vw); border: 3px solid rgba(167, 139, 250, 0.8); border-radius: 24px; box-shadow: 0 0 0 9999px rgba(0,0,0,0.5); position: relative; } .scan-frame::before, .scan-frame::after { content: ''; position: absolute; width: 40px; height: 40px; border-color: #a78bfa; border-style: solid; border-width: 0; } .scan-frame::before { top: -3px; left: -3px; border-top-width: 4px; border-left-width: 4px; border-top-left-radius: 24px; } .scan-frame::after { bottom: -3px; right: -3px; border-bottom-width: 4px; border-right-width: 4px; border-bottom-right-radius: 24px; } .instructions { position: absolute; bottom: 0; left: 0; right: 0; padding: 24px 20px; padding-bottom: max(24px, env(safe-area-inset-bottom)); background: linear-gradient(to top, rgba(0,0,0,0.9), transparent); text-align: center; } .instructions h1 { font-size: 16px; margin-bottom: 8px; font-weight: 600; } .instructions p { font-size: 12px; opacity: 0.7; line-height: 1.5; } .status { margin-top: 12px; font-size: 13px; color: #a78bfa; min-height: 20px; } .status.success { color: #4ade80; } .status.error { color: #f87171; } .illustration { display: flex; align-items: center; justify-content: center; gap: 12px; margin: 8px 0; font-size: 24px; } .illustration .arrow { font-size: 16px; opacity: 0.5; } @keyframes pulse { 0%, 100% { opacity: 0.8; } 50% { opacity: 1; } } .scan-frame { animation: pulse 2s ease-in-out infinite; } </style> </head> <body> <div class="container"> <div class="header"> <img src="https://assets.aesthetic.computer/kidlisp.com/feralfile-logo.png" alt="Feral File" class="ff1-logo" /> <div class="header-divider"></div> <span class="kidlisp-logo">KidLisp</span> </div> <div class="camera-container"> <video id="video" autoplay playsinline muted></video> <canvas id="canvas"></canvas> <div class="overlay"> <div class="scan-frame"></div> </div> </div> <div class="instructions"> <h1>Scan TV QR Code</h1> <div class="illustration"> <span>📱</span> <span class="arrow">→</span> <span>📺</span> </div> <p>Point at the QR code on your FF1 screen</p> <div class="status" id="status">Initializing camera…</div> </div> </div>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> <script> const statusEl = document.getElementById('status'); const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d');
const params = new URLSearchParams(window.location.search); let token = params.get('token'); if (!token) { const parts = window.location.pathname.split('/').filter(Boolean); token = parts[parts.length - 1] || ''; }
function setStatus(message, type) { statusEl.textContent = message; statusEl.className = `status${type ? ' ' + type : ''}`; }
function extractTopicId(text) { if (!text) return null; // FF1 QR format: https://link.feralfile.com/device_connect/DEVICE_ID|TOPIC_ID|...|...|... if (text.includes('feralfile.com') && text.includes('|')) { const parts = text.split('|'); if (parts.length >= 2 && parts[1]) { return parts[1].trim(); } } // Fallback patterns const patterns = [ /topicID=([A-Za-z0-9_-]+)/i, /topicId=([A-Za-z0-9_-]+)/i, /topic=([A-Za-z0-9_-]+)/i, ]; for (const pattern of patterns) { const match = text.match(pattern); if (match && match[1]) return match[1]; } const trimmed = text.trim(); if (/^[A-Za-z0-9_-]{6,}$/.test(trimmed)) return trimmed; return null; }
async function submitTopicId(topicID) { if (!token) { setStatus('Missing pairing token.', 'error'); return; } if (!topicID) { setStatus('No Topic ID found.', 'error'); return; }
setStatus('Pairing…'); try { const response = await fetch('/api/ff1/pair', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, topicID }) }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.error || `HTTP ${response.status}`); } setStatus('✓ Paired! You can close this window.', 'success'); // Stop scanning if (video.srcObject) { video.srcObject.getTracks().forEach(t => t.stop()); } } catch (err) { setStatus(`Error: ${err.message}`, 'error'); } }
async function startCamera() { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }); video.srcObject = stream; await video.play(); setStatus('Scanning…'); requestAnimationFrame(scanFrame); } catch (err) { setStatus('Camera access denied.', 'error'); } }
function scanFrame() { if (video.readyState === video.HAVE_ENOUGH_DATA) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' }); if (code && code.data) { console.log('QR data:', code.data); const topicID = extractTopicId(code.data); if (topicID) { submitTopicId(topicID); return; } } } requestAnimationFrame(scanFrame); }
startCamera(); </script> </body></html>