From e5a4032710c74915c70c4226de79df1ad2930e4f Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:01:15 +0200 Subject: [PATCH 01/25] work in progress: UI overhaul updates, new logo, and localization translations --- LANDINGPAGE_UI_OVERHAUL_PLAN.md | 153 +++++++ eslint.config.mjs | 2 + website/app.js | 387 ++++++++++++++++ website/assets/NewLogoIcon_64.webp | Bin 1282 -> 1282 bytes website/locales/de.json | 6 + website/locales/en.json | 8 +- website/locales/es.json | 6 + website/locales/fr.json | 6 + website/locales/it.json | 6 + website/locales/ja.json | 8 +- website/locales/ko.json | 8 +- website/locales/nl.json | 6 + website/locales/pl.json | 6 + website/locales/pt-BR.json | 6 + website/locales/pt.json | 6 + website/locales/ru.json | 6 + website/locales/tr.json | 6 + website/locales/uk.json | 6 + website/locales/zh.json | 18 +- website/style.css | 692 +++++++++++++++++++++++++++++ website/template.html | 117 ++++- 21 files changed, 1442 insertions(+), 17 deletions(-) create mode 100644 LANDINGPAGE_UI_OVERHAUL_PLAN.md diff --git a/LANDINGPAGE_UI_OVERHAUL_PLAN.md b/LANDINGPAGE_UI_OVERHAUL_PLAN.md new file mode 100644 index 0000000..e6e8dea --- /dev/null +++ b/LANDINGPAGE_UI_OVERHAUL_PLAN.md @@ -0,0 +1,153 @@ +> **TEMPORÄR:** Diese Datei dient nur der gemeinsamen Planung des Landingpage-UI-Overhauls +> auf dem Branch `landingpage-ui-overhaul`. Sie ist **kein** Teil des finalen Features und +> **muss vor dem Öffnen des PRs wieder gelöscht werden**. + +# Landingpage UI Overhaul — Hero Mockup + +## Ausgangslage +Die Landingpage (`website/template.html`, `website/style.css`, `website/app.js`, kompiliert +via `website/build.cjs` nach `website/www/`) ist bereits sehr gut, soll aber **statisch und +backend-frei** bleiben. Einziger Fokus dieses Overhauls: das rechte Hero-Mockup +(`.hero-mockup-wrapper` / `.extension-mockup`, `template.html:304-579`). + +**Heutiger Zustand:** Ein 1:1-Nachbau des Extension-Popups mit 4 Tabs (Room / Sync / +Settings / Status). Tab-Wechsel nur per Klick (`app.js:391-407`), keine Animationen, kein +Auto-Play, kein Bezug zu den eigentlichen Video-Tabs, die synchronisiert werden. + +## Ziel +Ein dynamischeres, platzoptimiertes Hero-Visual, das das Kernversprechen von KoalaSync +("zwei Browser-Tabs synchron abspielen") direkt zeigt — mit echten Animationen, einem +automatischen Durchlauf beim ersten Sichtbarwerden und voller manueller Bedienbarkeit +danach. Auf Mobile (Bildschirmgröße) bleibt exakt das heutige, statische Popup-Mockup +erhalten — kein neues Mobile-Design nötig, kein zusätzlicher Wartungsaufwand dort. + +## Getroffene Entscheidungen (aus Rückfrage-Runde) + +1. **Layout:** Gestapelt/Layered — zwei Browser-Tab-Karten leicht versetzt im Hintergrund + (Tiefeneffekt via `transform`/`box-shadow`), Extension-Popup öffnet sich als Overlay + davor/darüber. +2. **Tab-Inhalt:** Abstraktes Video-Mock — Gradient-Placeholder als "Video", Play-Button, + Fortschrittsbalken, Timestamp. Kein echtes Videomaterial, keine Marken/Logos, im + bestehenden minimalistischen `mock-*`-Stil. +3. **Auto-Play-Verhalten:** Einmaliger Durchlauf beim Sichtbarwerden (IntersectionObserver), + danach reiner manueller Modus. Kein Endlos-Loop im Hintergrund. +4. **Popup-Umfang:** Alle 4 Tabs (Room/Sync/Settings/Status) bleiben inhaltlich unverändert + — nur in die neue Animations-/Overlay-Logik eingebettet, kein Content-Kahlschlag. + +## Vorgeschlagenes Storyboard (einmaliger Auto-Durchlauf) + +1. **Ausgangszustand:** Zwei Tab-Karten ("Tab A" / "Tab B") sichtbar, Video-Mocks pausiert, + Fortschrittsbalken bei 0. Popup geschlossen, nur ein kleiner Launcher-Button sichtbar. +2. Popup öffnet sich (scale/slide-in) vor den Tab-Karten. +3. Simulierter Klick auf "Play" im Sync-Tab → beide Tab-Karten starten gleichzeitig + (Fortschrittsbalken laufen synchron), begleitet von einem kurzen "Sync-Pulse" + (Glow/Connection-Line) zwischen den beiden Karten. +4. Nach ein paar Sekunden: simulierter Klick auf "Pause" → beide Tabs pausieren synchron. +5. Popup schließt sich, Endzustand bleibt stehen (beide Tabs pausiert, synchron, + "Synced"-Indikator dezent sichtbar) — kein Reset auf Ausgangszustand. +6. **Danach/parallel volle manuelle Steuerung:** + - Klick auf eine Tab-Karte togglet deren eigenes Play/Pause *unabhängig* — spielt eine + Karte allein, laufen die Fortschrittsbalken bewusst auseinander ("out of sync"), um den + Schmerzpunkt zu illustrieren, den KoalaSync löst. + - Klick auf Play/Pause im Popup synchronisiert immer beide Tabs. + - Popup lässt sich jederzeit manuell öffnen/schließen, Tabs (Room/Sync/Settings/Status) + bleiben wie heute klickbar. + - Jede manuelle Interaktion während des Auto-Durchlaufs bricht diesen sofort ab und + übergibt die volle Kontrolle. + +## Technischer Ansatz (Vorschlag) + +- **Keine neuen Dependencies** — reines Vanilla JS + CSS, passend zur "Zero-Dependency"- + Philosophie des Projekts (siehe `website/README.md`). +- **Eine "Director"-Funktion** in `app.js`, die exakt dieselben Click-Handler wie die + manuelle Bedienung aufruft (kein doppelter Code-Pfad zwischen Auto- und Manual-Modus). +- **IntersectionObserver** triggert den einmaligen Durchlauf beim ersten Sichtbarwerden. +- **`prefers-reduced-motion`**: Auto-Durchlauf und Cursor-Ripple-Indikatoren werden + übersprungen, Endzustand wird direkt gesetzt (Pattern existiert bereits in + `style.css:2915-2939`). +- **Kein DOM-Duplikat für Mobile:** Das Popup-Markup bleibt eine einzige Quelle. Der neue + `.hero-mockup-dynamic`-Wrapper enthält die Tab-Karten + das Popup; unterhalb des + bestehenden Breakpoints (768px, siehe `style.css:1913`) werden die Tab-Karten und + Animationen per CSS ausgeblendet, das Popup fällt optisch auf das heutige, statische + Erscheinungsbild zurück, JS initialisiert dort keine Animationen. Vermeidet doppelte + i18n-Pflege der ~270 Zeilen Popup-Markup. +- **i18n-Vorsicht:** 15 Locale-Dateien unter `website/locales/*.json`. Neue sichtbare Texte + (Tab-Labels etc.) nach Möglichkeit vermeiden oder wie bestehende Mock-Inhalte + (z. B. "CoolUsername", "Stranger Things - S4E1") als nicht-lokalisierte Platzhalter + behandeln, um Übersetzungsaufwand in 15 Sprachen zu vermeiden. Neue `{{MOCK_XX}}`-Keys nur + falls wirklich nötig, dann mit Übersetzung in allen 15 Dateien. +- **Build:** Änderungen nur in den Source-Dateien (`template.html`, `style.css`, `app.js`), + niemals in `website/www/**` (generiert via `website/build.cjs`). + +## Detailentscheidungen (bei der Umsetzung getroffen) + +- [x] Breakpoint Mobile-Fallback: bestehende 768px übernommen (`matchMedia('(min-width: 769px)')` + im JS, `@media (max-width: 768px)` im CSS). +- [x] Auto-Durchlauf-Timing: Start ~1,4s nach 45% Sichtbarkeit (IntersectionObserver), dann + Cursor → Launcher → Sync-Tab → Play (läuft ~3s synchron) → Pause → Popup schließt. + Gesamtdauer ca. 10s. +- [x] Tab-Karten haben User-Badges passend zur Peer-Liste im Popup: "CoolUsername" (indigo) + und "KoalaPC" (grün), gleicher Episodentitel "Stranger Things - S4E1". +- [x] Echter animierter Ghost-Mauszeiger (SVG-Cursor mit Klick-Ripple), kein reiner + Ripple-Indikator — verkauft den Durchlauf deutlich besser. +- [x] Keine externe Referenz — eigener Stil, konsistent mit dem bestehenden `mock-*`-Design. +- [x] ~~Drift-Konzept~~ **Verworfen nach Feedback** — es stellte die Extension falsch dar + (als bräuchte sie das offene Popup). Ersetzt durch das **Broadcast-Modell**: Play/ + Pause/Seek auf *irgendeinem* Video wird automatisch zum Peer gespiegelt (Puls wandert + über die Verbindungslinie, Peer folgt ~260ms später, Engine gleicht die Zeit an). Der + Status-Chip bleibt grün "In sync" und blitzt bei jedem Broadcast kurz das Event auf + ("▶ CoolUsername", "❚❚ KoalaPC", "» …" für Seek). Das Popup ist nur die Fernbedienung + obendrauf, nie Voraussetzung. +- [x] Rework 2 nach Feedback: Kein reservierter Popup-Platz mehr; KoalaSync-Icon als + Extension-Button in der Toolbar des vorderen Fensters; Fortschrittsbalken klickbar + (Seek wird mitgesynct, Scrub-Thumb bei Hover). +- [x] Rework 3 nach Feedback: Fenster-Kaskade statt starker Überlappung — das **rechte** + Fenster (CoolUsername, mit dem Toolbar-Icon oben rechts) liegt im Vordergrund, das + linke (KoalaPC) versetzt dahinter unten links. Das Popup öffnet **rechts außen** + (right: 0, verankert unterm Icon), die beiden Tabs weichen dabei animiert nach links + aus und bleiben sichtbar. Verbindungslinie komplett entfernt (Sync-Feedback = Puls- + Ring auf beiden Karten + Event-Chip). Peer-Latenz von 260ms auf 90ms gesenkt — wirkt + quasi instant. +- [x] Rework 4 nach Feedback: **Story-Walkthrough in 5 Akten** beim ersten Sichtbarwerden — + (1) Extension über das Toolbar-Icon öffnen, (2) "Raum erstellen" (eigener Pre-Room- + Screen im Popup, `#demo-room-empty`), (3) Invite-Link fliegt animiert vom Popup zum + zweiten Browserfenster (Toast "Raum beigetreten", KoalaPC erscheint in der Peer-Liste, + "IN SYNC"-Chip blendet erst jetzt ein), (4) beide wählen den Video-Tab (Select-Flash im + Popup + Toast am zweiten Fenster), (5) Play für alle → Popup schließt → Pause vom + anderen Tab, Seek mit Nachziehen, Play. Danach voll manuell. Jede Interaktion während + der Story bricht ab und springt via idempotentem `finishDemo()` sofort in den End- + zustand (Raum verbunden, Chip an, Hint an; Popup schließt bei Klicks außerhalb); + der Nutzer-Klick wirkt anschließend normal. 4 neue Locale-Keys (DEMO_NO_ROOM, + DEMO_CREATE_ROOM, DEMO_JOINED, DEMO_TAB_SELECTED) in allen 15 Sprachen. +- [x] Abspiel-Animation: abstrakter **Strichfigur-Kurzfilm** pro Video (SVG: Hügel- + Silhouetten, laufende Figur mit schwingenden Armen/Beinen, hüpfender Ball) — läuft + nur bei `playing`, Pause friert beide Filme im selben Frame ein + (`animation-play-state`), was die Synchronität sofort sichtbar macht. + +## Umsetzungsstatus + +- [x] `website/template.html`: Hero-Mockup in `.hero-demo-scene` eingebettet (Chip, 2 Tab- + Karten, Verbindungslinie, Launcher, Ghost-Cursor, Hint); Popup-Inhalt unverändert, + nur IDs für Play/Pause/Force-Sync ergänzt. +- [x] `website/style.css`: Demo-Styles mit Container-Queries (cqw + px-Fallbacks), + Mobile-Fallback, `prefers-reduced-motion`-Support. +- [x] `website/app.js`: `initHeroDemo()` — Playback-Uhr (rAF, 8x Zeitraffer), Broadcast- + Logik (Play/Pause/Seek → Peer folgt), manuelle Steuerung, einmaliger Auto-Durchlauf + (Play in Tab A → Seek in Tab B → Pause in Tab B → Popup öffnen/Play/schließen); jede + Interaktion bricht den Durchlauf sofort ab. Auto- und Manuell-Modus nutzen dieselben + Click-Handler. Launcher-Klicks sind vom Karten-Play/Pause entkoppelt (closest-Guard). +- [x] `website/locales/*.json`: 3 neue Keys (`DEMO_SYNC`, `DEMO_DRIFT`, `DEMO_HINT`) in + allen 15 Sprachen. +- [x] `eslint.config.mjs`: `requestAnimationFrame`/`cancelAnimationFrame` zu den Globals. +- [x] Verifiziert im Browser-Preview: Auto-Durchlauf, Drift-Szenario, Chip-Rettungsweg, + Re-Sync, Mobile-Fallback (statisches Popup wie vorher), keine Konsolenfehler, + Lint sauber, Build ohne offene Platzhalter. +- [ ] `.claude/launch.json` zeigt auf einen Session-Scratchpad-Server — vor dem PR prüfen, + ob die Datei ins Repo soll (ggf. löschen oder auf dauerhaften Pfad umstellen). + +## Scope / Nicht-Ziele + +- Kein Backend, keine echten Videos/Assets, keine neuen npm-Dependencies. +- Kein Umbau des restlichen Landingpage-Contents (Features, Compat-Section, Footer etc.) — + ausschließlich das Hero-Mockup. +- Mobile-Ansicht bekommt **kein** neues Design, bleibt 1:1 der heutige Stand. diff --git a/eslint.config.mjs b/eslint.config.mjs index 985b2b6..7902694 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -18,6 +18,8 @@ export default [ setInterval: "readonly", clearTimeout: "readonly", clearInterval: "readonly", + requestAnimationFrame: "readonly", + cancelAnimationFrame: "readonly", fetch: "readonly", CustomEvent: "readonly", MutationObserver: "readonly", diff --git a/website/app.js b/website/app.js index ac494c5..36b2921 100644 --- a/website/app.js +++ b/website/app.js @@ -406,6 +406,393 @@ document.addEventListener('DOMContentLoaded', () => { }); }); + // --- Hero Live Demo (two synced video tabs + extension popup) --- + // Desktop only: on mobile the scene falls back to the classic static popup. + const initHeroDemo = () => { + const scene = document.getElementById('hero-demo'); + if (!scene || !window.matchMedia('(min-width: 769px)').matches) return; + + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + const launcher = scene.querySelector('#demo-launcher'); + const chip = scene.querySelector('#demo-sync-chip'); + const cursor = scene.querySelector('#demo-cursor'); + const hint = document.getElementById('demo-hint'); + const playBtn = scene.querySelector('#demo-play-btn'); + const pauseBtn = scene.querySelector('#demo-pause-btn'); + const forceSyncBtn = scene.querySelector('#demo-force-sync'); + const syncTabBtn = scene.querySelector('.mock-tab[data-target="mock-sync"]'); + const roomEmpty = scene.querySelector('#demo-room-empty'); + const roomJoined = scene.querySelector('#demo-room-joined'); + const createRoomBtn = scene.querySelector('#demo-create-room'); + const inviteCopyBtn = scene.querySelector('#demo-invite-copy'); + const videoSelect = scene.querySelector('#demo-video-select'); + const peerBs = scene.querySelectorAll('.demo-peer-b'); + const inviteFly = scene.querySelector('#demo-invite-fly'); + const toastB = scene.querySelector('#demo-toast-b'); + if (!launcher || !chip || !cursor || !playBtn || !pauseBtn) return; + + const EP_LEN = 2537; // fake 42:17 episode + const RATE = 8; // timelapse factor so the demo feels alive + const START_T = 754; // 12:34 + + const tabs = {}; + ['a', 'b'].forEach(k => { + const root = scene.querySelector('#demo-tab-' + k); + tabs[k] = { + root, + fill: root.querySelector('.demo-progress-fill'), + time: root.querySelector('.demo-time'), + t: START_T, + playing: false + }; + }); + + const fmt = (s) => { + s = Math.floor(s); + return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); + }; + + const renderTab = (tab) => { + tab.fill.style.width = ((tab.t / EP_LEN) * 100).toFixed(2) + '%'; + tab.time.textContent = fmt(tab.t); + }; + + const updateSyncUI = () => { + scene.classList.toggle('streaming', tabs.a.playing && tabs.b.playing); + }; + + // Playback clock (runs only while at least one tab is playing) + let rafId = null; + let lastTs = null; + + const tick = (ts) => { + rafId = null; + const dt = lastTs === null ? 0 : Math.min((ts - lastTs) / 1000, 0.5); + lastTs = ts; + let anyPlaying = false; + ['a', 'b'].forEach(k => { + const tab = tabs[k]; + if (!tab.playing) return; + anyPlaying = true; + tab.t = (tab.t + dt * RATE) % EP_LEN; + renderTab(tab); + }); + updateSyncUI(); + if (anyPlaying) rafId = requestAnimationFrame(tick); + else lastTs = null; + }; + + const ensureLoop = () => { + if (rafId === null && (tabs.a.playing || tabs.b.playing)) { + lastTs = null; + rafId = requestAnimationFrame(tick); + } + }; + + const setPlaying = (k, playing) => { + const tab = tabs[k]; + tab.playing = playing; + tab.root.classList.toggle('playing', playing); + tab.root.setAttribute('aria-pressed', playing ? 'true' : 'false'); + ensureLoop(); + updateSyncUI(); + }; + + const pulse = () => { + ['a', 'b'].forEach(k => { + const el = tabs[k].root; + el.classList.remove('sync-pulse'); + void el.offsetWidth; // restart the CSS animation + el.classList.add('sync-pulse'); + }); + }; + + const NAMES = { a: 'CoolUsername', b: 'KoalaPC' }; + const chipEvent = chip.querySelector('.demo-chip-event'); + + let eventTimer = null; + const showEvent = (text) => { + if (!chipEvent) return; + chipEvent.textContent = text; + chip.classList.add('event'); + clearTimeout(eventTimer); + eventTimer = setTimeout(() => chip.classList.remove('event'), 1600); + }; + + // Core of the demo: play/pause on EITHER side is broadcast to the peer + // tab — exactly what the extension does. The popup is just a remote + // control on top, never a requirement. + let broadcasting = false; + const broadcast = (sourceKey, playing) => { + if (broadcasting) return; + broadcasting = true; + const peerKey = sourceKey === 'a' ? 'b' : 'a'; + setPlaying(sourceKey, playing); + showEvent((playing ? '▶ ' : '❚❚ ') + NAMES[sourceKey]); + // the peer follows near-instantly — that is the whole point + setTimeout(() => { + tabs[peerKey].t = tabs[sourceKey].t; + setPlaying(peerKey, playing); + renderTab(tabs[peerKey]); + pulse(); + broadcasting = false; + }, 90); + }; + + // Seeking works the same way: scrub one tab, the peer jumps along + const seekTo = (sourceKey, fraction) => { + if (broadcasting) return; + broadcasting = true; + const peerKey = sourceKey === 'a' ? 'b' : 'a'; + tabs[sourceKey].t = fraction * EP_LEN; + renderTab(tabs[sourceKey]); + showEvent('» ' + NAMES[sourceKey]); + setTimeout(() => { + tabs[peerKey].t = tabs[sourceKey].t; + renderTab(tabs[peerKey]); + pulse(); + broadcasting = false; + }, 90); + }; + + const setPopupOpen = (open) => { + scene.classList.toggle('popup-open', open); + launcher.setAttribute('aria-expanded', open ? 'true' : 'false'); + }; + + // Manual controls (the automated walkthrough drives these same handlers) + launcher.addEventListener('click', () => setPopupOpen(!scene.classList.contains('popup-open'))); + + // The popup remote control acts as CoolUsername (tab A's user) + playBtn.addEventListener('click', () => { + if (tabs.a.playing && tabs.b.playing) { pulse(); return; } + broadcast('a', true); + }); + pauseBtn.addEventListener('click', () => { + if (!tabs.a.playing && !tabs.b.playing) { pulse(); return; } + broadcast('a', false); + }); + if (forceSyncBtn) forceSyncBtn.addEventListener('click', () => { + tabs.b.t = tabs.a.t = Math.max(tabs.a.t, tabs.b.t); + renderTab(tabs.a); + renderTab(tabs.b); + pulse(); + }); + + ['a', 'b'].forEach(k => { + const root = tabs[k].root; + const toggle = () => broadcast(k, !tabs[k].playing); + // the toolbar extension icon lives inside card A — its clicks + // toggle the popup, not playback + root.addEventListener('click', (e) => { + if (e.target.closest('.demo-ext-launcher')) return; + toggle(); + }); + root.addEventListener('keydown', (e) => { + if (e.target.closest('.demo-ext-launcher')) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggle(); + } + }); + + const progress = root.querySelector('.demo-progress'); + if (progress) { + progress.addEventListener('click', (e) => { + e.stopPropagation(); // a scrub must not toggle play/pause + const r = progress.getBoundingClientRect(); + seekTo(k, Math.min(Math.max((e.clientX - r.left) / r.width, 0), 1)); + }); + } + }); + + chip.addEventListener('click', pulse); + + // --- Story state (room creation -> invite -> connected) --- + const setRoomJoined = (joined) => { + if (roomEmpty) roomEmpty.style.display = joined ? 'none' : 'flex'; + if (roomJoined) roomJoined.style.display = joined ? '' : 'none'; + }; + + const setConnected = (connected) => { + scene.classList.toggle('connected', connected); + peerBs.forEach(el => { el.style.display = connected ? '' : 'none'; }); + }; + + let toastTimer = null; + const showToastB = (text) => { + if (!toastB || !text) return; + toastB.textContent = '✓ ' + text; + toastB.classList.add('show'); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => toastB.classList.remove('show'), 1700); + }; + + // The invite link visibly travels from the popup to the friend's window + const flyInvite = () => new Promise((resolve) => { + if (!inviteFly || !inviteCopyBtn) { resolve(); return; } + const sr = scene.getBoundingClientRect(); + const from = inviteCopyBtn.getBoundingClientRect(); + const to = tabs.b.root.querySelector('.demo-tab-titlebar').getBoundingClientRect(); + inviteFly.style.transition = 'none'; + inviteFly.style.left = (from.left - sr.left) + 'px'; + inviteFly.style.top = (from.top - sr.top) + 'px'; + inviteFly.style.opacity = '1'; + void inviteFly.offsetWidth; + inviteFly.style.transition = ''; + inviteFly.style.left = (to.left - sr.left + to.width / 2 - 40) + 'px'; + inviteFly.style.top = (to.top - sr.top + 2) + 'px'; + setTimeout(() => { + inviteFly.style.opacity = '0'; + resolve(); + }, 800); + }); + + const flashSelect = () => { + if (!videoSelect) return; + videoSelect.classList.remove('demo-attn'); + void videoSelect.offsetWidth; + videoSelect.classList.add('demo-attn'); + }; + + if (createRoomBtn) createRoomBtn.addEventListener('click', () => setRoomJoined(true)); + + renderTab(tabs.a); + renderTab(tabs.b); + updateSyncUI(); + + // JS is active: start with the popup tucked away and, if the scripted + // story is going to run, rewind to the "before" state (no transitions + // on load). + scene.classList.add('demo-no-anim'); + if (!reduceMotion) { + setPopupOpen(false); + setConnected(false); + setRoomJoined(false); + } + void scene.offsetWidth; + scene.classList.remove('demo-no-anim'); + + const showHint = () => { + if (hint) hint.classList.add('show'); + }; + + // Automated one-time walkthrough; any interaction aborts it and jumps + // straight to the finished end state, then the user's click applies. + let userTookOver = false; + let demoStarted = false; + let demoFinished = false; + let demoFinishedByStory = false; + + const finishDemo = () => { + if (demoFinished) return; + demoFinished = true; + setRoomJoined(true); + setConnected(true); + cursor.classList.remove('visible'); + showHint(); + }; + + const takeOver = (e) => { + const firstTake = !userTookOver; + userTookOver = true; + finishDemo(); + // if the story left the popup open and the user clicked elsewhere, + // tuck it away; clicks inside the popup (or on its icon) keep it + if (firstTake && !demoFinishedByStory && e && e.target && + !e.target.closest('.extension-mockup') && + !e.target.closest('.demo-ext-launcher')) { + setPopupOpen(false); + } + }; + scene.addEventListener('pointerdown', takeOver, true); + scene.addEventListener('keydown', takeOver, true); + + const runDemo = async () => { + if (demoStarted || userTookOver || reduceMotion) return; + demoStarted = true; + + const wait = (ms) => new Promise(r => setTimeout(r, ms)); + const moveTo = async (el, fx) => { + const sr = scene.getBoundingClientRect(); + const er = el.getBoundingClientRect(); + cursor.style.left = (er.left - sr.left + er.width * (fx || 0.5)) + 'px'; + cursor.style.top = (er.top - sr.top + er.height / 2) + 'px'; + await wait(700); + }; + const step = async (el, pause, action, fx) => { + if (userTookOver || !el) return false; + await moveTo(el, fx); + if (userTookOver) return false; + cursor.classList.remove('clicking'); + void cursor.offsetWidth; + cursor.classList.add('clicking'); + await wait(180); + if (userTookOver) return false; + if (action) action(); else el.click(); + await wait(320 + (pause || 0)); + return !userTookOver; + }; + + await wait(500); + if (userTookOver) return; + cursor.classList.add('visible'); + await wait(400); + + const progressA = tabs.a.root.querySelector('.demo-progress'); + + // Act 1: open the extension and create a room + if (!await step(launcher, 300)) return; + if (!await step(createRoomBtn, 500)) return; + + // Act 2: the invite link travels to the friend's browser + if (!await step(inviteCopyBtn, 0)) return; + await flyInvite(); + if (userTookOver) return; + setConnected(true); + showToastB(toastB ? toastB.dataset.joined : ''); + pulse(); + await wait(900); + if (userTookOver) return; + + // Act 3: both sides pick the video tab to sync + if (!await step(syncTabBtn, 250)) return; + if (!await step(videoSelect, 150, flashSelect)) return; + showToastB(toastB ? toastB.dataset.selected : ''); + await wait(700); + if (userTookOver) return; + + // Act 4: play for everyone, then tuck the popup away + if (!await step(playBtn, 1800)) return; + if (!await step(launcher, 500)) return; + + // Act 5: any side can control — pause there, seek here, play again + if (!await step(tabs.b.root, 1000)) return; + if (!await step(progressA, 1300, () => seekTo('a', 0.62), 0.62)) return; + if (!await step(tabs.a.root, 700)) return; + + demoFinishedByStory = true; + finishDemo(); + }; + + if ('IntersectionObserver' in window && !reduceMotion) { + const demoObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + demoObserver.disconnect(); + setTimeout(runDemo, 900); + } + }); + }, { threshold: 0.45 }); + demoObserver.observe(scene); + } else { + finishDemo(); + } + }; + + initHeroDemo(); + // Terminal Tab Switcher const termTabBtns = document.querySelectorAll('.terminal-tab-btn'); const termPanes = document.querySelectorAll('.terminal-pane'); diff --git a/website/assets/NewLogoIcon_64.webp b/website/assets/NewLogoIcon_64.webp index b1f4e43ede2c8f7304360593b419303eb296c0d2..660ffe28987821948744dafb8543121d3c9924ee 100644 GIT binary patch delta 175 zcmV;g08sye3W5r-j{*uRbh!h)p{C}lShJY|(*b{`0tbHB-=W1lvs8JmTg?5P$6NU| zO#PNrF}c9Ck3x?#EFIFIEEWqqr&MVE&)R*f)BkMkZ`0M?cCguvrw7XYD@K>HoHNx9RHcyI5?-@+?sg|AU>&-G4md zD$=Kt1fjo2eO@GCUGUCe6T6+N5TM#$rGCu%%0C_&{K4-ZrketIuEE5-AGFN%Jt!V_ dA-)%z8o>$A#3xK?o>SG_`v#;TD2(v9006+KRrvq_ diff --git a/website/locales/de.json b/website/locales/de.json index 06c8e57..54c82ff 100644 --- a/website/locales/de.json +++ b/website/locales/de.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Gemeinsam schauen.Perfekt synchron.", "HERO_SUBTITLE": "Dein Kino-Abend auf Distanz. Keine Lags, keine Anmeldung. Einfach Link teilen und zusammen schauen.", "HERO_MASCOT_ALT": "Ein niedlicher Koala steht da und schaut nach unten auf die Download-Buttons", + "DEMO_SYNC": "Synchron", + "DEMO_HINT": "Live-Demo — drück Play in einem der Tabs: KoalaSync hält beide synchron.", + "DEMO_NO_ROOM": "Noch nicht verbunden", + "DEMO_CREATE_ROOM": "Raum erstellen", + "DEMO_JOINED": "Raum beigetreten", + "DEMO_TAB_SELECTED": "Tab ausgewählt", "ADD_TO_CHROME": "Zu Chrome hinzufügen", "ADD_TO_FIREFOX": "Zu Firefox hinzufügen", "COMPAT_HEADING": "Funktioniert auf deinen Lieblingsplattformen", diff --git a/website/locales/en.json b/website/locales/en.json index 3024053..4134bbb 100644 --- a/website/locales/en.json +++ b/website/locales/en.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Watch Together.Sync Perfectly.", "HERO_SUBTITLE": "Your remote movie night without lags. No registration, no data collection. Just share a link and watch together.", "HERO_MASCOT_ALT": "A cute koala standing and looking down at the download buttons", + "DEMO_SYNC": "In sync", + "DEMO_HINT": "Live demo — press play in either tab: KoalaSync keeps both in sync.", + "DEMO_NO_ROOM": "Not connected yet", + "DEMO_CREATE_ROOM": "Create Room", + "DEMO_JOINED": "Room joined", + "DEMO_TAB_SELECTED": "Tab selected", "ADD_TO_CHROME": "Add to Chrome", "ADD_TO_FIREFOX": "Add to Firefox", "COMPAT_HEADING": "Works on your favorite platforms", @@ -194,7 +200,7 @@ "ALT_TELEPARTY_SUMMARY_BODY": "If you want built-in chat or mobile support, Teleparty may be the better fit. If you want free, open-source desktop browser sync that works with almost any normal video player, including self-hosted media servers like Emby, Jellyfin and Plex, KoalaSync is probably the better match.", "ALT_SCREEN_TITLE": "KoalaSync vs screen sharing: when is video sync better?", "ALT_SCREEN_META_DESC": "A practical comparison of KoalaSync and screen sharing for remote movie nights, playback control, video quality, privacy, subtitles and browser-based watching.", - "ALT_SCREEN_INTRO": "Screen sharing works, and sometimes it is honestly enough. One person shares their screen, everyone else watches, and you are done.

