Compare commits

...

6 Commits

Author SHA1 Message Date
KoalaDev 36f494fa55 feat(extension): one-click invite button in empty peer list + v2.4.1 changelog
- "No peers yet" state now shows a localized "Copy invite link" button (reuses BTN_COPY_INVITE, present in all 13 locales) so users can share without hunting for the field
- Add v2.4.1 changelog entry covering the onboarding tour improvements, empty-state action, and the mobile landing-page fixes from this session

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:50:04 +02:00
KoalaDev b5b3885d19 feat(extension): finish onboarding tour with closing step & cleaner welcome
- Add the previously-orphaned 5th onboarding step (ONBOARDING_5) so the tour ends on a closing card instead of stopping at the username field
- Make step 1 a centered welcome card instead of spotlighting the logo; guard querySelector for target-less steps
- Fix stale static placeholders in popup.html (Step 1 of 3 -> 5, progress 33% -> 20%)

Verified ONBOARDING_5/DONE exist across all 13 locales; rendered with the longest (German) copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:40:40 +02:00
KoalaDev 373f2d127a fix(website): mobile comparison cards, hamburger nav, hero symmetry & reveal fallback
- Comparison table: stack into per-feature cards on phones (no more horizontal scroll); bring feature descriptions back
- Re-enable mobile hamburger menu (was hidden via display:none !important); keep logo/language/button uncrowded down to ~320px
- Fix hero asymmetry: extension mockup forced the grid column wider than the container; make it width:100%/max-width:320px and use minmax(0,1fr)
- Add IntersectionObserver guards + <noscript> fallback so reveal-animated content never stays hidden

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:27:07 +02:00
KoalaDev 3ebe80aab2 refactor: replace dynamic GitHub API badges with static shields.io badges 2026-06-17 06:39:42 +02:00
Timo e7b59d61ac docs: update all docs for lazy-connect, refactor, and .cjs rename
- ARCHITECTURE.md: startup -> lazy connect, reconnect when in room
- HOW_IT_WORKS.md: on-demand connection, heartbeat while connected
- PRIVACY.md: clarify alarms only during active sessions
- SYNC_GUIDE.md: build-extension.cjs + episode-utils injection
- StoreDescription.md: on-demand relay, no persistent connection
- README.md + extension/README.md: npm run build:extension
- extension/README.md: replace Dual Heartbeat with On-Demand Connection
- website/privacy.html: add lazy-connect privacy note
2026-06-16 14:17:19 +02:00
GitHub Action 71f7ac425a chore(release): update versions to v2.4.0 [skip ci] 2026-06-16 12:00:03 +00:00
17 changed files with 237 additions and 57 deletions
+5 -5
View File
@@ -6,15 +6,15 @@
<p align="center">
<a href="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml"><img src="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml/badge.svg" alt="Release Status"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/github/v/release/Shik3i/KoalaSync" alt="GitHub release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Shik3i/KoalaSync?color=blue" alt="License"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v2.4.0-blue?logo=github" alt="GitHub release"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue" alt="License"></a>
<a href="https://addons.mozilla.org/de/firefox/addon/koalasync/"><img src="https://img.shields.io/badge/Firefox-Download-orange?logo=firefoxbrowser&logoColor=white" alt="Firefox Add-on"></a>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
</p>
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.3.2 Release!</b> — See what's changed</a></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.4.0 Release!</b> — See what's changed</a></p>
### 🌟 Why KoalaSync?
@@ -81,7 +81,7 @@ Both the official KoalaSync website and the **v2.0 Browser Extension** feature f
To build the extension from source and synchronize protocol constants:
```bash
npm install
node scripts/build-extension.js
npm run build:extension
```
The compiled artifacts will be available in the `dist/` directory.
@@ -103,7 +103,7 @@ To connect your extension to a self-hosted server, open the popup → **Room** t
To verify your relay is reachable from outside, visit `https://your-domain.com` in a browser — it should return `{"status":"online","service":"KoalaSync Relay"}`.
#### Supply Chain Security (v2.3.2+)
#### Supply Chain Security (v2.4.0+)
All official release artifacts (Docker images and extension binaries) are published with signed [artifact attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations) to prove they were built from this repository's source code.
+1 -1
View File
@@ -40,7 +40,7 @@ KoalaSync is built for private watch parties without unnecessary data collection
⚙️ UNDER THE HOOD
KoalaSync is lightweight, transparent, and built with privacy in mind.
Real-Time Relay: Playback state is synchronized through a custom WebSocket-based relay server for fast room updates.
On-Demand Relay: Playback state is synchronized through a custom WebSocket-based relay server. No persistent connection — the relay is only active while you're in a room. No background traffic, no idle connections.
• No Media Streaming: KoalaSync does not stream, proxy, upload, download, or redistribute any video content. Everyone watches from their own browser on the original website.
• Temporary Room State Only: The relay server only coordinates room state such as play, pause, seek position, active target, nickname, and readiness status.
• Docker Self-Hosting: The relay server can be self-hosted with Docker if you prefer to run your own private instance.
+6 -5
View File
@@ -2,9 +2,10 @@
This document describes the communication flows and internal logic of the KoalaSync system.
## 1. Extension Startup & Connection
- **Initialization**: On startup, `background.js` reads settings (Server URL, Username, Last Room) from `chrome.storage.sync`.
- **WebSocket Handshake**:
## 1. Extension Connection (Lazy Connect)
- **Initialization**: On startup, `background.js` reads settings (Server URL, Username, Last Room) from `chrome.storage.sync`. No WebSocket connection is established at this point.
- **On-Demand Connection**: The extension only connects when needed — either the user opens the popup with saved room credentials, or when actively in a room. When not in a room, no connection exists. This improves privacy (IP not exposed while idle) and reduces battery/network usage.
- **WebSocket Handshake (when connecting)**:
1. Background creates a `new WebSocket` to `/socket.io/?EIO=4&transport=websocket&version=1.0.0`.
2. Server performs security checks:
- **IP Rate Limit**: Checks if the IP has exceeded connection limits.
@@ -45,7 +46,7 @@ To maintain a clean room state and eliminate "Ghost Peers":
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
- **Reconnect Strategy**: Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin.
- **Reconnect Strategy (while in room)**: Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin. When not in a room, no reconnection occurs.
> [!CAUTION]
> **Identity Rule**: Differentiate between `peerId` and `socket.id`. Use `socket.id` exclusively for ephemeral transport routing on the server. Use `peerId` exclusively for identity, state management, and room tracking across the stack.
@@ -70,5 +71,5 @@ KoalaSync uses a megaphone routing approach to minimize server logic:
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
- **Relay Server & Extension Modules**: `background.js` and `popup.js` import constants directly from `shared/constants.js`.
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a synchronized copy of `EVENTS` and constants.
- **Automation**: The `node scripts/build-extension.js` script automatically injects these constants into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
- **Automation**: The `npm run build:extension` script automatically injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
- **Verification**: Any protocol change is automatically propagated across the stack by running the build script.
+16
View File
@@ -4,6 +4,22 @@ All notable changes to the KoalaSync browser extension and relay server.
---
## [v2.4.1] — 2026-06-19
### Added
- **Extension: Onboarding tour now has a closing step** — The first-run tour ends on a dedicated "You're all set!" card (the `ONBOARDING_5` copy that already existed in all 13 locales but was never shown). The tour no longer stops abruptly on the username step.
- **Extension: One-click invite from the empty peer list** — The "No peers yet" state now shows a **📋 Invite Link** button that copies the invite link to the clipboard, so users can share it without hunting for the field.
### Changed
- **Extension: Cleaner onboarding welcome** — Step 1 is now a centered welcome card instead of spotlighting the logo title. Added a guard so target-less tour steps center cleanly.
- **Website: Mobile comparison table** — The KoalaSync vs Teleparty table stacks into per-feature cards on phones instead of forcing horizontal scrolling; feature descriptions are shown again on mobile.
### Fixed
- **Extension: Onboarding step counter/progress placeholders** — Static `Step 1 of 3` / 33% fallbacks in `popup.html` corrected to match the actual 5-step tour (`Step 1 of 5` / 20%).
- **Website: Mobile navigation restored** — The header hamburger menu was hidden by a `display:none !important` rule, leaving the nav links unreachable on phones. Re-enabled, with spacing kept comfortable down to ~320px.
- **Website: Hero alignment on mobile** — A fixed-width extension mockup forced the hero grid column wider than the container, shifting all hero content off-center (larger left margin than right). The mockup is now responsive (`width:100%/max-width` + `minmax(0,1fr)` grid track).
- **Website: Reveal-animation fallback** — Added a `<noscript>` style fallback and `IntersectionObserver` feature guards so scroll-revealed content can never stay invisible if JavaScript is disabled or unsupported.
## [v2.4.0] — 2026-06-16
### Added
+3 -3
View File
@@ -16,9 +16,9 @@ This guide walks through the complete user flow of KoalaSync, from creating a ro
## Step 2: Connecting to the Relay Server
When you open the extension popup, the background service worker connects to the relay server:
When you open the extension popup (with saved room credentials) or when a saved room configuration exists from a previous session, the background service worker connects to the relay server:
1. **WebSocket Handshake**: `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket`.
1. **WebSocket Handshake** (on demand): `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket` only when needed (popup opened or active room).
2. **Security Checks** (server-side):
- The server checks the client's **IP rate limit** (max 10 connections per 60 seconds).
- The server validates the **authentication token** (hardcoded in `shared/constants.js`) to verify this is a legitimate KoalaSync client.
@@ -155,7 +155,7 @@ While in a room, two heartbeats keep the session alive:
| Heartbeat | Interval | Source | Purpose |
|:----------|:---------|:-------|:--------|
| **Background** | 30 seconds | `background.js` | Signals "I'm still connected" and triggers aggressive reconnect (500ms base, max 5s) |
| **Background** | 30 seconds | `background.js` | While connected, signals "I'm still connected" and triggers automatic reconnect (500ms base, max 5s). No heartbeats fire when idle (lazy connect). |
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, `mediaTitle`, `playbackState`, `volume`, `muted` |
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
+1 -1
View File
@@ -33,7 +33,7 @@ The browser extension requires the following permissions:
- `storage`: To remember your local preferences (username, server URL, room settings).
- `tabs` & `scripting`: To detect and control video elements on the pages you choose to sync.
- `<all_urls>` (host permission): Required to detect `<video>` elements on any website the user chooses to synchronize. The extension only activates on the specific tab the user has actively selected — it does not scan, monitor, or interact with any other tabs or pages.
- `alarms`: To keep the background service worker alive during active sync sessions.
- `alarms`: To keep the background service worker alive during active sync sessions (the extension only stays connected while you are in a room).
- `notifications`: To display sync status updates (e.g., "Peer joined", "Force Sync initiated").
- **No History Access**: We do not read, store, or transmit your browsing history. We only interact with the specific tab you have actively selected for synchronization.
+4 -2
View File
@@ -17,13 +17,15 @@ You MUST run the build script in any of the following scenarios:
Run the Node.js build script from the repository root:
```bash
node scripts/build-extension.js
node scripts/build-extension.cjs
# or simply:
npm run build:extension
```
## What does it do?
The build script performs the following actions:
1. Synchronizes protocol constants by copying `shared/constants.js`, `shared/blacklist.js`, and `shared/README.md` into `extension/shared/`.
2. Injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` via marker-based replacement.
2. Injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions (`extractEpisodeId`, `sameEpisode`) into `content.js` via marker-based replacement.
3. Compiles browser-specific manifest files.
4. Packages the final ready-to-publish extension artifacts for Chrome and Firefox into the `dist/` directory.
+3 -3
View File
@@ -6,7 +6,7 @@ A Manifest V3 Browser Extension (Chrome & Firefox) for synchronized video playba
- **Manifest V3**: Optimized Service Worker architecture with session persistence.
- **Pure Vanilla JS**: No external dependencies or heavy libraries.
- **Smart Peer IDs**: Hexadecimal IDs combined with customizable Usernames for easy identification.
- **Dual Heartbeat**: Advanced session tracking (Background) and video synchronization (Content) to prevent ghost sessions.
- **On-Demand Connection**: The service worker only maintains a WebSocket connection while you're in a room. No persistent background connections — privacy-first architecture. Based on `connectIntent` flag that gates all reconnect attempts.
- **Live Diagnostics**: Built-in "Dev" tab for real-time video state debugging (ReadyState, CurrentTime, etc.).
- **Dynamic i18n (Multi-Language)**: Fully localized in 6 languages (`en`, `de`, `fr`, `es`, `pt-BR`, `ru`) with auto-detected fallback and dynamic on-the-fly language selectors.
@@ -26,7 +26,7 @@ KoalaSync requires `<all_urls>` permission to detect and interact with video ele
## Installation
1. **Prepare Extension**: From the repository root, run:
```bash
node scripts/build-extension.js
npm run build:extension
```
2. Open Chrome and go to `chrome://extensions/`.
3. Enable **Developer mode** (top right).
@@ -35,7 +35,7 @@ KoalaSync requires `<all_urls>` permission to detect and interact with video ele
## Development
If you modify `shared/constants.js`, you must synchronize the changes by running the build script from the root:
```bash
node scripts/build-extension.js
npm run build:extension
```
This ensures that the `extension/shared` folder is updated with the latest protocol constants.
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"default_locale": "en",
"name": "KoalaSync",
"version": "2.3.2",
"version": "2.4.0",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
+2 -2
View File
@@ -659,14 +659,14 @@
<div id="onboarding-card" style="position:fixed; background:var(--card); padding:16px 20px; border-radius:12px; width:280px; box-shadow:0 8px 32px rgba(0,0,0,0.5); z-index:1002; transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-sizing:border-box; border:1px solid #334155; opacity:0; transform: scale(0.9); display:none;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<span id="onboarding-icon" style="font-size:24px;">👋</span>
<span id="onboarding-step-indicator" style="font-size:10px; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:0.5px;">Step 1 of 3</span>
<span id="onboarding-step-indicator" style="font-size:10px; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:0.5px;">Step 1 of 5</span>
</div>
<h2 id="onboarding-title" style="color:var(--accent); margin:0 0 6px; font-size:14px; font-weight:700; text-align:left;">Welcome to KoalaSync!</h2>
<p id="onboarding-text" style="color:var(--text-muted); font-size:12px; margin:0 0 16px; line-height:1.4; text-align:left;">Let's get you started.</p>
<!-- Progress Bar -->
<div id="onboarding-progress-container" style="height:4px; background:#334155; border-radius:2px; margin-bottom:16px; overflow:hidden;">
<div id="onboarding-progress-bar" style="height:100%; width:33%; background:var(--accent); transition:width 0.3s ease;"></div>
<div id="onboarding-progress-bar" style="height:100%; width:20%; background:var(--accent); transition:width 0.3s ease;"></div>
</div>
<div style="display:flex; gap:8px; justify-content:space-between; align-items:center;">
+29 -4
View File
@@ -503,6 +503,24 @@ function renderEmpty(container, type) {
wrapper.appendChild(iconDiv);
wrapper.appendChild(titleDiv);
wrapper.appendChild(hintDiv);
// "No peers yet" — turn the hint into a one-click action so the user can
// immediately copy and share the invite link instead of hunting for it.
if (type === 'peers' && elements.inviteLink && elements.inviteLink.value) {
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'primary';
copyBtn.style.cssText = 'display:inline-block; width:auto; margin-top:12px; padding:6px 14px; font-size:11px;';
copyBtn.textContent = getMessage('BTN_COPY_INVITE');
copyBtn.title = getMessage('BTN_COPY_INVITE_TOOLTIP');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(elements.inviteLink.value)
.then(() => showToast(getMessage('TOAST_INVITE_COPIED'), 'success', 2000))
.catch(() => showToast(getMessage('TOAST_COPY_FAILED'), 'error'));
});
wrapper.appendChild(copyBtn);
}
container.replaceChildren(wrapper);
}
@@ -2119,9 +2137,9 @@ const onboardingSteps = [
{
icon: '👋',
get title() { return getMessage('ONBOARDING_1_TITLE'); },
get text() { return getMessage('ONBOARDING_1_TEXT'); },
get text() { return getMessage('ONBOARDING_1_TEXT'); },
targetTab: 'tab-room',
targetSelector: 'h1'
targetSelector: null
},
{
icon: '🏠',
@@ -2140,9 +2158,16 @@ const onboardingSteps = [
{
icon: '⚙️',
get title() { return getMessage('ONBOARDING_4_TITLE'); },
get text() { return getMessage('ONBOARDING_4_TEXT'); },
get text() { return getMessage('ONBOARDING_4_TEXT'); },
targetTab: 'tab-settings',
targetSelector: '#username'
},
{
icon: '🍿',
get title() { return getMessage('ONBOARDING_5_TITLE'); },
get text() { return getMessage('ONBOARDING_5_TEXT'); },
targetTab: 'tab-room',
targetSelector: null
}
];
@@ -2316,7 +2341,7 @@ function renderOnboardingStep() {
}
onboardingTimeout = setTimeout(() => {
const targetEl = document.querySelector(step.targetSelector);
const targetEl = step.targetSelector ? document.querySelector(step.targetSelector) : null;
if (targetEl && targetEl.offsetParent !== null) {
positionSpotlightAndCard(targetEl);
} else {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "koalasync",
"version": "2.3.2",
"version": "2.4.0",
"description": "KoalaSync Build Scripts",
"private": true,
"type": "module",
+29 -21
View File
@@ -19,31 +19,39 @@ document.addEventListener('DOMContentLoaded', () => {
// Scroll Reveal Logic (IntersectionObserver for performance)
const revealElements = document.querySelectorAll('[data-reveal]');
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
revealObserver.unobserve(entry.target);
}
});
}, {
rootMargin: '0px 0px -150px 0px',
threshold: 0.1
});
revealElements.forEach(el => revealObserver.observe(el));
if ('IntersectionObserver' in window) {
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
revealObserver.unobserve(entry.target);
}
});
}, {
rootMargin: '0px 0px -150px 0px',
threshold: 0.1
});
revealElements.forEach(el => revealObserver.observe(el));
} else {
// Fallback: without IntersectionObserver support, reveal everything
// immediately so no content can ever stay hidden.
revealElements.forEach(el => el.classList.add('revealed'));
}
// Auto-update URL hash as user scrolls through sections
// (preserves position across language switches)
const sectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
history.replaceState(null, null, '#' + entry.target.id);
}
});
}, { threshold: 0.3 });
document.querySelectorAll('section[id], header[id]').forEach(el => sectionObserver.observe(el));
if ('IntersectionObserver' in window) {
const sectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
history.replaceState(null, null, '#' + entry.target.id);
}
});
}, { threshold: 0.3 });
document.querySelectorAll('section[id], header[id]').forEach(el => sectionObserver.observe(el));
}
// Navbar scroll effect
const nav = document.querySelector('nav');
+3
View File
@@ -113,6 +113,9 @@
<p>
To enable cross-device synchronization, the KoalaSync browser extension temporarily captures data from the currently active video tab (e.g., tab title, media metadata like the video title, and playback state). This data is exclusively sent to other participants in your room for synchronization. We explicitly <strong>do not read, store, or transmit your general browsing history</strong>.
</p>
<p style="margin-top: 0.5rem;">
The extension only connects to the relay server while you are actively in a room. No persistent background connection is maintained — your IP address is not exposed to the server when you are not using the extension.
</p>
</section>
<section>
+123 -5
View File
@@ -358,7 +358,8 @@ nav {
}
.extension-mockup {
width: 320px;
width: 100%;
max-width: 320px;
height: 520px;
background: #0f172a;
border-radius: 20px;
@@ -1900,7 +1901,7 @@ footer {
@media (max-width: 768px) {
.hero-grid, .step {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
text-align: center;
}
.hero-text h1 {
@@ -1931,7 +1932,7 @@ footer {
margin-left: auto;
}
.hamburger {
display: none !important;
display: inline-flex !important;
}
nav .container {
padding: 0 0.5rem;
@@ -1966,6 +1967,21 @@ footer {
}
}
/* Extra-narrow phones: make sure logo, language picker and the menu button
never crowd each other in the header (kept comfortable down to ~320px). */
@media (max-width: 380px) {
nav .container {
padding: 0 0.4rem;
}
.nav-right {
gap: 0.35rem;
}
.logo-area {
font-size: 1rem;
gap: 0.4rem;
}
}
/* --- Language Toggle --- */
html.lang-de [lang="en"] {
display: none !important;
@@ -2450,13 +2466,115 @@ html:not(.lang-de) [lang="de"] {
}
@media (max-width: 768px) {
.comparison-table th,
.comparison-table th,
.comparison-table td {
padding: 1rem;
font-size: 0.85rem;
}
}
/* On phones the 3-column table is turned into stacked cards so there is
no horizontal scrolling — one card per feature, KoalaSync above Teleparty. */
@media (max-width: 600px) {
.comparison-table-wrapper {
overflow-x: visible;
background: transparent;
border: none;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
margin-top: 0;
}
.comparison-table {
display: block;
font-size: 0.95rem;
}
/* Visually hide the header row but keep it for screen readers. */
.comparison-table thead {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.comparison-table tbody,
.comparison-table tr,
.comparison-table td {
display: block;
width: 100%;
}
.comparison-table tbody tr {
background: rgba(30, 41, 59, 0.35);
border: 1px solid var(--glass-border);
border-radius: 14px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
margin-bottom: 1rem;
overflow: hidden;
}
.comparison-table tbody tr:last-child {
margin-bottom: 0;
}
.comparison-table td {
padding: 0.85rem 1.1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.comparison-table tbody tr td:last-child {
border-bottom: none;
}
/* Feature name acts as the card header. */
.comparison-table td.feat-name {
background: rgba(15, 23, 42, 0.55);
padding: 0.9rem 1.1rem;
}
/* Room is back on the card, so show the description again. */
.feat-desc {
display: none;
display: block;
margin-top: 0.15rem;
}
/* Label each value cell with the product it belongs to. */
.comparison-table td.check,
.comparison-table td.cross {
display: flex;
align-items: baseline;
gap: 0.4rem;
}
.comparison-table td.check::before,
.comparison-table td.cross::before {
flex: 0 0 5.5rem;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
opacity: 0.85;
}
.comparison-table td.check::before {
content: "KoalaSync";
}
.comparison-table td.cross::before {
content: "Teleparty";
}
/* Keep the KoalaSync row subtly highlighted, like on desktop. */
.comparison-table td.check.highlight {
background: rgba(99, 102, 241, 0.08);
}
}
+8 -1
View File
@@ -102,7 +102,7 @@
"priceCurrency": "EUR"
},
"description": "Watch Netflix, YouTube, Twitch, Jellyfin, Emby and almost any HTML5 video in perfect sync with friends. Open source, privacy-first, free.",
"softwareVersion": "2.3.2",
"softwareVersion": "2.4.0",
"license": "https://opensource.org/licenses/MIT",
"sameAs": "https://github.com/Shik3i/KoalaSync",
"image": "https://sync.koalastuff.net/assets/NewLogoIcon.webp",
@@ -183,6 +183,13 @@
]
}
</script>
<noscript>
<!-- Without JS the scroll-reveal observer never runs, so make all
reveal-animated content visible by default. -->
<style>
[data-reveal] { opacity: 1 !important; transform: none !important; }
</style>
</noscript>
</head>
<body>
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "2.3.2",
"date": "2026-06-16T02:29:41Z"
"version": "2.4.0",
"date": "2026-06-16T12:00:03Z"
}