But for longer movie nights or watching episodes together, it can get annoying. The video quality depends on the sharer\u2019s upload, CPU, browser, and whatever compression Discord, Teamspeak, Zoom or another call app applies. Also, only one person really controls the playback.", + "ALT_SCREEN_INTRO": "Screen sharing works, and sometimes it is honestly enough. One person shares their screen, everyone else watches, and you are done.

But for longer movie nights or watching episodes together, it can get annoying. The video quality depends on the sharer’s upload, CPU, browser, and whatever compression Discord, Teamspeak, Zoom or another call app applies. Also, only one person really controls the playback.", "ALT_SCREEN_WHEN_ENOUGH_TITLE": "When screen sharing is enough", "ALT_SCREEN_WHEN_ENOUGH_BODY": "Screen sharing is fine when you want the fastest possible setup. If only one person has access to the video, or everyone is okay with one person controlling play, pause and seeking, it may be the simplest option.

It also makes sense when quality is not a big deal, when nobody needs their own subtitles or audio settings, and when the person sharing is comfortable showing that browser window or screen.", "ALT_SCREEN_WHEN_ANNOYING_TITLE": "Where screen sharing gets annoying", diff --git a/website/locales/es.json b/website/locales/es.json index 7f57203..342cca6 100644 --- a/website/locales/es.json +++ b/website/locales/es.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Miren juntos.Sincronización perfecta.", "HERO_SUBTITLE": "Tu noche de películas a distancia sin retrasos. Sin registros ni recopilación de datos. Solo comparte un enlace y miren juntos.", "HERO_MASCOT_ALT": "Un lindo koala de pie mirando hacia abajo a los botones de descarga", + "DEMO_SYNC": "Sincronizado", + "DEMO_HINT": "Demo en vivo — pulsa play en cualquiera de las pestañas: KoalaSync mantiene ambas sincronizadas.", + "DEMO_NO_ROOM": "Aún sin conexión", + "DEMO_CREATE_ROOM": "Crear sala", + "DEMO_JOINED": "Conectado a la sala", + "DEMO_TAB_SELECTED": "Pestaña seleccionada", "ADD_TO_CHROME": "Añadir a Chrome", "ADD_TO_FIREFOX": "Añadir a Firefox", "COMPAT_HEADING": "Funciona en tus plataformas favoritas", diff --git a/website/locales/fr.json b/website/locales/fr.json index f24715b..9a316d0 100644 --- a/website/locales/fr.json +++ b/website/locales/fr.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Regardez ensemble.Synchronisation parfaite.", "HERO_SUBTITLE": "Votre soirée cinéma à distance sans décalage. Pas d'inscription, pas de collecte de données. Partagez simplement un lien et regardez ensemble.", "HERO_MASCOT_ALT": "Un koala mignon debout et regardant vers le bas les boutons de téléchargement", + "DEMO_SYNC": "Synchronisé", + "DEMO_HINT": "Démo interactive — lancez la lecture dans un onglet : KoalaSync garde les deux synchronisés.", + "DEMO_NO_ROOM": "Pas encore connecté", + "DEMO_CREATE_ROOM": "Créer un salon", + "DEMO_JOINED": "Salon rejoint", + "DEMO_TAB_SELECTED": "Onglet sélectionné", "ADD_TO_CHROME": "Ajouter à Chrome", "ADD_TO_FIREFOX": "Ajouter à Firefox", "COMPAT_HEADING": "Fonctionne sur vos plateformes préférées", diff --git a/website/locales/it.json b/website/locales/it.json index f98e89c..ac8d658 100644 --- a/website/locales/it.json +++ b/website/locales/it.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Guardate Insieme.Sincronia Perfetta.", "HERO_SUBTITLE": "La tua serata cinema a distanza senza ritardi. Nessun account, nessun dato raccolto. Condividi un link e guardate insieme.", "HERO_MASCOT_ALT": "Un simpatico koala che guarda i pulsanti di download", + "DEMO_SYNC": "Sincronizzato", + "DEMO_HINT": "Demo dal vivo — premi play in una delle schede: KoalaSync le mantiene sincronizzate.", + "DEMO_NO_ROOM": "Non ancora connesso", + "DEMO_CREATE_ROOM": "Crea stanza", + "DEMO_JOINED": "Entrato nella stanza", + "DEMO_TAB_SELECTED": "Scheda selezionata", "ADD_TO_CHROME": "Aggiungi a Chrome", "ADD_TO_FIREFOX": "Aggiungi a Firefox", "COMPAT_HEADING": "Compatibile con le tue piattaforme preferite", diff --git a/website/locales/ja.json b/website/locales/ja.json index fdd90a0..3ffd60d 100644 --- a/website/locales/ja.json +++ b/website/locales/ja.json @@ -17,6 +17,12 @@ "HERO_TITLE": "一緒に観よう。完璧に同期。", "HERO_SUBTITLE": "遅延のないリモート映画鑑賞。登録不要、データ収集なし。リンクを共有するだけで、一緒に視聴できます。", "HERO_MASCOT_ALT": "ダウンロードボタンを見下ろしているかわいいコアラ", + "DEMO_SYNC": "同期中", + "DEMO_HINT": "ライブデモ — どちらかのタブで再生すると、KoalaSyncが両方を同期させます。", + "DEMO_NO_ROOM": "未接続", + "DEMO_CREATE_ROOM": "ルームを作成", + "DEMO_JOINED": "ルームに参加", + "DEMO_TAB_SELECTED": "タブを選択済み", "ADD_TO_CHROME": "Chromeに追加", "ADD_TO_FIREFOX": "Firefoxに追加", "COMPAT_HEADING": "お気に入りのプラットフォームで動作", @@ -179,7 +185,7 @@ "FOOTER_PRIVACY_LINK": "privacy", "FOOTER_DISCLAIMER": "KoalaSyncは、Netflix、Disney+、Amazon、YouTube、Twitch、またはその他のストリーミングプラットフォームと提携、承認、または関連付けられているものではありません。すべての商標はそれぞれの所有者に帰属します。", "FOOTER_SUPPORT": "Support KoalaSync", - "ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty:どちらのウォッチパーティツールを使うべき?", + "ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty:どちらのウォッチパーティツールを使うべき?", "ALT_TELEPARTY_META_DESC": "ウォッチパーティ向けツール、KoalaSyncとTelepartyの実践的比較。チャット機能、モバイル対応、ブラウザでの動画同期、セルフホスト型メディアサーバー、オープンソース利用といった観点から解説します。", "ALT_TELEPARTY_INTRO": "KoalaSyncとTelepartyは、どちらも「3、2、1、再生!」と声を掛け合う手間をかけずに、一緒に動画を楽しむという共通の課題を解決してくれます。ただ、そのアプローチは異なります。

チャット機能が組み込まれていることや、モバイル端末で視聴できることを重視するなら、Telepartyが最適です。一方、KoalaSyncは、DiscordやTeamSpeakなどで既に通話しながら、ブラウザ上で動画の再生タイミングだけを同期させたいという、デスクトップブラウザでの利用に適しています。

私がKoalaSyncを開発した主な理由は「柔軟性」です。特定のプラットフォームだけでなく、一般的なブラウザ用動画プレーヤーを使用するほぼすべてのウェブサイトで動作するように設計されています。これには、Emby、Jellyfin、Plexといったセルフホスト環境や、ローカル/プライベートドメイン、そしてブラウザ上の一般的な動画ページなどが含まれます。", "ALT_TELEPARTY_WHEN_TELE_TITLE": "Telepartyが適しているケース", diff --git a/website/locales/ko.json b/website/locales/ko.json index f7d144a..662dd1b 100644 --- a/website/locales/ko.json +++ b/website/locales/ko.json @@ -17,6 +17,12 @@ "HERO_TITLE": "함께 시청하세요.완벽하게 동기화됩니다.", "HERO_SUBTITLE": "지연 없는 원격 영화의 밤. 회원가입도, 데이터 수집도 없습니다. 링크를 공유하고 함께 시청하세요.", "HERO_MASCOT_ALT": "다운로드 버튼을 내려다보고 서 있는 귀여운 코알라", + "DEMO_SYNC": "동기화됨", + "DEMO_HINT": "라이브 데모 — 아무 탭에서나 재생을 누르면 KoalaSync가 두 탭을 동기화합니다.", + "DEMO_NO_ROOM": "아직 연결되지 않음", + "DEMO_CREATE_ROOM": "방 만들기", + "DEMO_JOINED": "방 참여됨", + "DEMO_TAB_SELECTED": "탭 선택됨", "ADD_TO_CHROME": "Chrome에 추가", "ADD_TO_FIREFOX": "Firefox에 추가", "COMPAT_HEADING": "가장 좋아하는 플랫폼에서 작동합니다", @@ -179,7 +185,7 @@ "FOOTER_PRIVACY_LINK": "privacy", "FOOTER_DISCLAIMER": "KoalaSync는 Netflix, Disney+, Amazon, YouTube, Twitch 또는 기타 스트리밍 플랫폼과 제휴, 보증 또는 연관되어 있지 않습니다. 모든 상표는 해당 소유자의 재산입니다.", "FOOTER_SUPPORT": "Support KoalaSync", - "ALT_TELEPARTY_TITLE": "KoalaSync 대 Teleparty: 어떤 '워치 파티(watch party)' 도구를 선택해야 할까요?", + "ALT_TELEPARTY_TITLE": "KoalaSync 대 Teleparty: 어떤 '워치 파티(watch party)' 도구를 선택해야 할까요?", "ALT_TELEPARTY_META_DESC": "워치 파티를 위한 KoalaSync와 Teleparty의 실용적 비교: 내장 채팅, 모바일 지원, 브라우저 영상 동기화, 자체 호스팅 미디어 서버 및 오픈 소스 활용 여부 등을 다룹니다.", "ALT_TELEPARTY_INTRO": "KoalaSync와 Teleparty는 모두 '3, 2, 1, 재생' 같은 번거로운 절차 없이 함께 영상을 시청할 수 있게 해준다는 점에서 같은 문제를 해결해 줍니다. 다만 접근 방식에는 차이가 있습니다.

내장 채팅 기능이 필요하거나 모바일에서 시청하고 싶다면 Teleparty가 가장 합리적인 선택입니다. 반면 KoalaSync는 Discord, Teamspeak 또는 기타 음성 통화 서비스를 통해 이미 대화를 나누고 있으면서, 단순히 영상 재생만 동기화하고 싶은 데스크톱 브라우저 기반의 워치 파티에 더 적합합니다.

제가 KoalaSync를 개발한 주된 이유는 유연성 때문입니다. 특정 플랫폼 목록에만 국한되지 않고, 일반적인 브라우저 비디오 플레이어를 사용하는 거의 모든 웹사이트에서 작동하도록 설계되었습니다. 여기에는 Emby, Jellyfin, Plex와 같은 자체 호스팅 환경, 로컬/비공개 도메인, 그리고 브라우저상의 수많은 일반 동영상 페이지가 포함됩니다.", "ALT_TELEPARTY_WHEN_TELE_TITLE": "Teleparty가 여전히 좋은 선택이 될 수 있는 경우", diff --git a/website/locales/nl.json b/website/locales/nl.json index e58b85f..7cbf669 100644 --- a/website/locales/nl.json +++ b/website/locales/nl.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Samen Kijken.Perfect Synchroniseren.", "HERO_SUBTITLE": "Uw filmavond op afstand zonder vertraging. Geen registratie, geen gegevensverzameling. Deel gewoon een link en kijk samen.", "HERO_MASCOT_ALT": "Een schattige koala die staat en naar beneden kijkt naar de downloadknoppen", + "DEMO_SYNC": "Synchroon", + "DEMO_HINT": "Live demo — druk op play in een van de tabbladen: KoalaSync houdt beide synchroon.", + "DEMO_NO_ROOM": "Nog niet verbonden", + "DEMO_CREATE_ROOM": "Kamer aanmaken", + "DEMO_JOINED": "In de kamer", + "DEMO_TAB_SELECTED": "Tabblad geselecteerd", "ADD_TO_CHROME": "Toevoegen aan Chrome", "ADD_TO_FIREFOX": "Toevoegen aan Firefox", "COMPAT_HEADING": "Werkt op uw favoriete platforms", diff --git a/website/locales/pl.json b/website/locales/pl.json index 80bf6e0..a43d46c 100644 --- a/website/locales/pl.json +++ b/website/locales/pl.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Oglądajcie Razem.Synchronizujcie Idealnie.", "HERO_SUBTITLE": "Twój zdalny wieczór filmowy bez opóźnień. Bez rejestracji, bez zbierania danych. Po prostu udostępnij link i oglądajcie razem.", "HERO_MASCOT_ALT": "Uroczy koala stojący i patrzący w dół na przyciski pobierania", + "DEMO_SYNC": "Zsynchronizowane", + "DEMO_HINT": "Demo na żywo — naciśnij play w dowolnej karcie: KoalaSync utrzyma obie w synchronizacji.", + "DEMO_NO_ROOM": "Jeszcze nie połączono", + "DEMO_CREATE_ROOM": "Utwórz pokój", + "DEMO_JOINED": "Dołączono do pokoju", + "DEMO_TAB_SELECTED": "Karta wybrana", "ADD_TO_CHROME": "Dodaj do Chrome", "ADD_TO_FIREFOX": "Dodaj do Firefox", "COMPAT_HEADING": "Działa na Twoich ulubionych platformach", diff --git a/website/locales/pt-BR.json b/website/locales/pt-BR.json index e9b4daf..075c032 100644 --- a/website/locales/pt-BR.json +++ b/website/locales/pt-BR.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Assistam Juntos.Sincronia Perfeita.", "HERO_SUBTITLE": "Sua noite de cinema à distância sem atrasos. Sem contas, sem coleta de dados. Apenas compartilhe o link e assistam juntos.", "HERO_MASCOT_ALT": "Um coala fofo olhando para os botões de download", + "DEMO_SYNC": "Sincronizado", + "DEMO_HINT": "Demo ao vivo — dê play em qualquer aba: o KoalaSync mantém as duas em sincronia.", + "DEMO_NO_ROOM": "Ainda não conectado", + "DEMO_CREATE_ROOM": "Criar sala", + "DEMO_JOINED": "Entrou na sala", + "DEMO_TAB_SELECTED": "Aba selecionada", "ADD_TO_CHROME": "Adicionar ao Chrome", "ADD_TO_FIREFOX": "Adicionar ao Firefox", "COMPAT_HEADING": "Funciona nas suas plataformas favoritas", diff --git a/website/locales/pt.json b/website/locales/pt.json index c127c0e..f3afdf7 100644 --- a/website/locales/pt.json +++ b/website/locales/pt.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Vejam Juntos.Sincronia Perfeita.", "HERO_SUBTITLE": "A sua noite de cinema à distância e sem atrasos. Sem contas, sem recolha de dados. Partilhe um link e vejam juntos.", "HERO_MASCOT_ALT": "Um coala fofo a olhar para os botões de download", + "DEMO_SYNC": "Sincronizado", + "DEMO_HINT": "Demo ao vivo — dê play em qualquer aba: o KoalaSync mantém as duas em sincronia.", + "DEMO_NO_ROOM": "Ainda não conectado", + "DEMO_CREATE_ROOM": "Criar sala", + "DEMO_JOINED": "Entrou na sala", + "DEMO_TAB_SELECTED": "Separador selecionado", "ADD_TO_CHROME": "Adicionar ao Chrome", "ADD_TO_FIREFOX": "Adicionar ao Firefox", "COMPAT_HEADING": "Funciona nas suas plataformas favoritas", diff --git a/website/locales/ru.json b/website/locales/ru.json index 1f59414..5bc8d70 100644 --- a/website/locales/ru.json +++ b/website/locales/ru.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Смотрите вместе.Синхронно на 100%.", "HERO_SUBTITLE": "Ваш киновечер на расстоянии без задержек. Без регистрации и сбора данных. Просто поделитесь ссылкой и смотрите вместе.", "HERO_MASCOT_ALT": "Милый коала стоит и смотрит вниз на кнопки загрузки", + "DEMO_SYNC": "Синхронно", + "DEMO_HINT": "Живое демо — нажмите play в любой вкладке: KoalaSync синхронизирует обе.", + "DEMO_NO_ROOM": "Ещё не подключено", + "DEMO_CREATE_ROOM": "Создать комнату", + "DEMO_JOINED": "В комнате", + "DEMO_TAB_SELECTED": "Вкладка выбрана", "ADD_TO_CHROME": "Установить в Chrome", "ADD_TO_FIREFOX": "Установить в Firefox", "COMPAT_HEADING": "Работает на ваших любимых платформах", diff --git a/website/locales/tr.json b/website/locales/tr.json index e3bf7b5..13ace76 100644 --- a/website/locales/tr.json +++ b/website/locales/tr.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Birlikte İzleyin.Mükemmel Eşitleyin.", "HERO_SUBTITLE": "Gecikmesiz uzaktan film geceniz. Kayıt yok, veri toplama yok. Sadece bir bağlantı paylaşın ve birlikte izleyin.", "HERO_MASCOT_ALT": "İndirme düğmelerine bakan sevimli bir koala", + "DEMO_SYNC": "Senkronize", + "DEMO_HINT": "Canlı demo — sekmelerden birinde oynat'a basın: KoalaSync ikisini de senkronize tutar.", + "DEMO_NO_ROOM": "Henüz bağlı değil", + "DEMO_CREATE_ROOM": "Oda oluştur", + "DEMO_JOINED": "Odaya katıldı", + "DEMO_TAB_SELECTED": "Sekme seçildi", "ADD_TO_CHROME": "Chrome'a Ekle", "ADD_TO_FIREFOX": "Firefox'a Ekle", "COMPAT_HEADING": "En sevdiğiniz platformlarda çalışır", diff --git a/website/locales/uk.json b/website/locales/uk.json index 8c6cf74..2b39483 100644 --- a/website/locales/uk.json +++ b/website/locales/uk.json @@ -17,6 +17,12 @@ "HERO_TITLE": "Дивіться разом.Ідеальна синхронізація.", "HERO_SUBTITLE": "Ваш віддалений кіновечір без лагів. Ні реєстрації, ні збору даних. Просто поділіться посиланням і дивіться разом.", "HERO_MASCOT_ALT": "Мила коала стоїть і дивиться вниз на кнопки завантаження", + "DEMO_SYNC": "Синхронно", + "DEMO_HINT": "Жива демонстрація — натисніть play у будь-якій вкладці: KoalaSync синхронізує обидві.", + "DEMO_NO_ROOM": "Ще не підключено", + "DEMO_CREATE_ROOM": "Створити кімнату", + "DEMO_JOINED": "У кімнаті", + "DEMO_TAB_SELECTED": "Вкладку вибрано", "ADD_TO_CHROME": "Додати до Chrome", "ADD_TO_FIREFOX": "Додати до Firefox", "COMPAT_HEADING": "Працює на ваших улюблених платформах", diff --git a/website/locales/zh.json b/website/locales/zh.json index d987fd7..21737a5 100644 --- a/website/locales/zh.json +++ b/website/locales/zh.json @@ -17,6 +17,12 @@ "HERO_TITLE": "一起观看. 完美同步.", "HERO_SUBTITLE": "您的远程电影之夜没有延迟。无需注册,无需收集数据。只需分享链接即可一起观看。", "HERO_MASCOT_ALT": "一只可爱的考拉站着,低头看着下载按钮", + "DEMO_SYNC": "同步中", + "DEMO_HINT": "在线演示 — 在任一标签页点击播放:KoalaSync 会让两边保持同步。", + "DEMO_NO_ROOM": "尚未连接", + "DEMO_CREATE_ROOM": "创建房间", + "DEMO_JOINED": "已加入房间", + "DEMO_TAB_SELECTED": "已选择标签页", "ADD_TO_CHROME": "添加到Chrome", "ADD_TO_FIREFOX": "添加到Firefox", "COMPAT_HEADING": "适用于您最喜欢的平台", @@ -179,15 +185,15 @@ "FOOTER_PRIVACY_LINK": "privacy", "FOOTER_DISCLAIMER": "KoalaSync 不隶属于 Netflix、Disney+、Amazon、YouTube、Twitch 或任何其他流媒体平台,也不受其认可或关联。所有商标均为其各自所有者的财产。", "FOOTER_SUPPORT": "Support KoalaSync", - "ALT_TELEPARTY_TITLE": "KoalaSync 与 Teleparty:你应该选择哪款在线同步观影工具?", + "ALT_TELEPARTY_TITLE": "KoalaSync 与 Teleparty:你应该选择哪款在线同步观影工具?", "ALT_TELEPARTY_META_DESC": "针对 KoalaSync 和 Teleparty 的实用对比,涵盖内置聊天功能、移动端支持、浏览器视频同步、自建媒体服务器以及开源特性等方面。", - "ALT_TELEPARTY_INTRO": "KoalaSync 和 Teleparty 都能解决同一个令人头疼的问题:如何一起观看视频,而无需经历\u201c3、2、1,开始播放\u201d这种繁琐的同步步骤。只不过,它们采取了不同的实现方式。

如果你需要内置聊天功能或希望在移动设备上观看,Teleparty 是更合理的选择。KoalaSync 则更适合桌面浏览器观影场景——即你已经通过 Discord、TeamSpeak 或其他语音通话软件进行交流,只需确保视频播放保持同步即可。

我开发 KoalaSync 的主要初衷是为了灵活性。它旨在支持几乎所有使用标准浏览器视频播放器的网站,而不仅仅局限于特定的平台列表。这包括 Emby、Jellyfin 和 Plex 等自建媒体服务,本地/私有域名,以及浏览器中的各类常规视频页面。", + "ALT_TELEPARTY_INTRO": "KoalaSync 和 Teleparty 都能解决同一个令人头疼的问题:如何一起观看视频,而无需经历“3、2、1,开始播放”这种繁琐的同步步骤。只不过,它们采取了不同的实现方式。

如果你需要内置聊天功能或希望在移动设备上观看,Teleparty 是更合理的选择。KoalaSync 则更适合桌面浏览器观影场景——即你已经通过 Discord、TeamSpeak 或其他语音通话软件进行交流,只需确保视频播放保持同步即可。

我开发 KoalaSync 的主要初衷是为了灵活性。它旨在支持几乎所有使用标准浏览器视频播放器的网站,而不仅仅局限于特定的平台列表。这包括 Emby、Jellyfin 和 Plex 等自建媒体服务,本地/私有域名,以及浏览器中的各类常规视频页面。", "ALT_TELEPARTY_WHEN_TELE_TITLE": "Teleparty 依然适用的场景", "ALT_TELEPARTY_WHEN_TELE_BODY": "如果你希望聊天功能集成在观影工具本身之中,Teleparty 仍然是更好的选择。KoalaSync 目前没有内置聊天功能;它默认你已经在其他平台(如 Discord、TeamSpeak、WhatsApp、Signal 或常规通话)上进行交流。

移动端支持是另一个关键因素。Teleparty 拥有专属应用程序,而 KoalaSync 目前仅支持桌面浏览器。如果你想在手机或平板电脑上同步观看 Netflix,KoalaSync 暂时并不适用。

因此,如果你最看重聊天功能和移动端支持,Teleparty 可能会更适合你的使用需求。", "ALT_TELEPARTY_WHEN_KOALA_TITLE": "KoalaSync 适用场景", - "ALT_TELEPARTY_WHEN_KOALA_BODY": "如果您想要一款灵活的桌面浏览器同步工具,而不是受限于特定平台列表的软件,那么 KoalaSync 是更合适的选择。

它适用于几乎所有使用标准浏览器视频播放器的网站。对于 Emby、Jellyfin、Plex、私有媒体服务器、本地域或传统\u201c在线观影派对\u201d工具通常忽略的小型视频网站来说,这一点尤为实用。

此外,它完全免费且开源,无需注册账号;如果您希望运行自己的中继服务器,还可以选择自托管。我开发它的初衷是寻找一款既注重隐私又能与我自己的 Emby 系统兼容的工具,而不是那种仅针对少数几家大型流媒体平台的封闭式平台。", + "ALT_TELEPARTY_WHEN_KOALA_BODY": "如果您想要一款灵活的桌面浏览器同步工具,而不是受限于特定平台列表的软件,那么 KoalaSync 是更合适的选择。

它适用于几乎所有使用标准浏览器视频播放器的网站。对于 Emby、Jellyfin、Plex、私有媒体服务器、本地域或传统“在线观影派对”工具通常忽略的小型视频网站来说,这一点尤为实用。

此外,它完全免费且开源,无需注册账号;如果您希望运行自己的中继服务器,还可以选择自托管。我开发它的初衷是寻找一款既注重隐私又能与我自己的 Emby 系统兼容的工具,而不是那种仅针对少数几家大型流媒体平台的封闭式平台。", "ALT_TELEPARTY_MAIN_DIFF_TITLE": "主要区别", - "ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty 更侧重于提供包含聊天和移动端支持的\u201c观影派对\u201d体验;而 KoalaSync 则更像是一个针对浏览器视频的灵活同步层。

这听起来似乎差别不大,但实际上极大地改变了使用场景。如果您需要一个内置聊天功能的完整观影派对应用,Teleparty 是合理的选择;如果您已有语音聊天渠道,仅需保持各浏览器间的视频播放控制同步,那么 KoalaSync 则是更简洁适用的方案。", + "ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty 更侧重于提供包含聊天和移动端支持的“观影派对”体验;而 KoalaSync 则更像是一个针对浏览器视频的灵活同步层。

这听起来似乎差别不大,但实际上极大地改变了使用场景。如果您需要一个内置聊天功能的完整观影派对应用,Teleparty 是合理的选择;如果您已有语音聊天渠道,仅需保持各浏览器间的视频播放控制同步,那么 KoalaSync 则是更简洁适用的方案。", "ALT_TELEPARTY_LIMITS_TITLE": "局限性说明", "ALT_TELEPARTY_LIMITS_BODY": "KoalaSync 并非万能,我们也坦诚其局限性。目前它仅支持桌面端浏览器,没有内置聊天功能,也没有移动端应用。

兼容性也取决于浏览器和视频播放器。虽然 KoalaSync 理论上支持几乎所有标准浏览器视频播放器,但网站随时可能更改其播放器、DRM(数字版权管理)机制或浏览器限制策略。

KoalaSync 与 Netflix、Disney+、Prime Video、YouTube、Teleparty、Emby、Jellyfin、Plex 或本文提及的任何其他品牌均无关联。提及这些名称仅为说明典型的使用场景。", "ALT_TELEPARTY_SUMMARY_TITLE": "总结", @@ -196,7 +202,7 @@ "ALT_SCREEN_META_DESC": "针对远程观影之夜,从播放控制、视频质量、隐私保护、字幕支持及浏览器观看体验等方面,对 KoalaSync 与屏幕共享进行实用对比。", "ALT_SCREEN_INTRO": "屏幕共享确实可行,而且老实说,有时这就足够了。一人共享屏幕,其他人观看,搞定。

但在长时间观影或一起追剧时,它可能会让人感到烦恼。视频质量取决于共享者的上传带宽、CPU、浏览器,以及 Discord、Teamspeak、Zoom 或其他通话应用所采用的压缩算法。此外,实际上只有一个人能控制播放。", "ALT_SCREEN_WHEN_ENOUGH_TITLE": "何时使用屏幕共享就足够了", - "ALT_SCREEN_WHEN_ENOUGH_BODY": "如果您追求最快的设置速度,屏幕共享是个不错的选择。如果只有一个人能访问该视频……\u201c或者,如果大家都能接受由一个人来控制播放、暂停和进度跳转,那这可能是最简单的方案。

此外,如果画质要求不高,大家不需要各自独立的字幕或音频设置,且分享者也不介意展示自己的浏览器窗口或屏幕,那么这种方式也很合理。", + "ALT_SCREEN_WHEN_ENOUGH_BODY": "如果您追求最快的设置速度,屏幕共享是个不错的选择。如果只有一个人能访问该视频……“或者,如果大家都能接受由一个人来控制播放、暂停和进度跳转,那这可能是最简单的方案。

此外,如果画质要求不高,大家不需要各自独立的字幕或音频设置,且分享者也不介意展示自己的浏览器窗口或屏幕,那么这种方式也很合理。", "ALT_SCREEN_WHEN_ANNOYING_TITLE": "屏幕共享可能带来的不便", "ALT_SCREEN_WHEN_ANNOYING_BODY": "最大的弊端在于,并非每个人都在自己的播放器中观看。由一人流式传输屏幕画面,其他人只能观看该画面。

这意味着视频画质可能较差、音频经过压缩、播放存在延迟,且其他人缺乏控制权。字幕和音轨也完全取决于分享者的选择。

此外还涉及隐私问题。如果你切换窗口或标签页,其他人可能会短暂看到你的浏览器标签页、电子邮件、Reddit 页面、聊天记录或其他私密内容。虽然可以通过使用独立窗口来规避这一问题,但这仍是需要考虑的因素。", "ALT_SCREEN_WHY_CLEANER_TITLE": "为何 KoalaSync 体验更清爽", @@ -204,6 +210,6 @@ "ALT_SCREEN_LIMITS_TITLE": "客观存在的局限性", "ALT_SCREEN_LIMITS_BODY": "KoalaSync 无法替代语音通话。如果您想在观看的同时进行交流,仍可使用 Discord、Teamspeak、Signal、WhatsApp 或您常用的任何通讯工具。

此外,每位参与者都需要在各自的设备上访问相应的视频或网页。KoalaSync 无法绕过订阅、登录验证、DRM(数字版权管理)、地域限制或平台规则。

该工具目前仅支持桌面端浏览器,不包含内置聊天或语音功能;其兼容性取决于具体的浏览器和视频播放器。", "ALT_SCREEN_SUMMARY_TITLE": "摘要", - "ALT_SCREEN_SUMMARY_BODY": "如果您追求快速便捷,且不太在意画质、隐私或个性化控制,那么屏幕共享是最简单的选择。而当您希望每位参与者都在各自的浏览器中观看,并营造出一种真正\u201c共同观看\u201d的氛围时,KoalaSync 则是更理想的方案。", + "ALT_SCREEN_SUMMARY_BODY": "如果您追求快速便捷,且不太在意画质、隐私或个性化控制,那么屏幕共享是最简单的选择。而当您希望每位参与者都在各自的浏览器中观看,并营造出一种真正“共同观看”的氛围时,KoalaSync 则是更理想的方案。", "ALT_TRY_KOALASYNC": "试用 KoalaSync" } diff --git a/website/style.css b/website/style.css index c851830..5f7fce9 100644 --- a/website/style.css +++ b/website/style.css @@ -2964,3 +2964,695 @@ header[id] { } } +/* --- Hero Live Demo Scene (two synced tabs + extension popup) --- */ +.hero-mockup-wrapper { + flex-direction: column; + gap: 0.9rem; + container-type: inline-size; +} + +/* The scene is sized for the two cascading tab cards; opening the popup + grows it. (Without JS the popup-open class is set in the markup, so the + static fallback always has room for the popup.) */ +.hero-demo-scene { + position: relative; + width: 100%; + height: 370px; + height: calc(63cqw + 84px); + transition: height 0.45s cubic-bezier(0.22, 1, 0.36, 1); +} + +.hero-demo-scene.popup-open { + height: 590px; +} + +/* While initializing, suppress all transitions so the popup can start closed + without a visible open->closed animation on page load. */ +.hero-demo-scene.demo-no-anim, +.hero-demo-scene.demo-no-anim * { + transition-duration: 0s !important; +} + +/* Sync status chip: green "in sync" at rest, flashes the broadcast event */ +.demo-sync-chip { + position: absolute; + top: 0; + left: 2px; + z-index: 8; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 5px 12px; + border-radius: 99px; + background: rgba(34, 197, 94, 0.12); + border: 1px solid rgba(34, 197, 94, 0.35); + color: #86efac; + font-family: inherit; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + cursor: default; + transition: background 0.3s, border-color 0.3s, color 0.3s; +} + +.demo-chip-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 8px #22c55e; + transition: background 0.3s, box-shadow 0.3s; +} + +.demo-sync-chip .demo-chip-event { display: none; } + +.demo-sync-chip.event { + background: rgba(99, 102, 241, 0.14); + border-color: rgba(99, 102, 241, 0.45); + color: #c7d2fe; +} + +.demo-sync-chip.event .demo-chip-dot { + background: #818cf8; + box-shadow: 0 0 8px #818cf8; +} + +.demo-sync-chip.event .demo-chip-label-sync { display: none; } +.demo-sync-chip.event .demo-chip-event { display: inline; } + +@keyframes demoBlink { + 50% { opacity: 0.35; } +} + +/* Video tab cards: a clean window cascade — the right one is in front */ +.demo-tab-card { + position: absolute; + width: 274px; + width: 62cqw; + background: #131c31; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 14px; + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.45); + overflow: hidden; + cursor: pointer; + user-select: none; + transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.25s; +} + +.demo-tab-card:hover, +.demo-tab-card:focus-visible { + border-color: rgba(99, 102, 241, 0.55); +} + +/* Card A = the foreground window at the top right ("your" browser, + hosts the extension icon in its toolbar) */ +.demo-tab-a { + top: 30px; + right: 0; + transform: rotate(1.2deg); + transform-origin: top right; + z-index: 3; +} + +/* Card B = the friend's window, lower left, tucked behind */ +.demo-tab-b { + top: 168px; + top: calc(28cqw + 44px); + left: 0; + transform: rotate(-1.5deg); + transform-origin: top left; + z-index: 2; +} + +/* When the popup slides in from the right edge, the tabs step aside */ +.hero-demo-scene.popup-open .demo-tab-a { + transform: rotate(1deg) translate(-55%, 2%) scale(0.9); +} + +.hero-demo-scene.popup-open .demo-tab-b { + transform: rotate(-1.5deg) scale(0.95); +} + +.demo-tab-titlebar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px 6px 10px; + background: #0b1222; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + font-size: 10.5px; + color: var(--text-muted); + font-weight: 600; +} + +.demo-tab-dots { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.demo-tab-dots i { + display: block; + width: 7px; + height: 7px; + border-radius: 50%; + background: #334155; +} + +.demo-tab-title { + display: flex; + align-items: center; + gap: 5px; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.demo-tab-title svg { flex-shrink: 0; } + +.demo-tab-user { + flex-shrink: 0; + font-size: 9.5px; + font-weight: 700; + padding: 2px 7px; + border-radius: 99px; +} + +.demo-user-a { + color: #a5b4fc; + background: rgba(99, 102, 241, 0.15); +} + +.demo-user-b { + color: #86efac; + background: rgba(34, 197, 94, 0.12); +} + +/* Extension launcher pinned in the foreground window's toolbar */ +.demo-ext-launcher { + position: relative; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border-radius: 7px; + background: #273449; + border: 1px solid rgba(255, 255, 255, 0.12); + cursor: pointer; + transition: border-color 0.25s, box-shadow 0.25s, transform 0.15s; +} + +.demo-ext-launcher img { + display: block; + width: 16px; + height: 16px; + border-radius: 4px; +} + +.demo-ext-launcher:hover { + border-color: var(--accent); + transform: translateY(-1px); +} + +.demo-ext-launcher:active { + transform: scale(0.94); +} + +.hero-demo-scene.popup-open .demo-ext-launcher { + border-color: rgba(99, 102, 241, 0.7); + box-shadow: 0 0 10px rgba(99, 102, 241, 0.45); +} + +.demo-ext-launcher::after { + content: ''; + position: absolute; + top: -3px; + right: -3px; + width: 8px; + height: 8px; + border-radius: 50%; + background: #6366f1; + box-shadow: 0 0 8px #6366f1; + opacity: 0; + transition: opacity 0.3s; +} + +.hero-demo-scene:not(.popup-open) .demo-ext-launcher::after { + opacity: 1; + animation: demoBlink 1.4s ease-in-out infinite; +} + +/* Abstract "video" artwork with a slow cinematic pan/zoom while playing */ +.demo-video { + position: relative; + aspect-ratio: 16 / 9; + background: #020617; + overflow: hidden; +} + +.demo-video-art { + position: absolute; + inset: 0; + background: + radial-gradient(circle at 78% 22%, rgba(248, 250, 252, 0.28) 0 4%, transparent 5.5%), + radial-gradient(120% 90% at 70% 115%, #312e81 0%, transparent 60%), + radial-gradient(100% 80% at 12% 112%, #1e1b4b 0%, transparent 55%), + linear-gradient(160deg, #1e293b 0%, #0b1024 55%, #020617 100%); + transform-origin: 60% 40%; + animation: + demoKenBurns 9s ease-in-out infinite alternate, + demoArtShift 7s ease-in-out infinite alternate; + animation-play-state: paused; +} + +/* Soft light drifting through the scene */ +.demo-video-art::before { + content: ''; + position: absolute; + top: -25%; + left: -12%; + width: 70%; + aspect-ratio: 1; + border-radius: 50%; + background: radial-gradient(circle, rgba(129, 140, 248, 0.32), transparent 65%); + animation: demoGlowDrift 7s ease-in-out infinite alternate; + animation-play-state: paused; +} + +.demo-video-art::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(105deg, transparent 30%, rgba(129, 140, 248, 0.14) 45%, rgba(56, 189, 248, 0.1) 55%, transparent 70%); + transform: translateX(-100%); + animation: demoSheen 2.8s linear infinite; + animation-play-state: paused; +} + +/* Playing runs the animations; pausing freezes them mid-frame, so a synced + pause is visible at a glance */ +.demo-tab-card.playing .demo-video-art, +.demo-tab-card.playing .demo-video-art::before, +.demo-tab-card.playing .demo-video-art::after { + animation-play-state: running; +} + +@keyframes demoKenBurns { + from { transform: scale(1) translate(0, 0); } + to { transform: scale(1.18) translate(-3%, 2%); } +} + +@keyframes demoArtShift { + from { filter: hue-rotate(0deg) brightness(1); } + to { filter: hue-rotate(40deg) brightness(1.15); } +} + +@keyframes demoGlowDrift { + from { transform: translate(0, 0); } + to { transform: translate(65%, 30%); } +} + +@keyframes demoSheen { + to { transform: translateX(100%); } +} + +/* Play / pause overlay */ +.demo-play-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(2, 6, 23, 0.35); + transition: opacity 0.25s, background 0.25s; +} + +.demo-icon-wrap { + display: flex; + align-items: center; + justify-content: center; + width: 46px; + height: 46px; + border-radius: 50%; + background: rgba(15, 23, 42, 0.75); + border: 1px solid rgba(255, 255, 255, 0.25); + color: white; + box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + transition: transform 0.2s; +} + +.demo-tab-card:hover .demo-icon-wrap { transform: scale(1.1); } + +.demo-play-overlay .icon-pause { display: none; } +.demo-tab-card.playing .icon-play { display: none; } +.demo-tab-card.playing .icon-pause { display: block; } + +.demo-tab-card.playing .demo-play-overlay { + opacity: 0; + background: transparent; +} + +.demo-tab-card.playing:hover .demo-play-overlay, +.demo-tab-card.playing:focus-visible .demo-play-overlay { + opacity: 1; +} + +/* Timestamp + seekable progress bar */ +.demo-video-controls { + position: absolute; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + gap: 8px; + padding: 14px 10px 8px; + background: linear-gradient(transparent, rgba(2, 6, 23, 0.85)); + font-size: 9.5px; + color: #e2e8f0; + z-index: 1; +} + +.demo-time { + flex-shrink: 0; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 700; + letter-spacing: 0.03em; + font-variant-numeric: tabular-nums; +} + +.demo-progress { + position: relative; + flex: 1; + height: 5px; + border-radius: 99px; + background: rgba(255, 255, 255, 0.18); + cursor: pointer; +} + +/* Generous invisible hit area for seeking */ +.demo-progress::before { + content: ''; + position: absolute; + inset: -8px 0; +} + +.demo-progress-fill { + position: relative; + height: 100%; + width: 30%; + border-radius: 99px; + background: linear-gradient(90deg, #6366f1, #a855f7); + pointer-events: none; +} + +/* Scrub thumb, revealed on hover */ +.demo-progress-fill::after { + content: ''; + position: absolute; + right: -5px; + top: 50%; + width: 11px; + height: 11px; + margin-top: -5.5px; + border-radius: 50%; + background: #e2e8f0; + box-shadow: 0 0 6px rgba(99, 102, 241, 0.8); + opacity: 0; + transform: scale(0.6); + transition: opacity 0.2s, transform 0.2s; +} + +.demo-tab-card:hover .demo-progress-fill::after { + opacity: 1; + transform: scale(1); +} + +/* Sync pulse ring on both cards */ +.demo-tab-card.sync-pulse { + animation: demoCardPulse 0.9s ease-out; +} + +@keyframes demoCardPulse { + 0% { box-shadow: 0 18px 36px rgba(0, 0, 0, 0.45), 0 0 0 0 rgba(99, 102, 241, 0.65); } + 100% { box-shadow: 0 18px 36px rgba(0, 0, 0, 0.45), 0 0 0 22px rgba(99, 102, 241, 0); } +} + +/* The existing popup mockup slides in at the right edge, dropping out of + the toolbar icon of the foreground window */ +.hero-demo-scene .extension-mockup { + position: absolute; + top: 64px; + right: 0; + z-index: 5; + width: 292px; + width: min(320px, 66cqw); + margin: 0; + transform-origin: top right; + transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s, visibility 0.45s; +} + +.hero-demo-scene:not(.popup-open) .extension-mockup { + transform: translate(2%, -3%) scale(0.5); + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +/* Ghost cursor for the automated walkthrough */ +.demo-cursor { + position: absolute; + top: 52%; + left: 46%; + z-index: 20; + width: 22px; + height: 22px; + opacity: 0; + pointer-events: none; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.6)); + transition: top 0.65s cubic-bezier(0.45, 0.05, 0.25, 1), left 0.65s cubic-bezier(0.45, 0.05, 0.25, 1), opacity 0.35s; +} + +.demo-cursor.visible { opacity: 1; } + +.demo-cursor svg { + display: block; + width: 100%; + height: 100%; +} + +.demo-cursor::before { + content: ''; + position: absolute; + top: -6px; + left: -8px; + width: 32px; + height: 32px; + border-radius: 50%; + border: 2px solid #818cf8; + opacity: 0; + transform: scale(0.35); +} + +.demo-cursor.clicking::before { + animation: demoClickRipple 0.45s ease-out; +} + +@keyframes demoClickRipple { + 0% { opacity: 0.9; transform: scale(0.35); } + 100% { opacity: 0; transform: scale(1.25); } +} + +/* Caption below the scene */ +.demo-hint { + margin: 0; + text-align: center; + font-size: 0.82rem; + color: var(--text-muted); + opacity: 0; + transition: opacity 0.6s; +} + +.demo-hint.show { opacity: 0.75; } + +/* Mobile: fall back to the classic static popup mockup */ +@media (max-width: 768px) { + .hero-demo-scene, + .hero-demo-scene.popup-open { + height: auto; + max-width: 320px; + margin: 0 auto; + } + + .demo-tab-card, + .demo-sync-chip, + .demo-cursor, + .demo-hint { + display: none !important; + } + + .hero-demo-scene .extension-mockup, + .hero-demo-scene:not(.popup-open) .extension-mockup { + position: static; + width: 100%; + transform: none; + opacity: 1; + visibility: visible; + pointer-events: auto; + } +} + +@media (prefers-reduced-motion: reduce) { + .hero-demo-scene:not(.popup-open) .demo-ext-launcher::after { + animation: none; + } +} + +/* --- Story walkthrough additions: room creation, invite flight, film --- */ + +/* The stick-figure film carries the motion now — keep only the color drift + on the backdrop (the animation shorthand resets play-state, so re-pause) */ +.demo-video-art { + animation: demoArtShift 7s ease-in-out infinite alternate; + animation-play-state: paused; +} + +/* Tiny abstract "movie": a walker strolls across, a ball bounces ahead. + Pausing freezes every element mid-frame — on both tabs at once. */ +.demo-film { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.film-limb { + stroke: #e2e8f0; + stroke-width: 2; + stroke-linecap: round; +} + +.film-walker { + transform: translate(-15px, 74px); + animation: filmWalk 10s linear infinite; +} + +@keyframes filmWalk { + to { transform: translate(175px, 74px); } +} + +.film-bob { + animation: filmBob 0.5s ease-in-out infinite alternate; +} + +@keyframes filmBob { + to { transform: translateY(-1.8px); } +} + +.film-swing-1, +.film-swing-2 { + transform-box: fill-box; + transform-origin: top center; +} + +.film-swing-1 { animation: filmSwing 0.5s ease-in-out infinite alternate; } +.film-swing-2 { animation: filmSwing 0.5s ease-in-out infinite alternate-reverse; } + +@keyframes filmSwing { + from { transform: rotate(30deg); } + to { transform: rotate(-30deg); } +} + +.film-ball-x { + animation: filmBallX 6.5s linear infinite; +} + +@keyframes filmBallX { + to { transform: translateX(175px); } +} + +.film-ball { + animation: filmBallY 0.55s cubic-bezier(0.4, 0, 0.6, 1) infinite alternate; +} + +@keyframes filmBallY { + to { transform: translateY(-16px); } +} + +.demo-film * { animation-play-state: paused; } +.demo-tab-card.playing .demo-film * { animation-play-state: running; } + +/* The sync chip only appears once the room is connected (story progression) */ +.demo-sync-chip { + opacity: 0; + transform: translateY(-4px); + pointer-events: none; + transition: background 0.3s, border-color 0.3s, color 0.3s, opacity 0.4s, transform 0.4s; +} + +.hero-demo-scene.connected .demo-sync-chip { + opacity: 1; + transform: none; + pointer-events: auto; +} + +/* Toast on the friend's window ("Room joined", "Tab selected") */ +.demo-tab-toast { + position: absolute; + top: 36px; + left: 50%; + transform: translate(-50%, -6px); + z-index: 2; + padding: 4px 10px; + border-radius: 99px; + background: rgba(15, 23, 42, 0.92); + border: 1px solid rgba(99, 102, 241, 0.5); + color: #c7d2fe; + font-size: 9.5px; + font-weight: 700; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s, transform 0.3s; +} + +.demo-tab-toast.show { + opacity: 1; + transform: translate(-50%, 0); +} + +/* Invite link pill flying from the popup to the friend's window */ +.demo-invite-fly { + position: absolute; + z-index: 30; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + border-radius: 99px; + background: var(--accent); + color: white; + font-size: 9.5px; + font-weight: 700; + white-space: nowrap; + box-shadow: 0 6px 18px rgba(99, 102, 241, 0.5); + opacity: 0; + pointer-events: none; + transition: left 0.75s cubic-bezier(0.3, 0.7, 0.4, 1), top 0.75s cubic-bezier(0.3, 0.7, 0.4, 1), opacity 0.3s; +} + +/* Attention flash (e.g. on the video select while "choosing the tab") */ +.demo-attn { + animation: demoAttn 0.7s ease-in-out 2; +} + +@keyframes demoAttn { + 50% { box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.75); } +} diff --git a/website/template.html b/website/template.html index 4eda357..34ae4b7 100644 --- a/website/template.html +++ b/website/template.html @@ -301,8 +301,91 @@ - +
+