mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 003741a63f | |||
| e644ab33cd | |||
| 9006ebbc67 | |||
| e40017f053 | |||
| fa7eea54d8 | |||
| 04d2360814 | |||
| 29aba936ce | |||
| 4bae88e107 | |||
| 3e4b97b11a | |||
| e555906553 | |||
| 4f1335242c | |||
| e683466bf0 | |||
| 788a3331d3 | |||
| 2d9b7bc099 | |||
| 717d37c693 | |||
| 1f48e9df47 | |||
| adab684ed0 | |||
| e23f9ab226 | |||
| 77203fe8fd | |||
| 5379778a20 | |||
| 9fc58dca4b | |||
| 17ac5c0a6f | |||
| 1ec2396c41 | |||
| 7c65fe80cb | |||
| a326087ca5 | |||
| de518ac205 | |||
| ce3cf0ef89 | |||
| 9626948f29 | |||
| dfa0a3d03e | |||
| 02c41776b8 | |||
| b32e2fa8cc | |||
| 9963da2ebc | |||
| 9feafab617 | |||
| d26b95985d | |||
| bb030a9737 | |||
| 95ff460856 | |||
| 96e2207cf3 | |||
| 2928393b4c | |||
| 5863991bfb | |||
| 1eca3fa5bb | |||
| 58af258fd3 | |||
| 69f8f4cfd7 | |||
| af9cf34f0b | |||
| b1ea280f0b | |||
| 9006570bd1 | |||
| f9dc9d4017 | |||
| 7b37d783b5 | |||
| 9f4be58172 | |||
| a968f708b3 | |||
| 2995afe970 | |||
| 68027a21d5 | |||
| 4aa6f6d7a6 | |||
| 7602bfdb21 | |||
| 3ccb967a45 | |||
| fe970de39a | |||
| e795339924 | |||
| 06b5ffbd0b | |||
| 24cac52674 | |||
| 6276ee134c | |||
| d847389eef | |||
| aa84b63c77 | |||
| bd60a14754 | |||
| a1bdcf4325 | |||
| b846803062 | |||
| 4bc7ad365d | |||
| 68b2205b0d | |||
| b450845522 | |||
| 27023ea58e | |||
| 1a7d23ce93 | |||
| d2c8ca1c1e | |||
| 1c404a7f11 | |||
| e2bba04efd | |||
| c7b29c277c | |||
| e06965b95c | |||
| 23d43741ce | |||
| 027d2dbde5 | |||
| a0c16df664 | |||
| 8fd866f20a | |||
| 8f72c238a2 | |||
| 24ad7c3732 |
@@ -0,0 +1,75 @@
|
||||
name: Beta Server Image
|
||||
|
||||
# Publishes the relay server as a Docker image under NON-production tags so a
|
||||
# feature branch can be deployed to a staging/backup server and used as a custom
|
||||
# server, without ever touching the ':latest' tag the official relay tracks.
|
||||
#
|
||||
# Tags produced (on ghcr.io/<owner>/<repo>):
|
||||
# - beta moving channel pointer to the newest build
|
||||
# - <branch-slug> e.g. feature-host-control-mode
|
||||
# - sha-<short-commit> immutable, pin to an exact build
|
||||
# - <custom> only on manual run, e.g. hostcontrol01
|
||||
# Never ':latest' (flavor: latest=false).
|
||||
#
|
||||
# Remove this workflow once the feature is merged & released the normal way.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- feature/host-control-mode
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Extra immutable tag for this build (e.g. hostcontrol01). Optional.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
concurrency:
|
||||
group: beta-image-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-beta-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
# Never publish ':latest' from a beta build.
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=beta
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,46 @@
|
||||
name: CI
|
||||
|
||||
# Runs the full verification suite (lint, unit/integration tests, production
|
||||
# audits, extension + website build) on every push to main and every PR, so a
|
||||
# regression can never reach main or a release tag unchecked.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Cancel superseded runs on the same ref to save CI minutes. Unlike the release
|
||||
# workflow, an interrupted CI run has no side effects.
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
server/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
|
||||
# The server test suite (test-server-ws/routes/ops) imports express,
|
||||
# socket.io, and dotenv from server/node_modules, so install them too.
|
||||
- name: Install server dependencies
|
||||
run: npm ci
|
||||
working-directory: server
|
||||
|
||||
- name: Run verification suite
|
||||
run: npm run verify
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Build Extensions
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
npm run build:extension
|
||||
|
||||
- name: Generate artifact attestation for extensions
|
||||
|
||||
@@ -27,6 +27,7 @@ Thumbs.db
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<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/badge/Release-v2.4.1-blue?logo=github" alt="GitHub release"></a>
|
||||
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v2.5.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>
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<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.4.1 Release!</b> — See what's changed</a></p>
|
||||
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.5.0 Release!</b> — See what's changed</a></p>
|
||||
|
||||
### 🌟 Why KoalaSync?
|
||||
|
||||
@@ -59,7 +59,7 @@ The easiest and safest way to install KoalaSync is directly through the official
|
||||
### 🌐 Localization & Translations
|
||||
|
||||
Both the official KoalaSync website and the **v2.0 Browser Extension** feature full dynamic localization:
|
||||
- **Available Languages**: Support is included for 13 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), and European Portuguese (`pt`).
|
||||
- **Available Languages**: Support is included for 15 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), Chinese (Simplified) (`zh`), Ukrainian (`uk`), and European Portuguese (`pt`).
|
||||
- **Real-Time Extension Localization**: Inside the extension Settings panel, users can swap languages instantly. The entire interface, notifications, Empty States, and onboarding guides re-translate dynamically in real-time.
|
||||
- **Contributing**: We welcome community translations for both the website and the extension! Please refer directly to the [TRANSLATION.md](docs/TRANSLATION.md) guide for step-by-step instructions on how to audit, refine, or add new languages.
|
||||
|
||||
@@ -138,7 +138,7 @@ gh attestation verify dist/koalasync-chrome.zip \
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub><a href="https://ko-fi.com/koaladev"><img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi"></a></sub>
|
||||
<sub><a href="https://support.koalastuff.net"><img src="https://img.shields.io/badge/Support-KoalaSync-FF5E5B" alt="Support KoalaSync"></a></sub>
|
||||
<sub><a href="https://gitgem.org/github/Shik3i/KoalaSync"><img src="https://gitgem.org/api/badge/github/Shik3i/KoalaSync.svg" alt="GitGem Badge" /></a></sub>
|
||||
|
||||
<sub>Built with ❤️ by <a href="https://github.com/Shik3i">Shik3i</a>. KoalaSync is Open Source under the <a href="LICENSE">MIT License</a>.</sub>
|
||||
|
||||
@@ -82,6 +82,12 @@ KoalaSync's security is grounded in its architecture:
|
||||
|
||||
If you find a way to bypass any of these protections, we want to know about it.
|
||||
|
||||
> [!NOTE]
|
||||
> Some frequently-reported "issues" are **intentional and out of scope** for our
|
||||
> threat model (ephemeral, account-less rooms of invited participants) — e.g. an
|
||||
> unauthenticated `peerId` or non-constant-time room-password compare. Before
|
||||
> reporting, please read **[`docs/KNOWN_LIMITATIONS.md`](docs/KNOWN_LIMITATIONS.md)**.
|
||||
|
||||
---
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# KoalaSync — Marketing Copy Kit
|
||||
|
||||
Ready-to-paste copy for product listings, launch pages, directory submissions, and anywhere else you keep re-typing the same pitch. Three lengths, one consistent message. Pick the one that fits the field limit.
|
||||
|
||||
---
|
||||
|
||||
## 1. One-Sentence Pitch
|
||||
|
||||
> KoalaSync is a privacy-first browser extension that synchronizes video playback across almost any website so you can watch with friends in real time — no accounts, no tracking, and your video never passes through anyone's server but the original site's.
|
||||
|
||||
**Shorter alternative** (for tight tagline fields):
|
||||
|
||||
> Private, universal watch parties on any website — no accounts, no tracking, no media proxying.
|
||||
|
||||
---
|
||||
|
||||
## 2. Three-Sentence Overview
|
||||
|
||||
> KoalaSync is a lightweight browser extension that keeps you and your friends perfectly in sync on YouTube, Netflix, Twitch, Prime Video, Jellyfin, Emby, and almost any other site with an HTML5 video player — press play once and everyone stays together. It's built privacy-first: no accounts, no telemetry, and the official relay server runs entirely in volatile RAM with zero persistence, so nothing about your sessions is ever stored. Open source under the MIT license and fully self-hostable with a single Docker command, KoalaSync is a transparent watch-party tool that works everywhere and respects your data sovereignty.
|
||||
|
||||
---
|
||||
|
||||
## 3. Full Description
|
||||
|
||||
### Watch together — on any site, on your terms.
|
||||
|
||||
Counting down "3, 2, 1, play" over voice chat doesn't scale past two people. KoalaSync fixes that with a tiny browser extension that synchronizes play, pause, and seeking across everyone in the room, on almost any website with a `<video>` element. Create a room, share a link, press play — that's it.
|
||||
|
||||
### What makes KoalaSync different
|
||||
|
||||
Most watch-party tools fall into one of two traps: they only work on a short allowlist of sites (site-specific extensions that need a separate build for every platform), or they route your video through their own player and servers. KoalaSync was built around three principles that break that mold.
|
||||
|
||||
**Universal by design.** If the site has an HTML5 `<video>` element, KoalaSync can usually sync it. YouTube, Netflix, Twitch, Prime Video, Disney+, Jellyfin, Emby, and countless niche sites work out of the box — no per-site integration to wait for, no extension swap when your friends want to switch services.
|
||||
|
||||
**Your video never touches our servers.** KoalaSync only relays tiny timing messages — play, pause, seek position, readiness — over a hand-rolled WebSocket protocol. The actual video keeps streaming directly from the original site to each viewer's browser. KoalaSync never proxies, transcodes, uploads, or redistributes a single frame, which also means there is no legal gray zone around redistribution.
|
||||
|
||||
**Privacy is the default, not an upgrade.** No accounts, no emails, no telemetry, no analytics, no behavior profiling. The official relay server runs entirely in volatile RAM and keeps zero persistent state — when the room closes, the data is gone. Pick a nickname or let KoalaSync generate one for you and you're in.
|
||||
|
||||
### Built for people who actually want to read the code
|
||||
|
||||
KoalaSync is MIT-licensed open source, built by a solo developer. Audit it, fork it, change it. The extension is dependency-free with a direct Socket.IO wire implementation — no opaque libraries, no framework bloat, no surprise third-party SDKs. Want full sovereignty? Self-host your own relay with a single Docker command and keep all watch-party coordination traffic inside your own infrastructure. The official public relay is there when you don't care, self-hosting is there when you do.
|
||||
|
||||
### Little touches you'll notice
|
||||
|
||||
- **Episode Auto-Sync** pauses the room when someone loads the next episode and resumes only when everyone is ready — no spoilers, no one left behind on the previous cliffhanger.
|
||||
- **Smart Audio Compressor** tames the modern "whisper dialogue, deafening explosion" mix with one click. Three presets or full manual control over threshold, ratio, attack, and release.
|
||||
- **One-click invite links** auto-configure the server and room for your friends — they just click the link and they're in. No fumbling with server URLs or room IDs.
|
||||
- **Dual-heartbeat architecture** kills ghost rooms and stale connections before they desync your session.
|
||||
- **15 languages** fully translated and switchable in real time from the settings panel — English, German, French, Spanish, Portuguese (Brazil + European), Russian, Italian, Polish, Turkish, Dutch, Japanese, Korean, Chinese, Ukrainian.
|
||||
|
||||
### Install and start in under a minute
|
||||
|
||||
Install KoalaSync from the Chrome Web Store or Firefox Add-ons, click "Create Room," share the invite link, and pick a video. The official relay is ready out of the box — no setup required unless you want to self-host.
|
||||
|
||||
- Website: https://sync.koalastuff.net
|
||||
- GitHub: https://github.com/Shik3i/KoalaSync
|
||||
|
||||
---
|
||||
|
||||
## Bonus: Taglines (for hero headlines, social bios, meta descriptions)
|
||||
|
||||
- Watch together. Anywhere. Privately.
|
||||
- The watch-party tool that works on every site and tracks none of them.
|
||||
- Sync play, pause, and seek on any video — no accounts, no logs, no lock-in.
|
||||
- Self-hostable, open-source watch parties for the post-"3, 2, 1, play" era.
|
||||
- Press play once. Stay together anywhere.
|
||||
+56
-1
@@ -4,6 +4,61 @@ All notable changes to the KoalaSync browser extension and relay server.
|
||||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- **Extension: Privacy title controls** - Advanced users can now disable sending browser tab titles separately from media titles. Media titles can still be sent in full, reduced to detected episode identifiers such as `S01E04`, or hidden entirely. Defaults remain full titles for backwards compatibility.
|
||||
|
||||
---
|
||||
|
||||
## [v2.5.0] — 2026-06-29
|
||||
|
||||
### Added
|
||||
- **Extension + Relay: Host Control Mode** — Room owners can now switch a room between open playback control and host-controlled playback. In host-only mode, guests stay synchronized but their local play, pause, and seek actions are not rebroadcast to the room.
|
||||
- **Backward-compatible Host Control rollout** — The extension only shows Host Control when the connected relay supports it, so users on older self-hosted servers do not see controls that cannot work yet.
|
||||
- **Extension: Clear host and guest states** — The popup shows the current control mode, host status, peer roles, and localized guest guidance so participants understand when playback is controlled by the host.
|
||||
- **Website: FAQ clarification for streaming access** — The landing page and FAQ structured data now state clearly that KoalaSync does not stream, host, share, or bypass access to video content. Every participant watches locally and needs their own access to services such as Netflix.
|
||||
|
||||
### Changed
|
||||
- **Playback sync now follows the room's control setting** — When Host Control is enabled, only the host can drive room-wide playback changes; guests can still watch in sync without accidentally changing playback for everyone.
|
||||
|
||||
---
|
||||
|
||||
## [v2.4.6] — 2026-06-23
|
||||
|
||||
### Fixed
|
||||
- **Room and settings are no longer stored in `chrome.storage.sync`** — Room ID, password, and username were being resurrected from synced storage on a fresh install (sync survives an uninstall in the user's Google account), which made the extension silently auto-connect to a dead room and appear permanently connected. `getSettings()` and all settings reads are now local-only, and legacy keys are actively purged from sync on install/update/startup. Only `onboardingComplete` and `dismissedHints` remain in sync.
|
||||
- **No server traffic while alone in a room** — When you are the only peer, heartbeats, force-sync, and episode auto-sync are now fully suppressed (previously the keepAlive heartbeat, force-sync, and episode lobby were still broadcast to an empty room). The solo state is re-evaluated live on every event — never cached — so the instant another peer joins, syncing resumes immediately, including an instant state push so the newcomer sees your current position without waiting for the next heartbeat.
|
||||
|
||||
## [v2.4.4] — 2026-06-23
|
||||
|
||||
### Changed
|
||||
- **Server: Event rate limit raised 30 → 50 per 10s**, and all connection/event/health rate-limit thresholds and windows extracted into named constants.
|
||||
- **Extension: Reconnect backoff tuned and jittered** — capped at ~8 attempts/60s (under the per-IP connection limit) with ±20% jitter to de-synchronize reconnect herds after a server blip.
|
||||
- **CI: Added a verification workflow** running lint, tests, audits, and builds on every push/PR; the release build now uses `npm ci`.
|
||||
|
||||
### Fixed
|
||||
- **Extension: Offline event-queue flush is now paced** (small batches instead of one synchronous burst) so a reconnect after a long outage no longer trips the server event limit and gets disconnected on rejoin.
|
||||
- **Extension: Ping liveness tolerates one missed PONG** — a reconnect is forced only after 2 consecutive misses (~20s) instead of a single 5s timeout, avoiding spurious drops under transient load.
|
||||
- **Extension: `socket.send()` failures are caught and re-queued** instead of losing the event on a disconnect race.
|
||||
|
||||
## [v2.4.3] — 2026-06-19
|
||||
|
||||
### Added
|
||||
- **Two new languages: Ukrainian (`uk`) and Chinese (`zh`, Simplified)** — added across the extension (UI strings + Chrome `_locales`) and the website (localized pages, hreflang/Open Graph/schema tags, language selector), bringing the total to 15 languages.
|
||||
|
||||
### Changed
|
||||
- **Play/pause sync coalescing** — The content script now collapses rapid bursts of native play/pause events (source swaps, ABR/quality switches, ad transitions, page teardown) into a single relayed command: the first event is sent instantly and a short 150ms window absorbs the rest. This cuts redundant relay traffic and stops bursts from tripping the server's per-socket event rate limit.
|
||||
|
||||
### Fixed
|
||||
- **zh/uk translation quality** — Corrected systematic machine-translation word-sense errors in the two new locales (e.g. "Play", "Status", "Leave Room", "Clear", "Open", "peers", and audio compressor terms) and translated the remaining English leftovers.
|
||||
- **Relay logging** — An `EVENT_ACK` aimed at a peer that already left is now logged quietly instead of as a `[SECURITY]` cross-room event, so genuine cross-room attempts stand out in the logs.
|
||||
|
||||
## [v2.4.2] — 2026-06-19
|
||||
|
||||
### Changed
|
||||
- **Extension: Optimized uninstall URL registration** — Extracted registration into a reusable, race-condition-protected `initUninstallURL()` helper. It registers the uninstall feedback URL with browser context on both extension installation/update and browser startup to prevent state loss, without storing or sending an installation token.
|
||||
|
||||
## [v2.4.1] — 2026-06-19
|
||||
|
||||
### Added
|
||||
@@ -144,7 +199,7 @@ All notable changes to the KoalaSync browser extension and relay server.
|
||||
- **Feature Hint System**: Generic `dismissedHints` array in sync storage for announcing new features. First hint highlights the Audio Options entry in Settings. Extensible for future features.
|
||||
|
||||
### Changed
|
||||
- **Ko-Fi Support Links**: Static footer badges on the Settings and Status tabs linking to the developer's support page. README and website footer updated with Ko-Fi badge.
|
||||
- **Support Links**: Static footer badges on the Settings and Status tabs linking to the developer's support page. README and website footer updated with a Support KoalaSync badge.
|
||||
|
||||
### Fixed
|
||||
- **Portuguese (PT) locale**: Removed Italian contamination — "sincronizzazione" → "sincronização", "tempo reale" → "tempo real", "Link di Invito" → "Link de Convite", "Sair della Sala" → "Sair da Sala".
|
||||
|
||||
@@ -156,7 +156,7 @@ While in a room, two heartbeats keep the session alive:
|
||||
| Heartbeat | Interval | Source | Purpose |
|
||||
|:----------|:---------|:-------|:--------|
|
||||
| **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` |
|
||||
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, privacy-filtered `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").
|
||||
- **Room Cleanup**: Empty rooms are deleted immediately. Inactive rooms are pruned after 2 hours.
|
||||
@@ -184,7 +184,7 @@ When a user clicks **"Leave"** or closes their browser:
|
||||
When watching a series and an episode ends:
|
||||
|
||||
1. `content.js` monitors the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) for title changes.
|
||||
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected new title.
|
||||
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected title after applying the local media title privacy setting. In episode-only mode this is an identifier such as `S01E04`; when media titles are not sent, the client does not create a new episode lobby.
|
||||
3. All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
|
||||
4. Each peer's `content.js` polls for the new title to appear in the `<video>` element's metadata.
|
||||
5. Once a peer detects the matching title, they send `EPISODE_READY`.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Known Limitations & Accepted Non-Issues (NOFIX)
|
||||
|
||||
This file records design decisions that are **intentional** and **out of scope** for
|
||||
KoalaSync's threat model. They are frequently re-discovered by reviewers, linters, and
|
||||
AI agents and re-reported as "bugs" or "vulnerabilities." They are not. Please read the
|
||||
threat model below before flagging any of them.
|
||||
|
||||
> **For reviewers & agents:** Do **not** re-report the NOFIX items below. They are
|
||||
> deliberate. If you think the *threat model itself* should change (e.g. KoalaSync
|
||||
> starts handling accounts, payments, or sensitive data), raise **that** discussion
|
||||
> first — these items only stand because the threat model holds.
|
||||
|
||||
---
|
||||
|
||||
## Threat model
|
||||
|
||||
KoalaSync is a **privacy-preserving watch-party sync tool**, not a security product.
|
||||
|
||||
- **Rooms are ephemeral.** They exist for a few hours and are auto-reaped. There are no
|
||||
accounts, no persistent storage, no money, and no sensitive content on the relay.
|
||||
- **The relay is a dumb, stateless message bus.** It forwards play/pause/seek between
|
||||
peers who *chose* to watch together and joined via an invite link shared out-of-band.
|
||||
- **Participants are invited.** Anyone in a room was let in. The social contract is
|
||||
"we're watching a video together," not "mutually distrusting parties."
|
||||
|
||||
### What we DO defend against
|
||||
- **Accidental disruption** — the entire point of Host Control Mode.
|
||||
- **Spam / DoS** that degrades the relay for everyone — rate limits, 4 KB payload cap,
|
||||
server-side gating, lazy-cached health responses.
|
||||
- **Resource exhaustion / memory leaks** — bounded maps, periodic cleanup, room/peer reaping.
|
||||
- **Crashes from malformed input** — strict sanitization and clamping of every field.
|
||||
- **Genuine boundary breaches** — admin-metrics auth (constant-time), CORS, WSS upgrade,
|
||||
invite-hash isolation, strict CSP. Reports here are very welcome (see `SECURITY.md`).
|
||||
|
||||
### What we explicitly DO NOT defend against
|
||||
A **determined participant who modifies their own client to misbehave inside a room they
|
||||
were invited to.** The worst they achieve is sending playback commands or seizing the
|
||||
"host" role in a temporary room they could already disrupt by other means. That is a
|
||||
**social** problem, solved socially: kick them, or start a new room. Engineering real
|
||||
identity/auth to prevent it would destroy the account-less, frictionless, privacy-first
|
||||
design — a bad trade for an ad-hoc movie night.
|
||||
|
||||
---
|
||||
|
||||
## NOFIX entries
|
||||
|
||||
### NOFIX-1 — `peerId` is unauthenticated; a crafted client can impersonate or seize the host
|
||||
**Flag:** `peerId` is client-asserted and broadcast to every peer (in `ROOM_DATA` /
|
||||
`PEER_STATUS`). A modified client can join with the host's `peerId`, dedupe-kick the real
|
||||
host, and become host — controlling or locking `host-only` mode.
|
||||
|
||||
**Why NOFIX:** Requires a *modified client* + an *invited* participant + a `peerId` that is
|
||||
only meaningful inside that one *temporary* room. The payoff is sending play/pause or
|
||||
locking a room the attacker is already in — pure trolling, instantly reversible (kick /
|
||||
new room). Cryptographic per-user identity is wildly disproportionate for an ad-hoc,
|
||||
account-less, ephemeral watch party. **Out of threat model.**
|
||||
Do **not** "fix" with accounts, signed peerIds, or per-user tokens — that breaks the
|
||||
core design.
|
||||
|
||||
### NOFIX-2 — Room-password comparison is not constant-time
|
||||
**Flag:** room password hashes are compared with `!==` (`server/index.js`), so the compare
|
||||
is theoretically timing-attackable.
|
||||
|
||||
**Why NOFIX:** The compared value is an **HMAC-SHA256 hash that never leaves the server** —
|
||||
an attacker cannot observe it to mount a timing attack. Even a hypothetical success only
|
||||
lets someone join a *temporary* room to send playback commands. Not worth defending.
|
||||
(The admin-metrics bearer token — a real boundary — **does** use `crypto.timingSafeEqual`.
|
||||
That is the line we actually guard.)
|
||||
|
||||
### NOFIX-3 — `OFFICIAL_SERVER_TOKEN` is public in the repo
|
||||
**Flag:** the connection token in `shared/constants.js` is committed, so anyone can connect.
|
||||
|
||||
**Why NOFIX:** It is a **coarse filter** to keep random scanners off the relay, **not
|
||||
authentication**. The relay is a public message bus by design; rate limits and per-room
|
||||
behavior are the real protections.
|
||||
|
||||
### NOFIX-4 — Room IDs are enumerable via `GET_ROOMS`
|
||||
**Flag:** any connected client can list all room IDs (and whether each has a password).
|
||||
|
||||
**Why NOFIX:** This is the intended **"Public Rooms"** feature. Rooms wanting privacy set a
|
||||
password; listing the IDs of password-less rooms only lets someone join a watch party —
|
||||
the same as being handed the invite link.
|
||||
|
||||
### NOFIX-5 — A pause/seek can only be reverted, not prevented
|
||||
**Flag:** in `host-only` mode a guest's pause still fires locally before the extension can
|
||||
react, so there is a brief flicker before snap-back.
|
||||
|
||||
**Why NOFIX:** A content script cannot intercept a `<video>` event before the element
|
||||
acts. Reacting (snap-back) is the only option and is by design; the ~½s flicker is
|
||||
acceptable. Not a bug.
|
||||
|
||||
---
|
||||
|
||||
## Not NOFIX — just deferred (may be revisited)
|
||||
|
||||
These are *not* accepted-forever; they are scoped out of v1 and tracked separately
|
||||
(see the host-control-mode design docs in `docs/`):
|
||||
|
||||
- **Host grace on a long disconnect (EC-10).** A brief reconnect/second-tab keeps the host
|
||||
(handled), but a long real disconnect still falls back to `everyone`. A ~30s host-reserve
|
||||
grace could be added later.
|
||||
- **Intent-classifier / snap-back tuning.** Thresholds are first-pass; real-device testing
|
||||
may adjust them.
|
||||
+1
-1
@@ -8,7 +8,7 @@ KoalaSync is designed with a **Security-First & Volatile** architecture. This me
|
||||
|
||||
## 1. Data Processing (In-Memory Only)
|
||||
KoalaSync does not use a database. All active session data exists only in the server's RAM and is purged immediately when no longer needed.
|
||||
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId`, `username`, and the `title` of the video you are watching. Additionally, playback metadata (`mediaTitle`, `playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
|
||||
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId` and `username`. By default, KoalaSync also shares the selected tab title and media title with the room so peers can identify matching videos and coordinate episode transitions. Privacy Settings let you disable sending the tab title separately, and choose whether media titles are sent in full, reduced to a detected episode identifier (for example `S01E04`), or not sent. Playback metadata (`playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
|
||||
- **Room Passwords**: If you set a room password, it is stored only as an in-memory **keyed SHA-256 HMAC hash**. The server receives the plaintext password only during join validation, never stores it, and keeps only the hash for the short room lifetime.
|
||||
- **Routing Maps**: The server maintains ephemeral lookup tables (`socketToRoom`, `peerToSocket`) to route messages between peers. These contain only transport identifiers and are purged on disconnect.
|
||||
|
||||
|
||||
@@ -30,15 +30,6 @@
|
||||
|
||||
*Prioritized for upcoming phases.*
|
||||
|
||||
### 1. Split large JavaScript files into smaller modules
|
||||
|
||||
- **Priority:** P1
|
||||
- **Category:** Maintainability / AI Context Optimization
|
||||
- **Background:** Core files like `background.js` and `popup.js` have grown large and exceed 800 lines. This makes manual debugging harder and wastes context window space for AI models.
|
||||
- **Planned solution:**
|
||||
- Structurally split logic into separate focused modules (e.g., UI Renderer, Message Router, Storage Manager, Socket Client).
|
||||
- Use ES modules for clean separation and better reusability.
|
||||
|
||||
### 2. Invite link with target URL for auto-redirect
|
||||
|
||||
- **Priority:** P2
|
||||
|
||||
+60
-44
@@ -1,78 +1,91 @@
|
||||
# KoalaSync Translation & Localization Guide
|
||||
|
||||
Welcome to the **KoalaSync** translation guide! We rely on the open-source community to make KoalaSync accessible to users worldwide.
|
||||
Welcome to the **KoalaSync** translation guide. We rely on the open-source community to make KoalaSync accessible to users worldwide.
|
||||
|
||||
KoalaSync is split into two independent translation areas. You can translate either one, or both:
|
||||
|
||||
1. **The Browser Extension** (`extension/locales/`): The core product that users interact with daily.
|
||||
2. **The Website** (`website/locales/`): The landing page and invitation bridge.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Supported Languages Dashboard
|
||||
## Supported Languages Dashboard
|
||||
|
||||
We divide supported languages into two tiers: **Core Languages** (fully hand-crafted and audited by native speakers) and **Extended Languages** (auto-generated using translation models to expand initial coverage).
|
||||
|
||||
> [!TIP]
|
||||
> **Help Us Improve!**
|
||||
> We welcome community contributions to audit "Auto-Generated" translations and elevate them to "Verified" status.
|
||||
> We welcome community contributions to audit `Auto-Generated` translations and elevate them to `100% Manually Verified` status.
|
||||
|
||||
| Language Code | Language Name | Verification Status | Rationale / Context |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `en` | 🇬🇧 **English** | `100% Manually Verified` | Global default language (Verified by developer) |
|
||||
| `de` | 🇩🇪 **German** | `100% Manually Verified` | Developer's native language |
|
||||
| `fr` | 🇫🇷 **French** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `es` | 🇪🇸 **Spanish** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `pt-BR` | 🇧🇷 **Portuguese (Brasil)** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `ru` | 🇷🇺 **Russian** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `it` | 🇮🇹 **Italian** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `pl` | 🇵🇱 **Polish** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `tr` | 🇹🇷 **Turkish** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `nl` | 🇳🇱 **Dutch** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `ja` | 🇯🇵 **Japanese** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `ko` | 🇰🇷 **Korean** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `pt` | 🇵🇹 **European Portuguese** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `en` | **English** | `100% Manually Verified` | Global default language (verified by developer) |
|
||||
| `de` | **German** | `100% Manually Verified` | Developer's native language |
|
||||
| `fr` | **French** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `es` | **Spanish** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `pt-BR` | **Portuguese (Brazil)** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `ru` | **Russian** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `it` | **Italian** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `pl` | **Polish** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `tr` | **Turkish** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `nl` | **Dutch** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `ja` | **Japanese** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `ko` | **Korean** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `pt` | **European Portuguese** | `100% Manually Verified` | Manual native review by Alenia Studios |
|
||||
| `zh` | **Chinese (Simplified)** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
| `uk` | **Ukrainian** | `Auto-Generated` | Needs manual native review and polishing |
|
||||
|
||||
> [!WARNING]
|
||||
> **Autogeneration Quality Rule**
|
||||
> Any newly contributed languages must be marked as `"Auto-Generated"` in this table until fully reviewed and signed off by a native speaker in a pull request.
|
||||
> Any newly contributed languages must be marked as `Auto-Generated` in this table until fully reviewed and signed off by a native speaker in a pull request.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ How to Translate KoalaSync
|
||||
## How to Translate KoalaSync
|
||||
|
||||
Here is the exact step-by-step process for contributing translations.
|
||||
|
||||
### Step 1: Fork and Clone the Repository
|
||||
If you are an external contributor, start with the standard Open Source workflow:
|
||||
|
||||
If you are an external contributor, start with the standard open-source workflow:
|
||||
|
||||
1. Click the "Fork" button on GitHub to create your own copy of the repository.
|
||||
2. Clone your fork locally: `git clone https://github.com/YOUR-USERNAME/KoalaSync.git`
|
||||
3. Create a branch: `git checkout -b translation/my-language`
|
||||
|
||||
### Step 2: Translate the Extension
|
||||
|
||||
The browser extension handles real-time syncing, settings, and popups.
|
||||
1. Navigate to [`extension/locales/`](file:///Users/koala/Documents/Workspaces/KoalaSync/extension/locales/).
|
||||
2. Edit an existing `[lang].json` or copy `en.json` to create a new one (e.g., `it.json`).
|
||||
3. Translate all the string values. **Do not change the JSON keys.**
|
||||
|
||||
1. Navigate to `extension/locales/`.
|
||||
2. Edit an existing `[lang].json` or copy `en.json` to create a new one (for example, `it.json`).
|
||||
3. Translate all string values. **Do not change the JSON keys.**
|
||||
|
||||
### Step 3: Translate the Website
|
||||
|
||||
The website hosts the landing page and invitation bridge.
|
||||
1. Navigate to [`website/locales/`](file:///Users/koala/Documents/Workspaces/KoalaSync/website/locales/).
|
||||
|
||||
1. Navigate to `website/locales/`.
|
||||
2. Edit an existing `[lang].json` or copy `en.json` to create a new one.
|
||||
3. Translate all the string values. **Do not change the JSON keys.**
|
||||
4. If creating a **brand new language**, configure the metadata at the top of your JSON file:
|
||||
```json
|
||||
{
|
||||
"LANG_CODE": "it",
|
||||
"HTML_CLASS": "lang-it",
|
||||
"CANONICAL_PATH": "it/",
|
||||
"LANG_TOGGLE_URL": "../",
|
||||
"LANG_TOGGLE_TEXT": "EN"
|
||||
}
|
||||
```
|
||||
5. If creating a **brand new language**, register it in `website/build.cjs` by adding it to the `languages` array.
|
||||
3. Translate all string values. **Do not change the JSON keys.**
|
||||
4. If creating a brand new language, configure the metadata at the top of your JSON file:
|
||||
|
||||
```json
|
||||
{
|
||||
"LANG_CODE": "it",
|
||||
"HTML_CLASS": "lang-it",
|
||||
"CANONICAL_PATH": "it/",
|
||||
"LANG_TOGGLE_URL": "../",
|
||||
"LANG_TOGGLE_TEXT": "EN"
|
||||
}
|
||||
```
|
||||
|
||||
5. If creating a brand new language, register it in `website/build.cjs` by adding it to the `languages` array.
|
||||
|
||||
### Step 4: Verify Locally
|
||||
|
||||
Ensure your JSON files are valid and all keys match the English baseline. Open your terminal in the KoalaSync root folder and run:
|
||||
|
||||
```bash
|
||||
# Tests the extension locales for missing keys or syntax errors
|
||||
node scripts/test-locales.cjs
|
||||
@@ -83,23 +96,26 @@ node scripts/test-website-locales.mjs
|
||||
# Builds the website with your new translations
|
||||
node website/build.cjs
|
||||
```
|
||||
*Note: If you receive any errors about missing keys or `TODO` placeholders, please fix them before submitting.*
|
||||
|
||||
If you receive any errors about missing keys or placeholder strings, fix them before submitting.
|
||||
|
||||
### Step 5: Commit and Pull Request
|
||||
1. Open this `TRANSLATION.md` file and add/update your language in the **Supported Languages Dashboard** above. Mark it as `100% Manually Verified` if you are a native speaker.
|
||||
|
||||
1. Open this `TRANSLATION.md` file and add or update your language in the **Supported Languages Dashboard** above. Mark it as `100% Manually Verified` only if it has been reviewed by a native speaker.
|
||||
2. Commit your changes: `git commit -m "Update Italian translations"`
|
||||
3. Push to your fork: `git push origin translation/my-language`
|
||||
4. Open a **Pull Request** on the main KoalaSync repository on GitHub.
|
||||
4. Open a pull request on the main KoalaSync repository on GitHub.
|
||||
|
||||
---
|
||||
|
||||
## ⚖️ Strict Legal Exclusion Rule
|
||||
## Strict Legal Exclusion Rule
|
||||
|
||||
Our legal pages have strict constraints to protect user privacy and avoid regulatory liabilities.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **DO NOT TRANSLATE LEGAL DOCUMENTS**
|
||||
> The legal notice ([impressum.html](file:///Users/koala/Documents/Workspaces/KoalaSync/website/impressum.html)) and privacy policy ([datenschutz.html](file:///Users/koala/Documents/Workspaces/KoalaSync/website/datenschutz.html)) **MUST remain exclusively in English and German**.
|
||||
>
|
||||
> * **Rationale:** Legal compliance under the European Union General Data Protection Regulation (GDPR) and the German Digital Services Act (DDG). Offering automated translations of legally binding notices introduces compliance risks due to potential mistranslations of liability limits.
|
||||
> * **Technical Fallback:** Our system automatically falls back to **English** for legal pages if a user visits them in an unsupported language, so you do not need to worry about this.
|
||||
> **Do Not Translate Legal Documents**
|
||||
> The legal notice (`website/impressum.html`) and privacy policy (`website/datenschutz.html`) **MUST remain exclusively in English and German**.
|
||||
>
|
||||
> **Rationale:** Legal compliance under the European Union General Data Protection Regulation (GDPR) and the German Digital Services Act (DDG). Offering automated translations of legally binding notices introduces compliance risks due to potential mistranslations of liability limits.
|
||||
>
|
||||
> **Technical fallback:** Our system automatically falls back to English for legal pages if a user visits them in an unsupported language, so you do not need to translate them.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# DevOps Release Workflow
|
||||
|
||||
This document describes the deployment and release process for KoalaSync.
|
||||
|
||||
## Tag-Based Releases
|
||||
|
||||
KoalaSync uses a fully automated release pipeline triggered by Git tags.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **DO NOT** manually bump the version numbers in any files (such as `package.json`, `manifest.base.json`, `shared/constants.js`, etc.) before creating a release.
|
||||
> Bumping versions manually is redundant, leads to conflicts, and is completely handled by the CI/CD pipeline.
|
||||
|
||||
### How it Works
|
||||
|
||||
When you push a Git tag matching `v*` (e.g., `v2.5.1`), the GitHub Actions release workflow (`.github/workflows/release.yml`) is triggered. The workflow performs the following actions:
|
||||
|
||||
1. **Extracts the version** from the tag (e.g., `2.5.1` from `v2.5.1`).
|
||||
2. **Injects the version** automatically into the following files:
|
||||
- `extension/manifest.base.json`
|
||||
- `shared/constants.js` (updates `APP_VERSION`)
|
||||
- `package.json`
|
||||
- `website/version.json`
|
||||
- `website/template.html` (updates `softwareVersion` schema)
|
||||
- `README.md` (updates badge and announcement banner)
|
||||
- `website/sitemap.xml` (updates `lastmod` dates)
|
||||
3. **Commits and pushes** these version updates back to the `main` branch automatically with the commit message `chore(release): update versions to vX.X.X [skip ci]`.
|
||||
4. **Builds the extension** for both Chrome and Firefox and publishes the zipped archives.
|
||||
5. **Builds the website** and uploads website artifacts.
|
||||
6. **Builds and publishes** the Docker image for the relay server to the GitHub Container Registry (`ghcr.io`).
|
||||
|
||||
---
|
||||
|
||||
## Steps to Deploy a New Release
|
||||
|
||||
To release a new version (e.g., `v2.5.1`), follow these steps:
|
||||
|
||||
1. Make sure your local repository is synced on `main`:
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
```
|
||||
2. Create a local Git tag:
|
||||
```bash
|
||||
git tag v2.5.1
|
||||
```
|
||||
3. Push the tag to GitHub:
|
||||
```bash
|
||||
git push origin v2.5.1
|
||||
```
|
||||
|
||||
The release pipeline will take care of the rest! You can monitor the progress under the **Actions** tab of the GitHub repository.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Co-Host (Multi-Controller) — Implementation Plan
|
||||
|
||||
Branch base: `feature/host-control-mode` (builds directly on it).
|
||||
Goal: let the room owner grant **playback control to several peers** (co-hosts), not
|
||||
just one — e.g. 4 of N people in a room may drive play/pause/seek, the rest are guests.
|
||||
|
||||
This is the second server-gated feature the `capabilities` hook was designed for
|
||||
(`CAPABILITIES.CO_HOST = 'co-host'`, already stubbed in `shared/constants.js`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Roles
|
||||
|
||||
| Role | Can drive (play/pause/seek/force-sync/episode-lobby) | Can promote/demote + toggle mode |
|
||||
|------|------|------|
|
||||
| **Owner** (room creator, = today's "host") | yes (always a controller) | **yes** |
|
||||
| **Controller** (co-host) | yes (in `host-only` mode) | no |
|
||||
| **Guest** | only in `everyone` mode | no |
|
||||
|
||||
The single-host feature is just the special case `controllers = { owner }`.
|
||||
|
||||
## 2. Data model
|
||||
|
||||
### Server (`room` object)
|
||||
- `ownerPeerId` — the creator / manager. Keep `hostPeerId` as an **alias** (= ownerPeerId)
|
||||
so older clients keep working.
|
||||
- `controllers: Set<peerId>` — peers allowed to drive. **Always contains ownerPeerId.**
|
||||
- `controlMode: 'everyone' | 'host-only'` — unchanged wire values (`'host-only'` now means
|
||||
"restricted to controllers", not "single host").
|
||||
- `MAX_CONTROLLERS` cap (e.g. 10) to bound the set + payload.
|
||||
|
||||
### Shared constants
|
||||
- `CAPABILITIES.CO_HOST = 'co-host'` (un-stub it) → add to `SERVER_CAPABILITIES`.
|
||||
- New events:
|
||||
- `SET_PEER_ROLE` (client→server): `{ peerId, controller: boolean }` — owner promotes/demotes.
|
||||
- Extend `CONTROL_MODE` (server→client) payload: `{ controlMode, ownerPeerId, hostPeerId, controllers: [peerId...] }`.
|
||||
- `ROOM_DATA` gains `ownerPeerId` + `controllers`.
|
||||
|
||||
## 3. Gate generalization (the core change)
|
||||
|
||||
Today the gate compares against a single `hostPeerId`. Generalize to set membership:
|
||||
|
||||
- **Server relay gate** (`server/index.js`): `controlMode === 'host-only' && !room.controllers.has(mapping.peerId)` → drop. (Was `mapping.peerId !== room.hostPeerId`.)
|
||||
- **Background gates** (sender + receiver): replace `amHost()` / `senderId !== hostPeerId`
|
||||
with controller-set membership: `controllers.includes(myPeerId)` / `senderId ∈ controllers`.
|
||||
- **Helpers:** split `amHost()` into `amOwner()` (manage rights) and `amController()`
|
||||
(drive rights). The desync/snap-back path keys on `!amController()` instead of `!amHost()`.
|
||||
|
||||
`SET_PEER_ROLE` handler (server): validate sender is owner, target is a current peer in the
|
||||
room, enforce `MAX_CONTROLLERS`, always keep owner in the set, then broadcast `CONTROL_MODE`
|
||||
with the new `controllers`.
|
||||
|
||||
## 4. Client + UI
|
||||
|
||||
- **Owner** sees the peer list with a per-peer **"Controller" toggle** (promote/demote) plus
|
||||
the existing mode toggle.
|
||||
- **Controllers** see a "Controller" badge and are NOT locked out of the remote-control buttons.
|
||||
- **Guests** see "Guest" + the host-only notice (unchanged).
|
||||
- The promote UI + co-host badges render only when the relay advertises the `co-host`
|
||||
capability (feature detection, same pattern as `hostControlSupported`).
|
||||
- i18n: new keys (`ROLE_CONTROLLER`, `BTN_PROMOTE`, `BTN_DEMOTE`, …) across all locales.
|
||||
|
||||
## 5. Backwards compatibility
|
||||
|
||||
- **New client + old server** (host-control only, no `co-host` capability): no co-host UI;
|
||||
behaves as today's single-host. ✓
|
||||
- **Old client + new server**: ignores `controllers` / `SET_PEER_ROLE`. An old client that
|
||||
the owner promotes still **gates itself** (its sender-gate only knows `!amHost`), so it
|
||||
can't drive — it degrades to a guest. Co-host requires a client that understands
|
||||
`controllers`. Document this; not a crash. ✓
|
||||
- No `PROTOCOL_VERSION` bump needed — purely additive, same as host-control.
|
||||
|
||||
## 6. Edge cases
|
||||
- **Controller leaves** → `removePeerFromRoom` also does `room.controllers.delete(peerId)`.
|
||||
- **Owner leaves** → fallback: promote the earliest remaining **controller** to owner (prefer
|
||||
a controller over a random peer); if none, earliest peer; keep the rest of the set. Reuse
|
||||
the `peerJoinLocks` guard so a reconnect/second-tab doesn't demote (same fix as host).
|
||||
- **Promote a peer not in the room** → server rejects (target must be a live peer).
|
||||
- **Promote beyond `MAX_CONTROLLERS`** → server rejects, re-syncs the owner's UI.
|
||||
- **`everyone` mode** → the `controllers` set is still maintained (so flipping to `host-only`
|
||||
keeps the chosen co-hosts), it just isn't enforced while in `everyone`.
|
||||
- **peerId spoofing** → unchanged accepted limitation (see `docs/KNOWN_LIMITATIONS.md`);
|
||||
co-host doesn't widen it materially (still bounded to a temporary room).
|
||||
|
||||
## 7. Scale: the "4 of 510 people" part — read this
|
||||
|
||||
The role change above is moderate. **Putting 510 people in one room is a separate, larger
|
||||
problem** and should be its own track:
|
||||
|
||||
- `MAX_PEERS_PER_ROOM` is **25** today. 510 needs a large raise + load testing.
|
||||
- **The real bottleneck at scale is heartbeat fan-out, not control events.** Every peer
|
||||
heartbeats and the relay broadcasts each to all peers → O(N²) per interval. At 510 that's
|
||||
~510×509 / 15s ≈ **17k msg/s just for heartbeats** — the scaling wall.
|
||||
- **Co-host actually *helps* the control-event side:** in `host-only` mode only the few
|
||||
controllers emit play/pause/seek, so event *sources* drop from N to K (e.g. 4). Restricting
|
||||
who can drive is synergistic with big rooms.
|
||||
- Large rooms therefore need (independent of co-host):
|
||||
- **Heartbeat fan-out reduction** — e.g. only relay controller/owner heartbeats to everyone,
|
||||
relay guest heartbeats only to the owner/controllers (for the UI), or server-side
|
||||
aggregation into periodic snapshots instead of per-peer relay.
|
||||
- **`ROOM_DATA` payload trimming** — a 510-entry peer list is large; send counts + controller
|
||||
details, lazy-load the full roster.
|
||||
- Possibly the **socket.io Redis adapter** for horizontal scaling, and broadcast tuning.
|
||||
|
||||
## 8. Effort estimate
|
||||
- **Co-host roles** (server gate generalization + `SET_PEER_ROLE` + owner-leave fallback +
|
||||
client gates + promote UI + i18n), at the current ≤25-peer scale: **~3–4 dev days** (same
|
||||
shape as host-control itself — mostly generalizing host→controller-set).
|
||||
- **Large-room scaling (510)**: a **separate ~1–2 week** track (heartbeat redesign + payload
|
||||
trimming + cap raise + load testing), independent of co-host. Recommend shipping co-host at
|
||||
the current cap first, then scaling rooms as its own project.
|
||||
|
||||
## 9. Suggested sequencing
|
||||
1. `CAPABILITIES.CO_HOST` + `controllers`/`ownerPeerId` in room state + `ROOM_DATA` (additive).
|
||||
2. Server `SET_PEER_ROLE` + gate generalization + owner-leave fallback + WS tests.
|
||||
3. Background: controller-set membership in both gates + `amOwner`/`amController`.
|
||||
4. Popup: promote/demote toggles (owner) + Controller badge + i18n.
|
||||
5. (Separate track) large-room scaling.
|
||||
@@ -0,0 +1,283 @@
|
||||
# Host Control Mode — Branch Overview, Goals & Edge Cases
|
||||
|
||||
> **This is the canonical entry-point doc for branch `feature/host-control-mode`.**
|
||||
> If you're an agent or contributor picking this up: read this file first, then the
|
||||
> implementation plan in [`host-control-mode-plan.md`](./host-control-mode-plan.md).
|
||||
> Temporary working doc — delete or fold into permanent docs before merging to `main`.
|
||||
>
|
||||
> Status legend: 🔴 open / unresolved · 🟡 idea, needs testing · 🟢 decided
|
||||
|
||||
---
|
||||
|
||||
## 0. Implementation status
|
||||
|
||||
All five layers are implemented & pushed (server + background + content + popup + i18n).
|
||||
Automated checks green: ESLint, WS integration tests (incl. host-only gate, toggle
|
||||
reject, host-leave fallback), content video-finder, locale consistency (15 langs),
|
||||
full release verification.
|
||||
|
||||
**Still needs real-device testing** — the EC test matrix in §7 (YouTube/Netflix/
|
||||
Twitch/Disney+/Jellyfin): involuntary-pause classification (EC-1/EC-5/EC-8), snap-back
|
||||
reliability and fight-loops (EC-4), and the desync/resync flow across players. The
|
||||
intent classifier (EC-9) and snap-back cooldown are first-pass heuristics tuned by
|
||||
reading the code, not yet by watching them behave on each site.
|
||||
|
||||
Deferred by decision (see §8): host grace period on disconnect (EC-10).
|
||||
|
||||
### Capability detection (forward-compat hook)
|
||||
The relay advertises `capabilities: ['host-control']` in `ROOM_DATA`
|
||||
(`SERVER_CAPABILITIES` in server, `CAPABILITIES` in shared/constants). The client
|
||||
enables host-control UI/behavior only when the flag is present, so the feature
|
||||
degrades cleanly on an older relay (absent → off) and old clients ignore the
|
||||
field. This is the extensible hook for the planned **co-host** feature (owner
|
||||
promotes guests to additional controllers): it will add a `'co-host'` capability
|
||||
+ events without a protocol bump or breaking older relays/clients. Add new flags
|
||||
to `CAPABILITIES` / `SERVER_CAPABILITIES` as features land.
|
||||
|
||||
### Pre-test self-audit (fixed)
|
||||
- **Popup remote buttons froze for guests** — in host-only a guest's Play/Pause/SYNC
|
||||
click was gated server-side but the button stuck on "Playing"/disabled with no
|
||||
feedback. Now the remote controls are locked (disabled + tooltip) for guests, with
|
||||
backstop guards in the handlers. (popup.js)
|
||||
- **Desync dialog could break under strict CSP** — it used `innerHTML` with inline
|
||||
`style=""` attributes, which Netflix/YouTube/Disney+ strip via `style-src`. Rebuilt
|
||||
with the DOM API (CSSOM `.style` is CSP-safe) inside a **Shadow DOM** so page CSS
|
||||
can't restyle/hide it. (content.js)
|
||||
- **Live-DVR not detected (EC-15)** — `duration === Infinity` misses Twitch/YouTube
|
||||
live-DVR (finite, sliding duration). Added a `seekable.start(0) > 1` sliding-window
|
||||
heuristic in `hcmIsLive()`. (content.js)
|
||||
|
||||
### Pre-test self-audit
|
||||
- ~~**EC-4/EC-1 snap-back thrash:**~~ FIXED — implemented the **buffer-aware deferred
|
||||
snap-back** (`hcmDeferredSnapBack`): on an involuntary event, if the player isn't
|
||||
ready (`readyState<3` or seeking) we wait (poll, 8s cap) until it can play, then snap
|
||||
ONCE to the host's re-queried position instead of repeatedly fighting the buffer.
|
||||
Done defensively/player-agnostically — we can't enumerate every site, so this is safe
|
||||
whether a player fires `pause()` or only `waiting`. Aborts if the user goes solo or is
|
||||
no longer a gated guest; single pending poll (no stacking).
|
||||
- ~~**Control-mode race at join:**~~ FIXED — `hcmHandleBlocked` now treats `HOST_BLOCKED`
|
||||
as authoritative (adopts host-only/guest role) instead of re-checking local mode,
|
||||
since background only sends it to gated guests.
|
||||
- ~~**Dialog/badge text is English-only**~~ FIXED — background resolves the strings
|
||||
via GET_HCM_STRINGS (it has the i18n loader); content fetches them on init with
|
||||
English fallback. 6 new keys (HCM_DIALOG_*/HCM_BADGE_*) across all 15 locales.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this branch is for
|
||||
|
||||
Origin: a GitHub feature request. When watching with larger groups, anyone can pause
|
||||
or seek and disrupt everyone else. The requester wants the room creator to optionally
|
||||
restrict control to a single **host**, the way Teleparty works — guests who try to
|
||||
pause get asked whether they want to pause *only their own* player (and desync), and
|
||||
otherwise get snapped back to the room's position.
|
||||
|
||||
**Goal:** Add an optional per-room **Host Control Mode**. A room can be switched
|
||||
between:
|
||||
- **`everyone`** (default, current behavior): anyone can play/pause/seek for the room.
|
||||
- **`host-only`**: only the host drives the room. A guest's deliberate play/pause/seek
|
||||
is not broadcast; instead they're snapped back to the room position — unless they
|
||||
explicitly choose to desync (go solo) with a "Resync" escape hatch.
|
||||
|
||||
## 2. Trust model (read this before over-engineering)
|
||||
|
||||
This is **client-side trust, by design**. It's a watch party, not a security boundary.
|
||||
The point is preventing *accidental* and *casual* disruption, not stopping someone
|
||||
determined to patch their own extension. We do **not** add auth, tokens, or
|
||||
cryptographic host identity. `peerId` is unauthenticated and that's fine here.
|
||||
|
||||
(We still gate server-side as the robust chokepoint — see plan — but that's about
|
||||
killing spam reliably, not about defeating a hostile client.)
|
||||
|
||||
## 3. Scope / non-goals
|
||||
|
||||
In scope:
|
||||
- Host designation (first joiner = host), mode toggle, host-only gating of all
|
||||
room-moving events, guest snap-back, deliberate-desync flow + resync, host UI.
|
||||
|
||||
Explicit non-goals (for this branch):
|
||||
- Authenticated / spoof-proof host identity.
|
||||
- Persistent host across server restarts (room state is in-memory).
|
||||
- Syncing around personalized ad breaks.
|
||||
- Host transfer UI (auto-fallback to `everyone` when host leaves; manual transfer
|
||||
is a possible later add).
|
||||
|
||||
## 4. Architecture summary
|
||||
|
||||
Three-layer gate for room-moving events from a non-host in host-only mode
|
||||
(`PLAY`, `PAUSE`, `SEEK`, `FORCE_SYNC_PREPARE`, `FORCE_SYNC_EXECUTE`, `EPISODE_LOBBY`):
|
||||
1. **Server** — doesn't relay them (robust chokepoint, kills spam regardless of client).
|
||||
2. **Sender (guest)** — doesn't emit; shows confirm dialog / disables host-only buttons.
|
||||
3. **Receiver** — drops any that slip through (covers old/buggy/modified clients).
|
||||
|
||||
Snap-back reuses the existing `_setSuppress` mechanism (content.js:442) so applying
|
||||
the room state programmatically doesn't echo back as a new event. Target position is
|
||||
extrapolated from the host's last known state (±1s). Full detail + code hooks in the
|
||||
plan doc.
|
||||
|
||||
---
|
||||
|
||||
## 5. The central challenge
|
||||
|
||||
Everything hard about this feature reduces to **one question** (see EC-9):
|
||||
|
||||
> How do we reliably tell a **deliberate** guest pause/seek from an **involuntary**
|
||||
> player/browser event (buffering, ads, tab throttling, source swaps, DRM hiccups)?
|
||||
|
||||
If we get this wrong, guests get spammed with desync dialogs and snap-back loops for
|
||||
things they never did. The host/role plumbing is the easy part; this classifier is the
|
||||
real work. **Design the intent-classifier before writing the gate.**
|
||||
|
||||
---
|
||||
|
||||
## 6. Edge cases
|
||||
|
||||
### EC-1 🔴 Buffering / loading fires a `pause` event
|
||||
content.js listens to `play`/`pause`/`seeked`/`loadeddata` only (content.js:1000-1003),
|
||||
not `waiting`/`stalled`. Pure HTML5 buffering fires `waiting` → harmless. But custom
|
||||
players (Netflix/YouTube/Twitch/JW) often call `video.pause()` during buffering/ads →
|
||||
real `pause` → guest gate would mis-classify as deliberate. Sub-cases: (a) initial load
|
||||
sits paused, no event, fine; (b) mid-stream stall, player-dependent; (c) seek-induced
|
||||
re-buffering may outlast the suppress window and leak. Mitigation: `isBuffering` flag
|
||||
from `waiting`/`playing`, or grace window; in host-only the guest's own state is
|
||||
irrelevant so just ignore involuntary pauses and let catch-up logic (content.js:489)
|
||||
re-sync them.
|
||||
|
||||
### EC-2 🟢 Force-Sync / Episode-Lobby abuse by guests
|
||||
Guest could seek + spam Force-Sync to drag everyone, or spam Episode-Lobby to pause
|
||||
everyone. Decision: host-only blocks guest *initiation* of `FORCE_SYNC_*` and
|
||||
`EPISODE_LOBBY`; guests may only respond (`FORCE_SYNC_ACK`, `EPISODE_READY`). Guests'
|
||||
legitimate path is the personal "Resync" button.
|
||||
|
||||
### EC-3 🟢 Host leaves the room
|
||||
Fall back to `controlMode = 'everyone'`, broadcast `CONTROL_MODE`. Never a stuck locked
|
||||
room. (Auto-promote next peer deferred.)
|
||||
|
||||
### EC-4 🔴 Snap-back fight loop (pause/play/skip back/pause/play)
|
||||
Mashing controls or a janky player → each snap-back may emit events → ping-pong.
|
||||
Mitigation: cooldown (~600ms) after snap-back; ensure snap-back runs fully under
|
||||
suppress. Also: if target is unreachable (seek past buffered range), retry K times then
|
||||
give up — no infinite loop.
|
||||
|
||||
### EC-5 🔴 Ad breaks (YouTube/Twitch/…)
|
||||
Mid-roll ads pause/swap the media element, differ per peer → desync is unavoidable and
|
||||
must NOT spam the dialog. Probably covered by EC-1 buffering grace; flag for explicit
|
||||
testing.
|
||||
|
||||
### EC-6 🟡 Snap-back target accuracy
|
||||
No continuous room clock; extrapolate from host's `currentTime` + `lastHeartbeat`
|
||||
(±1s, worse if stale). The follow-up host correction must also be suppressed so it
|
||||
doesn't read as guest input.
|
||||
|
||||
### EC-7 🟢 Old / buggy / modified guest client
|
||||
Covered by receiver-side + server-side gates.
|
||||
|
||||
### EC-8 🔴 Tab throttling / background tab
|
||||
Backgrounding throttles timers and may pause media. There's existing
|
||||
`visibilityGraceUntil` handling for seeks (content.js:892). Confirm a
|
||||
background-induced pause isn't treated as deliberate in host-only; reuse the grace flag.
|
||||
|
||||
### EC-9 🔴 What counts as "deliberate" — the central unresolved question
|
||||
Collapses EC-1/EC-5/EC-8. Candidate signals: `readyState`/`networkState`/`video.seeking`
|
||||
at event time; recent `waiting`; recent user gesture (`navigator.userActivation`,
|
||||
keydown/click); visibility/focus. Build one shared **intent-classifier** helper in
|
||||
content.js that all host-only gating flows through.
|
||||
|
||||
### EC-10 🔴 Host brief disconnect / reconnect (network blip)
|
||||
Host's wifi drops for 3s and reconnects. With "host leaves → fallback to everyone",
|
||||
a blip would silently unlock the room and demote the host (peerId persists in
|
||||
chrome.storage so they rejoin with the same id, but the server already cleared
|
||||
`hostPeerId`). Mitigation idea: short **host grace period** (e.g. keep `hostPeerId`
|
||||
reserved for ~30s after disconnect; if the same peerId rejoins, restore host + mode).
|
||||
Needs the server reaper (server:644) and `removePeerFromRoom` (server:168) to cooperate.
|
||||
|
||||
### EC-11 🔴 New guest joins mid-session in host-only mode
|
||||
On join they must (a) immediately sync to the host's current position without the host
|
||||
doing anything, and (b) see they're a guest in the UI. ROOM_DATA already carries peers;
|
||||
add `hostPeerId`/`controlMode` so a fresh client knows its role instantly. Verify the
|
||||
existing "newcomer syncs without waiting" path (content.js:542) still fires.
|
||||
|
||||
### EC-12 🔴 Desync semantics — what does "solo" actually mean?
|
||||
When a guest chooses "pause only me", do they (a) fully ignore all subsequent host
|
||||
events until they Resync, or (b) keep receiving but not auto-applying? Define clearly.
|
||||
Proposed: full solo — ignore host play/pause/seek while desynced; Resync re-attaches and
|
||||
snaps to current host position. Also: what state does Resync land them in if the host is
|
||||
currently paused vs playing?
|
||||
|
||||
### EC-13 🔴 Race: host flips to host-only exactly as a guest pauses
|
||||
Event ordering between `SET_CONTROL_MODE`/`CONTROL_MODE` and an in-flight guest `PAUSE`.
|
||||
The `seq` ordering helps, but define the tie-break. Likely: server is authoritative —
|
||||
once it has `host-only`, it drops the guest event regardless of client-side timing.
|
||||
|
||||
### EC-14 🟡 Volume / mute / audio-options must NOT be gated
|
||||
Those are per-peer, not room control. The gate must target only play/pause/seek +
|
||||
forcesync/episode — not `PEER_STATUS` volume/mute fields. Easy to over-block; add a test.
|
||||
|
||||
### EC-15 🔴 Live streams (Twitch live, live DVR)
|
||||
"Room timestamp" is fuzzy on live edge; seeking semantics differ. Decide whether
|
||||
host-only even makes sense for live, or degrade gracefully. Low priority but log it.
|
||||
|
||||
### EC-16 🟡 Host's own involuntary events still drive the room
|
||||
If the host buffers and the player auto-pauses, that pause is "allowed" and pauses
|
||||
everyone. That's existing behavior, but in host-only it means the host's buffering
|
||||
stalls the whole room. Acceptable? Probably yes (host is authoritative), but note it.
|
||||
|
||||
### EC-17 🟡 Server restart drops room state
|
||||
`hostPeerId`/`controlMode` are in-memory. After a server restart, whoever rejoins first
|
||||
becomes the new host and mode resets to `everyone`. Acceptable for now (non-goal), but
|
||||
document so it's not a surprise.
|
||||
|
||||
### EC-18 🟡 Dialog dismissed without choosing
|
||||
Guest clicks away / presses Esc on the desync prompt. Default = treat as "No" → snap
|
||||
back. Make sure an un-answered dialog can't leave them in limbo (paused + not desynced +
|
||||
no dialog).
|
||||
|
||||
### EC-19 🟡 Multiple video elements / element swap (SPA, ad → content)
|
||||
Players that swap the `<video>` element mid-session: re-attach handlers (content.js
|
||||
already re-binds on `loadeddata`) and make sure host-only gating follows the new element.
|
||||
|
||||
### EC-20 🟡 Peer list shape (object vs legacy string)
|
||||
Throughout background.js peers may be objects or bare peerId strings
|
||||
(`typeof p === 'object' ? p.peerId : p`). All new `hostPeerId` comparisons must handle
|
||||
both forms, or we get a host that's never recognized.
|
||||
|
||||
### EC-21 🟡 Mode toggle spam / rate limiting
|
||||
Host hammering the toggle → many `SET_CONTROL_MODE`. Covered by existing
|
||||
`checkEventRate` (server), but debounce in the UI and ignore no-op transitions.
|
||||
|
||||
---
|
||||
|
||||
## 7. Test matrix (fill in during dev)
|
||||
|
||||
| Player | Buffer→`pause`? | Ad behavior | Snap-back works? | Element swap? | Notes |
|
||||
|-------------------|-----------------|-------------|------------------|---------------|-------|
|
||||
| Generic HTML5 | | | | | |
|
||||
| YouTube | | | | | |
|
||||
| Netflix | | | | | |
|
||||
| Twitch (VOD) | | | | | |
|
||||
| Twitch (live) | | | | | |
|
||||
| Disney+ / DRM | | | | | |
|
||||
| Jellyfin / Emby | | | | | |
|
||||
|
||||
## 8. Decisions (audited)
|
||||
- [x] **Intent-classifier (EC-9):** A `pause`/`seek` is **involuntary** if ANY of:
|
||||
`readyState < 3`, `video.seeking`, a `waiting` fired < ~1500ms ago (`isBuffering`
|
||||
flag), inside `visibilityGraceUntil`, OR no own-tracked user gesture
|
||||
(`Date.now() - lastUserGestureAt < 1000`, via capturing keydown/pointerdown — do
|
||||
NOT use sticky `navigator.userActivation.hasBeenActive`). Bias: only *clearly*
|
||||
involuntary is ignored; everything else = deliberate. **Note:** in host-only the
|
||||
guest never broadcasts anyway, so this only decides dialog-vs-silent — a UX call,
|
||||
not a room-integrity call. Start simple, tune later.
|
||||
- [x] **Host grace on disconnect (EC-10): NOT in v1.** Immediate fallback to
|
||||
`everyone` (EC-3). A grace window risks a multi-second hard-lock if the host never
|
||||
returns. Revisit as polish once the core flow works.
|
||||
- [x] **Desync semantics (EC-12): full solo.** Ignore host play/pause/seek while
|
||||
desynced; Resync snaps to host position + adopts host play/pause state. Desync
|
||||
auto-clears on new media/episode. Requires a persistent, obvious "You are desynced"
|
||||
UI.
|
||||
- [x] **Snap-back cooldown (EC-4): until-settled, not fixed.** Suppress re-trigger
|
||||
until `readyState>=3 && playing && |Δt|<tol`, hard-cap ~1500ms. Retry target up to
|
||||
3×, then give up (no infinite loop).
|
||||
- [x] **Live streams (EC-15): degrade.** Disable the gate when
|
||||
`video.duration === Infinity`. Caveat: live-DVR may report finite duration and slip
|
||||
through — acceptable for v1.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Host Control Mode — Beta Testing Guide
|
||||
|
||||
> Temporary doc for branch `feature/host-control-mode`. Remove before merge.
|
||||
> The feature needs the **server** half too — the official relay
|
||||
> (`wss://syncserver.koalastuff.net`) doesn't run it yet, so test against a beta
|
||||
> server first.
|
||||
|
||||
## 1. Run the beta relay (Docker)
|
||||
|
||||
The branch publishes the server image to GHCR under non-production tags
|
||||
(`:beta` = newest branch build, `:sha-<commit>` = immutable pin). `:latest` is
|
||||
never touched.
|
||||
|
||||
```bash
|
||||
# Log in (the package is private → PAT with read:packages)
|
||||
echo "$GHCR_PAT" | docker login ghcr.io -u <your-gh-user> --password-stdin
|
||||
|
||||
# Pull + (re)create — NOTE: `docker restart` does NOT pick up a new image,
|
||||
# you must remove and re-run (or use compose / Watchtower).
|
||||
docker pull ghcr.io/shik3i/koalasync:beta
|
||||
docker rm -f koala-beta 2>/dev/null || true
|
||||
docker run -d --name koala-beta -p 3000:3000 \
|
||||
-e SERVER_SALT='choose-your-own-salt' \
|
||||
ghcr.io/shik3i/koalasync:beta
|
||||
```
|
||||
|
||||
To always run the newest beta automatically, point **Watchtower** at the
|
||||
container — it does the pull → recreate whenever `:beta` moves.
|
||||
|
||||
The `OFFICIAL_SERVER_TOKEN` is baked into `shared/constants.js`, so no token env
|
||||
is needed. Set `SERVER_SALT` (used for room-password hashing).
|
||||
|
||||
## 2. Connect the extension to it
|
||||
|
||||
⚠️ The client **force-upgrades `ws://` to `wss://` for any non-localhost host**
|
||||
(see background.js "Upgraded to wss:// for remote host"). So a bare
|
||||
`ws://your-server:3000` will fail without TLS. Two options:
|
||||
|
||||
- **Quick (no TLS):** SSH-tunnel the port so it counts as local:
|
||||
```bash
|
||||
ssh -L 3000:localhost:3000 your-beta-server
|
||||
```
|
||||
then in the popup → **Manual Connect / Advanced → Custom →** `ws://localhost:3000`.
|
||||
- **Proper:** put the container behind a TLS reverse proxy (Caddy does automatic
|
||||
HTTPS) → `wss://beta.yourdomain`.
|
||||
|
||||
Then create/join a room.
|
||||
|
||||
## 3. ⚠️ Use two *new* clients
|
||||
|
||||
Test with **two browser profiles both running this branch build** (load unpacked
|
||||
from `extension/`, or install the built zip). A stock release client as a guest
|
||||
will be correctly gated by the server but has none of the content-side code, so
|
||||
it silently desyncs with no dialog/snap-back — that's expected degradation, not a
|
||||
bug, but it looks like one during testing.
|
||||
|
||||
## 4. Verification checklist
|
||||
|
||||
| # | Step | Expect |
|
||||
|---|------|--------|
|
||||
| 1 | Create a room (host) | Host Control card shows **Host** + the toggle |
|
||||
| 2 | Second profile joins | Guest sees no card while mode is "everyone" |
|
||||
| 3 | Host enables "Only I can control" | Guest's Play/Pause/SYNC buttons lock; card shows **Guest** |
|
||||
| 4 | Guest presses pause/space on the video | Brief flicker, snaps back to host position |
|
||||
| 5 | Guest pauses again | Dialog: "Stay in sync" / "Watch on my own" |
|
||||
| 6 | Guest → "Watch on my own" | Persistent "Solo" badge; host's peer list shows **Solo** |
|
||||
| 7 | Guest → "Resync" | Snaps back to host; Solo badge clears on both sides |
|
||||
| 8 | Guest tries Force-Sync / seek spam | Nothing propagates to the room |
|
||||
| 9 | Host disables host-only | Card hides for guest; controls unlock |
|
||||
| 10 | Host leaves the room | Room falls back to "everyone"; a remaining peer becomes host |
|
||||
| 11 | Reload the guest's page while desynced | Still shows Solo (state survives reload) |
|
||||
| 12 | Switch the popup language | Dialog/badge text is localized |
|
||||
|
||||
## 5. Capability detection
|
||||
|
||||
The relay advertises `capabilities: ['host-control']` in `ROOM_DATA`. The client
|
||||
only enables the feature when that flag is present, so:
|
||||
- against this beta server → feature on;
|
||||
- against the old official server → feature cleanly hidden (no errors).
|
||||
|
||||
This is the extensible hook for the planned **co-host** feature (owner promotes
|
||||
guests to additional controllers) — it'll add a `'co-host'` capability + events
|
||||
without breaking older relays/clients.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Host Control Mode — Implementierungsplan
|
||||
|
||||
Branch: `feature/host-control-mode`
|
||||
Issue: GitHub feature request (wasserrutschentester) — nur Host darf den Raum steuern; Gäste werden zurückgesnappt oder gehen bewusst in Desync.
|
||||
|
||||
## Ziel
|
||||
|
||||
Ein Raum kann zwischen zwei Modi umgeschaltet werden:
|
||||
- **`everyone`** (Default, heutiges Verhalten): jeder kann play/pause/seek für alle auslösen.
|
||||
- **`host-only`**: nur der Host steuert den Raum. Pause/Seek eines Gasts wird **nicht** gebroadcastet; stattdessen snappt die eigene Extension den Gast zurück auf den Raum-Zustand — es sei denn, der Gast entscheidet sich bewusst für Desync.
|
||||
|
||||
Trust-Modell: client-seitig durchgesetzt. Kein Token, keine Auth. Es geht um versehentliches Stören, nicht um Angriffsschutz.
|
||||
|
||||
---
|
||||
|
||||
## Datenmodell
|
||||
|
||||
### Server (`server/index.js`, Room-Objekt ~Z.331)
|
||||
Room bekommt zwei neue Felder:
|
||||
```js
|
||||
room = {
|
||||
...,
|
||||
hostPeerId: peerId, // gesetzt beim Anlegen = erster Joiner
|
||||
controlMode: 'everyone', // 'everyone' | 'host-only'
|
||||
}
|
||||
```
|
||||
- In `ROOM_DATA` (~Z.418) mitschicken: `hostPeerId`, `controlMode`.
|
||||
- Neues Event `SET_CONTROL_MODE` (siehe unten): nur akzeptieren, wenn `senderPeerId === room.hostPeerId`. Server setzt `room.controlMode`, broadcastet die Änderung an alle.
|
||||
- **Host-Migration:** in `removePeerFromRoom` (~Z.168) — wenn der gehende Peer `hostPeerId` war: entweder neuen Host bestimmen (nächster Peer) **oder** `controlMode` auf `everyone` zurückfallen lassen. → Entscheidung: **Fallback auf `everyone`** (simpel, nie verwaister gesperrter Raum). Optional später: Host-Transfer-Button.
|
||||
|
||||
### Shared Constants (`shared/constants.js`)
|
||||
Neue Events im `EVENTS`-Objekt:
|
||||
```js
|
||||
SET_CONTROL_MODE: "set_control_mode", // Client->Server: Host ändert Modus
|
||||
CONTROL_MODE: "control_mode", // Server->Client: Modus geändert { controlMode, hostPeerId }
|
||||
```
|
||||
⚠️ Danach `node scripts/build-extension.cjs` laufen lassen (Single Source of Truth propagieren). Ggf. `PROTOCOL_VERSION` bumpen — **nein, nur wenn alte Clients brechen würden**. Da alles additiv ist und alte Clients die neuen Felder/Events einfach ignorieren, ist KEIN Protokoll-Bump nötig. (Alter Client in host-only-Raum kennt den Modus nicht und sendet weiter → Host-Extensions ignorieren fremde Events nicht... → siehe Edge Case 7. Evtl. doch Bump erwägen.)
|
||||
|
||||
### Extension State (`background.js`)
|
||||
```js
|
||||
let controlMode = 'everyone';
|
||||
let hostPeerId = null;
|
||||
// abgeleitet: const amHost = () => hostPeerId === peerId;
|
||||
```
|
||||
Aus `ROOM_DATA` / `CONTROL_MODE` befüllen, in `chrome.storage.session` persistieren (wie `currentRoom`).
|
||||
|
||||
---
|
||||
|
||||
## Implementierung nach Schichten
|
||||
|
||||
### 1. Server (`server/index.js`)
|
||||
- [ ] Room-Objekt um `hostPeerId` + `controlMode` erweitern (~Z.331).
|
||||
- [ ] `ROOM_DATA`-Payload erweitern (~Z.418).
|
||||
- [ ] Handler `SET_CONTROL_MODE`: validieren (Host-Check + Wert in {everyone, host-only}), setzen, `CONTROL_MODE` an Raum broadcasten.
|
||||
- [ ] Host-Migration in `removePeerFromRoom`: Fallback auf `everyone` + neues `CONTROL_MODE` broadcasten, wenn Host geht.
|
||||
- [ ] `SET_CONTROL_MODE` in die `relayEvents`-Liste? **Nein** — eigener Handler, da Sonderlogik + Host-Check. (relayEvents broadcastet blind.)
|
||||
|
||||
### 2. Shared / Build
|
||||
- [ ] `EVENTS.SET_CONTROL_MODE`, `EVENTS.CONTROL_MODE` ergänzen.
|
||||
- [ ] `node scripts/build-extension.cjs`.
|
||||
|
||||
### 3. background.js (Gast-Logik = Kern)
|
||||
- [ ] `controlMode` / `hostPeerId` aus `ROOM_DATA` (~Z.875) und neuem `CONTROL_MODE`-Case übernehmen + persistieren + an Popup/Content pushen.
|
||||
- [ ] **Emit-Gate** im SEND-Pfad (~Z.1786): bei `host-only && !amHost()` und action ∈ {play, pause, seek}:
|
||||
- NICHT `emit`en.
|
||||
- Stattdessen Content-Script anweisen: "snap back" ODER Desync-Confirm anzeigen.
|
||||
- [ ] **Snap-Back-Zielzeit berechnen:** aus Host-Peer-State (`playbackState`, `currentTime`, `lastHeartbeat`) extrapolieren:
|
||||
`targetTime = host.currentTime + (host.playbackState==='playing' ? (now - host.lastHeartbeat)/1000 : 0)`.
|
||||
Genauigkeit ~±1s, für Watchparty ok. (Force-Sync-Maschinerie als Referenz für Ziel-Zeit-Koordination.)
|
||||
- [ ] Nachricht an content.js: `{ type: 'HOST_BLOCK', action, targetTime, hostPlaybackState }`.
|
||||
|
||||
### 4. content.js (Player-Reaktion + Dialog)
|
||||
- [ ] Handler für `HOST_BLOCK`: Confirm-Dialog im Player-Overlay rendern:
|
||||
"Pause only your own player and desync from the group? [Yes] [No]".
|
||||
- **No** (Default): Player via bestehende `_setSuppress`-Mechanik ([content.js:442](../extension/content.js:442)) wieder in Raum-Zustand zwingen (play + seek auf targetTime). Suppress verhindert Re-Broadcast.
|
||||
- **Yes:** lokal pausiert lassen, `isDesynced = true` setzen, dezenten "Desynced — Resync"-Button zeigen.
|
||||
- [ ] "Resync"-Button → snappt zurück auf aktuelle Raum-Zeit, `isDesynced = false`.
|
||||
- [ ] **Loop-Schutz:** nach einer Snap-Back-Aktion kurzes Cooldown-Fenster (z.B. 600ms), in dem weitere lokale pause/seek-Events nicht erneut den Dialog triggern (verhindert pause→play→pause-Pingpong).
|
||||
|
||||
### 5. popup (Host-UI)
|
||||
- [ ] Host-Toggle "Only I can control" (nur sichtbar wenn `amHost()`), sendet `SET_CONTROL_MODE`.
|
||||
- [ ] Rollen-Badge: "Host" / "Guest" + aktueller Modus.
|
||||
- [ ] Gast-Hinweis wenn host-only aktiv: "The host controls playback".
|
||||
- [ ] i18n-Keys in `extension/_locales` / `locales` für ~15 Sprachen.
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases (Test-Checkliste)
|
||||
|
||||
1. **Pause nicht verhinderbar, nur revidierbar** → kurzer Flicker (~½s) beim Gast ist erwartet/akzeptabel.
|
||||
2. **Snap-Back-Zielzeit** aus Heartbeat extrapoliert, ±1s. Bei stark veraltetem Host-State (kein Heartbeat) → letzten bekannten Wert nehmen.
|
||||
3. **Kampf-Loop pause/play/skip back/pause/play** → Cooldown-Fenster nach Snap-Back. Testen mit aggressivem Mashing.
|
||||
4. **Desync-Escape:** Gast kann bewusst pausieren (Klo/Telefon) → "Yes" → solo, dann Resync.
|
||||
5. **Host verlässt Raum** → Fallback auf `everyone`, alle bekommen `CONTROL_MODE`-Update. Testen: Host schließt Tab / Disconnect / Netzabbruch.
|
||||
6. **host-only + Episode-Auto-Sync / Force-Sync** (server/index.js:503-516): **Gast darf NICHT initiieren.** Force-Sync trägt eine `targetTime` und zwingt ALLE darauf (background.js:1261) — ein Gast könnte seeken → Force-Sync spammen und damit host-only komplett aushebeln. Episode-Lobby pausiert ebenfalls alle. → Im host-only-Modus dürfen `FORCE_SYNC_PREPARE`/`FORCE_SYNC_EXECUTE` und `EPISODE_LOBBY` nur vom Host **initiiert** werden. Gäste dürfen weiterhin nur **reagieren**: `FORCE_SYNC_ACK`, `EPISODE_READY`. Gäste brauchen Force-Sync nicht — ihr legitimer Fall ist der "Resync"-Button (snappt nur sie selbst, nicht alle).
|
||||
7. **Alter Client (ohne Feature) in host-only-Raum** → kennt Modus nicht, sendet weiter pause/seek → andere Extensions wenden es an. Mitigation: Empfänger-seitiges Gate (host-only-Clients ignorieren play/pause/seek von Nicht-Host) ODER `MIN_VERSION`/Protokoll-Bump. → **Empfehlung: zusätzlich Empfänger-seitig filtern** (robuster als nur Sender-Gate).
|
||||
8. **Seek getrennt von Pause** → host-only blockt auch Gast-Seeks, nicht nur Pausen.
|
||||
9. **Mehrere Tabs / Multi-Peer mit gleicher peerId** (Dedup, server:381) → Host-Identität bleibt an peerId hängen, ok.
|
||||
10. **DAU-Verwirrung** "warum kann ich nicht mehr pausieren?" → klare UI-Botschaft + der Desync-Dialog erklärt sich selbst.
|
||||
|
||||
## Architektur-Entscheidung zu Edge Case 7 (wichtig)
|
||||
Wir setzen das Gate **doppelt** und über **alle raum-verschiebenden Events**, nicht nur play/pause/seek:
|
||||
Geblockte Initiierungen für Nicht-Host im host-only-Modus:
|
||||
`PLAY`, `PAUSE`, `SEEK`, `FORCE_SYNC_PREPARE`, `FORCE_SYNC_EXECUTE`, `EPISODE_LOBBY`.
|
||||
Weiterhin erlaubt für Gäste (reine Reaktion, verschiebt niemanden): `FORCE_SYNC_ACK`, `EPISODE_READY`, `PEER_STATUS`, `PING`/`PONG`.
|
||||
|
||||
- **Sender-seitig** (Gast sendet erst gar nicht) → saubere UX, Confirm-Dialog bei play/pause/seek; Force-Sync-/Episode-Lobby-Buttons im Gast-UI deaktiviert/ausgeblendet.
|
||||
- **Empfänger-seitig** (in `handleServerEvent`, background.js:969 + Force-Sync-/Episode-Cases): wenn `host-only` und `data.senderId !== hostPeerId` → Event verwerfen (nicht an Content routen, keine State-Mutation).
|
||||
So sind auch alte/buggy/manipulierte Clients abgedeckt, ohne harten Protokoll-Bump.
|
||||
|
||||
Optional zusätzlich **server-seitig** in den `relayEvents` (server/index.js:445): im host-only-Modus Initiierungs-Events von Nicht-Host gar nicht erst relayen. Spart Traffic + deckt alles zentral ab. Empfehlenswert, da der Server `hostPeerId`/`controlMode` ohnehin kennt.
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge der Umsetzung (kleine, testbare Schritte)
|
||||
1. Constants + Build (Events da, nichts kaputt).
|
||||
2. Server: hostPeerId/controlMode + ROOM_DATA + SET_CONTROL_MODE + Migration.
|
||||
3. background.js: State übernehmen + Empfänger-seitiges Gate (Edge 7) — testbar ohne UI.
|
||||
4. background.js: Sender-seitiges Gate + Snap-Back-Zielzeit.
|
||||
5. content.js: Snap-Back-Apply + Confirm-Dialog + Loop-Cooldown.
|
||||
6. popup: Host-Toggle + Badge + i18n.
|
||||
7. Durchtesten der Edge-Case-Liste auf YT / Netflix / generischem HTML5-Player.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "Синхронізуйте відтворення відео на YouTube, Netflix, Emby, Jellyfin і будь-якому сайті HTML5 у режимі реального часу з друзями."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"appName": {
|
||||
"message": "KoalaSync"
|
||||
},
|
||||
"appDesc": {
|
||||
"message": "与朋友实时同步YouTube、Netflix、Emby、Jellyfin和任何HTML5网站上的视频播放。"
|
||||
}
|
||||
}
|
||||
@@ -126,17 +126,13 @@ function setCustomParam(param, value) {
|
||||
}
|
||||
|
||||
async function init() {
|
||||
let audioData = (await chrome.storage.local.get(['audioSettings'])).audioSettings;
|
||||
const syncData = await chrome.storage.sync.get(['audioSettings', 'locale']);
|
||||
if (!audioData && syncData.audioSettings) {
|
||||
audioData = syncData.audioSettings;
|
||||
await chrome.storage.local.set({ audioSettings: audioData });
|
||||
}
|
||||
const lang = syncData.locale || getSystemLanguage();
|
||||
// Local-only: audioSettings/locale are never read from storage.sync.
|
||||
const { audioSettings, locale } = await chrome.storage.local.get(['audioSettings', 'locale']);
|
||||
const lang = locale || getSystemLanguage();
|
||||
await loadLocale(lang);
|
||||
translateDOM();
|
||||
|
||||
currentSettings = mergeAudioSettings(audioData);
|
||||
currentSettings = mergeAudioSettings(audioSettings);
|
||||
render();
|
||||
}
|
||||
|
||||
|
||||
+589
-187
File diff suppressed because it is too large
Load Diff
+489
-35
@@ -57,6 +57,29 @@
|
||||
let seekDebounceTimer = null; // debounce timer for rapid seek events
|
||||
let expectedSeekTime = null; // strictly track programmatic seeks
|
||||
|
||||
// --- Play/Pause Coalescing (leading + trailing) ---
|
||||
// Media players (HLS/DASH, ad insertion, ABR/quality switches, source swaps,
|
||||
// page teardown) fire bursts of native play/pause events within a few hundred
|
||||
// ms. Relaying each as a distinct command spams peers and the relay.
|
||||
//
|
||||
// Strategy: emit the FIRST event immediately (leading edge → a deliberate
|
||||
// single play/pause has zero added latency), then hold a short window. If
|
||||
// more play/pause events arrive during the window it's a burst — we suppress
|
||||
// the intermediate churn and, once it settles, emit the FINAL state on the
|
||||
// trailing edge.
|
||||
//
|
||||
// We deliberately do NOT dedup the trailing send against the leading one. A
|
||||
// remote play/pause may be applied mid-window (e.g. I pause, peer plays, I
|
||||
// pause again within 150ms): the settled state then equals my last *sent*
|
||||
// state yet is a genuine change versus the now-shared state — suppressing it
|
||||
// would desync. Re-sending an unchanged state is a harmless no-op on peers,
|
||||
// so re-sending is always the safe choice. Echo-suppression and seek-flush
|
||||
// still run synchronously on event arrival (see reportEvent) — only the
|
||||
// network emit is governed here.
|
||||
const PLAY_PAUSE_COALESCE_MS = 150;
|
||||
let playPauseCoalesceTimer = null; // non-null = a coalescing window is open
|
||||
let pendingPlayPauseAction = null; // last play/pause seen during the window, awaiting trailing flush
|
||||
|
||||
// --- Episode Auto-Sync State ---
|
||||
let lastKnownMediaTitle = null;
|
||||
let episodeTransitionDebounce = null;
|
||||
@@ -66,25 +89,8 @@
|
||||
let _audioSettings = null;
|
||||
let _audioProcessingAllowed = true;
|
||||
|
||||
// Cache the autoSyncNextEpisode setting
|
||||
// Cache the autoSyncNextEpisode setting (local-only; never read from sync)
|
||||
chrome.storage.local.get(['autoSyncNextEpisode', 'audioSettings'], (data) => {
|
||||
if (data.autoSyncNextEpisode === undefined || data.audioSettings === undefined) {
|
||||
chrome.storage.sync.get(['autoSyncNextEpisode', 'audioSettings'], (syncData) => {
|
||||
const migrate = {};
|
||||
if (data.autoSyncNextEpisode === undefined && syncData.autoSyncNextEpisode !== undefined) {
|
||||
migrate.autoSyncNextEpisode = syncData.autoSyncNextEpisode;
|
||||
}
|
||||
if (data.audioSettings === undefined && syncData.audioSettings !== undefined) {
|
||||
migrate.audioSettings = syncData.audioSettings;
|
||||
}
|
||||
if (Object.keys(migrate).length) chrome.storage.local.set(migrate);
|
||||
_autoSyncEnabled = syncData.autoSyncNextEpisode !== false;
|
||||
_audioSettings = mergeAudioSettings(syncData.audioSettings);
|
||||
const v = findVideo();
|
||||
if (v && _audioProcessingAllowed) applyAudioSettings(v, _audioSettings);
|
||||
});
|
||||
return;
|
||||
}
|
||||
_autoSyncEnabled = data.autoSyncNextEpisode !== false;
|
||||
_audioSettings = mergeAudioSettings(data.audioSettings);
|
||||
const video = findVideo();
|
||||
@@ -96,6 +102,306 @@
|
||||
}
|
||||
});
|
||||
|
||||
// --- Host Control Mode (guest-side) ---
|
||||
// When a room is in 'host-only' mode and we're a guest, a deliberate local
|
||||
// pause/seek must not drive the room (background/server already drop it). Here
|
||||
// we handle the *local* UX: snap back to the host's position, or — if the user
|
||||
// really wants to — let them go solo (desync) with a resync escape hatch.
|
||||
let hcmControlMode = 'everyone'; // mirror of room control mode
|
||||
let hcmAmController = false; // are we allowed to drive (owner or co-host)?
|
||||
let hcmHostPeerId = null; // last known host peerId (room/host identity)
|
||||
let hcmDesynced = false; // user chose to go solo
|
||||
let hcmSnapBackCooldownUntil = 0; // suppress re-trigger right after a snap-back
|
||||
let hcmDeferredSnapPending = false; // a buffer-aware snap-back is waiting for readiness
|
||||
let hcmLastUserGestureAt = 0; // for deliberate-vs-involuntary classification
|
||||
let hcmBufferingUntil = 0; // set on 'waiting' — buffering grace window
|
||||
let hcmDialogTimer = null; // 8s auto-stay timer — cleared on dialog replace (H-4)
|
||||
// Localized strings for the in-page dialog/badge (content has no i18n loader;
|
||||
// background resolves them via GET_HCM_STRINGS on init). English fallbacks here.
|
||||
const hcmStrings = {
|
||||
title: 'KoalaSync · Host controls this room',
|
||||
body: 'Only the host can control playback in this room. Keep watching together, or watch on your own?',
|
||||
stay: 'Stay in sync',
|
||||
solo: 'Watch on my own',
|
||||
badge: 'Watching on your own',
|
||||
resync: 'Resync'
|
||||
};
|
||||
const HCM_USER_GESTURE_MS = 1000;
|
||||
const HCM_BUFFERING_GRACE_MS = 1500;
|
||||
const HCM_SNAP_BACK_COOLDOWN_MS = 1000;
|
||||
const HCM_BUFFER_WAIT_MS = 8000; // cap on waiting for a buffering player before snapping anyway
|
||||
|
||||
// Track genuine user input so we can tell a deliberate pause/seek from a
|
||||
// player-/browser-initiated one. Capturing + passive so we never interfere.
|
||||
const _hcmGesture = () => { hcmLastUserGestureAt = Date.now(); };
|
||||
document.addEventListener('keydown', _hcmGesture, { capture: true, passive: true });
|
||||
document.addEventListener('pointerdown', _hcmGesture, { capture: true, passive: true });
|
||||
|
||||
function hcmIsGuestGated() {
|
||||
return hcmControlMode === 'host-only' && !hcmAmController;
|
||||
}
|
||||
|
||||
// EC-9 intent classifier: only a *clearly deliberate* guest action triggers the
|
||||
// dialog/snap-back. Anything that smells involuntary (buffering, seeking, tab
|
||||
// refocus, no recent gesture) is treated as involuntary. Bias intentional —
|
||||
// in host-only the guest never broadcasts anyway, so this only tunes UX.
|
||||
function hcmClassifyIntent() {
|
||||
const video = findVideo();
|
||||
if (!video) return 'involuntary';
|
||||
if (hcmIsLive(video)) return 'live'; // EC-15: degrade, don't gate
|
||||
if (video.readyState < 3) return 'involuntary'; // buffering / not enough data
|
||||
if (video.seeking) return 'involuntary';
|
||||
if (Date.now() < hcmBufferingUntil) return 'involuntary';
|
||||
if (Date.now() < visibilityGraceUntil) return 'involuntary';
|
||||
if (Date.now() - hcmLastUserGestureAt > HCM_USER_GESTURE_MS) return 'involuntary';
|
||||
return 'deliberate';
|
||||
}
|
||||
|
||||
// Live detection (EC-15 + DVR). Pure live reports duration Infinity/NaN. Live-DVR
|
||||
// (Twitch/YouTube-live with rewind) reports a *finite, sliding* duration — its
|
||||
// seekable window doesn't start at 0, which we use as the DVR signal.
|
||||
function hcmIsLive(video) {
|
||||
// Don't trust duration before metadata has loaded (readyState >= 1) —
|
||||
// otherwise pre-loaded videos report NaN and get misclassified as live,
|
||||
// which suppresses the desync dialog (L-2).
|
||||
if (video.readyState < 1) return false;
|
||||
if (!Number.isFinite(video.duration)) return true;
|
||||
try {
|
||||
const s = video.seekable;
|
||||
if (s && s.length > 0 && s.start(0) > 1) return true; // sliding DVR window
|
||||
} catch (_e) { /* seekable may throw if empty */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snap the local player back to the host's current position/state.
|
||||
function hcmSnapBackToHost(target) {
|
||||
if (hcmDesynced) return; // user opted out — never yank them back automatically
|
||||
hcmSnapBackCooldownUntil = Date.now() + HCM_SNAP_BACK_COOLDOWN_MS;
|
||||
const video = findVideo();
|
||||
if (!video) return;
|
||||
if (target && Number.isFinite(target.targetTime)) {
|
||||
tryMediaAction(EVENTS.SEEK, { targetTime: target.targetTime });
|
||||
}
|
||||
// Adopt the host's play/pause state — but ONLY if we actually know it.
|
||||
// Defaulting to PLAY when the state is unknown would auto-resume a paused
|
||||
// video against the host's real state (H-1).
|
||||
if (target && target.playbackState === 'paused') {
|
||||
tryMediaAction(EVENTS.PAUSE);
|
||||
} else if (target && target.playbackState === 'playing') {
|
||||
tryMediaAction(EVENTS.PLAY);
|
||||
}
|
||||
reportLog('Host-only: snapped back to host position', 'info');
|
||||
}
|
||||
|
||||
// Resync to the host's current position, retrying briefly if the host's state
|
||||
// isn't known yet (e.g. they just paused and no heartbeat has propagated) —
|
||||
// otherwise the request is a silent no-op and the user thinks they're synced
|
||||
// when they aren't. Shared by the dialog's "Stay in sync" and the Resync badge.
|
||||
function hcmRequestHostSyncWithRetry() {
|
||||
let attempts = 0;
|
||||
const tryOnce = () => {
|
||||
chrome.runtime.sendMessage({ type: 'REQUEST_HOST_SYNC' }, (res) => {
|
||||
if (chrome.runtime.lastError || !res || !res.target) {
|
||||
if (++attempts < 5) setTimeout(tryOnce, 250);
|
||||
else reportLog('Host-only: resync requested but host state unavailable', 'warn');
|
||||
return;
|
||||
}
|
||||
hcmSnapBackToHost(res.target);
|
||||
});
|
||||
};
|
||||
tryOnce();
|
||||
}
|
||||
|
||||
// Buffer-aware snap-back (#3). An involuntary pause/seek often coincides with the
|
||||
// player buffering — and a player can't actually play while it's stalled. Snapping
|
||||
// immediately just fights the buffer (seek → re-buffer → another pause → …), which
|
||||
// looks like stutter. Instead: if the player isn't ready, wait until it can play
|
||||
// (readyState>=3, not seeking), then snap ONCE to the host's *current* position
|
||||
// (re-queried, since the captured target may be stale by the time buffering ends).
|
||||
// Player-agnostic on purpose — we can't enumerate every site, so this must be safe
|
||||
// regardless of whether a given player fires 'pause' or only 'waiting'.
|
||||
function hcmDeferredSnapBack() {
|
||||
if (hcmDeferredSnapPending) return; // already waiting — don't stack polls
|
||||
hcmDeferredSnapPending = true;
|
||||
const deadline = Date.now() + HCM_BUFFER_WAIT_MS;
|
||||
const poll = () => {
|
||||
// Abort if the reason to snap is gone: user went solo, we're no longer a
|
||||
// gated guest, or the video vanished.
|
||||
if (hcmDesynced || !hcmIsGuestGated()) { hcmDeferredSnapPending = false; return; }
|
||||
const video = findVideo();
|
||||
const ready = video && video.readyState >= 3 && !video.seeking;
|
||||
if (ready || Date.now() >= deadline) {
|
||||
hcmDeferredSnapPending = false;
|
||||
hcmRequestHostSyncWithRetry(); // fresh host position + snap once
|
||||
return;
|
||||
}
|
||||
setTimeout(poll, 300);
|
||||
};
|
||||
poll();
|
||||
}
|
||||
|
||||
// Entry point: background told us our local action was blocked in host-only.
|
||||
function hcmHandleBlocked(action, target) {
|
||||
// HOST_BLOCKED is only ever sent to a gated guest (background verifies
|
||||
// host-only + !host before sending), so it's authoritative. Adopt the
|
||||
// role/mode from it in case our CONTROL_MODE broadcast hasn't landed yet
|
||||
// (join race, EC-5) — otherwise we'd miss the dialog/snap-back.
|
||||
hcmControlMode = 'host-only';
|
||||
hcmAmController = false;
|
||||
if (hcmDesynced) return; // already solo, nothing to do
|
||||
|
||||
const intent = hcmClassifyIntent();
|
||||
if (intent === 'live') return; // EC-15: leave the guest alone on live
|
||||
if (intent === 'involuntary') {
|
||||
// EC-4 loop guard: only the silent auto snap-back is suppressed by the
|
||||
// cooldown — the deliberate dialog path below must still go through,
|
||||
// otherwise a second deliberate pause inside the cooldown window leaves
|
||||
// the user stuck paused with no UI (M-3).
|
||||
if (Date.now() < hcmSnapBackCooldownUntil || hcmDeferredSnapPending) return;
|
||||
// Buffering/ads/throttle — silently re-sync, no dialog spam.
|
||||
const video = findVideo();
|
||||
if (video && video.readyState >= 3 && !video.seeking) {
|
||||
// Ready now → snap immediately. Use the captured target if it's
|
||||
// usable, otherwise re-query+retry (host state may not be known yet)
|
||||
// so we never leave the guest silently stuck (consistent with the
|
||||
// deferred and "Stay in sync" paths).
|
||||
if (target && Number.isFinite(target.targetTime)) hcmSnapBackToHost(target);
|
||||
else hcmRequestHostSyncWithRetry();
|
||||
} else {
|
||||
hcmDeferredSnapBack(); // buffering → wait for ready, then snap once (#3)
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Deliberate: offer the choice (Teleparty-style), default = snap back.
|
||||
hcmShowDesyncDialog(action, target);
|
||||
}
|
||||
|
||||
// --- In-page UI (dialog + persistent desync badge) ---
|
||||
// Built with the DOM API (CSSOM .style is CSP-safe; inline style="" in innerHTML
|
||||
// is stripped by strict style-src on Netflix/YouTube/Disney+). Hosted in a
|
||||
// Shadow DOM so the page's CSS can't restyle or hide our controls.
|
||||
let hcmDialogHost = null; // shadow host element for the dialog
|
||||
let hcmBadgeHost = null; // shadow host element for the persistent badge
|
||||
let hcmBadgePending = false; // retry flag for early-injection badge creation (L-4)
|
||||
|
||||
function hcmEl(tag, css, text) {
|
||||
const el = document.createElement(tag);
|
||||
if (css) el.style.cssText = css; // CSSOM assignment — not gated by CSP
|
||||
if (text != null) el.textContent = text;
|
||||
return el;
|
||||
}
|
||||
|
||||
function hcmRemoveDialog() {
|
||||
// Cancel any pending auto-stay timer so a replaced dialog's stale closure
|
||||
// can't later remove its successor / snap to an outdated target (H-4).
|
||||
if (hcmDialogTimer) { clearTimeout(hcmDialogTimer); hcmDialogTimer = null; }
|
||||
if (hcmDialogHost) { hcmDialogHost.remove(); hcmDialogHost = null; }
|
||||
}
|
||||
|
||||
function hcmShowDesyncDialog(action, target) {
|
||||
if (!document.body) { hcmSnapBackToHost(target); return; }
|
||||
hcmRemoveDialog();
|
||||
const host = hcmEl('div', 'all:initial');
|
||||
const root = host.attachShadow({ mode: 'open' });
|
||||
|
||||
const wrap = hcmEl('div', 'position:fixed;z-index:2147483647;left:50%;bottom:32px;transform:translateX(-50%);background:#1f2937;color:#f9fafb;font:14px/1.4 system-ui,sans-serif;padding:16px 18px;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.45);max-width:360px;border:1px solid #374151');
|
||||
wrap.setAttribute('role', 'dialog');
|
||||
const title = hcmEl('div', 'font-weight:600;margin-bottom:6px', hcmStrings.title);
|
||||
const body = hcmEl('div', 'margin-bottom:12px;color:#d1d5db', hcmStrings.body);
|
||||
const btnRow = hcmEl('div', 'display:flex;gap:8px;justify-content:flex-end');
|
||||
const soloBtn = hcmEl('button', 'background:#374151;color:#f9fafb;border:0;padding:8px 12px;border-radius:8px;cursor:pointer', hcmStrings.solo);
|
||||
const stayBtn = hcmEl('button', 'background:#10b981;color:#062a20;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600', hcmStrings.stay);
|
||||
btnRow.append(soloBtn, stayBtn);
|
||||
wrap.append(title, body, btnRow);
|
||||
root.appendChild(wrap);
|
||||
document.body.appendChild(host);
|
||||
hcmDialogHost = host;
|
||||
|
||||
let settled = false;
|
||||
// Re-query the host's current position on click instead of using the
|
||||
// potentially stale target captured at HOST_BLOCKED time (M-1).
|
||||
const stay = () => {
|
||||
if (settled) return; settled = true; hcmRemoveDialog();
|
||||
hcmRequestHostSyncWithRetry();
|
||||
};
|
||||
const solo = () => { if (settled) return; settled = true; hcmRemoveDialog(); hcmEnterDesync(); };
|
||||
stayBtn.addEventListener('click', stay);
|
||||
soloBtn.addEventListener('click', solo);
|
||||
// EC-18: if the user ignores the prompt, default to staying in sync.
|
||||
hcmDialogTimer = setTimeout(() => { if (!settled) stay(); }, 8000);
|
||||
}
|
||||
|
||||
function hcmEnterDesync() {
|
||||
hcmDesynced = true;
|
||||
reportLog('Host-only: you chose to watch on your own (desynced)', 'warn');
|
||||
// Notify background so it can relay our desynced state to the host via
|
||||
// heartbeats — the host's UI then knows we're not following commands
|
||||
// instead of appearing silently un-ACK'd.
|
||||
chrome.runtime.sendMessage({ type: 'HCM_DESYNC_STATE', desynced: true }).catch(() => {});
|
||||
hcmShowBadge();
|
||||
}
|
||||
|
||||
function hcmExitDesync() {
|
||||
const wasDesynced = hcmDesynced;
|
||||
hcmDesynced = false;
|
||||
hcmRemoveBadge();
|
||||
if (wasDesynced) {
|
||||
chrome.runtime.sendMessage({ type: 'HCM_DESYNC_STATE', desynced: false }).catch(() => {});
|
||||
}
|
||||
// Resync to the host's current position (retries if host state not yet known).
|
||||
hcmRequestHostSyncWithRetry();
|
||||
reportLog('Host-only: resynced with the host', 'info');
|
||||
}
|
||||
|
||||
function hcmShowBadge() {
|
||||
if (hcmBadgeHost) return;
|
||||
if (!document.body) {
|
||||
// Body not ready yet (very early injection). Defer until DOMReady,
|
||||
// otherwise the desynced user silently never sees the badge (L-4).
|
||||
if (!hcmBadgePending) {
|
||||
hcmBadgePending = true;
|
||||
const retry = () => {
|
||||
hcmBadgePending = false;
|
||||
if (hcmDesynced && !hcmBadgeHost) hcmShowBadge();
|
||||
};
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', retry, { once: true });
|
||||
} else {
|
||||
setTimeout(retry, 50);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const host = hcmEl('div', 'all:initial');
|
||||
const root = host.attachShadow({ mode: 'open' });
|
||||
const b = hcmEl('div', 'position:fixed;z-index:2147483646;right:16px;bottom:16px;background:#b45309;color:#fff;font:13px/1.3 system-ui,sans-serif;padding:8px 12px;border-radius:10px;box-shadow:0 6px 20px rgba(0,0,0,.4);cursor:pointer;display:flex;align-items:center;gap:8px');
|
||||
b.append(hcmEl('span', null, '● ' + hcmStrings.badge), hcmEl('span', 'text-decoration:underline', hcmStrings.resync));
|
||||
b.addEventListener('click', hcmExitDesync);
|
||||
root.appendChild(b);
|
||||
document.body.appendChild(host);
|
||||
hcmBadgeHost = host;
|
||||
}
|
||||
|
||||
function hcmRemoveBadge() {
|
||||
if (hcmBadgeHost) { hcmBadgeHost.remove(); hcmBadgeHost = null; }
|
||||
}
|
||||
|
||||
function hcmReset() {
|
||||
const wasDesynced = hcmDesynced;
|
||||
hcmDesynced = false;
|
||||
hcmDeferredSnapPending = false;
|
||||
hcmSnapBackCooldownUntil = 0; // don't let a stale cooldown swallow the next snap-back
|
||||
hcmBufferingUntil = 0;
|
||||
hcmRemoveDialog();
|
||||
hcmRemoveBadge();
|
||||
// If we were desynced, notify background so it stops reporting us as
|
||||
// desynced in heartbeats (otherwise the host's UI keeps showing the
|
||||
// stale Solo badge until the next state change).
|
||||
if (wasDesynced) {
|
||||
chrome.runtime.sendMessage({ type: 'HCM_DESYNC_STATE', desynced: false }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function reportLog(message, level = 'info') {
|
||||
chrome.runtime.sendMessage({ type: 'LOG', message, level }).catch(() => {});
|
||||
}
|
||||
@@ -350,6 +656,10 @@
|
||||
|
||||
reportLog(`Episode transition detected: "${newTitle}"`, 'info');
|
||||
|
||||
// EC-12: a new episode dissolves any solo/desync state — the guest rejoins
|
||||
// the room for the fresh content rather than staying stuck on the old one.
|
||||
if (hcmDesynced) hcmReset();
|
||||
|
||||
// Do NOT pause here. We notify background.js first.
|
||||
// Background checks the setting; if enabled it creates a lobby
|
||||
// and sends back PAUSE_FOR_LOBBY so we only freeze if the feature is on.
|
||||
@@ -532,10 +842,56 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
// Host Control Mode: room mode/role changed.
|
||||
if (message.type === 'CONTROL_MODE') {
|
||||
const wasGated = hcmIsGuestGated();
|
||||
const prevHostPeerId = hcmHostPeerId;
|
||||
hcmControlMode = message.controlMode || 'everyone';
|
||||
hcmAmController = !!message.amController;
|
||||
hcmHostPeerId = message.hostPeerId || null;
|
||||
// Reset guest-side state when leaving the gated state, OR when the
|
||||
// host identity changes (room switch, host-leave fallback, missed
|
||||
// teardown broadcast) — clears stale desync so a rejoin starts clean (H-3).
|
||||
const hostChanged = prevHostPeerId !== null && hcmHostPeerId !== prevHostPeerId;
|
||||
if ((wasGated && !hcmIsGuestGated()) || hostChanged) hcmReset();
|
||||
sendResponse({ ok: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Host Control Mode: background blocked our local action — handle UX locally.
|
||||
if (message.type === 'HOST_BLOCKED') {
|
||||
hcmHandleBlocked(message.action, message.target || null);
|
||||
sendResponse({ ok: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Background asks for an immediate state push (e.g. the first peer just
|
||||
// joined while we were solo) so the newcomer syncs without waiting.
|
||||
if (message.type === 'REQUEST_HEARTBEAT') {
|
||||
sendHeartbeat();
|
||||
sendResponse({ ok: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SERVER_COMMAND') {
|
||||
const { action, payload } = message;
|
||||
let actionCompleted = false;
|
||||
|
||||
// Host Control Mode: while watching on our own (desynced), don't apply
|
||||
// host commands. Only ACK FORCE_SYNC_PREPARE — that's the one the host's
|
||||
// force-sync flow actually waits on. Skipping CMD_ACKs for PLAY/PAUSE/SEEK
|
||||
// is intentional so the host's UI honestly reflects that we didn't apply
|
||||
// the command (M-2); sending them would make the host think we're synced.
|
||||
if (hcmDesynced) {
|
||||
const soloIgnored = [EVENTS.PLAY, EVENTS.PAUSE, EVENTS.SEEK, EVENTS.FORCE_SYNC_PREPARE, EVENTS.FORCE_SYNC_EXECUTE];
|
||||
if (soloIgnored.includes(action)) {
|
||||
if (action === EVENTS.FORCE_SYNC_PREPARE) {
|
||||
chrome.runtime.sendMessage({ type: 'FORCE_SYNC_ACK' }).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Guard: Don't execute sync commands if peers are on different episodes.
|
||||
// Only active when autoSyncNextEpisode setting is enabled (default: on).
|
||||
// Only blocks when BOTH sides have parseable S01E01-style IDs that differ.
|
||||
@@ -605,6 +961,15 @@
|
||||
|
||||
// Episode Auto-Sync: Lobby notification from background
|
||||
if (message.type === 'EPISODE_LOBBY') {
|
||||
// Host Control Mode: a desynced guest is watching on their own and must
|
||||
// not join the lobby flow. Otherwise they'd pause on title match, report
|
||||
// ready, but then ignore the host's FORCE_SYNC_* (hcmDesynced skip in
|
||||
// SERVER_COMMAND) and end up frozen in pause. They also can't be counted
|
||||
// toward lobby completion (background filters them out).
|
||||
if (hcmDesynced) {
|
||||
sendResponse({ status: 'ignored_desynced' });
|
||||
return true;
|
||||
}
|
||||
const expectedTitle = message.expectedTitle;
|
||||
if (expectedTitle) {
|
||||
reportLog(`Episode lobby received: waiting for "${expectedTitle}"`, 'info');
|
||||
@@ -768,6 +1133,46 @@
|
||||
});
|
||||
|
||||
// Detect native events
|
||||
// Build the relay payload from the *current* media state and send it to
|
||||
// background.js. Re-reads the video each call so deferred (coalesced) emits
|
||||
// carry an up-to-date position. Safe with no/invalid video — it no-ops.
|
||||
function sendContentEvent(action) {
|
||||
const video = findVideo();
|
||||
if (!video) return;
|
||||
|
||||
const current = video.currentTime;
|
||||
if (!Number.isFinite(current)) return;
|
||||
|
||||
const mediaTitle = (navigator.mediaSession && navigator.mediaSession.metadata) ? navigator.mediaSession.metadata.title : null;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'CONTENT_EVENT',
|
||||
action,
|
||||
payload: {
|
||||
currentTime: current,
|
||||
targetTime: current,
|
||||
mediaTitle: mediaTitle,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// Trigger proactive heartbeat to push stabilized state
|
||||
scheduleProactiveHeartbeat();
|
||||
}
|
||||
|
||||
// Trailing-edge flush of a coalesced play/pause burst: emit the final settled
|
||||
// state. No-ops only when no burst followed the leading edge. We do NOT skip
|
||||
// when the state matches the leading send — a remote command may have changed
|
||||
// the shared state mid-window, so re-sending is the safe (idempotent) choice.
|
||||
function flushPlayPause() {
|
||||
playPauseCoalesceTimer = null;
|
||||
const finalAction = pendingPlayPauseAction;
|
||||
pendingPlayPauseAction = null;
|
||||
// No burst follow-up (only the leading edge fired), or invalid state.
|
||||
if (finalAction !== EVENTS.PLAY && finalAction !== EVENTS.PAUSE) return;
|
||||
sendContentEvent(finalAction);
|
||||
}
|
||||
|
||||
function reportEvent(action) {
|
||||
if (seekDebounceTimer && (action === EVENTS.PLAY || action === EVENTS.PAUSE)) {
|
||||
clearTimeout(seekDebounceTimer);
|
||||
@@ -786,10 +1191,11 @@
|
||||
const current = video.currentTime;
|
||||
if (!Number.isFinite(current)) return;
|
||||
|
||||
const mediaTitle = (navigator.mediaSession && navigator.mediaSession.metadata) ? navigator.mediaSession.metadata.title : null;
|
||||
|
||||
const eventState = action === EVENTS.PLAY ? 'playing' : (action === EVENTS.PAUSE ? 'paused' : (action === EVENTS.SEEK ? 'seek' : null));
|
||||
|
||||
|
||||
// Echo-suppression for remotely-applied commands. MUST stay synchronous
|
||||
// on event arrival: the suppress timer is short-lived (300ms) and would
|
||||
// expire if deferred, leaking an echo back to peers.
|
||||
if (_suppressTimers[eventState]) {
|
||||
_clearSuppress(eventState);
|
||||
return;
|
||||
@@ -798,20 +1204,29 @@
|
||||
// Suppress only SEEK during visibility grace period (tab re-focus ghost jump).
|
||||
// Play/Pause pass through — user may want to immediately pause after tabbing back.
|
||||
if (Date.now() < visibilityGraceUntil && action === EVENTS.SEEK) return;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'CONTENT_EVENT',
|
||||
action,
|
||||
payload: {
|
||||
currentTime: current,
|
||||
targetTime: current,
|
||||
mediaTitle: mediaTitle,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// Trigger proactive heartbeat to push stabilized state
|
||||
scheduleProactiveHeartbeat();
|
||||
// Coalesce play/pause bursts (source swaps, ABR, ads, teardown). The
|
||||
// synchronous gates above have already run; only the network emit is
|
||||
// governed here. Leading edge sends the first event instantly; further
|
||||
// events within the window are collapsed to the final state by the
|
||||
// trailing flush. A pending burst is REPLACED, never dropped — the last
|
||||
// event in the window always wins.
|
||||
if (action === EVENTS.PLAY || action === EVENTS.PAUSE) {
|
||||
if (playPauseCoalesceTimer === null) {
|
||||
// Window closed → fresh action. Emit now (zero added latency).
|
||||
sendContentEvent(action);
|
||||
pendingPlayPauseAction = null;
|
||||
} else {
|
||||
// Window open → part of a burst. Defer to the trailing flush.
|
||||
pendingPlayPauseAction = action;
|
||||
clearTimeout(playPauseCoalesceTimer);
|
||||
}
|
||||
playPauseCoalesceTimer = setTimeout(flushPlayPause, PLAY_PAUSE_COALESCE_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
// SEEK (and any non play/pause action): emit immediately.
|
||||
sendContentEvent(action);
|
||||
}
|
||||
|
||||
// --- Tab Visibility Handling ---
|
||||
@@ -843,6 +1258,10 @@
|
||||
if (lobbyPollTimer) { clearInterval(lobbyPollTimer); lobbyPollTimer = null; }
|
||||
if (heartbeatTimeout) { clearTimeout(heartbeatTimeout); heartbeatTimeout = null; }
|
||||
if (proactiveHeartbeatTimeout) { clearTimeout(proactiveHeartbeatTimeout); proactiveHeartbeatTimeout = null; }
|
||||
// Drop any pending coalesced play/pause: we are tearing down and will
|
||||
// disconnect — peers learn we are gone via PEER_STATUS 'left'. This also
|
||||
// intentionally suppresses the teardown play/pause burst at the source.
|
||||
if (playPauseCoalesceTimer) { clearTimeout(playPauseCoalesceTimer); playPauseCoalesceTimer = null; pendingPlayPauseAction = null; }
|
||||
observer.disconnect();
|
||||
});
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
@@ -856,6 +1275,9 @@
|
||||
|
||||
const handlePlay = () => reportEvent(EVENTS.PLAY);
|
||||
const handlePause = () => reportEvent(EVENTS.PAUSE);
|
||||
// Host Control Mode: a 'waiting' (buffering) event opens a grace window so the
|
||||
// pause it may trigger isn't misread as a deliberate guest action (EC-1).
|
||||
const handleWaiting = () => { hcmBufferingUntil = Date.now() + HCM_BUFFERING_GRACE_MS; };
|
||||
|
||||
// Seek filtering: ignore HLS/DASH buffering micro-seeks.
|
||||
// Only relay if delta >= MIN_SEEK_DELTA AND not already debouncing.
|
||||
@@ -926,13 +1348,15 @@
|
||||
video.removeEventListener('pause', existing.pause);
|
||||
video.removeEventListener('seeked', existing.seeked);
|
||||
video.removeEventListener('loadeddata', existing.loadeddata);
|
||||
if (existing.waiting) video.removeEventListener('waiting', existing.waiting);
|
||||
}
|
||||
video._koalaHandlers = { play: handlePlay, pause: handlePause, seeked: handleSeeked, loadeddata: handleLoadedData };
|
||||
video._koalaHandlers = { play: handlePlay, pause: handlePause, seeked: handleSeeked, loadeddata: handleLoadedData, waiting: handleWaiting };
|
||||
|
||||
video.addEventListener('play', handlePlay);
|
||||
video.addEventListener('pause', handlePause);
|
||||
video.addEventListener('seeked', handleSeeked);
|
||||
video.addEventListener('loadeddata', handleLoadedData);
|
||||
video.addEventListener('waiting', handleWaiting);
|
||||
video.dataset.koalaAttached = 'true';
|
||||
lastVideoSrc = video.currentSrc || video.src || null;
|
||||
|
||||
@@ -1068,4 +1492,34 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Host Control Mode: fetch current room mode/role on injection (we may have
|
||||
// been injected after ROOM_DATA already arrived, missing the broadcast).
|
||||
chrome.runtime.sendMessage({ type: 'GET_CONTROL_MODE' }, (res) => {
|
||||
if (chrome.runtime.lastError || !res) return;
|
||||
hcmControlMode = res.controlMode || 'everyone';
|
||||
hcmAmController = !!res.amController;
|
||||
hcmHostPeerId = res.hostPeerId || null;
|
||||
// Re-adopt persisted desync after a page reload so we don't start synced
|
||||
// while background still relays us as "Solo" to the host (split-brain).
|
||||
// Only when we're actually a gated guest — never adopt a stale flag as a
|
||||
// controller or in 'everyone' mode (would self-label "Solo" / ignore commands).
|
||||
if (res.desynced && res.controlMode === 'host-only' && !res.amController && !hcmDesynced) {
|
||||
hcmDesynced = true;
|
||||
hcmShowBadge();
|
||||
}
|
||||
});
|
||||
|
||||
// Pull localized strings for the in-page dialog/badge (English fallback above).
|
||||
chrome.runtime.sendMessage({ type: 'GET_HCM_STRINGS' }, (res) => {
|
||||
if (chrome.runtime.lastError || !res) return;
|
||||
Object.keys(hcmStrings).forEach(k => { if (res[k]) hcmStrings[k] = res[k]; });
|
||||
// If the badge is already showing (early desync), refresh its text in place.
|
||||
// Re-creating the host element nukes the click target mid-poll and can drop a
|
||||
// click that landed between remove() and the re-create (L-4).
|
||||
if (hcmBadgeHost) {
|
||||
const span = hcmBadgeHost.shadowRoot && hcmBadgeHost.shadowRoot.querySelector('span');
|
||||
if (span) span.textContent = '● ' + hcmStrings.badge;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// extension/i18n.js
|
||||
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'];
|
||||
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'];
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
let activeDictionary = {};
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Teile diesen Link mit Freunden, damit sie beitreten können",
|
||||
"LABEL_PEERS_IN_ROOM": "Teilnehmer im Raum",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere Benutzer, die derzeit mit diesem Raum verbunden sind",
|
||||
"LABEL_HOST_CONTROL": "Host-Steuerung",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Festlegen, wer die Wiedergabe in diesem Raum steuern darf",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Nur ich darf die Wiedergabe steuern",
|
||||
"NOTICE_HOST_CONTROLS": "Der Host steuert die Wiedergabe für alle.",
|
||||
"NOTICE_COHOST_HINT": "Tipp: Gib einzelnen Teilnehmern unten in der Teilnehmerliste die Steuerung.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Gast",
|
||||
"BADGE_CONTROLLER": "Controller",
|
||||
"BTN_GIVE_CONTROL": "Steuerung geben",
|
||||
"BTN_REVOKE_CONTROL": "Entziehen",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Schaut alleine — ignoriert die Befehle des Hosts",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Der Host steuert diesen Raum",
|
||||
"HCM_DIALOG_BODY": "Nur der Host kann die Wiedergabe in diesem Raum steuern. Weiter gemeinsam schauen oder allein weiterschauen?",
|
||||
"HCM_DIALOG_STAY": "Synchron bleiben",
|
||||
"HCM_DIALOG_SOLO": "Allein weiterschauen",
|
||||
"HCM_BADGE_SOLO": "Du schaust allein",
|
||||
"HCM_BADGE_RESYNC": "Neu sync",
|
||||
"NO_PEERS_CONNECTED": "Keine Teilnehmer verbunden",
|
||||
"BTN_LEAVE_ROOM": "Raum verlassen",
|
||||
"LABEL_SELECT_VIDEO": "Video auswählen",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Generiere deine interne ID neu und verbinde dich erneut",
|
||||
"REGEN_ID_DESC": "Verwende dies, wenn du Fehler wegen doppelter Identität siehst.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Anderes Problem? Öffne ein GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identität neu generiert — Verbindung wird wiederhergestellt…",
|
||||
"LABEL_CONN_STATUS": "Verbindungsstatus",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Aktueller WebSocket-Verbindungsstatus",
|
||||
"CONN_STATUS_DISCONNECTED": "Getrennt",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Verbinde...",
|
||||
"STATUS_FAILED": "Fehlgeschlagen",
|
||||
"STATUS_DISCONNECTED": "Getrennt",
|
||||
"STATUS_IDLE": "Bereit zum Verbinden",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync ist bereit. Tritt einem Raum bei oder erstelle einen, um die Verbindung herzustellen und die Synchronisierung zu starten.",
|
||||
"BTN_STATE_JOINING": "🚀 Trete bei...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Verbinde erneut...",
|
||||
"BTN_STATE_PLAYING": "▶ Spiele ab...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zeitüberschreitung — nicht alle Teilnehmer haben die Episode geladen",
|
||||
"LOBBY_CANCEL_USER": "Vom Benutzer abgebrochen",
|
||||
"NOTIF_ERROR_TITLE": "KoalaSync-Fehler",
|
||||
"FOOTER_SUPPORT": "☕ Kauf mir einen Kaffee",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Bewerten",
|
||||
"FOOTER_SUPPORT_PROMPT": "Gefällt dir KoalaSync? Hinterlasse eine Bewertung!",
|
||||
"LABEL_AUDIO_PROCESSING": "Audio-Verarbeitung",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Demnächst",
|
||||
"BTN_RESTART_TOUR": "Einführung neu starten",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Startet die Einführung für neue Benutzer erneut",
|
||||
"HINT_SELECT_VIDEO": "Wähle hier dein Video aus!"
|
||||
"HINT_SELECT_VIDEO": "Wähle hier dein Video aus!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Voll senden",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Nur Episoden-ID senden",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Nicht senden",
|
||||
"LABEL_PRIVACY_SETTINGS": "Privatsphäre-Einstellungen",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Lege fest, welche Tab- und Medientitel an den Raum gesendet werden.",
|
||||
"LABEL_SEND_TAB_TITLE": "Tab-Titel senden",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Sendet den Titel des ausgewählten Browser-Tabs an den Raum.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Medientitel",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Lege fest, wie Medientitel an den Raum gesendet werden."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Share this link with friends so they can join",
|
||||
"LABEL_PEERS_IN_ROOM": "Peers in Room",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Other users currently connected to this room",
|
||||
"LABEL_HOST_CONTROL": "Host Control",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Restrict who can control playback in this room",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Only I can control playback",
|
||||
"NOTICE_HOST_CONTROLS": "The host controls playback for everyone.",
|
||||
"NOTICE_COHOST_HINT": "Tip: grant control to individual viewers in the participant list below.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Guest",
|
||||
"BADGE_CONTROLLER": "Controller",
|
||||
"BTN_GIVE_CONTROL": "Give control",
|
||||
"BTN_REVOKE_CONTROL": "Revoke",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Watching on their own — host's commands are ignored",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Host controls this room",
|
||||
"HCM_DIALOG_BODY": "Only the host can control playback in this room. Keep watching together, or watch on your own?",
|
||||
"HCM_DIALOG_STAY": "Stay in sync",
|
||||
"HCM_DIALOG_SOLO": "Watch on my own",
|
||||
"HCM_BADGE_SOLO": "Watching on your own",
|
||||
"HCM_BADGE_RESYNC": "Resync",
|
||||
"NO_PEERS_CONNECTED": "No peers connected",
|
||||
"BTN_LEAVE_ROOM": "Leave Room",
|
||||
"LABEL_SELECT_VIDEO": "Select Video",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Regenerate your internal ID and reconnect",
|
||||
"REGEN_ID_DESC": "Use this if you see \"Duplicate Identity\" errors.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Other issue? Open a GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identity regenerated — reconnecting…",
|
||||
"LABEL_CONN_STATUS": "Connection Status",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Current WebSocket connection state",
|
||||
"CONN_STATUS_DISCONNECTED": "Disconnected",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Connecting...",
|
||||
"STATUS_FAILED": "Failed",
|
||||
"STATUS_DISCONNECTED": "Disconnected",
|
||||
"STATUS_IDLE": "Ready to connect",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync is ready. Join or create a room to connect and start syncing.",
|
||||
"BTN_STATE_JOINING": "🚀 Joining...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Reconnecting...",
|
||||
"BTN_STATE_PLAYING": "▶ Playing...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — not all peers loaded the episode",
|
||||
"LOBBY_CANCEL_USER": "Cancelled by user",
|
||||
"NOTIF_ERROR_TITLE": "KoalaSync Error",
|
||||
"FOOTER_SUPPORT": "☕ Buy me a coffee",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Rate us",
|
||||
"FOOTER_SUPPORT_PROMPT": "Enjoying KoalaSync? Leave a review!",
|
||||
"LABEL_AUDIO_PROCESSING": "Audio Processing",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Coming soon",
|
||||
"BTN_RESTART_TOUR": "Restart Tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Restart the onboarding tutorial",
|
||||
"HINT_SELECT_VIDEO": "Select your video here!"
|
||||
"HINT_SELECT_VIDEO": "Select your video here!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Send full title",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Send episode ID",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Do not send",
|
||||
"LABEL_PRIVACY_SETTINGS": "Privacy Settings",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choose which tab and media titles are sent to the room.",
|
||||
"LABEL_SEND_TAB_TITLE": "Send tab title",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Send the selected browser tab title to the room.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Media title",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choose how media titles are sent to the room."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Comparte este enlace con amigos para que se unan",
|
||||
"LABEL_PEERS_IN_ROOM": "Participantes en la sala",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Otros usuarios conectados actualmente a esta sala",
|
||||
"LABEL_HOST_CONTROL": "Control del anfitrión",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Restringir quién puede controlar la reproducción en esta sala",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Solo yo puedo controlar la reproducción",
|
||||
"NOTICE_HOST_CONTROLS": "El anfitrión controla la reproducción para todos.",
|
||||
"NOTICE_COHOST_HINT": "Consejo: concede el control a usuarios concretos en la lista de participantes de abajo.",
|
||||
"BADGE_HOST": "Anfitrión",
|
||||
"BADGE_GUEST": "Invitado",
|
||||
"BADGE_CONTROLLER": "Controlador",
|
||||
"BTN_GIVE_CONTROL": "Dar control",
|
||||
"BTN_REVOKE_CONTROL": "Quitar",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Viendo por su cuenta — ignora los comandos del anfitrión",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · El anfitrión controla esta sala",
|
||||
"HCM_DIALOG_BODY": "Solo el anfitrión puede controlar la reproducción en esta sala. ¿Seguir viendo juntos o ver por tu cuenta?",
|
||||
"HCM_DIALOG_STAY": "Seguir sincronizado",
|
||||
"HCM_DIALOG_SOLO": "Ver por mi cuenta",
|
||||
"HCM_BADGE_SOLO": "Viendo por tu cuenta",
|
||||
"HCM_BADGE_RESYNC": "Resincronizar",
|
||||
"NO_PEERS_CONNECTED": "Sin participantes conectados",
|
||||
"BTN_LEAVE_ROOM": "Salir de la sala",
|
||||
"LABEL_SELECT_VIDEO": "Seleccionar video",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Regenerar tu ID interno y volver a conectarte",
|
||||
"REGEN_ID_DESC": "Usa esto si ves errores de 'Identidad duplicada'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "¿Tienes otro problema? Abre un reporte en GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identidad regenerada — reconectando…",
|
||||
"LABEL_CONN_STATUS": "Estado de la conexión",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Estado actual de la conexión WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Desconectado",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Conectando...",
|
||||
"STATUS_FAILED": "Error",
|
||||
"STATUS_DISCONNECTED": "Desconectado",
|
||||
"STATUS_IDLE": "Listo para conectar",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync está listo. Únete o crea una sala para conectarte y empezar a sincronizar.",
|
||||
"BTN_STATE_JOINING": "🚀 Uniéndose...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
|
||||
"BTN_STATE_PLAYING": "▶ Reproduciendo...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tiempo de espera agotado: no todos los participantes cargaron el episodio",
|
||||
"LOBBY_CANCEL_USER": "Cancelado por el usuario",
|
||||
"NOTIF_ERROR_TITLE": "Error de KoalaSync",
|
||||
"FOOTER_SUPPORT": "☕ Invítame a un café",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Valorar",
|
||||
"FOOTER_SUPPORT_PROMPT": "¿Te gusta KoalaSync? ¡Deja una reseña!",
|
||||
"LABEL_AUDIO_PROCESSING": "Procesamiento de audio",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Próximamente",
|
||||
"BTN_RESTART_TOUR": "Reiniciar tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar el tutorial de inicio",
|
||||
"HINT_SELECT_VIDEO": "¡Selecciona tu video aquí!"
|
||||
"HINT_SELECT_VIDEO": "¡Selecciona tu video aquí!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID de episodio",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "No enviar",
|
||||
"LABEL_PRIVACY_SETTINGS": "Ajustes de privacidad",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Elige qué títulos de pestaña y multimedia se envían a la sala.",
|
||||
"LABEL_SEND_TAB_TITLE": "Enviar título de pestaña",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envía a la sala el título de la pestaña del navegador seleccionada.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Título multimedia",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Elige cómo se envían los títulos multimedia a la sala."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Partagez ce lien avec vos amis pour qu'ils vous rejoignent",
|
||||
"LABEL_PEERS_IN_ROOM": "Membres dans le salon",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Autres utilisateurs connectés à ce salon",
|
||||
"LABEL_HOST_CONTROL": "Contrôle de l'hôte",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Définir qui peut contrôler la lecture dans ce salon",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Moi seul peux contrôler la lecture",
|
||||
"NOTICE_HOST_CONTROLS": "L'hôte contrôle la lecture pour tout le monde.",
|
||||
"NOTICE_COHOST_HINT": "Astuce : donnez le contrôle à des participants précis dans la liste ci-dessous.",
|
||||
"BADGE_HOST": "Hôte",
|
||||
"BADGE_GUEST": "Invité",
|
||||
"BADGE_CONTROLLER": "Contrôleur",
|
||||
"BTN_GIVE_CONTROL": "Donner le contrôle",
|
||||
"BTN_REVOKE_CONTROL": "Retirer",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Regarde de son côté — ignore les commandes de l'hôte",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · L'hôte contrôle ce salon",
|
||||
"HCM_DIALOG_BODY": "Seul l'hôte peut contrôler la lecture dans ce salon. Continuer à regarder ensemble, ou regarder de votre côté ?",
|
||||
"HCM_DIALOG_STAY": "Rester synchronisé",
|
||||
"HCM_DIALOG_SOLO": "Regarder de mon côté",
|
||||
"HCM_BADGE_SOLO": "Vous regardez seul",
|
||||
"HCM_BADGE_RESYNC": "Resynchroniser",
|
||||
"NO_PEERS_CONNECTED": "Aucun membre connecté",
|
||||
"BTN_LEAVE_ROOM": "Quitter le salon",
|
||||
"LABEL_SELECT_VIDEO": "Choisir une vidéo",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Régénérer votre identifiant interne et vous reconnecter",
|
||||
"REGEN_ID_DESC": "Utilisez cette option si vous rencontrez des erreurs de 'Double identité'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Autre problème? Ouvrez un Issue GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identité régénérée — reconnexion…",
|
||||
"LABEL_CONN_STATUS": "Statut de la connexion",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Statut actuel de la connexion WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Déconnecté",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Connexion...",
|
||||
"STATUS_FAILED": "Échec",
|
||||
"STATUS_DISCONNECTED": "Déconnecté",
|
||||
"STATUS_IDLE": "Prêt à se connecter",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync est prêt. Rejoignez ou créez un salon pour vous connecter et commencer la synchronisation.",
|
||||
"BTN_STATE_JOINING": "🚀 Connexion...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Reconnexion...",
|
||||
"BTN_STATE_PLAYING": "▶ Lecture...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Délai dépassé — tous les membres n'ont pas chargé l'épisode",
|
||||
"LOBBY_CANCEL_USER": "Annulé par l'utilisateur",
|
||||
"NOTIF_ERROR_TITLE": "Erreur KoalaSync",
|
||||
"FOOTER_SUPPORT": "☕ Offre-moi un café",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Évaluer",
|
||||
"FOOTER_SUPPORT_PROMPT": "Tu aimes KoalaSync? Laisse un avis!",
|
||||
"LABEL_AUDIO_PROCESSING": "Traitement audio",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Bientôt disponible",
|
||||
"BTN_RESTART_TOUR": "Redémarrer le tutoriel",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Recommencer le tutoriel d'intégration",
|
||||
"HINT_SELECT_VIDEO": "Sélectionnez votre vidéo ici !"
|
||||
"HINT_SELECT_VIDEO": "Sélectionnez votre vidéo ici !",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Envoyer le titre complet",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Envoyer l’identifiant d’épisode",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Ne pas envoyer",
|
||||
"LABEL_PRIVACY_SETTINGS": "Paramètres de confidentialité",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choisissez quels titres d’onglet et de média sont envoyés au salon.",
|
||||
"LABEL_SEND_TAB_TITLE": "Envoyer le titre de l’onglet",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envoie au salon le titre de l’onglet de navigateur sélectionné.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Titre du média",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choisissez comment les titres des médias sont envoyés au salon."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Condividi questo link per invitare i tuoi amici",
|
||||
"LABEL_PEERS_IN_ROOM": "Partecipanti",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Utenti connessi a questa stanza",
|
||||
"LABEL_HOST_CONTROL": "Controllo host",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Limita chi può controllare la riproduzione in questa stanza",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Solo io posso controllare la riproduzione",
|
||||
"NOTICE_HOST_CONTROLS": "L'host controlla la riproduzione per tutti.",
|
||||
"NOTICE_COHOST_HINT": "Suggerimento: concedi il controllo ai singoli partecipanti nell'elenco qui sotto.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Ospite",
|
||||
"BADGE_CONTROLLER": "Controller",
|
||||
"BTN_GIVE_CONTROL": "Dai il controllo",
|
||||
"BTN_REVOKE_CONTROL": "Revoca",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Guarda per conto proprio — ignora i comandi dell'host",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · L'host controlla questa stanza",
|
||||
"HCM_DIALOG_BODY": "Solo l'host può controllare la riproduzione in questa stanza. Continuare a guardare insieme o guardare per conto tuo?",
|
||||
"HCM_DIALOG_STAY": "Resta sincronizzato",
|
||||
"HCM_DIALOG_SOLO": "Guarda per conto mio",
|
||||
"HCM_BADGE_SOLO": "Stai guardando da solo",
|
||||
"HCM_BADGE_RESYNC": "Risincronizza",
|
||||
"NO_PEERS_CONNECTED": "Nessun partecipante connesso",
|
||||
"BTN_LEAVE_ROOM": "Lascia Stanza",
|
||||
"LABEL_SELECT_VIDEO": "Seleziona Video",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Genera un nuovo ID interno e riconnettiti",
|
||||
"REGEN_ID_DESC": "Usa questa opzione se riscontri errori di 'Identità Duplicata'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Hai altri problemi? Apri una segnalazione su GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identità rigenerata — riconnessione…",
|
||||
"LABEL_CONN_STATUS": "Stato Connessione",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Stato attuale della connessione",
|
||||
"CONN_STATUS_DISCONNECTED": "Disconnesso",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Connessione in corso...",
|
||||
"STATUS_FAILED": "Errore",
|
||||
"STATUS_DISCONNECTED": "Disconnesso",
|
||||
"STATUS_IDLE": "Pronto a connettersi",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync è pronto. Entra o crea una stanza per connetterti e avviare la sincronizzazione.",
|
||||
"BTN_STATE_JOINING": "🚀 Entrando...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Riconnessione...",
|
||||
"BTN_STATE_PLAYING": "▶ In riproduzione...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo scaduto: caricamento incompleto per alcuni partecipanti",
|
||||
"LOBBY_CANCEL_USER": "Annullato dall'utente",
|
||||
"NOTIF_ERROR_TITLE": "Errore KoalaSync",
|
||||
"FOOTER_SUPPORT": "☕ Offrimi un caffè",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Valutaci",
|
||||
"FOOTER_SUPPORT_PROMPT": "Ti piace KoalaSync? Lascia una recensione!",
|
||||
"LABEL_AUDIO_PROCESSING": "Elaborazione Audio",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Prossimamente",
|
||||
"BTN_RESTART_TOUR": "Riavvia Tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Riavvia il tutorial introduttivo",
|
||||
"HINT_SELECT_VIDEO": "Scegli il tuo video qui!"
|
||||
"HINT_SELECT_VIDEO": "Scegli il tuo video qui!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Invia titolo completo",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Invia ID episodio",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Non inviare",
|
||||
"LABEL_PRIVACY_SETTINGS": "Impostazioni privacy",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Scegli quali titoli di scheda e contenuto multimediale vengono inviati alla stanza.",
|
||||
"LABEL_SEND_TAB_TITLE": "Invia titolo scheda",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Invia alla stanza il titolo della scheda del browser selezionata.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Titolo multimediale",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Scegli come inviare alla stanza i titoli multimediali."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "友達とこのリンクを共有して参加してもらいましょう",
|
||||
"LABEL_PEERS_IN_ROOM": "ルーム内のメンバー",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "現在このルームに接続している他のユーザー",
|
||||
"LABEL_HOST_CONTROL": "ホスト操作",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "このルームで再生を操作できる人を制限します",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "再生を操作できるのは自分だけ",
|
||||
"NOTICE_HOST_CONTROLS": "ホストが全員の再生を操作します。",
|
||||
"NOTICE_COHOST_HINT": "ヒント: 下の参加者リストで個別の参加者に操作権限を付与できます。",
|
||||
"BADGE_HOST": "ホスト",
|
||||
"BADGE_GUEST": "ゲスト",
|
||||
"BADGE_CONTROLLER": "操作権あり",
|
||||
"BTN_GIVE_CONTROL": "操作権を付与",
|
||||
"BTN_REVOKE_CONTROL": "取り消す",
|
||||
"BADGE_DESYNCED": "ソロ",
|
||||
"TOOLTIP_PEER_DESYNCED": "単独で視聴中 — ホストのコマンドを無視しています",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · ホストがこのルームを操作中",
|
||||
"HCM_DIALOG_BODY": "このルームではホストのみが再生を操作できます。一緒に視聴を続けますか、それとも自分だけで視聴しますか?",
|
||||
"HCM_DIALOG_STAY": "同期を維持",
|
||||
"HCM_DIALOG_SOLO": "自分だけで視聴",
|
||||
"HCM_BADGE_SOLO": "自分だけで視聴中",
|
||||
"HCM_BADGE_RESYNC": "再同期",
|
||||
"NO_PEERS_CONNECTED": "接続しているメンバーはいません",
|
||||
"BTN_LEAVE_ROOM": "ルームを退室",
|
||||
"LABEL_SELECT_VIDEO": "ビデオを選択",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "内部IDを再生成して再接続します",
|
||||
"REGEN_ID_DESC": "「Duplicate Identity」(ID重複)エラーが表示される場合に使用します。",
|
||||
"REGEN_ID_OTHER_ISSUE": "他の問題?GitHub Issueを開く",
|
||||
"TOAST_ID_REGENERATED": "IDを再生成しました — 再接続中…",
|
||||
"LABEL_CONN_STATUS": "接続状態",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "現在のWebSocket接続状態",
|
||||
"CONN_STATUS_DISCONNECTED": "切断されました",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "接続中...",
|
||||
"STATUS_FAILED": "失敗",
|
||||
"STATUS_DISCONNECTED": "切断されました",
|
||||
"STATUS_IDLE": "接続準備完了",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSyncの準備ができました。ルームに参加するか作成して接続し、同期を開始してください。",
|
||||
"BTN_STATE_JOINING": "🚀 参加中...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 再接続中...",
|
||||
"BTN_STATE_PLAYING": "▶ 再生中...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "タイムアウト — 一部のメンバーがエピソードを読み込めませんでした",
|
||||
"LOBBY_CANCEL_USER": "ユーザーによってキャンセルされました",
|
||||
"NOTIF_ERROR_TITLE": "KoalaSyncエラー",
|
||||
"FOOTER_SUPPORT": "☕ コーヒーをおごってね",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ 評価する",
|
||||
"FOOTER_SUPPORT_PROMPT": "KoalaSyncはいかが?レビューを書いてください!",
|
||||
"LABEL_AUDIO_PROCESSING": "オーディオ処理",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "近日公開",
|
||||
"BTN_RESTART_TOUR": "チュートリアルを再起動",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "オンボーディングチュートリアルを再起動する",
|
||||
"HINT_SELECT_VIDEO": "ここで動画を選択してください!"
|
||||
"HINT_SELECT_VIDEO": "ここで動画を選択してください!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "完全なタイトルを送信",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "エピソードIDを送信",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "送信しない",
|
||||
"LABEL_PRIVACY_SETTINGS": "プライバシー設定",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "ルームに送信するタブタイトルとメディアタイトルを選択します。",
|
||||
"LABEL_SEND_TAB_TITLE": "タブタイトルを送信",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "選択中のブラウザータブのタイトルをルームに送信します。",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "メディアタイトル",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "メディアタイトルをルームに送信する方法を選択します。"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "친구들이 참여할 수 있도록 이 링크를 공유하세요",
|
||||
"LABEL_PEERS_IN_ROOM": "방의 참여자",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "현재 이 방에 연결되어 있는 다른 사용자들",
|
||||
"LABEL_HOST_CONTROL": "호스트 제어",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "이 방에서 재생을 제어할 수 있는 사람 제한",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "나만 재생을 제어할 수 있음",
|
||||
"NOTICE_HOST_CONTROLS": "호스트가 모두의 재생을 제어합니다.",
|
||||
"NOTICE_COHOST_HINT": "팁: 아래 참가자 목록에서 개별 참가자에게 제어 권한을 부여하세요.",
|
||||
"BADGE_HOST": "호스트",
|
||||
"BADGE_GUEST": "게스트",
|
||||
"BADGE_CONTROLLER": "제어자",
|
||||
"BTN_GIVE_CONTROL": "제어 권한 부여",
|
||||
"BTN_REVOKE_CONTROL": "취소",
|
||||
"BADGE_DESYNCED": "단독",
|
||||
"TOOLTIP_PEER_DESYNCED": "혼자 시청 중 — 호스트 명령을 무시합니다",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · 호스트가 이 방을 제어합니다",
|
||||
"HCM_DIALOG_BODY": "이 방에서는 호스트만 재생을 제어할 수 있습니다. 계속 함께 보시겠어요, 아니면 혼자 보시겠어요?",
|
||||
"HCM_DIALOG_STAY": "동기화 유지",
|
||||
"HCM_DIALOG_SOLO": "혼자 보기",
|
||||
"HCM_BADGE_SOLO": "혼자 시청 중",
|
||||
"HCM_BADGE_RESYNC": "다시 동기화",
|
||||
"NO_PEERS_CONNECTED": "연결된 참여자가 없습니다",
|
||||
"BTN_LEAVE_ROOM": "방 나가기",
|
||||
"LABEL_SELECT_VIDEO": "비디오 선택",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "내부 ID를 재생성하고 다시 연결합니다",
|
||||
"REGEN_ID_DESC": "\"중복된 ID\" 오류가 발생할 경우에 사용하세요.",
|
||||
"REGEN_ID_OTHER_ISSUE": "다른 문제? GitHub Issue 열기",
|
||||
"TOAST_ID_REGENERATED": "ID가 재생성되었습니다 — 다시 연결 중…",
|
||||
"LABEL_CONN_STATUS": "연결 상태",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "현재 WebSocket 연결 상태",
|
||||
"CONN_STATUS_DISCONNECTED": "연결 끊김",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "연결 중...",
|
||||
"STATUS_FAILED": "실패",
|
||||
"STATUS_DISCONNECTED": "연결 끊김",
|
||||
"STATUS_IDLE": "연결 준비 완료",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync가 준비되었습니다. 방에 참여하거나 생성하여 연결하고 동기화를 시작하세요.",
|
||||
"BTN_STATE_JOINING": "🚀 참여 중...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 재연결 중...",
|
||||
"BTN_STATE_PLAYING": "▶ 재생 중...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "시간 초과 — 모든 피어가 에피소드를 로드하지 못했습니다",
|
||||
"LOBBY_CANCEL_USER": "사용자에 의해 취소됨",
|
||||
"NOTIF_ERROR_TITLE": "KoalaSync 오류",
|
||||
"FOOTER_SUPPORT": "☕ 커피 한 잔 사주세요",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ 평가하기",
|
||||
"FOOTER_SUPPORT_PROMPT": "KoalaSync가 마음에 드세요? 리뷰를 남겨주세요!",
|
||||
"LABEL_AUDIO_PROCESSING": "오디오 처리",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "출시 예정",
|
||||
"BTN_RESTART_TOUR": "튜토리얼 다시 시작",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "온보딩 튜토리얼을 다시 시작합니다",
|
||||
"HINT_SELECT_VIDEO": "여기에서 비디오를 선택하세요!"
|
||||
"HINT_SELECT_VIDEO": "여기에서 비디오를 선택하세요!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "전체 제목 보내기",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "에피소드 ID 보내기",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "보내지 않음",
|
||||
"LABEL_PRIVACY_SETTINGS": "개인정보 설정",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "방에 보낼 탭 제목과 미디어 제목을 선택합니다.",
|
||||
"LABEL_SEND_TAB_TITLE": "탭 제목 보내기",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "선택한 브라우저 탭 제목을 방에 보냅니다.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "미디어 제목",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "미디어 제목을 방에 보내는 방식을 선택합니다."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Deel deze link met vrienden zodat ze kunnen deelnemen",
|
||||
"LABEL_PEERS_IN_ROOM": "Deelnemers in kamer",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere gebruikers die momenteel zijn verbonden met deze kamer",
|
||||
"LABEL_HOST_CONTROL": "Hostbeheer",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Bepaal wie de weergave in deze kamer mag bedienen",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Alleen ik mag de weergave bedienen",
|
||||
"NOTICE_HOST_CONTROLS": "De host bedient de weergave voor iedereen.",
|
||||
"NOTICE_COHOST_HINT": "Tip: geef afzonderlijke deelnemers de besturing in de lijst hieronder.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Gast",
|
||||
"BADGE_CONTROLLER": "Bediener",
|
||||
"BTN_GIVE_CONTROL": "Bediening geven",
|
||||
"BTN_REVOKE_CONTROL": "Intrekken",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Zelfstandig aan het kijken — negeert de commando's van de host",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · De host bedient deze kamer",
|
||||
"HCM_DIALOG_BODY": "Alleen de host kan de weergave in deze kamer bedienen. Samen blijven kijken of zelf verder kijken?",
|
||||
"HCM_DIALOG_STAY": "Gesynchroniseerd blijven",
|
||||
"HCM_DIALOG_SOLO": "Zelf verder kijken",
|
||||
"HCM_BADGE_SOLO": "Je kijkt zelfstandig",
|
||||
"HCM_BADGE_RESYNC": "Opnieuw sync",
|
||||
"NO_PEERS_CONNECTED": "Geen deelnemers verbonden",
|
||||
"BTN_LEAVE_ROOM": "Kamer verlaten",
|
||||
"LABEL_SELECT_VIDEO": "Selecteer video",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Genereer uw interne ID opnieuw en maak opnieuw verbinding",
|
||||
"REGEN_ID_DESC": "Gebruik dit als u fouten ziet over 'Duplicate Identity'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Ander probleem? Open een GitHub Issue",
|
||||
"TOAST_ID_REGENERATED": "Identiteit opnieuw gegenereerd — opnieuw verbinden…",
|
||||
"LABEL_CONN_STATUS": "Verbindingsstatus",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Huidige WebSocket-verbindingsstatus",
|
||||
"CONN_STATUS_DISCONNECTED": "Verbinding verbroken",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Verbinden...",
|
||||
"STATUS_FAILED": "Mislukt",
|
||||
"STATUS_DISCONNECTED": "Verbinding verbroken",
|
||||
"STATUS_IDLE": "Gereed om te verbinden",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync is gereed. Neem deel aan een kamer of maak er een aan om te verbinden en te synchroniseren.",
|
||||
"BTN_STATE_JOINING": "🚀 Deelnemen...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Opnieuw verbinden...",
|
||||
"BTN_STATE_PLAYING": "▶ Afspelen...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Time-out — niet alle deelnemers hebben de aflevering geladen",
|
||||
"LOBBY_CANCEL_USER": "Geannuleerd door gebruiker",
|
||||
"NOTIF_ERROR_TITLE": "KoalaSync-fout",
|
||||
"FOOTER_SUPPORT": "☕ Trakteer op een koffie",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Beoordelen",
|
||||
"FOOTER_SUPPORT_PROMPT": "Vind je KoalaSync leuk? Laat een review achter!",
|
||||
"LABEL_AUDIO_PROCESSING": "Audioverwerking",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Binnenkort beschikbaar",
|
||||
"BTN_RESTART_TOUR": "Tutorial opnieuw starten",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Start de introductie-tutorial opnieuw",
|
||||
"HINT_SELECT_VIDEO": "Selecteer hier je video!"
|
||||
"HINT_SELECT_VIDEO": "Selecteer hier je video!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Volledige titel verzenden",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Afleverings-ID verzenden",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Niet verzenden",
|
||||
"LABEL_PRIVACY_SETTINGS": "Privacy-instellingen",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Kies welke tab- en mediatitels naar de kamer worden gestuurd.",
|
||||
"LABEL_SEND_TAB_TITLE": "Tabtitel verzenden",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Stuurt de titel van het geselecteerde browsertabblad naar de kamer.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Mediatitel",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Kies hoe mediatitels naar de kamer worden gestuurd."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Udostępnij ten link znajomym, aby mogli dołączyć",
|
||||
"LABEL_PEERS_IN_ROOM": "Uczestnicy w pokoju",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Inni użytkownicy aktualnie połączeni z tym pokojem",
|
||||
"LABEL_HOST_CONTROL": "Kontrola hosta",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Określ, kto może sterować odtwarzaniem w tym pokoju",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Tylko ja mogę sterować odtwarzaniem",
|
||||
"NOTICE_HOST_CONTROLS": "Host steruje odtwarzaniem dla wszystkich.",
|
||||
"NOTICE_COHOST_HINT": "Wskazówka: przyznaj sterowanie wybranym uczestnikom na liście poniżej.",
|
||||
"BADGE_HOST": "Host",
|
||||
"BADGE_GUEST": "Gość",
|
||||
"BADGE_CONTROLLER": "Sterujący",
|
||||
"BTN_GIVE_CONTROL": "Daj sterowanie",
|
||||
"BTN_REVOKE_CONTROL": "Odbierz",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Ogląda samodzielnie — ignoruje polecenia hosta",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Host steruje tym pokojem",
|
||||
"HCM_DIALOG_BODY": "Tylko host może sterować odtwarzaniem w tym pokoju. Oglądać dalej razem czy oglądać samodzielnie?",
|
||||
"HCM_DIALOG_STAY": "Pozostań zsynchronizowany",
|
||||
"HCM_DIALOG_SOLO": "Oglądaj samodzielnie",
|
||||
"HCM_BADGE_SOLO": "Oglądasz samodzielnie",
|
||||
"HCM_BADGE_RESYNC": "Zsynchronizuj",
|
||||
"NO_PEERS_CONNECTED": "Brak połączonych uczestników",
|
||||
"BTN_LEAVE_ROOM": "Opuść pokój",
|
||||
"LABEL_SELECT_VIDEO": "Wybierz wideo",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Wygeneruj ponownie swój identyfikator i połącz się ponownie",
|
||||
"REGEN_ID_DESC": "Użyj tego, jeśli widzisz błędy o zduplikowanej tożsamości.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Inny problem? Otwórz Issue na GitHub",
|
||||
"TOAST_ID_REGENERATED": "Tożsamość wygenerowana ponownie — ponowne łączenie…",
|
||||
"LABEL_CONN_STATUS": "Status połączenia",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Bieżący stan połączenia WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Rozłączono",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Łączenie...",
|
||||
"STATUS_FAILED": "Nieudane",
|
||||
"STATUS_DISCONNECTED": "Rozłączono",
|
||||
"STATUS_IDLE": "Gotowy do połączenia",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync jest gotowy. Dołącz do pokoju lub utwórz go, aby się połączyć i rozpocząć synchronizację.",
|
||||
"BTN_STATE_JOINING": "🚀 Dołączanie...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Ponowne łączenie...",
|
||||
"BTN_STATE_PLAYING": "▶ Odtwarzanie...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Limit czasu — nie wszyscy uczestnicy załadowali odcinek",
|
||||
"LOBBY_CANCEL_USER": "Anulowane przez użytkownika",
|
||||
"NOTIF_ERROR_TITLE": "Błąd KoalaSync",
|
||||
"FOOTER_SUPPORT": "☕ Postaw kawę",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Oceń",
|
||||
"FOOTER_SUPPORT_PROMPT": "Podoba Ci się KoalaSync? Zostaw recenzję!",
|
||||
"LABEL_AUDIO_PROCESSING": "Przetwarzanie dźwięku",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Wkrótce",
|
||||
"BTN_RESTART_TOUR": "Uruchom samouczek ponownie",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Uruchom ponownie samouczek powitalny",
|
||||
"HINT_SELECT_VIDEO": "Wybierz swoje wideo tutaj!"
|
||||
"HINT_SELECT_VIDEO": "Wybierz swoje wideo tutaj!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Wysyłaj pełny tytuł",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Wysyłaj ID odcinka",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Nie wysyłaj",
|
||||
"LABEL_PRIVACY_SETTINGS": "Ustawienia prywatności",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Wybierz, które tytuły kart i multimediów są wysyłane do pokoju.",
|
||||
"LABEL_SEND_TAB_TITLE": "Wysyłaj tytuł karty",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Wysyła do pokoju tytuł wybranej karty przeglądarki.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Tytuł multimediów",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Wybierz, jak tytuły multimediów są wysyłane do pokoju."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Compartilhe este link com seus amigos para que eles entrem",
|
||||
"LABEL_PEERS_IN_ROOM": "Participantes",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Outros usuários conectados a esta sala",
|
||||
"LABEL_HOST_CONTROL": "Controle do anfitrião",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Restringir quem pode controlar a reprodução nesta sala",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Somente eu posso controlar a reprodução",
|
||||
"NOTICE_HOST_CONTROLS": "O anfitrião controla a reprodução para todos.",
|
||||
"NOTICE_COHOST_HINT": "Dica: conceda o controle a participantes específicos na lista abaixo.",
|
||||
"BADGE_HOST": "Anfitrião",
|
||||
"BADGE_GUEST": "Convidado",
|
||||
"BADGE_CONTROLLER": "Controlador",
|
||||
"BTN_GIVE_CONTROL": "Dar controle",
|
||||
"BTN_REVOKE_CONTROL": "Revogar",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Assistindo por conta própria — ignora os comandos do anfitrião",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · O anfitrião controla esta sala",
|
||||
"HCM_DIALOG_BODY": "Somente o anfitrião pode controlar a reprodução nesta sala. Continuar assistindo juntos ou assistir por conta própria?",
|
||||
"HCM_DIALOG_STAY": "Manter sincronizado",
|
||||
"HCM_DIALOG_SOLO": "Assistir por conta própria",
|
||||
"HCM_BADGE_SOLO": "Assistindo por conta própria",
|
||||
"HCM_BADGE_RESYNC": "Ressincronizar",
|
||||
"NO_PEERS_CONNECTED": "Sem participantes conectados",
|
||||
"BTN_LEAVE_ROOM": "Sair da sala",
|
||||
"LABEL_SELECT_VIDEO": "Selecionar vídeo",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Gerar um novo ID interno e conectar novamente",
|
||||
"REGEN_ID_DESC": "Use esta opção se aparecer o erro de 'Identidade duplicada'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um relatório no GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identidade regenerada — reconectando…",
|
||||
"LABEL_CONN_STATUS": "Status da conexão",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Status atual da conexão WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Desconectado",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Conectando...",
|
||||
"STATUS_FAILED": "Falhou",
|
||||
"STATUS_DISCONNECTED": "Desconectado",
|
||||
"STATUS_IDLE": "Pronto para conectar",
|
||||
"STATUS_IDLE_TOOLTIP": "O KoalaSync está pronto. Entre ou crie uma sala para conectar e começar a sincronizar.",
|
||||
"BTN_STATE_JOINING": "🚀 Entrando...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
|
||||
"BTN_STATE_PLAYING": "▶ Reproduzindo...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo limite — nem todos carregaram o episódio",
|
||||
"LOBBY_CANCEL_USER": "Cancelado pelo usuário",
|
||||
"NOTIF_ERROR_TITLE": "Erro no KoalaSync",
|
||||
"FOOTER_SUPPORT": "☕ Me pague um café",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Avaliar",
|
||||
"FOOTER_SUPPORT_PROMPT": "Gostou do KoalaSync? Deixe uma avaliação!",
|
||||
"LABEL_AUDIO_PROCESSING": "Processamento de áudio",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Em breve",
|
||||
"BTN_RESTART_TOUR": "Reiniciar tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar o tutorial de introdução",
|
||||
"HINT_SELECT_VIDEO": "Selecione seu vídeo aqui!"
|
||||
"HINT_SELECT_VIDEO": "Selecione seu vídeo aqui!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID do episódio",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Não enviar",
|
||||
"LABEL_PRIVACY_SETTINGS": "Configurações de privacidade",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Escolha quais títulos de aba e mídia são enviados para a sala.",
|
||||
"LABEL_SEND_TAB_TITLE": "Enviar título da aba",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envia para a sala o título da aba selecionada do navegador.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Título da mídia",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Escolha como os títulos de mídia são enviados para a sala."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Partilhe este link com os amigos para que possam entrar",
|
||||
"LABEL_PEERS_IN_ROOM": "Participantes na Sala",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Utilizadores atualmente ligados a esta sala",
|
||||
"LABEL_HOST_CONTROL": "Controlo do anfitrião",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Restringir quem pode controlar a reprodução nesta sala",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Só eu posso controlar a reprodução",
|
||||
"NOTICE_HOST_CONTROLS": "O anfitrião controla a reprodução para todos.",
|
||||
"NOTICE_COHOST_HINT": "Dica: conceda o controlo a participantes específicos na lista abaixo.",
|
||||
"BADGE_HOST": "Anfitrião",
|
||||
"BADGE_GUEST": "Convidado",
|
||||
"BADGE_CONTROLLER": "Controlador",
|
||||
"BTN_GIVE_CONTROL": "Dar controlo",
|
||||
"BTN_REVOKE_CONTROL": "Revogar",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "A ver por conta própria — ignora os comandos do anfitrião",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · O anfitrião controla esta sala",
|
||||
"HCM_DIALOG_BODY": "Só o anfitrião pode controlar a reprodução nesta sala. Continuar a ver em conjunto ou ver por conta própria?",
|
||||
"HCM_DIALOG_STAY": "Manter sincronizado",
|
||||
"HCM_DIALOG_SOLO": "Ver por minha conta",
|
||||
"HCM_BADGE_SOLO": "A ver por tua conta",
|
||||
"HCM_BADGE_RESYNC": "Ressincronizar",
|
||||
"NO_PEERS_CONNECTED": "Nenhum participante ligado",
|
||||
"BTN_LEAVE_ROOM": "Sair da Sala",
|
||||
"LABEL_SELECT_VIDEO": "Selecionar Vídeo",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Regerar o seu ID interno e voltar a ligar",
|
||||
"REGEN_ID_DESC": "Use isto se encontrar erros de 'Identidade Duplicada'.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um relatório no GitHub",
|
||||
"TOAST_ID_REGENERATED": "Identidade regenerada — a voltar a ligar…",
|
||||
"LABEL_CONN_STATUS": "Estado da Ligação",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Estado atual da ligação",
|
||||
"CONN_STATUS_DISCONNECTED": "Desligado",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "A ligar...",
|
||||
"STATUS_FAILED": "Falhou",
|
||||
"STATUS_DISCONNECTED": "Desligado",
|
||||
"STATUS_IDLE": "Pronto para ligar",
|
||||
"STATUS_IDLE_TOOLTIP": "O KoalaSync está pronto. Entre ou crie uma sala para ligar e iniciar a sincronização.",
|
||||
"BTN_STATE_JOINING": "🚀 A entrar...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 A voltar a ligar...",
|
||||
"BTN_STATE_PLAYING": "▶ A reproduzir...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo limite — alguns participantes não carregaram o episódio",
|
||||
"LOBBY_CANCEL_USER": "Cancelado pelo utilizador",
|
||||
"NOTIF_ERROR_TITLE": "Erro no KoalaSync",
|
||||
"FOOTER_SUPPORT": "☕ Oferece-me um café",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Avaliar",
|
||||
"FOOTER_SUPPORT_PROMPT": "Gostas do KoalaSync? Deixa uma avaliação!",
|
||||
"LABEL_AUDIO_PROCESSING": "Processamento de Áudio",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Em breve",
|
||||
"BTN_RESTART_TOUR": "Reiniciar Tutorial",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar o tutorial introdutório",
|
||||
"HINT_SELECT_VIDEO": "Selecione o seu vídeo aqui!"
|
||||
"HINT_SELECT_VIDEO": "Selecione o seu vídeo aqui!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID do episódio",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Não enviar",
|
||||
"LABEL_PRIVACY_SETTINGS": "Definições de privacidade",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Escolhe que títulos do separador e do conteúdo multimédia são enviados para a sala.",
|
||||
"LABEL_SEND_TAB_TITLE": "Enviar título do separador",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envia para a sala o título do separador do navegador selecionado.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Título multimédia",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Escolhe como os títulos multimédia são enviados para a sala."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Поделитесь этой ссылкой с друзьями, чтобы они вошли",
|
||||
"LABEL_PEERS_IN_ROOM": "Участники в комнате",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Другие пользователи, подключенные к этой комнате",
|
||||
"LABEL_HOST_CONTROL": "Управление ведущим",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Ограничить, кто может управлять воспроизведением в этой комнате",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Только я могу управлять воспроизведением",
|
||||
"NOTICE_HOST_CONTROLS": "Ведущий управляет воспроизведением для всех.",
|
||||
"NOTICE_COHOST_HINT": "Совет: предоставьте управление отдельным участникам в списке ниже.",
|
||||
"BADGE_HOST": "Ведущий",
|
||||
"BADGE_GUEST": "Гость",
|
||||
"BADGE_CONTROLLER": "Управляющий",
|
||||
"BTN_GIVE_CONTROL": "Дать управление",
|
||||
"BTN_REVOKE_CONTROL": "Отозвать",
|
||||
"BADGE_DESYNCED": "Соло",
|
||||
"TOOLTIP_PEER_DESYNCED": "Смотрит отдельно — игнорирует команды ведущего",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Комнатой управляет ведущий",
|
||||
"HCM_DIALOG_BODY": "Управлять воспроизведением в этой комнате может только ведущий. Продолжить смотреть вместе или смотреть самостоятельно?",
|
||||
"HCM_DIALOG_STAY": "Оставаться синхронно",
|
||||
"HCM_DIALOG_SOLO": "Смотреть самостоятельно",
|
||||
"HCM_BADGE_SOLO": "Вы смотрите самостоятельно",
|
||||
"HCM_BADGE_RESYNC": "Синхронизировать",
|
||||
"NO_PEERS_CONNECTED": "Нет подключенных участников",
|
||||
"BTN_LEAVE_ROOM": "Выйти из комнаты",
|
||||
"LABEL_SELECT_VIDEO": "Выбрать видео",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Сбросить ваш внутренний идентификатор и переподключиться",
|
||||
"REGEN_ID_DESC": "Используйте при появлении ошибок дублирования идентификации.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Другая проблема? Откройте Issue на GitHub",
|
||||
"TOAST_ID_REGENERATED": "Идентичность перегенерирована — переподключение…",
|
||||
"LABEL_CONN_STATUS": "Статус подключения",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Текущий статус WebSocket-соединения с сервером",
|
||||
"CONN_STATUS_DISCONNECTED": "Отключено",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Подключение...",
|
||||
"STATUS_FAILED": "Ошибка",
|
||||
"STATUS_DISCONNECTED": "Отключено",
|
||||
"STATUS_IDLE": "Готов к подключению",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync готов. Присоединитесь к комнате или создайте её, чтобы подключиться и начать синхронизацию.",
|
||||
"BTN_STATE_JOINING": "🚀 Вход...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Переподключение...",
|
||||
"BTN_STATE_PLAYING": "▶ Запуск...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Время ожидания истекло — не все участники загрузили серию",
|
||||
"LOBBY_CANCEL_USER": "Отменено пользователем",
|
||||
"NOTIF_ERROR_TITLE": "Ошибка KoalaSync",
|
||||
"FOOTER_SUPPORT": "☕ Угости кофе",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Оценить",
|
||||
"FOOTER_SUPPORT_PROMPT": "Нравится KoalaSync? Оставьте отзыв!",
|
||||
"LABEL_AUDIO_PROCESSING": "Обработка звука",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Скоро",
|
||||
"BTN_RESTART_TOUR": "Перезапустить обучение",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Запустить приветственное руководство заново",
|
||||
"HINT_SELECT_VIDEO": "Выберите ваше видео здесь!"
|
||||
"HINT_SELECT_VIDEO": "Выберите ваше видео здесь!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Отправлять полное название",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Отправлять ID эпизода",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Не отправлять",
|
||||
"LABEL_PRIVACY_SETTINGS": "Настройки приватности",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Выберите, какие названия вкладки и медиа отправляются в комнату.",
|
||||
"LABEL_SEND_TAB_TITLE": "Отправлять название вкладки",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Отправляет в комнату название выбранной вкладки браузера.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Название медиа",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Выберите, как названия медиа отправляются в комнату."
|
||||
}
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Katılabilmeleri için bu bağlantıyı arkadaşlarınızla paylaşın",
|
||||
"LABEL_PEERS_IN_ROOM": "Odadaki Kişiler",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Şu anda bu odaya bağlı olan diğer kullanıcılar",
|
||||
"LABEL_HOST_CONTROL": "Sunucu Kontrolü",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Bu odada oynatmayı kimin kontrol edebileceğini belirleyin",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Oynatmayı yalnızca ben kontrol edebilirim",
|
||||
"NOTICE_HOST_CONTROLS": "Oynatmayı herkes için sunucu kontrol eder.",
|
||||
"NOTICE_COHOST_HINT": "İpucu: aşağıdaki katılımcı listesinde tek tek katılımcılara kontrol verin.",
|
||||
"BADGE_HOST": "Sunucu",
|
||||
"BADGE_GUEST": "Misafir",
|
||||
"BADGE_CONTROLLER": "Kontrolcü",
|
||||
"BTN_GIVE_CONTROL": "Kontrol ver",
|
||||
"BTN_REVOKE_CONTROL": "Geri al",
|
||||
"BADGE_DESYNCED": "Solo",
|
||||
"TOOLTIP_PEER_DESYNCED": "Tek başına izliyor — sunucu komutlarını yok sayar",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Bu odayı sunucu kontrol ediyor",
|
||||
"HCM_DIALOG_BODY": "Bu odada oynatmayı yalnızca sunucu kontrol edebilir. Birlikte izlemeye devam mı, yoksa kendi başına mı izlemek istiyorsun?",
|
||||
"HCM_DIALOG_STAY": "Senkron kal",
|
||||
"HCM_DIALOG_SOLO": "Kendi başıma izle",
|
||||
"HCM_BADGE_SOLO": "Kendi başına izliyorsun",
|
||||
"HCM_BADGE_RESYNC": "Yeniden senkronize et",
|
||||
"NO_PEERS_CONNECTED": "Bağlı kimse yok",
|
||||
"BTN_LEAVE_ROOM": "Odadan Ayrıl",
|
||||
"LABEL_SELECT_VIDEO": "Video Seç",
|
||||
@@ -92,6 +110,7 @@
|
||||
"BTN_REGEN_ID_TOOLTIP": "Dahili kimliğinizi yeniden oluşturun ve tekrar bağlanın",
|
||||
"REGEN_ID_DESC": "\"Duplicate Identity\" (Mükerrer Kimlik) hataları görüyorsanız bunu kullanın.",
|
||||
"REGEN_ID_OTHER_ISSUE": "Başka bir sorun? GitHub Issue açın",
|
||||
"TOAST_ID_REGENERATED": "Kimlik yeniden oluşturuldu — yeniden bağlanılıyor…",
|
||||
"LABEL_CONN_STATUS": "Bağlantı Durumu",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Mevcut WebSocket bağlantı durumu",
|
||||
"CONN_STATUS_DISCONNECTED": "Bağlantı Kesildi",
|
||||
@@ -143,6 +162,8 @@
|
||||
"STATUS_CONNECTING": "Bağlanılıyor...",
|
||||
"STATUS_FAILED": "Başarısız",
|
||||
"STATUS_DISCONNECTED": "Bağlantı Kesildi",
|
||||
"STATUS_IDLE": "Bağlanmaya hazır",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync hazır. Bağlanmak ve senkronizasyonu başlatmak için bir odaya katılın veya oluşturun.",
|
||||
"BTN_STATE_JOINING": "🚀 Katılınıyor...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Yeniden bağlanılıyor...",
|
||||
"BTN_STATE_PLAYING": "▶ Oynatılıyor...",
|
||||
@@ -181,7 +202,7 @@
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zaman aşımı — tüm bağlantılar bölümü yüklemedi",
|
||||
"LOBBY_CANCEL_USER": "Kullanıcı tarafından iptal edildi",
|
||||
"NOTIF_ERROR_TITLE": "KoalaSync Hatası",
|
||||
"FOOTER_SUPPORT": "☕ Bana kahve ısmarla",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Değerlendir",
|
||||
"FOOTER_SUPPORT_PROMPT": "KoalaSync'i beğendin mi? Bir yorum bırak!",
|
||||
"LABEL_AUDIO_PROCESSING": "Ses işleme",
|
||||
@@ -208,5 +229,14 @@
|
||||
"AUDIO_COMING_SOON": "Çok yakında",
|
||||
"BTN_RESTART_TOUR": "Eğitimi Yeniden Başlat",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Tanıtım eğitimini yeniden başlat",
|
||||
"HINT_SELECT_VIDEO": "Videonuzu buradan seçin!"
|
||||
"HINT_SELECT_VIDEO": "Videonuzu buradan seçin!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Tam başlığı gönder",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Bölüm ID’sini gönder",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Gönderme",
|
||||
"LABEL_PRIVACY_SETTINGS": "Gizlilik ayarları",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Odaya hangi sekme ve medya başlıklarının gönderileceğini seçin.",
|
||||
"LABEL_SEND_TAB_TITLE": "Sekme başlığını gönder",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Seçili tarayıcı sekmesinin başlığını odaya gönderir.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Medya başlığı",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Medya başlıklarının odaya nasıl gönderileceğini seçin."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "uk",
|
||||
"HTML_CLASS": "lang-uk",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "Кімната",
|
||||
"TAB_ROOM_TOOLTIP": "Налаштування кімнати та підключення",
|
||||
"TAB_SYNC": "Синхронізувати",
|
||||
"TAB_SYNC_TOOLTIP": "Елементи керування синхронізацією відео та віддалені дії",
|
||||
"TAB_SETTINGS": "Налаштування",
|
||||
"TAB_SETTINGS_TOOLTIP": "Параметри розширення",
|
||||
"TAB_STATUS": "Статус",
|
||||
"TAB_STATUS_TOOLTIP": "Розширена діагностика та журнали",
|
||||
"BTN_CREATE_ROOM": "+ Створити нову кімнату",
|
||||
"MANUAL_CONNECT_HEADER": "Ручне підключення / Додатково",
|
||||
"LABEL_SERVER": "Сервер",
|
||||
"BTN_SERVER_OFFICIAL": "Офіційний",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "Використовуйте офіційний надійний сервер",
|
||||
"BTN_SERVER_CUSTOM": "Власний",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "Підключіться до власного серверу, розміщеного на власному хостингу",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
|
||||
"LABEL_ROOM_ID": "ID кімнати",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "Унікальний ідентифікатор вашої кімнати синхронізації",
|
||||
"PLACEHOLDER_ROOM_ID": "Введіть Room ID",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Унікальний ідентифікатор кімнати, до якої ви хочете приєднатися",
|
||||
"LABEL_PASSWORD": "Пароль (необов'язково)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "Додатковий пароль для обмеження доступу до кімнати",
|
||||
"PLACEHOLDER_PASSWORD": "Пароль кімнати (необов'язково)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "Пароль для кімнати (залиште порожнім, якщо немає)",
|
||||
"BTN_JOIN_ROOM": "Приєднатися / Створити кімнату",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "Підключіться до кімнати",
|
||||
"LABEL_PUBLIC_ROOMS": "Громадські кімнати",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Список загальнодоступних кімнат на цьому сервері",
|
||||
"BTN_REFRESH": "ОНОВИТИ",
|
||||
"BTN_REFRESH_TOOLTIP": "Оновити список громадських кімнат",
|
||||
"PUBLIC_ROOMS_REFRESHING": "Оновлення...",
|
||||
"BTN_REFRESH_COOLDOWN": "ЧЕКАЙТЕ {seconds}s",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Оновлення списку кімнат охолоджується. Повторіть спробу в {seconds}s.",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Оновлення громадських кімнат. Наступне оновлення доступне в {seconds}s.",
|
||||
"LABEL_ACTIVE_ROOM": "Активна кімната",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "Кімната, до якої ви зараз підключені",
|
||||
"ACTIVE_ROOM_NONE": "ЖОДНОГО",
|
||||
"ACTIVE_SERVER_OFFICIAL": "Офіційний сервер",
|
||||
"LABEL_INVITE_LINK": "Посилання на запрошення",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "Поділіться цим посиланням з друзями, щоб вони могли приєднатися",
|
||||
"LABEL_PEERS_IN_ROOM": "Учасники в кімнаті",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Інші користувачі зараз підключені до цієї кімнати",
|
||||
"LABEL_HOST_CONTROL": "Керування ведучим",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "Обмежити, хто може керувати відтворенням у цій кімнаті",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "Лише я можу керувати відтворенням",
|
||||
"NOTICE_HOST_CONTROLS": "Ведучий керує відтворенням для всіх.",
|
||||
"NOTICE_COHOST_HINT": "Порада: надайте керування окремим учасникам у списку нижче.",
|
||||
"BADGE_HOST": "Ведучий",
|
||||
"BADGE_GUEST": "Гість",
|
||||
"BADGE_CONTROLLER": "Керуючий",
|
||||
"BTN_GIVE_CONTROL": "Дати керування",
|
||||
"BTN_REVOKE_CONTROL": "Відкликати",
|
||||
"BADGE_DESYNCED": "Соло",
|
||||
"TOOLTIP_PEER_DESYNCED": "Дивиться окремо — ігнорує команди ведучого",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · Кімнатою керує ведучий",
|
||||
"HCM_DIALOG_BODY": "Керувати відтворенням у цій кімнаті може лише ведучий. Продовжити дивитися разом чи дивитися самостійно?",
|
||||
"HCM_DIALOG_STAY": "Залишатися синхронно",
|
||||
"HCM_DIALOG_SOLO": "Дивитися самостійно",
|
||||
"HCM_BADGE_SOLO": "Ви дивитеся самостійно",
|
||||
"HCM_BADGE_RESYNC": "Синхронізувати",
|
||||
"NO_PEERS_CONNECTED": "Немає підключених однорангових пристроїв",
|
||||
"BTN_LEAVE_ROOM": "Вийти з кімнати",
|
||||
"LABEL_SELECT_VIDEO": "Виберіть Відео",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "Виберіть вкладку браузера, яка містить відео для синхронізації",
|
||||
"OPTION_SELECT_TAB": "-- Виберіть вкладку --",
|
||||
"LABEL_REMOTE_CONTROL": "Пульт дистанційного керування",
|
||||
"BTN_COPY_INVITE": "📋 Посилання на запрошення",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "Копіювати посилання для запрошення",
|
||||
"BTN_PLAY": "▶ Відтворити",
|
||||
"BTN_PLAY_TOOLTIP": "Надішліть усім команду Play",
|
||||
"BTN_PAUSE": "⏸ Пауза",
|
||||
"BTN_PAUSE_TOOLTIP": "Надішліть команду паузи всім",
|
||||
"BTN_SYNC": "⚡ СИНХР",
|
||||
"BTN_SYNC_TOOLTIP": "Змусити всіх користувачів синхронізуватися",
|
||||
"OPTION_JUMP_TO_OTHERS": "Перейти до інших",
|
||||
"OPTION_JUMP_TO_ME": "Перейти до мене",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "Виберіть ціль синхронізації",
|
||||
"LABEL_LAST_ACTIVITY": "Статус останньої дії",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "Показує останню команду відтворення, паузи або перемотування",
|
||||
"NO_RECENT_COMMANDS": "Немає останніх команд",
|
||||
"LOBBY_HEADER": "ЕПІЗОДНЕ ЛОБІ",
|
||||
"LOBBY_WAITING_FOR": "🎬 Чекаємо: \"{title}\"",
|
||||
"LOBBY_WAITING_PEERS": "Очікування учасників...",
|
||||
"BTN_SKIP_PLAY": "Все одно пропустити та відтворити",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "Скасуйте лобі та все одно відтворюйте",
|
||||
"LOBBY_CONNECT_FIRST": "Спочатку підключіться до кімнати",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "Вам потрібно приєднатися до кімнати за посиланням із запрошенням або створити нову, щоб синхронізувати відео.",
|
||||
"BTN_CREATE_ROOM_ALT": "Створити нову кімнату",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Створіть нову випадкову кімнату та приєднайтеся до неї",
|
||||
"LABEL_USERNAME": "Ваше ім'я користувача",
|
||||
"LABEL_USERNAME_TOOLTIP": "Ім'я користувача допомагає іншим ідентифікувати вас.",
|
||||
"PLACEHOLDER_USERNAME": "Анонімна коала",
|
||||
"LABEL_HIDE_CLUTTER": "Приховати безладні вкладки",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Відфільтровує вкладки, не пов’язані з відео, і непов’язані домени, щоб зберегти список чистим",
|
||||
"LABEL_AUTO_SYNC_NEXT": "Автоматична синхронізація наступного епізоду",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Автоматично призупиняється та чекає на всіх однорангових пристроїв, коли епізод зміниться, а потім синхронізація починається разом.",
|
||||
"LABEL_AUTO_COPY_INVITE": "Автоматичне копіювання посилання для запрошення",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Автоматично копіює посилання запрошення в буфер обміну під час створення нової кімнати.",
|
||||
"LABEL_NOTIFICATIONS": "Сповіщення браузера",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "Показує рідні системні сповіщення, коли хтось приєднується/виходить або відтворює/призупиняє.",
|
||||
"LABEL_LANGUAGE": "Мова програми",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "Виберіть бажану мову розширення",
|
||||
"LABEL_TROUBLESHOOTING": "Усунення несправностей",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "Інструменти для вирішення проблем з підключенням",
|
||||
"BTN_REGEN_ID": "Регенерувати Peer ID",
|
||||
"BTN_REGEN_ID_TOOLTIP": "Повторно згенеруйте свій внутрішній ідентифікатор і повторно підключіться",
|
||||
"REGEN_ID_DESC": "Використовуйте це, якщо ви бачите помилку \"Дублікати ідентифікатора\".",
|
||||
"REGEN_ID_OTHER_ISSUE": "Інша проблема? Відкрийте випуск GitHub",
|
||||
"TOAST_ID_REGENERATED": "Ідентичність перегенеровано — повторне підключення…",
|
||||
"LABEL_CONN_STATUS": "Статус підключення",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "Поточний стан підключення WebSocket",
|
||||
"CONN_STATUS_DISCONNECTED": "Відключено",
|
||||
"BTN_RETRY": "ПОВТОРИТИ",
|
||||
"BTN_RETRY_TOOLTIP": "Спробуйте повторно підключитися до сервера",
|
||||
"BTN_COPY_LOGS": "Копіювати журнали",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "Скопіюйте журнали в буфер обміну для спільного використання",
|
||||
"LABEL_VIDEO_DEBUG": "Інформація про налагодження відео",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "Технічні відомості про поточний вибраний елемент відео",
|
||||
"VIDEO_DEBUG_EMPTY": "Вкладка не вибрана або відео не виявлено.",
|
||||
"LABEL_HISTORY": "Повна історія дій",
|
||||
"LABEL_HISTORY_TOOLTIP": "Хронологічний журнал усіх команд синхронізації в кімнаті",
|
||||
"HISTORY_EMPTY": "Активності ще немає",
|
||||
"LABEL_LOGS": "Журнали (останні 50)",
|
||||
"LABEL_LOGS_TOOLTIP": "Журнали технічного підключення для налагодження",
|
||||
"BTN_CLEAR": "Очистити",
|
||||
"BTN_CLEAR_TOOLTIP": "Очистити журнал",
|
||||
"LABEL_GITHUB": "Репозиторій GitHub",
|
||||
"BTN_ONBOARDING_SKIP": "Пропустити",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "Пропустити підручник",
|
||||
"BTN_ONBOARDING_NEXT": "Далі",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "Перейти до наступного кроку",
|
||||
"ONBOARDING_1_TITLE": "Ласкаво просимо до KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "Дивіться відео разом у ідеальній синхронізації — де б ви не були. Давайте зробимо короткий тур!",
|
||||
"ONBOARDING_2_TITLE": "1. Створіть кімнату",
|
||||
"ONBOARDING_2_TEXT": "Почніть тут. Створіть кімнату та поділіться посиланням запрошення з друзями.",
|
||||
"ONBOARDING_3_TITLE": "2. Виберіть Відео",
|
||||
"ONBOARDING_3_TEXT": "Перейдіть сюди, щоб вибрати відео, яке потрібно синхронізувати. Відтворення, пауза та пошук — усі залишаються синхронізованими.",
|
||||
"ONBOARDING_4_TITLE": "3. Персоналізація",
|
||||
"ONBOARDING_4_TEXT": "Виберіть цікаве ім’я користувача, щоб ваші друзі знали, хто ви.",
|
||||
"ONBOARDING_5_TITLE": "Усе готово!",
|
||||
"ONBOARDING_5_TEXT": "Час взяти попкорн. Приємного перегляду разом!",
|
||||
"ERR_CONN_TIMEOUT": "Час очікування підключення минув. Спробуйте ще раз.",
|
||||
"ERR_INVALID_SERVER_URL": "Недійсний формат URL-адреси сервера.",
|
||||
"ERR_IDENTITY_NOT_LOADED": "Посвідчення особи ще не завантажено. Зачекайте хвилинку та повторіть спробу.",
|
||||
"ERR_NO_PEERS_TIME": "Немає інших аналогів із відомим часом. Переключіться на «Перейти до мене».",
|
||||
"ERR_NO_VIDEO_TAB": "Не вдалося підключитися до вкладки відео.",
|
||||
"ERR_SELECT_VIDEO": "Спочатку виберіть відео!",
|
||||
"TOAST_INVITE_COPIED": "Посилання на запрошення скопійовано!",
|
||||
"TOAST_COPY_FAILED": "Не вдалося скопіювати в буфер обміну",
|
||||
"TOAST_LOBBY_SKIPPED": "Лобі епізоду пропущено.",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "Не вдалося пропустити лобі.",
|
||||
"TOAST_LOGS_COPIED": "Скопійовано!",
|
||||
"TOAST_PEER_JOINED": "{name} приєднався до кімнати",
|
||||
"TOAST_PEER_LEFT": "{name} вийшов з кімнати",
|
||||
"TOAST_PEER_ACTION": "{name} {action}",
|
||||
"STATUS_CONNECTED": "Підключено",
|
||||
"STATUS_RECONNECTING": "Повторне підключення...",
|
||||
"STATUS_CONNECTING": "Підключення...",
|
||||
"STATUS_FAILED": "Не вдалося",
|
||||
"STATUS_DISCONNECTED": "Відключено",
|
||||
"STATUS_IDLE": "Готовий до підключення",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync готовий. Приєднайтеся до кімнати або створіть її, щоб підключитися та почати синхронізацію.",
|
||||
"BTN_STATE_JOINING": "🚀 Приєднуюсь...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 Повторне підключення...",
|
||||
"BTN_STATE_PLAYING": "▶ Грає...",
|
||||
"BTN_STATE_PAUSING": "⏸ Призупинення...",
|
||||
"BTN_STATE_SYNCING_GROUP": "Синхронізація з групою ({time})...",
|
||||
"BTN_STATE_SYNCING": "Синхронізація...",
|
||||
"BTN_STATE_SYNCED": "✅ Синхронізовано!",
|
||||
"NOTIF_PLAY": "почав відтворення",
|
||||
"NOTIF_PAUSE": "призупинено відтворення",
|
||||
"NOTIF_SEEK": "перемотав відео",
|
||||
"NOTIF_FORCE_PREPARE": "почав примусову синхронізацію",
|
||||
"NOTIF_FORCE_EXECUTE": "синхронізував усіх",
|
||||
"DEBUG_NO_TAB": "Цільова вкладка не вибрана.",
|
||||
"DEBUG_COMM_FAIL": "Не вдалося зв’язатися з відео вкладки.",
|
||||
"EMPTY_PEERS_TITLE": "Учасників ще немає",
|
||||
"EMPTY_PEERS_HINT": "Поділіться своїм запрошенням, щоб почати",
|
||||
"EMPTY_HISTORY_TITLE": "Активності ще немає",
|
||||
"EMPTY_HISTORY_HINT": "Відтворення, призупинення або перемотування, щоб побачити історію",
|
||||
"EMPTY_LOGS_TITLE": "Без журналів",
|
||||
"EMPTY_LOGS_HINT": "Тут відображатимуться події підключення",
|
||||
"EMPTY_ROOMS_TITLE": "Немає активних кімнат",
|
||||
"EMPTY_ROOMS_HINT": "Створіть кімнату або оновіть, щоб знайти загальнодоступні",
|
||||
"LABEL_YOU": "Ви",
|
||||
"ONBOARDING_DONE": "Готово!",
|
||||
"LABEL_LOBBY_PEER_READY": "Готовий",
|
||||
"LABEL_LOBBY_PEER_LOADING": "Завантаження...",
|
||||
"LABEL_PASSWORD_PROTECTED": "Захищено паролем",
|
||||
"LABEL_PEERS_COUNT": "{count} учасників",
|
||||
"LABEL_CUSTOM_SERVER": "Спеціальний сервер",
|
||||
"BTN_STATE_CREATING": "🚀 Створення кімнати...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Помилка синхронізації епізоду",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "Автоматичну синхронізацію скасовано: {reason}. Можливо, вам знадобиться виконати синхронізацію вручну.",
|
||||
"LOBBY_CANCEL_TIMEOUT": "Час очікування",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Тайм-аут (відновлено)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "Усі інші учасники вийшли",
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Тайм-аут — не всі вузли завантажили епізод",
|
||||
"LOBBY_CANCEL_USER": "Скасовано користувачем",
|
||||
"NOTIF_ERROR_TITLE": "Помилка KoalaSync",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ Оцініть нас",
|
||||
"FOOTER_SUPPORT_PROMPT": "Подобається KoalaSync? Залиште відгук!",
|
||||
"LABEL_AUDIO_PROCESSING": "Обробка аудіо",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Застосуйте звукові ефекти, такі як стиснення, до відтворення відео",
|
||||
"AUDIO_OPEN_SETTINGS": "Відкрити",
|
||||
"NEW_FEATURE_AUDIO": "Новинка: обробка звуку — спробуйте компресор!",
|
||||
"AUDIO_BACK": "← Назад",
|
||||
"AUDIO_PAGE_TITLE": "Параметри звуку",
|
||||
"AUDIO_MASTER_TOGGLE": "Обробка аудіо",
|
||||
"AUDIO_COMPRESSOR": "Компресор",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "Увімкнено",
|
||||
"AUDIO_PRESET": "Попереднє налаштування",
|
||||
"AUDIO_PRESET_RECOMMENDED": "Рекомендовано",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "Динамічний діапазон",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Покращення голосу",
|
||||
"AUDIO_PRESET_SMOOTH": "Плавний",
|
||||
"AUDIO_PRESET_CUSTOM": "Власний",
|
||||
"AUDIO_PARAM_THRESHOLD": "Поріг",
|
||||
"AUDIO_PARAM_KNEE": "Коліно",
|
||||
"AUDIO_PARAM_RATIO": "Співвідношення",
|
||||
"AUDIO_PARAM_ATTACK": "Атака",
|
||||
"AUDIO_PARAM_RELEASE": "Спад",
|
||||
"AUDIO_EQUALIZER": "Еквалайзер",
|
||||
"AUDIO_COMING_SOON": "Скоро буде",
|
||||
"BTN_RESTART_TOUR": "Перезапустіть підручник",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "Перезапустіть навчальний посібник із адаптації",
|
||||
"HINT_SELECT_VIDEO": "Виберіть своє відео тут!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "Надсилати повну назву",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "Надсилати ID епізоду",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "Не надсилати",
|
||||
"LABEL_PRIVACY_SETTINGS": "Налаштування приватності",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Виберіть, які назви вкладки й медіа надсилати до кімнати.",
|
||||
"LABEL_SEND_TAB_TITLE": "Надсилати назву вкладки",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Надсилає до кімнати назву вибраної вкладки браузера.",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "Назва медіа",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Виберіть, як назви медіа надсилати до кімнати."
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"LANG_CODE": "zh",
|
||||
"HTML_CLASS": "lang-zh",
|
||||
"APP_TITLE": "KoalaSync",
|
||||
"TAB_ROOM": "房间",
|
||||
"TAB_ROOM_TOOLTIP": "房间设置和连接",
|
||||
"TAB_SYNC": "同步",
|
||||
"TAB_SYNC_TOOLTIP": "视频同步控制和远程操作",
|
||||
"TAB_SETTINGS": "设置",
|
||||
"TAB_SETTINGS_TOOLTIP": "扩展首选项",
|
||||
"TAB_STATUS": "状态",
|
||||
"TAB_STATUS_TOOLTIP": "高级诊断和日志",
|
||||
"BTN_CREATE_ROOM": "+ 创建新房间",
|
||||
"MANUAL_CONNECT_HEADER": "手动连接/高级",
|
||||
"LABEL_SERVER": "服务器",
|
||||
"BTN_SERVER_OFFICIAL": "官方的",
|
||||
"BTN_SERVER_OFFICIAL_TOOLTIP": "使用官方可靠服务器",
|
||||
"BTN_SERVER_CUSTOM": "自定义",
|
||||
"BTN_SERVER_CUSTOM_TOOLTIP": "连接到您自己的自托管服务器",
|
||||
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
|
||||
"LABEL_ROOM_ID": "房间 ID",
|
||||
"LABEL_ROOM_ID_TOOLTIP": "您的同步室的唯一标识符",
|
||||
"PLACEHOLDER_ROOM_ID": "输入Room ID",
|
||||
"PLACEHOLDER_ROOM_ID_TOOLTIP": "您要加入的房间的唯一ID",
|
||||
"LABEL_PASSWORD": "密码(可选)",
|
||||
"LABEL_PASSWORD_TOOLTIP": "可选密码以限制房间访问",
|
||||
"PLACEHOLDER_PASSWORD": "房间密码(可选)",
|
||||
"PLACEHOLDER_PASSWORD_TOOLTIP": "房间密码(无则留空)",
|
||||
"BTN_JOIN_ROOM": "加入/创建房间",
|
||||
"BTN_JOIN_ROOM_TOOLTIP": "连接到房间",
|
||||
"LABEL_PUBLIC_ROOMS": "公共房间",
|
||||
"LABEL_PUBLIC_ROOMS_TOOLTIP": "该服务器上公开可用的房间列表",
|
||||
"BTN_REFRESH": "刷新",
|
||||
"BTN_REFRESH_TOOLTIP": "刷新公共房间列表",
|
||||
"PUBLIC_ROOMS_REFRESHING": "刷新中...",
|
||||
"BTN_REFRESH_COOLDOWN": "等等{seconds}",
|
||||
"BTN_REFRESH_COOLDOWN_TOOLTIP": "房间列表刷新正在冷却。在 {seconds} 中重试。",
|
||||
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "公共房间列表刷新冷却中。下次刷新将在 {seconds} 后可用。",
|
||||
"LABEL_ACTIVE_ROOM": "活动室",
|
||||
"LABEL_ACTIVE_ROOM_TOOLTIP": "您当前连接的房间",
|
||||
"ACTIVE_ROOM_NONE": "没有任何",
|
||||
"ACTIVE_SERVER_OFFICIAL": "官方服务器",
|
||||
"LABEL_INVITE_LINK": "邀请链接",
|
||||
"LABEL_INVITE_LINK_TOOLTIP": "与朋友分享此链接,以便他们可以加入",
|
||||
"LABEL_PEERS_IN_ROOM": "房间里的同伴",
|
||||
"LABEL_PEERS_IN_ROOM_TOOLTIP": "当前连接到该房间的其他用户",
|
||||
"LABEL_HOST_CONTROL": "主持人控制",
|
||||
"LABEL_HOST_CONTROL_TOOLTIP": "限制谁可以在此房间中控制播放",
|
||||
"LABEL_HOST_ONLY_TOGGLE": "仅我可以控制播放",
|
||||
"NOTICE_HOST_CONTROLS": "由主持人为所有人控制播放。",
|
||||
"NOTICE_COHOST_HINT": "提示:在下方的参与者列表中将控制权授予单个参与者。",
|
||||
"BADGE_HOST": "主持人",
|
||||
"BADGE_GUEST": "访客",
|
||||
"BADGE_CONTROLLER": "控制者",
|
||||
"BTN_GIVE_CONTROL": "授予控制权",
|
||||
"BTN_REVOKE_CONTROL": "撤销",
|
||||
"BADGE_DESYNCED": "单独",
|
||||
"TOOLTIP_PEER_DESYNCED": "单独观看中 — 忽略主持人的命令",
|
||||
"HCM_DIALOG_TITLE": "KoalaSync · 主持人控制本房间",
|
||||
"HCM_DIALOG_BODY": "本房间只有主持人可以控制播放。继续一起观看,还是自己单独观看?",
|
||||
"HCM_DIALOG_STAY": "保持同步",
|
||||
"HCM_DIALOG_SOLO": "自己单独观看",
|
||||
"HCM_BADGE_SOLO": "正在单独观看",
|
||||
"HCM_BADGE_RESYNC": "重新同步",
|
||||
"NO_PEERS_CONNECTED": "没有对等点连接",
|
||||
"BTN_LEAVE_ROOM": "离开房间",
|
||||
"LABEL_SELECT_VIDEO": "选择视频",
|
||||
"LABEL_SELECT_VIDEO_TOOLTIP": "选择包含要同步的视频的浏览器选项卡",
|
||||
"OPTION_SELECT_TAB": "-- 选择一个选项卡 --",
|
||||
"LABEL_REMOTE_CONTROL": "遥控",
|
||||
"BTN_COPY_INVITE": "📋 邀请链接",
|
||||
"BTN_COPY_INVITE_TOOLTIP": "复制邀请链接",
|
||||
"BTN_PLAY": "▶ 播放",
|
||||
"BTN_PLAY_TOOLTIP": "向所有人发送播放命令",
|
||||
"BTN_PAUSE": "⏸ 暂停",
|
||||
"BTN_PAUSE_TOOLTIP": "向所有人发送暂停命令",
|
||||
"BTN_SYNC": "⚡ 同步",
|
||||
"BTN_SYNC_TOOLTIP": "强制所有用户同步",
|
||||
"OPTION_JUMP_TO_OTHERS": "跳转到其他",
|
||||
"OPTION_JUMP_TO_ME": "跳到我这里",
|
||||
"OPTION_JUMP_MODE_TOOLTIP": "选择同步目标",
|
||||
"LABEL_LAST_ACTIVITY": "上次活动状态",
|
||||
"LABEL_LAST_ACTIVITY_TOOLTIP": "显示最近的播放、暂停或跳转命令",
|
||||
"NO_RECENT_COMMANDS": "没有最近的命令",
|
||||
"LOBBY_HEADER": "剧集大厅",
|
||||
"LOBBY_WAITING_FOR": "🎬 等待:“{title}”",
|
||||
"LOBBY_WAITING_PEERS": "等待同行...",
|
||||
"BTN_SKIP_PLAY": "仍然跳过并播放",
|
||||
"BTN_SKIP_PLAY_TOOLTIP": "取消大厅并继续播放",
|
||||
"LOBBY_CONNECT_FIRST": "首先连接到房间",
|
||||
"LOBBY_CONNECT_FIRST_DESC": "您需要通过邀请链接加入房间或创建一个新房间来同步视频。",
|
||||
"BTN_CREATE_ROOM_ALT": "创建新房间",
|
||||
"BTN_CREATE_ROOM_ALT_TOOLTIP": "创建一个新的随机房间并加入它",
|
||||
"LABEL_USERNAME": "您的用户名",
|
||||
"LABEL_USERNAME_TOOLTIP": "用户名可以帮助其他人识别您。",
|
||||
"PLACEHOLDER_USERNAME": "无名考拉",
|
||||
"LABEL_HIDE_CLUTTER": "隐藏杂乱标签",
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "过滤掉非视频选项卡和不相关的域以保持列表干净",
|
||||
"LABEL_AUTO_SYNC_NEXT": "自动同步下一集",
|
||||
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "当情节发生变化时,自动暂停并等待所有对等点,然后一起开始同步。",
|
||||
"LABEL_AUTO_COPY_INVITE": "自动复制邀请链接",
|
||||
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "创建新房间时,自动将邀请链接复制到剪贴板。",
|
||||
"LABEL_NOTIFICATIONS": "浏览器通知",
|
||||
"LABEL_NOTIFICATIONS_TOOLTIP": "当有人加入/离开或播放/暂停时显示本机系统通知。",
|
||||
"LABEL_LANGUAGE": "应用程序语言",
|
||||
"LABEL_LANGUAGE_TOOLTIP": "选择您喜欢的扩展语言",
|
||||
"LABEL_TROUBLESHOOTING": "故障排除",
|
||||
"LABEL_TROUBLESHOOTING_TOOLTIP": "用于修复连接问题的工具",
|
||||
"BTN_REGEN_ID": "再生Peer ID",
|
||||
"BTN_REGEN_ID_TOOLTIP": "重新生成您的内部 ID 并重新连接",
|
||||
"REGEN_ID_DESC": "如果您看到“重复身份”错误,请使用此选项。",
|
||||
"REGEN_ID_OTHER_ISSUE": "其他问题?打开 GitHub 问题",
|
||||
"TOAST_ID_REGENERATED": "身份已重新生成 — 正在重新连接…",
|
||||
"LABEL_CONN_STATUS": "连接状态",
|
||||
"LABEL_CONN_STATUS_TOOLTIP": "当前WebSocket连接状态",
|
||||
"CONN_STATUS_DISCONNECTED": "已断开连接",
|
||||
"BTN_RETRY": "重试",
|
||||
"BTN_RETRY_TOOLTIP": "尝试重新连接服务器",
|
||||
"BTN_COPY_LOGS": "复制日志",
|
||||
"BTN_COPY_LOGS_TOOLTIP": "将日志复制到剪贴板以便共享",
|
||||
"LABEL_VIDEO_DEBUG": "视频调试信息",
|
||||
"LABEL_VIDEO_DEBUG_TOOLTIP": "有关当前所选视频元素的技术详细信息",
|
||||
"VIDEO_DEBUG_EMPTY": "未选择任何选项卡或检测到视频。",
|
||||
"LABEL_HISTORY": "完整的行动历史",
|
||||
"LABEL_HISTORY_TOOLTIP": "房间内所有同步命令的时间日志",
|
||||
"HISTORY_EMPTY": "还没有活动",
|
||||
"LABEL_LOGS": "日志(最后 50 条)",
|
||||
"LABEL_LOGS_TOOLTIP": "用于调试的技术连接日志",
|
||||
"BTN_CLEAR": "清除",
|
||||
"BTN_CLEAR_TOOLTIP": "清除日志输出",
|
||||
"LABEL_GITHUB": "GitHub 存储库",
|
||||
"BTN_ONBOARDING_SKIP": "跳过",
|
||||
"BTN_ONBOARDING_SKIP_TOOLTIP": "跳过教程",
|
||||
"BTN_ONBOARDING_NEXT": "下一个",
|
||||
"BTN_ONBOARDING_NEXT_TOOLTIP": "进入下一步",
|
||||
"ONBOARDING_1_TITLE": "欢迎来到KoalaSync!",
|
||||
"ONBOARDING_1_TEXT": "无论您身在何处,都能完美同步地一起观看视频。让我们快速浏览一下吧!",
|
||||
"ONBOARDING_2_TITLE": "1. 创建房间",
|
||||
"ONBOARDING_2_TEXT": "从这里开始。创建一个房间并与您的朋友分享邀请链接。",
|
||||
"ONBOARDING_3_TITLE": "2. 选择视频",
|
||||
"ONBOARDING_3_TEXT": "导航此处选择您要同步的视频。播放、暂停和搜索——每个人都保持同步。",
|
||||
"ONBOARDING_4_TITLE": "3. 个性化",
|
||||
"ONBOARDING_4_TEXT": "选择一个有趣的用户名,以便您的朋友知道您是谁。",
|
||||
"ONBOARDING_5_TITLE": "你都准备好了!",
|
||||
"ONBOARDING_5_TEXT": "是时候去买点爆米花了。一起观赏吧!",
|
||||
"ERR_CONN_TIMEOUT": "连接超时。请再试一次。",
|
||||
"ERR_INVALID_SERVER_URL": "服务器 URL 格式无效。",
|
||||
"ERR_IDENTITY_NOT_LOADED": "身份尚未加载。稍等片刻,然后重试。",
|
||||
"ERR_NO_PEERS_TIME": "没有其他已知时间的同行。切换到“跳到我这里”。",
|
||||
"ERR_NO_VIDEO_TAB": "无法连接到视频选项卡。",
|
||||
"ERR_SELECT_VIDEO": "请先选择视频!",
|
||||
"TOAST_INVITE_COPIED": "邀请链接已复制!",
|
||||
"TOAST_COPY_FAILED": "无法复制到剪贴板",
|
||||
"TOAST_LOBBY_SKIPPED": "剧集大厅已跳过。",
|
||||
"TOAST_LOBBY_SKIP_FAILED": "无法跳过大厅。",
|
||||
"TOAST_LOGS_COPIED": "复制了!",
|
||||
"TOAST_PEER_JOINED": "{name} 加入房间",
|
||||
"TOAST_PEER_LEFT": "{name} 离开房间",
|
||||
"TOAST_PEER_ACTION": "{name} {action}",
|
||||
"STATUS_CONNECTED": "已连接",
|
||||
"STATUS_RECONNECTING": "正在重新连接...",
|
||||
"STATUS_CONNECTING": "正在连接...",
|
||||
"STATUS_FAILED": "失败的",
|
||||
"STATUS_DISCONNECTED": "已断开连接",
|
||||
"STATUS_IDLE": "已准备好连接",
|
||||
"STATUS_IDLE_TOOLTIP": "KoalaSync 已准备就绪。加入或创建房间以连接并开始同步。",
|
||||
"BTN_STATE_JOINING": "🚀 正在加入...",
|
||||
"BTN_STATE_RECONNECTING": "🔄 正在重新连接...",
|
||||
"BTN_STATE_PLAYING": "▶ 播放中...",
|
||||
"BTN_STATE_PAUSING": "⏸ 暂停...",
|
||||
"BTN_STATE_SYNCING_GROUP": "正在同步到群组 ({time})...",
|
||||
"BTN_STATE_SYNCING": "正在同步...",
|
||||
"BTN_STATE_SYNCED": "✅ 已同步!",
|
||||
"NOTIF_PLAY": "开始播放",
|
||||
"NOTIF_PAUSE": "暂停播放",
|
||||
"NOTIF_SEEK": "跳转视频",
|
||||
"NOTIF_FORCE_PREPARE": "开始强制同步",
|
||||
"NOTIF_FORCE_EXECUTE": "同步所有人",
|
||||
"DEBUG_NO_TAB": "未选择目标选项卡。",
|
||||
"DEBUG_COMM_FAIL": "无法与标签视频通信。",
|
||||
"EMPTY_PEERS_TITLE": "还没有同行",
|
||||
"EMPTY_PEERS_HINT": "分享您的邀请链接以开始使用",
|
||||
"EMPTY_HISTORY_TITLE": "还没有活动",
|
||||
"EMPTY_HISTORY_HINT": "播放、暂停或跳转以查看历史",
|
||||
"EMPTY_LOGS_TITLE": "无日志",
|
||||
"EMPTY_LOGS_HINT": "连接事件将出现在这里",
|
||||
"EMPTY_ROOMS_TITLE": "没有活动房间",
|
||||
"EMPTY_ROOMS_HINT": "创建房间或刷新以查找公共房间",
|
||||
"LABEL_YOU": "你",
|
||||
"ONBOARDING_DONE": "完毕!",
|
||||
"LABEL_LOBBY_PEER_READY": "准备好",
|
||||
"LABEL_LOBBY_PEER_LOADING": "加载中...",
|
||||
"LABEL_PASSWORD_PROTECTED": "密码保护",
|
||||
"LABEL_PEERS_COUNT": "{count} 同行",
|
||||
"LABEL_CUSTOM_SERVER": "定制服务器",
|
||||
"BTN_STATE_CREATING": "🚀 正在创建房间...",
|
||||
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — 剧集同步失败",
|
||||
"NOTIF_LOBBY_CANCEL_MSG": "自动同步取消:{reason}。您可能需要手动同步。",
|
||||
"LOBBY_CANCEL_TIMEOUT": "暂停",
|
||||
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "超时(已恢复)",
|
||||
"LOBBY_CANCEL_PEERS_LEFT": "所有其他同伴都离开了",
|
||||
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "超时——并非所有同行都加载了该剧集",
|
||||
"LOBBY_CANCEL_USER": "已被用户取消",
|
||||
"NOTIF_ERROR_TITLE": "KoalaSync 错误",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"FOOTER_REVIEW": "★ 评价我们",
|
||||
"FOOTER_SUPPORT_PROMPT": "喜欢 KoalaSync 吗?留下评论!",
|
||||
"LABEL_AUDIO_PROCESSING": "音频处理",
|
||||
"LABEL_AUDIO_PROCESSING_TOOLTIP": "将压缩等音频效果应用于视频播放",
|
||||
"AUDIO_OPEN_SETTINGS": "打开",
|
||||
"NEW_FEATURE_AUDIO": "新功能:音频处理 - 尝试压缩器!",
|
||||
"AUDIO_BACK": "← 返回",
|
||||
"AUDIO_PAGE_TITLE": "音频设置",
|
||||
"AUDIO_MASTER_TOGGLE": "音频处理",
|
||||
"AUDIO_COMPRESSOR": "压缩器",
|
||||
"AUDIO_COMPRESSOR_ENABLE": "启用",
|
||||
"AUDIO_PRESET": "预设",
|
||||
"AUDIO_PRESET_RECOMMENDED": "推荐",
|
||||
"AUDIO_PRESET_DYNAMIC_RANGE": "动态范围",
|
||||
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "声音增强",
|
||||
"AUDIO_PRESET_SMOOTH": "平滑",
|
||||
"AUDIO_PRESET_CUSTOM": "自定义",
|
||||
"AUDIO_PARAM_THRESHOLD": "阈值",
|
||||
"AUDIO_PARAM_KNEE": "拐点",
|
||||
"AUDIO_PARAM_RATIO": "比率",
|
||||
"AUDIO_PARAM_ATTACK": "起音",
|
||||
"AUDIO_PARAM_RELEASE": "释放",
|
||||
"AUDIO_EQUALIZER": "均衡器",
|
||||
"AUDIO_COMING_SOON": "即将推出",
|
||||
"BTN_RESTART_TOUR": "重启教程",
|
||||
"BTN_RESTART_TOUR_TOOLTIP": "重新启动入门教程",
|
||||
"HINT_SELECT_VIDEO": "在这里选择您的视频!",
|
||||
"OPTION_TITLE_PRIVACY_FULL": "发送完整标题",
|
||||
"OPTION_TITLE_PRIVACY_EPISODE": "发送剧集 ID",
|
||||
"OPTION_TITLE_PRIVACY_HIDDEN": "不发送",
|
||||
"LABEL_PRIVACY_SETTINGS": "隐私设置",
|
||||
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "选择要发送到房间的标签页标题和媒体标题。",
|
||||
"LABEL_SEND_TAB_TITLE": "发送标签页标题",
|
||||
"LABEL_SEND_TAB_TITLE_TOOLTIP": "将所选浏览器标签页的标题发送到房间。",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY": "媒体标题",
|
||||
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "选择媒体标题发送到房间的方式。"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"default_locale": "en",
|
||||
"name": "KoalaSync",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.0",
|
||||
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
|
||||
"permissions": [
|
||||
"storage",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
export function initTabManager({
|
||||
getCurrentTabId,
|
||||
setCurrentTabId,
|
||||
setCurrentTabTitle,
|
||||
setLastContentHeartbeatAt,
|
||||
setRoomIdleSince,
|
||||
getCurrentRoom,
|
||||
getPeerId,
|
||||
getStorageInitialized,
|
||||
updateBadgeStatus,
|
||||
addLog,
|
||||
getSettings,
|
||||
emit,
|
||||
applyAudioSettingsToTab,
|
||||
ensureState,
|
||||
EVENTS
|
||||
}) {
|
||||
chrome.storage.onChanged.addListener(async (changes, area) => {
|
||||
if (area !== 'local' || !changes.audioSettings) return;
|
||||
await ensureState();
|
||||
const tabId = getCurrentTabId();
|
||||
if (!tabId) return;
|
||||
|
||||
chrome.tabs.sendMessage(tabId, {
|
||||
action: 'APPLY_AUDIO_SETTINGS',
|
||||
settings: changes.audioSettings.newValue
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
chrome.tabs.onRemoved.addListener(async (tabId) => {
|
||||
await ensureState();
|
||||
if (tabId === getCurrentTabId()) {
|
||||
const wasInRoom = !!getCurrentRoom();
|
||||
setCurrentTabId(null);
|
||||
setCurrentTabTitle(null);
|
||||
setLastContentHeartbeatAt(null);
|
||||
const now = Date.now();
|
||||
setRoomIdleSince(now);
|
||||
chrome.storage.session.set({
|
||||
currentTabId: null,
|
||||
currentTabTitle: null,
|
||||
roomIdleSince: now,
|
||||
lastContentHeartbeatAt: null
|
||||
});
|
||||
updateBadgeStatus();
|
||||
addLog('Target tab closed.', 'warn');
|
||||
|
||||
if (wasInRoom) {
|
||||
const roomAtClose = getCurrentRoom();
|
||||
getSettings().then(settings => {
|
||||
if (getCurrentRoom() !== roomAtClose) return;
|
||||
|
||||
emit(EVENTS.PEER_STATUS, {
|
||||
peerId: getPeerId(),
|
||||
playbackState: 'paused',
|
||||
currentTime: null,
|
||||
mediaTitle: null,
|
||||
username: settings.username,
|
||||
tabTitle: null
|
||||
});
|
||||
|
||||
const room = getCurrentRoom();
|
||||
if (room && Array.isArray(room.peers)) {
|
||||
const me = room.peers.find(p => (p.peerId || p) === getPeerId());
|
||||
if (me && typeof me === 'object') {
|
||||
me.playbackState = 'paused';
|
||||
me.currentTime = null;
|
||||
me.mediaTitle = null;
|
||||
me.tabTitle = null;
|
||||
me.lastHeartbeat = Date.now();
|
||||
if (getStorageInitialized()) {
|
||||
chrome.storage.session.set({ currentRoom: room });
|
||||
}
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: room.peers }).catch(() => {});
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
|
||||
await ensureState();
|
||||
const curTabId = getCurrentTabId();
|
||||
if (curTabId && tabId === parseInt(curTabId) && changeInfo.status === 'complete') {
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['content.js']
|
||||
})
|
||||
.then(() => applyAudioSettingsToTab(tabId))
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
+53
-9
@@ -402,14 +402,18 @@
|
||||
<details style="margin-bottom: 12px; background: var(--card); border: 1px solid #334155; border-radius: 8px; padding: 8px;">
|
||||
<summary style="font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; cursor: pointer; outline: none;" data-i18n="MANUAL_CONNECT_HEADER">Manual Connect / Advanced</summary>
|
||||
<div style="margin-top: 12px;">
|
||||
<div class="form-group">
|
||||
<label title="Select which KoalaSync server to use" data-i18n="LABEL_SERVER">Server</label>
|
||||
<div style="display:flex; gap:4px; margin-bottom:8px;">
|
||||
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_OFFICIAL" data-i18n-title="BTN_SERVER_OFFICIAL_TOOLTIP" title="Use the official reliable server">Official</button>
|
||||
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_CUSTOM" data-i18n-title="BTN_SERVER_CUSTOM_TOOLTIP" title="Connect to your own self-hosted server">Custom</button>
|
||||
<!-- Server selection — its own concern, kept visually separate from the room fields. -->
|
||||
<div style="background: var(--bg); border: 1px solid #334155; border-radius: 8px; padding: 10px; margin-bottom: 14px;">
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label title="Select which KoalaSync server to use" data-i18n="LABEL_SERVER">Server</label>
|
||||
<div style="display:flex; gap:4px; margin-bottom:0;">
|
||||
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_OFFICIAL" data-i18n-title="BTN_SERVER_OFFICIAL_TOOLTIP" title="Use the official reliable server">Official</button>
|
||||
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_CUSTOM" data-i18n-title="BTN_SERVER_CUSTOM_TOOLTIP" title="Connect to your own self-hosted server">Custom</button>
|
||||
</div>
|
||||
<input type="text" id="serverUrl" data-i18n-placeholder="PLACEHOLDER_SERVER_URL" placeholder="wss://your-server:3000" style="display:none; margin-top:8px;">
|
||||
</div>
|
||||
<input type="text" id="serverUrl" data-i18n-placeholder="PLACEHOLDER_SERVER_URL" placeholder="wss://your-server:3000" style="display:none;">
|
||||
</div>
|
||||
<!-- Room credentials — distinct group below the server picker. -->
|
||||
<div class="form-group">
|
||||
<label title="The unique identifier for your sync room" data-i18n="LABEL_ROOM_ID" data-i18n-title="LABEL_ROOM_ID_TOOLTIP">Room ID</label>
|
||||
<input type="text" id="roomId" data-i18n-placeholder="PLACEHOLDER_ROOM_ID" data-i18n-title="PLACEHOLDER_ROOM_ID_TOOLTIP" placeholder="Enter Room ID" title="The unique ID of the room you want to join">
|
||||
@@ -450,6 +454,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hostControlCard" class="info-card" style="margin-bottom: 20px; display:none;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<label style="margin:0;" title="Restrict who can control playback in this room" data-i18n="LABEL_HOST_CONTROL" data-i18n-title="LABEL_HOST_CONTROL_TOOLTIP">Host Control</label>
|
||||
<span id="hostRoleBadge" style="font-size:10px; font-weight:700; text-transform:uppercase; padding:2px 8px; border-radius:8px; background:var(--accent); color:#fff;"></span>
|
||||
</div>
|
||||
<div id="hostControlToggleRow" style="display:none; align-items:center; justify-content:space-between; gap:12px; margin-top:12px;">
|
||||
<span style="font-size:12px; color:var(--text); line-height:1.3;" data-i18n="LABEL_HOST_ONLY_TOGGLE">Only I can control playback</span>
|
||||
<label class="toggle-switch" style="flex:none;">
|
||||
<input type="checkbox" id="hostControlToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="hostControlCohostHint" style="display:none; margin-top:10px; padding-top:10px; border-top:1px solid #334155; font-size:11px; color:var(--text-muted); line-height:1.4;" data-i18n="NOTICE_COHOST_HINT">Tip: grant control to individual viewers in the participant list below.</div>
|
||||
<div id="hostControlGuestNote" style="display:none; margin-top:10px; font-size:12px; color:var(--text-muted);" data-i18n="NOTICE_HOST_CONTROLS">The host controls playback for everyone.</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label title="Other users currently connected to this room" data-i18n="LABEL_PEERS_IN_ROOM" data-i18n-title="LABEL_PEERS_IN_ROOM_TOOLTIP">Peers in Room</label>
|
||||
<div id="peerList" class="info-card">
|
||||
@@ -530,6 +550,25 @@
|
||||
<input type="text" id="username" data-i18n-placeholder="PLACEHOLDER_USERNAME" placeholder="Anonymous Koala" maxlength="20" style="width: 150px;">
|
||||
</div>
|
||||
|
||||
<details class="form-group" style="background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
|
||||
<summary style="cursor: pointer; font-size: 12px; font-weight: 700; color: var(--text);" title="Choose which titles are sent to the room." data-i18n="LABEL_PRIVACY_SETTINGS" data-i18n-title="LABEL_PRIVACY_SETTINGS_TOOLTIP">Privacy Settings</summary>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px;">
|
||||
<label style="margin-bottom: 0; cursor: help;" title="Send the selected browser tab title to the room." data-i18n="LABEL_SEND_TAB_TITLE" data-i18n-title="LABEL_SEND_TAB_TITLE_TOOLTIP">Send tab title</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="sendTabTitle" checked>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px;">
|
||||
<label style="margin-bottom: 0; cursor: help;" title="Choose how media titles are sent to the room." data-i18n="LABEL_MEDIA_TITLE_PRIVACY" data-i18n-title="LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP">Media title</label>
|
||||
<select id="mediaTitlePrivacyMode" style="width: 165px; background: var(--bg); border: 1px solid #334155; color: white; padding: 6px 10px; border-radius: 8px; font-size: 12px; cursor: pointer; outline: none; font-family: inherit;">
|
||||
<option value="full" data-i18n="OPTION_TITLE_PRIVACY_FULL">Send full title</option>
|
||||
<option value="episode" data-i18n="OPTION_TITLE_PRIVACY_EPISODE">Send episode ID</option>
|
||||
<option value="hidden" data-i18n="OPTION_TITLE_PRIVACY_HIDDEN">Do not send</option>
|
||||
</select>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
|
||||
<label style="margin-bottom: 0; cursor: help;" title="Filters out non-video tabs and unrelated domains to keep the list clean" data-i18n="LABEL_HIDE_CLUTTER" data-i18n-title="LABEL_HIDE_CLUTTER_TOOLTIP">Hide Clutter Tabs</label>
|
||||
<label class="toggle-switch">
|
||||
@@ -584,6 +623,8 @@
|
||||
<option value="nl">🇳🇱 Nederlands</option>
|
||||
<option value="ja">🇯🇵 日本語</option>
|
||||
<option value="ko">🇰🇷 한국어</option>
|
||||
<option value="zh">🇨🇳 中文</option>
|
||||
<option value="uk">🇺🇦 Українська</option>
|
||||
<option value="pt">🇵🇹 Português (Portugal)</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -600,7 +641,7 @@
|
||||
<div style="font-size: 9px; color: var(--text-muted); opacity: 0.6; margin-bottom: 6px; line-height: 1.3;" data-i18n="FOOTER_SUPPORT_PROMPT">Enjoying KoalaSync? Leave a review!</div>
|
||||
<a id="settingsReviewLink" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
|
||||
<span> · </span>
|
||||
<a id="settingsSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">☕ Buy me a coffee</a>
|
||||
<a id="settingsSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">Support KoalaSync</a>
|
||||
<span> · </span>
|
||||
<span id="settingsVersion">v0.0.0</span>
|
||||
</div>
|
||||
@@ -611,7 +652,10 @@
|
||||
<label title="Current WebSocket connection state" data-i18n="LABEL_CONN_STATUS" data-i18n-title="LABEL_CONN_STATUS_TOOLTIP">Connection Status</label>
|
||||
<div id="connStatus" class="info-card" style="display:flex; align-items:center; gap: 10px;">
|
||||
<span id="connDot" class="status-dot status-offline"></span>
|
||||
<span id="connText" style="flex:1;">Disconnected</span>
|
||||
<span style="flex:1; display:flex; align-items:center; gap:6px; min-width:0;">
|
||||
<span id="connText">Disconnected</span>
|
||||
<span id="connInfo" data-i18n-title="STATUS_IDLE_TOOLTIP" title="KoalaSync is ready. Join or create a room to connect and start syncing." style="display:none; cursor:help; flex:none; width:15px; height:15px; line-height:14px; text-align:center; border-radius:50%; border:1px solid var(--text-muted); color:var(--text-muted); font-size:10px; font-weight:700; font-style:normal;">i</span>
|
||||
</span>
|
||||
<span id="connPing" style="font-size:11px; font-family:monospace; font-weight:600; opacity:0.8;"></span>
|
||||
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;" title="Attempt to reconnect to the server" data-i18n="BTN_RETRY" data-i18n-title="BTN_RETRY_TOOLTIP">RETRY</button>
|
||||
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px; margin: 0;" title="Copy logs to clipboard for sharing" data-i18n="BTN_COPY_LOGS" data-i18n-title="BTN_COPY_LOGS_TOOLTIP">Copy Logs</button>
|
||||
@@ -641,7 +685,7 @@
|
||||
<div style="font-size: 9px; color: var(--text-muted); opacity: 0.6; margin-bottom: 6px; line-height: 1.3;" data-i18n="FOOTER_SUPPORT_PROMPT">Enjoying KoalaSync? Leave a review!</div>
|
||||
<a id="devReviewLink" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
|
||||
<span> · </span>
|
||||
<a id="devSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">☕ Buy me a coffee</a>
|
||||
<a id="devSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">Support KoalaSync</a>
|
||||
<span> · </span>
|
||||
<span id="appVersion">v0.0.0</span>
|
||||
</div>
|
||||
|
||||
+236
-23
@@ -2,6 +2,7 @@ import { EVENTS, OFFICIAL_LANDING_PAGE_URL, SUPPORT_URL, getReviewUrl } from './
|
||||
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
|
||||
import { getAvatarForName, generateUsername, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './shared/names.js';
|
||||
import { loadLocale, translateDOM, getMessage, getSystemLanguage } from './i18n.js';
|
||||
import { TITLE_PRIVACY_MODES, normalizeSendTabTitle } from './title-privacy.js';
|
||||
|
||||
|
||||
const elements = {
|
||||
@@ -16,6 +17,7 @@ const elements = {
|
||||
clearLogs: document.getElementById('clearLogs'),
|
||||
connDot: document.getElementById('connDot'),
|
||||
connText: document.getElementById('connText'),
|
||||
connInfo: document.getElementById('connInfo'),
|
||||
connPing: document.getElementById('connPing'),
|
||||
serverUrl: document.getElementById('serverUrl'),
|
||||
serverOfficial: document.getElementById('serverOfficial'),
|
||||
@@ -40,6 +42,12 @@ const elements = {
|
||||
retryBtn: document.getElementById('retryBtn'),
|
||||
sectionJoin: document.getElementById('section-join'),
|
||||
sectionActive: document.getElementById('section-active'),
|
||||
hostControlCard: document.getElementById('hostControlCard'),
|
||||
hostRoleBadge: document.getElementById('hostRoleBadge'),
|
||||
hostControlToggleRow: document.getElementById('hostControlToggleRow'),
|
||||
hostControlToggle: document.getElementById('hostControlToggle'),
|
||||
hostControlGuestNote: document.getElementById('hostControlGuestNote'),
|
||||
hostControlCohostHint: document.getElementById('hostControlCohostHint'),
|
||||
activeRoomId: document.getElementById('activeRoomId'),
|
||||
activeServer: document.getElementById('activeServer'),
|
||||
peerListSync: document.getElementById('peerListSync'),
|
||||
@@ -47,6 +55,8 @@ const elements = {
|
||||
playBtn: document.getElementById('playBtn'),
|
||||
pauseBtn: document.getElementById('pauseBtn'),
|
||||
autoSyncNextEpisode: document.getElementById('autoSyncNextEpisode'),
|
||||
sendTabTitle: document.getElementById('sendTabTitle'),
|
||||
mediaTitlePrivacyMode: document.getElementById('mediaTitlePrivacyMode'),
|
||||
episodeLobbyCard: document.getElementById('episodeLobbyCard'),
|
||||
lobbyTitle: document.getElementById('lobbyTitle'),
|
||||
lobbyPeerStatus: document.getElementById('lobbyPeerStatus'),
|
||||
@@ -175,19 +185,9 @@ function setRoomRefreshCooldown() {
|
||||
|
||||
// --- Initialization ---
|
||||
async function init() {
|
||||
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab']);
|
||||
// Migrate preferences from sync → local for existing users
|
||||
const oldSync = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings']);
|
||||
const toMigrate = {};
|
||||
for (const key of ['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings']) {
|
||||
if (localData[key] === undefined && oldSync[key] !== undefined) {
|
||||
toMigrate[key] = oldSync[key];
|
||||
localData[key] = oldSync[key];
|
||||
}
|
||||
}
|
||||
if (Object.keys(toMigrate).length) {
|
||||
await chrome.storage.local.set(toMigrate);
|
||||
}
|
||||
// Local-only by design — settings and room credentials never come from
|
||||
// storage.sync (only onboardingComplete + dismissedHints live there).
|
||||
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab']);
|
||||
|
||||
let activeLang = localData.locale;
|
||||
if (!activeLang) {
|
||||
@@ -212,6 +212,10 @@ async function init() {
|
||||
elements.username.value = username;
|
||||
if (elements.filterNoise) elements.filterNoise.checked = localData.filterNoise !== false;
|
||||
if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = localData.autoSyncNextEpisode !== false;
|
||||
const legacyTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.titlePrivacyMode) ? localData.titlePrivacyMode : TITLE_PRIVACY_MODES.FULL;
|
||||
const mediaTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.mediaTitlePrivacyMode) ? localData.mediaTitlePrivacyMode : legacyTitlePrivacyMode;
|
||||
if (elements.sendTabTitle) elements.sendTabTitle.checked = normalizeSendTabTitle(localData.sendTabTitle, legacyTitlePrivacyMode);
|
||||
if (elements.mediaTitlePrivacyMode) elements.mediaTitlePrivacyMode.value = mediaTitlePrivacyMode;
|
||||
if (elements.forceSyncMode) elements.forceSyncMode.value = localData.forceSyncMode || 'jump-to-others';
|
||||
if (elements.browserNotifications) elements.browserNotifications.checked = localData.browserNotifications === true;
|
||||
if (elements.autoCopyInvite) elements.autoCopyInvite.checked = localData.autoCopyInvite !== false;
|
||||
@@ -253,11 +257,12 @@ async function init() {
|
||||
updatePingDisplay(res.ping);
|
||||
updatePeerList(res.peers);
|
||||
lastKnownPeers = res.peers || [];
|
||||
updateHostControlUI({ controlMode: res.controlMode, amHost: res.amHost, amController: res.amController, controllers: res.controllers, hostPeerId: res.hostPeerId, hostControlSupported: res.hostControlSupported, coHostSupported: res.coHostSupported, inRoom: res.status === 'connected' });
|
||||
if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers);
|
||||
|
||||
// If user has a room configured but background is not connected,
|
||||
// If user has a room configured but background is not connected (disconnected or idle),
|
||||
// trigger connection now — the popup opening is explicit user intent.
|
||||
if (res.status === 'disconnected' && localData.roomId) {
|
||||
if ((res.status === 'disconnected' || res.status === 'idle') && localData.roomId) {
|
||||
chrome.runtime.sendMessage({ type: 'CONNECT' }).catch(() => {});
|
||||
applyConnectionStatus('connecting');
|
||||
}
|
||||
@@ -312,6 +317,98 @@ function toggleUIState(inRoom) {
|
||||
if (syncInactive) syncInactive.style.display = inRoom ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// --- Host Control Mode UI ---
|
||||
// True when we're a guest in a host-only room → remote-control buttons are locked.
|
||||
let hcmGuestLocked = false;
|
||||
|
||||
// Co-Host state mirrored for the peer-list renderer (promote/demote + role badges).
|
||||
let hcmAmOwner = false;
|
||||
let hcmControllers = [];
|
||||
let hcmCoHostSupported = false;
|
||||
let hcmOwnerPeerId = null;
|
||||
let hcmRoomIsHostOnly = false;
|
||||
|
||||
function updateHostControlUI(state) {
|
||||
const s = state || {};
|
||||
const controlMode = s.controlMode;
|
||||
const amHost = !!s.amHost;
|
||||
const amController = !!s.amController;
|
||||
const hostOnly = controlMode === 'host-only';
|
||||
|
||||
// Stash for the peer-list renderer, then force it to re-render with new roles.
|
||||
hcmAmOwner = amHost;
|
||||
hcmControllers = Array.isArray(s.controllers) ? s.controllers : [];
|
||||
hcmCoHostSupported = !!s.coHostSupported;
|
||||
hcmOwnerPeerId = s.hostPeerId || null;
|
||||
hcmRoomIsHostOnly = hostOnly;
|
||||
lastPeersJson = '';
|
||||
if (activePeers) updatePeerList(activePeers);
|
||||
|
||||
const card = elements.hostControlCard;
|
||||
if (!card) return;
|
||||
// Explicit capability advertised by the relay. Against an older relay it's
|
||||
// false/absent → feature unavailable, so hide the card instead of misleading UI.
|
||||
const serverSupportsHostControl = !!s.hostControlSupported;
|
||||
// Show the card only when meaningful: owner always (to manage); a non-controller
|
||||
// guest only while host-only is active. A guest in a normal room sees nothing.
|
||||
const show = s.inRoom && serverSupportsHostControl && (amHost || hostOnly);
|
||||
if (!show) {
|
||||
card.style.display = 'none';
|
||||
hcmGuestLocked = false;
|
||||
setRemoteControlsLocked(false);
|
||||
return;
|
||||
}
|
||||
card.style.display = 'block';
|
||||
if (elements.hostRoleBadge) {
|
||||
const role = amHost ? (getMessage('BADGE_HOST') || 'Host')
|
||||
: amController ? (getMessage('BADGE_CONTROLLER') || 'Controller')
|
||||
: (getMessage('BADGE_GUEST') || 'Guest');
|
||||
elements.hostRoleBadge.textContent = role;
|
||||
elements.hostRoleBadge.style.background = (amHost || amController) ? 'var(--accent)' : 'var(--text-muted)';
|
||||
}
|
||||
if (elements.hostControlToggleRow) elements.hostControlToggleRow.style.display = amHost ? 'flex' : 'none';
|
||||
if (elements.hostControlToggle) elements.hostControlToggle.checked = hostOnly;
|
||||
if (elements.hostControlGuestNote) elements.hostControlGuestNote.style.display = (!amController && hostOnly) ? 'block' : 'none';
|
||||
// Owner-only discoverability hint: co-hosting is granted down in the peer list,
|
||||
// which isn't obvious from this card. Only show it when it's actionable — owner,
|
||||
// relay supports co-host, and the room is locked to host-only (the buttons only
|
||||
// render in that mode).
|
||||
if (elements.hostControlCohostHint) elements.hostControlCohostHint.style.display = (amHost && !!s.coHostSupported && hostOnly) ? 'block' : 'none';
|
||||
|
||||
// Only a non-controller is locked out of the remote controls.
|
||||
hcmGuestLocked = (!amController && hostOnly);
|
||||
setRemoteControlsLocked(hcmGuestLocked);
|
||||
}
|
||||
|
||||
function setRemoteControlsLocked(locked) {
|
||||
[elements.playBtn, elements.pauseBtn, elements.forceSyncBtn].forEach(btn => {
|
||||
if (!btn) return;
|
||||
btn.disabled = locked;
|
||||
btn.style.opacity = locked ? '0.5' : '';
|
||||
btn.style.cursor = locked ? 'not-allowed' : '';
|
||||
btn.title = locked ? (getMessage('NOTICE_HOST_CONTROLS') || 'The host controls playback for everyone.') : '';
|
||||
});
|
||||
// Always restore the default labels. The action handlers leave the text in a
|
||||
// transitional state ("Playing..." / "Pausing...") and the 2.5s safety reset
|
||||
// skips the refresh while guest-locked — so reset on BOTH transitions: on lock
|
||||
// (host enabled host-only just as the guest clicked → don't freeze "Playing...")
|
||||
// and on unlock (L-1).
|
||||
if (elements.playBtn) elements.playBtn.textContent = getMessage('BTN_PLAY') || 'Play';
|
||||
if (elements.pauseBtn) elements.pauseBtn.textContent = getMessage('BTN_PAUSE') || 'Pause';
|
||||
}
|
||||
|
||||
if (elements.hostControlToggle) {
|
||||
elements.hostControlToggle.addEventListener('change', () => {
|
||||
const mode = elements.hostControlToggle.checked ? 'host-only' : 'everyone';
|
||||
chrome.runtime.sendMessage({ type: 'SET_CONTROL_MODE', controlMode: mode }, (res) => {
|
||||
// Server broadcasts CONTROL_MODE back to refresh the UI; revert on failure.
|
||||
if (chrome.runtime.lastError || !res || res.status !== 'ok') {
|
||||
elements.hostControlToggle.checked = (mode === 'everyone');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateUI(roomId, password, useCustomServer = false, serverUrl = '') {
|
||||
const inRoom = !!roomId;
|
||||
toggleUIState(inRoom);
|
||||
@@ -586,6 +683,11 @@ function updatePeerList(peers) {
|
||||
|
||||
header.appendChild(nameSpan);
|
||||
|
||||
// Right-side badges + actions, kept in one group so they sit together
|
||||
// on the right instead of being scattered by the header's space-between.
|
||||
const rightGroup = document.createElement('div');
|
||||
rightGroup.style.cssText = 'display:flex; align-items:center; gap:6px; flex-shrink:0;';
|
||||
|
||||
// Volume Icon (Top Right)
|
||||
if (p.volume !== undefined && p.volume !== null) {
|
||||
const volIcon = document.createElement('div');
|
||||
@@ -599,9 +701,57 @@ function updatePeerList(peers) {
|
||||
const you = document.createElement('span');
|
||||
you.style.cssText = 'font-size:10px; color:var(--accent); font-weight:bold;';
|
||||
you.textContent = getMessage('LABEL_YOU') || 'YOU';
|
||||
header.appendChild(you);
|
||||
rightGroup.appendChild(you);
|
||||
}
|
||||
|
||||
// Host Control Mode: show "Solo" badge for peers watching on their own.
|
||||
if (typeof p === 'object' && p.desynced) {
|
||||
const solo = document.createElement('span');
|
||||
solo.style.cssText = 'font-size:10px; color:#fff; background:#b45309; padding:2px 6px; border-radius:6px; font-weight:600;';
|
||||
const soloText = getMessage('BADGE_DESYNCED') || 'Solo';
|
||||
solo.textContent = soloText;
|
||||
solo.title = getMessage('TOOLTIP_PEER_DESYNCED') || soloText;
|
||||
rightGroup.appendChild(solo);
|
||||
}
|
||||
|
||||
// Co-Host: role badges + the owner's promote/demote control. Only meaningful
|
||||
// once the room is locked to host-only — in 'everyone' mode anyone can already
|
||||
// control, so roles are moot and we show nothing (no badge/button noise).
|
||||
// The promote/demote button additionally requires owner privileges (below).
|
||||
const showRoles = !!hcmOwnerPeerId && hcmRoomIsHostOnly;
|
||||
if (showRoles) {
|
||||
const isOwner = pId === hcmOwnerPeerId;
|
||||
const isController = !isOwner && hcmControllers.includes(pId);
|
||||
if (isOwner || isController) {
|
||||
const roleBadge = document.createElement('span');
|
||||
roleBadge.style.cssText = 'font-size:10px; color:#fff; background:var(--accent); padding:2px 6px; border-radius:6px; font-weight:600;';
|
||||
roleBadge.textContent = isOwner ? (getMessage('BADGE_HOST') || 'Host') : (getMessage('BADGE_CONTROLLER') || 'Controller');
|
||||
rightGroup.appendChild(roleBadge);
|
||||
}
|
||||
// Owner can promote/demote any non-owner peer.
|
||||
if (hcmAmOwner && hcmCoHostSupported && !isOwner) {
|
||||
const btn = document.createElement('button');
|
||||
const revoke = isController;
|
||||
btn.style.cssText = `font-size:10px; padding:3px 9px; border-radius:6px; cursor:pointer; white-space:nowrap; font-weight:600; transition:background .15s,color .15s; border:1px solid ${revoke ? 'var(--text-muted)' : 'var(--accent)'}; background:transparent; color:${revoke ? 'var(--text-muted)' : 'var(--accent)'};`;
|
||||
btn.textContent = revoke ? (getMessage('BTN_REVOKE_CONTROL') || 'Revoke') : (getMessage('BTN_GIVE_CONTROL') || 'Give control');
|
||||
btn.title = revoke ? (getMessage('BTN_REVOKE_CONTROL') || 'Revoke') : (getMessage('BTN_GIVE_CONTROL') || 'Give control');
|
||||
btn.addEventListener('mouseenter', () => {
|
||||
btn.style.background = revoke ? 'var(--text-muted)' : 'var(--accent)';
|
||||
btn.style.color = '#fff';
|
||||
});
|
||||
btn.addEventListener('mouseleave', () => {
|
||||
btn.style.background = 'transparent';
|
||||
btn.style.color = revoke ? 'var(--text-muted)' : 'var(--accent)';
|
||||
});
|
||||
btn.addEventListener('click', () => {
|
||||
chrome.runtime.sendMessage({ type: 'SET_PEER_ROLE', peerId: pId, controller: !isController }, () => {});
|
||||
});
|
||||
rightGroup.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
if (rightGroup.childNodes.length) header.appendChild(rightGroup);
|
||||
|
||||
peerItem.appendChild(header);
|
||||
|
||||
// Media Info
|
||||
@@ -828,19 +978,33 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
|
||||
}
|
||||
|
||||
function applyConnectionStatus(status) {
|
||||
// Coerce 'idle' to 'disconnected' if a room is actively configured.
|
||||
// An 'idle' status with a configured room indicates the background
|
||||
// worker dropped connection intent due to a server error.
|
||||
if (status === 'idle' && elements.sectionActive && elements.sectionActive.style.display !== 'none') {
|
||||
status = 'disconnected';
|
||||
}
|
||||
|
||||
const connected = status === 'connected';
|
||||
const connecting = status === 'connecting';
|
||||
const reconnecting = status === 'reconnecting';
|
||||
// 'idle' = not in a room and not trying to connect. This is the normal
|
||||
// resting state (lazy connect), NOT an error — surface it neutrally.
|
||||
const idle = status === 'idle';
|
||||
|
||||
if (elements.connDot) {
|
||||
elements.connDot.className = 'status-dot ' + (connected ? 'status-online' : ((connecting || reconnecting) ? 'status-online' : 'status-offline'));
|
||||
|
||||
|
||||
if (reconnecting) {
|
||||
elements.connDot.style.background = '#f59e0b';
|
||||
elements.connDot.style.boxShadow = '0 0 8px #f59e0b';
|
||||
} else if (connecting) {
|
||||
elements.connDot.style.background = '#fbbf24';
|
||||
elements.connDot.style.boxShadow = '0 0 8px #fbbf24';
|
||||
} else if (idle) {
|
||||
// Neutral grey — ready, not failed.
|
||||
elements.connDot.style.background = '#9ca3af';
|
||||
elements.connDot.style.boxShadow = 'none';
|
||||
} else if (!connected) {
|
||||
elements.connDot.style.background = '#ef4444';
|
||||
elements.connDot.style.boxShadow = 'none';
|
||||
@@ -851,7 +1015,12 @@ function applyConnectionStatus(status) {
|
||||
}
|
||||
|
||||
if (elements.connText) {
|
||||
elements.connText.textContent = connected ? getMessage('STATUS_CONNECTED') : (reconnecting ? getMessage('STATUS_RECONNECTING') : (connecting ? getMessage('STATUS_CONNECTING') : getMessage('STATUS_DISCONNECTED')));
|
||||
elements.connText.textContent = connected ? getMessage('STATUS_CONNECTED') : (reconnecting ? getMessage('STATUS_RECONNECTING') : (connecting ? getMessage('STATUS_CONNECTING') : (idle ? getMessage('STATUS_IDLE') : getMessage('STATUS_DISCONNECTED'))));
|
||||
}
|
||||
|
||||
// Show the info "i" + tooltip only in the idle state.
|
||||
if (elements.connInfo) {
|
||||
elements.connInfo.style.display = idle ? '' : 'none';
|
||||
}
|
||||
if (!connected) {
|
||||
updatePingDisplay(null);
|
||||
@@ -1060,6 +1229,22 @@ elements.autoSyncNextEpisode.addEventListener('change', () => {
|
||||
chrome.storage.local.set({ autoSyncNextEpisode: elements.autoSyncNextEpisode.checked });
|
||||
});
|
||||
|
||||
if (elements.sendTabTitle) {
|
||||
elements.sendTabTitle.addEventListener('change', () => {
|
||||
chrome.storage.local.set({ sendTabTitle: elements.sendTabTitle.checked }, () => {
|
||||
chrome.runtime.sendMessage({ type: 'TITLE_PRIVACY_CHANGED' }).catch(() => {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.mediaTitlePrivacyMode) {
|
||||
elements.mediaTitlePrivacyMode.addEventListener('change', () => {
|
||||
chrome.storage.local.set({ mediaTitlePrivacyMode: elements.mediaTitlePrivacyMode.value }, () => {
|
||||
chrome.runtime.sendMessage({ type: 'TITLE_PRIVACY_CHANGED' }).catch(() => {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
elements.browserNotifications.addEventListener('change', () => {
|
||||
chrome.storage.local.set({ browserNotifications: elements.browserNotifications.checked });
|
||||
});
|
||||
@@ -1135,7 +1320,11 @@ if (elements.langSelector) {
|
||||
elements.serverUrl.addEventListener('change', () => {
|
||||
let url = elements.serverUrl.value.trim();
|
||||
if (url && !url.includes('://')) {
|
||||
url = 'ws://' + url;
|
||||
// Default to secure wss:// — plain ws:// is only valid for a local relay
|
||||
// (and the background rejects/upgrades ws:// for any remote host anyway).
|
||||
const host = url.split('/')[0].split(':')[0].toLowerCase();
|
||||
const isLocal = host === 'localhost' || host === '127.0.0.1';
|
||||
url = (isLocal ? 'ws://' : 'wss://') + url;
|
||||
elements.serverUrl.value = url;
|
||||
chrome.storage.local.set({ serverUrl: url });
|
||||
}
|
||||
@@ -1252,7 +1441,7 @@ elements.joinBtn.addEventListener('click', async () => {
|
||||
}
|
||||
if (useCustom && serverUrl) {
|
||||
try {
|
||||
const urlToCheck = serverUrl.includes('://') ? serverUrl : 'ws://' + serverUrl;
|
||||
const urlToCheck = serverUrl.includes('://') ? serverUrl : 'wss://' + serverUrl;
|
||||
new URL(urlToCheck);
|
||||
} catch (_e) {
|
||||
showError(getMessage('ERR_INVALID_SERVER_URL'));
|
||||
@@ -1356,6 +1545,7 @@ elements.targetTab.addEventListener('change', () => {
|
||||
});
|
||||
|
||||
elements.forceSyncBtn.addEventListener('click', async () => {
|
||||
if (hcmGuestLocked) return; // guest in host-only room — backstop (M-2/L-3)
|
||||
if (elements.forceSyncBtn.disabled) return;
|
||||
|
||||
const originalText = elements.forceSyncBtn.textContent;
|
||||
@@ -1397,7 +1587,10 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
||||
const peerCount = (status.peers || []).filter(p => (typeof p === 'object' ? p.peerId : p) !== localPeerId).length;
|
||||
const syncTimeoutMs = peerCount === 0 ? 3000 : 12000;
|
||||
const forceSyncReset = () => {
|
||||
if (!forceSyncDone) {
|
||||
// Don't unlock a button that's locked because we became a guest mid-flight —
|
||||
// hcmGuestLocked is the source of truth for the lock state, and the next
|
||||
// CONTROL_MODE update restores the correct label.
|
||||
if (!forceSyncDone && !hcmGuestLocked) {
|
||||
elements.forceSyncBtn.disabled = false;
|
||||
elements.forceSyncBtn.textContent = originalText;
|
||||
}
|
||||
@@ -1444,6 +1637,7 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
||||
});
|
||||
|
||||
elements.playBtn.addEventListener('click', () => {
|
||||
if (hcmGuestLocked) return; // guest in host-only room — backstop
|
||||
if (!elements.targetTab.value) {
|
||||
showToast(getMessage('ERR_SELECT_VIDEO'), 'warning');
|
||||
return;
|
||||
@@ -1462,7 +1656,7 @@ elements.playBtn.addEventListener('click', () => {
|
||||
});
|
||||
// Safety reset: restore button after 2.5s in case no peers respond
|
||||
setTimeout(() => {
|
||||
if (elements.playBtn.disabled) {
|
||||
if (elements.playBtn.disabled && !hcmGuestLocked) {
|
||||
elements.playBtn.textContent = getMessage('BTN_PLAY');
|
||||
elements.playBtn.disabled = false;
|
||||
}
|
||||
@@ -1470,6 +1664,7 @@ elements.playBtn.addEventListener('click', () => {
|
||||
});
|
||||
|
||||
elements.pauseBtn.addEventListener('click', () => {
|
||||
if (hcmGuestLocked) return; // guest in host-only room — backstop
|
||||
if (!elements.targetTab.value) {
|
||||
showToast(getMessage('ERR_SELECT_VIDEO'), 'warning');
|
||||
return;
|
||||
@@ -1488,7 +1683,7 @@ elements.pauseBtn.addEventListener('click', () => {
|
||||
});
|
||||
// Safety reset: restore button after 2.5s in case no peers respond
|
||||
setTimeout(() => {
|
||||
if (elements.pauseBtn.disabled) {
|
||||
if (elements.pauseBtn.disabled && !hcmGuestLocked) {
|
||||
elements.pauseBtn.textContent = getMessage('BTN_PAUSE');
|
||||
elements.pauseBtn.disabled = false;
|
||||
}
|
||||
@@ -1501,6 +1696,20 @@ elements.clearLogs.addEventListener('click', () => {
|
||||
});
|
||||
});
|
||||
|
||||
if (elements.regenId) {
|
||||
elements.regenId.addEventListener('click', () => {
|
||||
elements.regenId.disabled = true;
|
||||
chrome.runtime.sendMessage({ type: 'REGENERATE_ID' }, (res) => {
|
||||
elements.regenId.disabled = false;
|
||||
if (chrome.runtime.lastError || !res || !res.peerId) {
|
||||
showToast(getMessage('TOAST_ID_REGENERATED') || 'Failed to regenerate identity', 'error');
|
||||
return;
|
||||
}
|
||||
showToast(getMessage('TOAST_ID_REGENERATED') || 'Identity regenerated — reconnecting…', 'success', 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
elements.copyInvite.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(elements.inviteLink.value).then(() => {
|
||||
const original = elements.copyInvite.textContent;
|
||||
@@ -1646,6 +1855,9 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||
} else if (msg.type === 'PEER_UPDATE') {
|
||||
updatePeerList(msg.peers);
|
||||
if (msg.peers) detectPeerChanges(msg.peers);
|
||||
} else if (msg.type === 'CONTROL_MODE') {
|
||||
const inRoom = elements.sectionActive && elements.sectionActive.style.display === 'block';
|
||||
updateHostControlUI({ controlMode: msg.controlMode, amHost: msg.amHost, amController: msg.amController, controllers: msg.controllers, hostPeerId: msg.hostPeerId, hostControlSupported: msg.hostControlSupported, coHostSupported: msg.coHostSupported, inRoom });
|
||||
} else if (msg.type === 'CONNECTION_STATUS') {
|
||||
if (msg.status === 'connected' || msg.status === 'disconnected') {
|
||||
if (joinBtnTimeout) { clearTimeout(joinBtnTimeout); joinBtnTimeout = null; }
|
||||
@@ -1663,6 +1875,7 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (res.peers) updatePeerList(res.peers);
|
||||
if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers);
|
||||
updatePingDisplay(res.ping);
|
||||
updateHostControlUI({ controlMode: res.controlMode, amHost: res.amHost, amController: res.amController, controllers: res.controllers, hostPeerId: res.hostPeerId, hostControlSupported: res.hostControlSupported, coHostSupported: res.coHostSupported, inRoom: true });
|
||||
});
|
||||
}
|
||||
if (msg.status === 'disconnected') {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { extractEpisodeId } from './episode-utils.js';
|
||||
|
||||
export const TITLE_PRIVACY_MODES = Object.freeze({
|
||||
FULL: 'full',
|
||||
EPISODE: 'episode',
|
||||
HIDDEN: 'hidden'
|
||||
});
|
||||
|
||||
export function normalizeTitlePrivacyMode(mode) {
|
||||
return Object.values(TITLE_PRIVACY_MODES).includes(mode)
|
||||
? mode
|
||||
: TITLE_PRIVACY_MODES.FULL;
|
||||
}
|
||||
|
||||
export function normalizeSendTabTitle(sendTabTitle, legacyMode = TITLE_PRIVACY_MODES.FULL) {
|
||||
if (typeof sendTabTitle === 'boolean') return sendTabTitle;
|
||||
return normalizeTitlePrivacyMode(legacyMode) === TITLE_PRIVACY_MODES.FULL;
|
||||
}
|
||||
|
||||
export function sanitizeTabTitle(title, sendTabTitle) {
|
||||
if (!sendTabTitle) return null;
|
||||
return typeof title === 'string' && title.length > 0 ? title : null;
|
||||
}
|
||||
|
||||
export function sanitizeSharedTitle(title, mode) {
|
||||
const normalizedMode = normalizeTitlePrivacyMode(mode);
|
||||
if (normalizedMode === TITLE_PRIVACY_MODES.HIDDEN) return null;
|
||||
|
||||
if (typeof title !== 'string' || title.length === 0) return null;
|
||||
if (normalizedMode === TITLE_PRIVACY_MODES.EPISODE) {
|
||||
return extractEpisodeId(title) || null;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
export function applyTitlePrivacyToPayload(payload, mode, keys = ['mediaTitle', 'expectedTitle', 'title']) {
|
||||
const source = payload && typeof payload === 'object' ? payload : {};
|
||||
const next = { ...source };
|
||||
keys.forEach(key => {
|
||||
if (Object.prototype.hasOwnProperty.call(next, key)) {
|
||||
next[key] = sanitizeSharedTitle(next[key], mode);
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "2.4.1",
|
||||
"version": "2.5.0",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -8,6 +8,7 @@
|
||||
"build:extension": "node scripts/build-extension.cjs",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "node scripts/verify-release.mjs",
|
||||
"verify": "node scripts/verify-release.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -128,7 +128,8 @@ const chromeLocalesDir = path.join(__dirname, '..', 'extension', '_locales');
|
||||
const chromeLocaleMap = {
|
||||
'en': 'en', 'de': 'de', 'fr': 'fr', 'es': 'es', 'it': 'it',
|
||||
'ja': 'ja', 'ko': 'ko', 'nl': 'nl', 'pl': 'pl',
|
||||
'pt-BR': 'pt_BR', 'pt': 'pt_PT', 'ru': 'ru', 'tr': 'tr'
|
||||
'pt-BR': 'pt_BR', 'pt': 'pt_PT', 'ru': 'ru', 'tr': 'tr',
|
||||
'zh': 'zh_CN', 'uk': 'uk'
|
||||
};
|
||||
|
||||
// Read SUPPORTED_LANGUAGES from i18n.js
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
roomListCooldowns,
|
||||
rateLimitDenied,
|
||||
startRateLimitCleanup,
|
||||
stopRateLimitCleanup
|
||||
stopRateLimitCleanup,
|
||||
CONNECTION_RATE_LIMIT,
|
||||
EVENT_RATE_LIMIT
|
||||
} from '../server/rate-limiter.js';
|
||||
|
||||
// Helper: mock io for cleanup
|
||||
@@ -31,8 +33,9 @@ function reset() {
|
||||
// --- checkConnectionRate ---
|
||||
reset();
|
||||
assert.equal(checkConnectionRate('1.1.1.1'), true, 'first connection allowed');
|
||||
for (let i = 0; i < 9; i++) checkConnectionRate('1.1.1.1');
|
||||
assert.equal(checkConnectionRate('1.1.1.1'), false, '11th connection blocked');
|
||||
// Exhaust the rest of the budget (first call above counted as 1).
|
||||
for (let i = 0; i < CONNECTION_RATE_LIMIT - 1; i++) checkConnectionRate('1.1.1.1');
|
||||
assert.equal(checkConnectionRate('1.1.1.1'), false, `connection beyond ${CONNECTION_RATE_LIMIT}/window blocked`);
|
||||
assert.equal(rateLimitDenied.connections, 1, 'denial counter incremented');
|
||||
|
||||
reset();
|
||||
@@ -41,8 +44,9 @@ assert.equal(checkConnectionRate('2.2.2.2'), true, 'separate IP independent');
|
||||
// --- checkEventRate ---
|
||||
reset();
|
||||
assert.equal(checkEventRate('sock1'), true, 'first event allowed');
|
||||
for (let i = 0; i < 29; i++) checkEventRate('sock1');
|
||||
assert.equal(checkEventRate('sock1'), false, '31st event blocked');
|
||||
// Exhaust the rest of the budget (first call above counted as 1).
|
||||
for (let i = 0; i < EVENT_RATE_LIMIT - 1; i++) checkEventRate('sock1');
|
||||
assert.equal(checkEventRate('sock1'), false, `event beyond ${EVENT_RATE_LIMIT}/window blocked`);
|
||||
assert.equal(rateLimitDenied.events, 1);
|
||||
|
||||
reset();
|
||||
|
||||
+316
-1
@@ -3,6 +3,7 @@ import http from 'node:http';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
import { connectionCounts, clearRateLimitMaps } from '../server/rate-limiter.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(path.join(__dirname, '..', 'server', 'package.json'));
|
||||
@@ -23,6 +24,9 @@ function a(ws) { if (ws._m.length) { const r=ws._m.shift(); return r.startsWith(
|
||||
async function w(ws, evt, ms=3000) { const st=Date.now(); while(Date.now()-st<ms) { for(let i=0;i<ws._m.length;i++){const r=ws._m[i];ws._m.splice(i,1);if(r.startsWith('42')){try{const[e]=JSON.parse(r.substring(2));if(e===evt)return e}catch{/* skip */}}} await new Promise(r=>setTimeout(r,50));} throw Error(`wait:${evt}`); }
|
||||
async function j(ws, rid, pid, pw=null) { s(ws,'join_room',{roomId:rid,peerId:pid,password:pw,protocolVersion:'1.0.0'}); assert.equal((await a(ws))[0],'room_data'); }
|
||||
function close() { clients.forEach(w=>{try{w.close()}catch{/* ignore */}}); clients.length=0; }
|
||||
// Test suite opens >10 connections/min — clear the IP connection counter so the
|
||||
// connection rate limiter doesn't mask test failures (test-only, never at runtime).
|
||||
function resetConnectionRate() { connectionCounts.clear(); clearRateLimitMaps(); }
|
||||
|
||||
try {
|
||||
process.env.ADMIN_METRICS_TOKEN = 'ws-integration-test-32chars-minimum!';
|
||||
@@ -57,6 +61,317 @@ try {
|
||||
s(p1,'leave_room',{}); const [ev,d]=await a(p2); assert.equal(ev,'peer_status');assert.equal(d.status,'left');
|
||||
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- Capabilities: ROOM_DATA advertises server features for client detection ---
|
||||
const capClient = await c();
|
||||
s(capClient, 'join_room', { roomId: 'cap-'+Date.now(), peerId: 'capp', protocolVersion: '1.0.0' });
|
||||
const [capEv, capData] = await a(capClient);
|
||||
assert.equal(capEv, 'room_data');
|
||||
assert.ok(Array.isArray(capData.capabilities) && capData.capabilities.includes('host-control'),
|
||||
'ROOM_DATA advertises the host-control capability');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- Default 'everyone' mode does NOT gate anyone (host-control OFF = unchanged) ---
|
||||
// Confirms that with host-only off, a non-host guest can still drive every
|
||||
// room-moving event exactly like before the feature existed.
|
||||
const erid = 'every-'+Date.now();
|
||||
const e1 = await c(), e2 = await c(); // e1 = creator/host, e2 = guest
|
||||
await j(e1, erid, 'ehost'); await j(e2, erid, 'eguest'); e1._m.length = e2._m.length = 0;
|
||||
s(e2,'play',{currentTime:1}); await w(e1,'play');
|
||||
s(e2,'pause',{currentTime:2}); await w(e1,'pause');
|
||||
s(e2,'seek',{currentTime:3}); await w(e1,'seek');
|
||||
s(e2,'force_sync_prepare',{targetTime:0}); await w(e1,'force_sync_prepare');
|
||||
s(e2,'episode_lobby',{expectedTitle:'S1E1'}); await w(e1,'episode_lobby');
|
||||
// (reaching here without a wait timeout == nothing was gated)
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- Host Control Mode ---
|
||||
const hrid = 'host-'+Date.now();
|
||||
const h1 = await c(), h2 = await c(); // h1 = host (first joiner), h2 = guest
|
||||
await j(h1, hrid, 'host1'); await j(h2, hrid, 'guest1'); h1._m.length = h2._m.length = 0;
|
||||
|
||||
// Host enables host-only -> both peers get the control_mode broadcast
|
||||
s(h1,'set_control_mode',{controlMode:'host-only'});
|
||||
await w(h1,'control_mode'); await w(h2,'control_mode');
|
||||
h1._m.length = h2._m.length = 0;
|
||||
|
||||
// Guest's room-moving event (pause) is dropped -> host must NOT receive it
|
||||
s(h2,'pause',{currentTime:5});
|
||||
let guestPauseDropped = false; try { await w(h1,'pause',600); } catch { guestPauseDropped = true; }
|
||||
assert.ok(guestPauseDropped, 'guest pause dropped in host-only');
|
||||
|
||||
// Host's own pause still relays to the guest
|
||||
s(h1,'pause',{currentTime:7}); await w(h2,'pause');
|
||||
h1._m.length = h2._m.length = 0;
|
||||
|
||||
// desynced flag is relayed through PEER_STATUS heartbeats so the host's UI
|
||||
// can show "Solo" for guests watching on their own.
|
||||
s(h2,'peer_status',{status:'heartbeat',desynced:true,currentTime:42,playbackState:'playing'});
|
||||
let hbData = null; const hbStart = Date.now();
|
||||
while (Date.now()-hbStart < 600 && !hbData) {
|
||||
for (let i=0;i<h1._m.length;i++){ const r=h1._m[i]; if(r.startsWith('42')){ const [e,dd]=JSON.parse(r.substring(2)); if(e==='peer_status'){ h1._m.splice(i,1); hbData=dd; break; } } }
|
||||
await new Promise(r=>setTimeout(r,30));
|
||||
}
|
||||
assert.ok(hbData && hbData.desynced === true, 'desynced=true relayed in heartbeat');
|
||||
h1._m.length = h2._m.length = 0;
|
||||
|
||||
// Guest cannot change the control mode -> host must NOT receive a broadcast.
|
||||
// The rejected sender gets a unicast of the *actual* state so any optimistic
|
||||
// UI reverts (H-5); assert both halves.
|
||||
s(h2,'set_control_mode',{controlMode:'everyone'});
|
||||
let guestSetBlocked = false; try { await w(h1,'control_mode',600); } catch { guestSetBlocked = true; }
|
||||
assert.ok(guestSetBlocked, 'non-host cannot set control mode');
|
||||
let rejectSync = null; const rsStart = Date.now();
|
||||
while (Date.now()-rsStart < 600 && !rejectSync) {
|
||||
for (let i=0;i<h2._m.length;i++){ const r=h2._m[i]; if(r.startsWith('42')){ const [e,dd]=JSON.parse(r.substring(2)); if(e==='control_mode'){ h2._m.splice(i,1); rejectSync=dd; break; } } }
|
||||
await new Promise(r=>setTimeout(r,30));
|
||||
}
|
||||
assert.ok(rejectSync && rejectSync.controlMode==='host-only' && rejectSync.hostPeerId==='host1',
|
||||
'rejected sender is re-synced to actual state');
|
||||
h1._m.length = h2._m.length = 0;
|
||||
|
||||
// Host leaves -> room falls back to 'everyone' and reassigns host to the guest
|
||||
s(h1,'leave_room',{});
|
||||
let fb = null; const fbStart = Date.now();
|
||||
while (Date.now()-fbStart < 2000 && !fb) {
|
||||
for (let i=0;i<h2._m.length;i++){ const r=h2._m[i]; if(r.startsWith('42')){ const [e,dd]=JSON.parse(r.substring(2)); if(e==='control_mode'){ h2._m.splice(i,1); fb=dd; break; } } }
|
||||
await new Promise(r=>setTimeout(r,50));
|
||||
}
|
||||
assert.ok(fb && fb.controlMode==='everyone' && fb.hostPeerId==='guest1', 'host leave -> fallback everyone + new host');
|
||||
close();
|
||||
|
||||
// --- M-4: rapid control-mode toggles are debounced per-room ---
|
||||
const drid = 'debounce-'+Date.now();
|
||||
const db1 = await c(), db2 = await c();
|
||||
await j(db1, drid, 'dhost'); await j(db2, drid, 'dguest'); db1._m.length = db2._m.length = 0;
|
||||
|
||||
// First toggle (everyone → host-only) goes through.
|
||||
s(db1,'set_control_mode',{controlMode:'host-only'});
|
||||
await w(db1,'control_mode'); await w(db2,'control_mode');
|
||||
db1._m.length = db2._m.length = 0;
|
||||
|
||||
// Immediate second toggle (host-only → everyone) should be debounced:
|
||||
// broadcast goes to neither peer, but sender gets a re-sync unicast.
|
||||
s(db1,'set_control_mode',{controlMode:'everyone'});
|
||||
let dGuestGotIt = false; try { await w(db2,'control_mode',600); } catch { dGuestGotIt = true; }
|
||||
assert.ok(dGuestGotIt, 'rapid control-mode toggle is debounced (no broadcast)');
|
||||
let dSenderResync = null; const dsStart = Date.now();
|
||||
while (Date.now()-dsStart < 600 && !dSenderResync) {
|
||||
for (let i=0;i<db1._m.length;i++){ const r=db1._m[i]; if(r.startsWith('42')){ const [e,dd]=JSON.parse(r.substring(2)); if(e==='control_mode'){ db1._m.splice(i,1); dSenderResync=dd; break; } } }
|
||||
await new Promise(r=>setTimeout(r,30));
|
||||
}
|
||||
assert.ok(dSenderResync && dSenderResync.controlMode==='host-only',
|
||||
'debounced toggle re-syncs sender to actual state');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- Host role survives peerId dedup (reconnect / second tab) ---
|
||||
const hdrid = 'dedup-host-'+Date.now();
|
||||
const hd1 = await c(), hd2 = await c();
|
||||
await j(hd1, hdrid, 'dhost'); await j(hd2, hdrid, 'dguest'); hd1._m.length = hd2._m.length = 0;
|
||||
s(hd1,'set_control_mode',{controlMode:'host-only'});
|
||||
await w(hd1,'control_mode'); await w(hd2,'control_mode');
|
||||
hd1._m.length = hd2._m.length = 0;
|
||||
// The host's peerId re-joins on a fresh socket → server dedupes the old socket.
|
||||
// This must NOT demote the host or reset the mode (a network blip / second tab).
|
||||
const hd3 = await c();
|
||||
s(hd3,'join_room',{roomId:hdrid,peerId:'dhost',protocolVersion:'1.0.0'});
|
||||
const hdrd = await a(hd3);
|
||||
assert.equal(hdrd[0],'room_data');
|
||||
assert.ok(hdrd[1].controlMode === 'host-only' && hdrd[1].hostPeerId === 'dhost',
|
||||
'host role + host-only mode survive peerId dedup (reconnect/second tab)');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- Co-Host: owner promotes a guest to controller (can drive); demote re-gates ---
|
||||
const crid = 'cohost-'+Date.now();
|
||||
const co1 = await c(), co2 = await c(), co3 = await c(); // owner / to-promote / stays guest
|
||||
await j(co1, crid, 'owner'); await j(co2, crid, 'cohost'); await j(co3, crid, 'guestx');
|
||||
co1._m.length = co2._m.length = co3._m.length = 0;
|
||||
s(co1,'set_control_mode',{controlMode:'host-only'});
|
||||
await w(co1,'control_mode'); await w(co2,'control_mode'); await w(co3,'control_mode');
|
||||
co1._m.length = co2._m.length = co3._m.length = 0;
|
||||
// before promotion the co-host is gated
|
||||
s(co2,'pause',{currentTime:1});
|
||||
let coGatedBefore=false; try { await w(co1,'pause',500); } catch { coGatedBefore=true; }
|
||||
assert.ok(coGatedBefore, 'co-host gated before promotion');
|
||||
// owner promotes co-host → controllers broadcast includes owner + cohost
|
||||
s(co1,'set_peer_role',{peerId:'cohost',controller:true});
|
||||
let promo=null; const pps=Date.now();
|
||||
while(Date.now()-pps<800 && !promo){ for(let i=0;i<co2._m.length;i++){const r=co2._m[i];if(r.startsWith('42')){const[e,dd]=JSON.parse(r.substring(2));if(e==='control_mode'){co2._m.splice(i,1);promo=dd;break;}}} await new Promise(r=>setTimeout(r,30)); }
|
||||
assert.ok(promo && Array.isArray(promo.controllers) && promo.controllers.includes('cohost') && promo.controllers.includes('owner'),
|
||||
'promotion broadcasts controllers (owner + cohost)');
|
||||
co1._m.length = co2._m.length = co3._m.length = 0;
|
||||
// promoted co-host can now drive; a plain guest still cannot
|
||||
s(co2,'pause',{currentTime:2}); await w(co1,'pause');
|
||||
s(co3,'play',{currentTime:3});
|
||||
let plainGuestGated=false; try { await w(co1,'play',500); } catch { plainGuestGated=true; }
|
||||
assert.ok(plainGuestGated, 'plain guest still gated after a co-host is promoted');
|
||||
co1._m.length = co2._m.length = co3._m.length = 0;
|
||||
// a non-owner (the co-host) cannot promote anyone
|
||||
s(co2,'set_peer_role',{peerId:'guestx',controller:true});
|
||||
let nonOwnerBlocked=false; try { await w(co3,'control_mode',500); } catch { nonOwnerBlocked=true; }
|
||||
assert.ok(nonOwnerBlocked, 'non-owner cannot promote (no room broadcast)');
|
||||
co1._m.length = co2._m.length = co3._m.length = 0;
|
||||
// owner demotes the co-host → gated again
|
||||
s(co1,'set_peer_role',{peerId:'cohost',controller:false});
|
||||
await w(co2,'control_mode');
|
||||
co1._m.length = co2._m.length = co3._m.length = 0;
|
||||
s(co2,'seek',{currentTime:4});
|
||||
let coGatedAfter=false; try { await w(co1,'seek',500); } catch { coGatedAfter=true; }
|
||||
assert.ok(coGatedAfter, 'demoted co-host is gated again');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- H-1: a demoted co-host's FORCE_SYNC_EXECUTE still relays when they
|
||||
// initiated the in-flight PREPARE. Without the initiator exemption, the
|
||||
// EXECUTE would be dropped by the host-only gate and every peer would be
|
||||
// left stuck paused. ---
|
||||
const h1rid = 'h1-'+Date.now();
|
||||
const ho = await c(), hc = await c(), hg = await c(); // owner / co-host / guest
|
||||
await j(ho, h1rid, 'owner'); await j(hc, h1rid, 'cohost'); await j(hg, h1rid, 'guest');
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
s(ho,'set_control_mode',{controlMode:'host-only'});
|
||||
await w(ho,'control_mode'); await w(hc,'control_mode'); await w(hg,'control_mode');
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
// owner promotes co-host; co-host initiates force sync
|
||||
s(ho,'set_peer_role',{peerId:'cohost',controller:true});
|
||||
await w(hc,'control_mode');
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
s(hc,'force_sync_prepare',{targetTime:0});
|
||||
await w(ho,'force_sync_prepare'); await w(hg,'force_sync_prepare');
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
// owner demotes the co-host mid-flight — the EXECUTE must still go through.
|
||||
// Wait out the per-room role-change debounce (M-4) so this demote broadcasts:
|
||||
// in real usage the host can't promote, run a force-sync, and demote inside 500ms.
|
||||
await new Promise(r => setTimeout(r, 550));
|
||||
s(ho,'set_peer_role',{peerId:'cohost',controller:false});
|
||||
await w(hc,'control_mode');
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
s(hc,'force_sync_execute',{});
|
||||
await w(ho,'force_sync_execute'); await w(hg,'force_sync_execute');
|
||||
// After the EXECUTE, the initiator slot is cleared: a fresh EXECUTE from the
|
||||
// (now plain guest) co-host is gated again, confirming the exemption is scoped.
|
||||
ho._m.length = hc._m.length = hg._m.length = 0;
|
||||
s(hc,'force_sync_execute',{});
|
||||
let reGated=false; try { await w(ho,'force_sync_execute',500); } catch { reGated=true; }
|
||||
assert.ok(reGated, 'initiator exemption is cleared after the EXECUTE relayes');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- A guest's stray EXECUTE (no matching PREPARE they initiated) is still gated ---
|
||||
const grid = 'h1b-'+Date.now();
|
||||
const go = await c(), gg = await c();
|
||||
await j(go, grid, 'own'); await j(gg, grid, 'gst');
|
||||
go._m.length = gg._m.length = 0;
|
||||
s(go,'set_control_mode',{controlMode:'host-only'});
|
||||
await w(go,'control_mode'); await w(gg,'control_mode');
|
||||
go._m.length = gg._m.length = 0;
|
||||
s(gg,'force_sync_execute',{});
|
||||
let uninitGated=false; try { await w(go,'force_sync_execute',500); } catch { uninitGated=true; }
|
||||
assert.ok(uninitGated, 'guest FORCE_SYNC_EXECUTE without a matching PREPARE is gated');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// =====================================================================
|
||||
// BACKWARD COMPATIBILITY — old clients (pre-HCM build) against new server.
|
||||
// These tests simulate an old client by deliberately omitting fields the
|
||||
// new feature added (desynced in heartbeats, capabilities expectation)
|
||||
// and by ignoring CONTROL_MODE broadcasts. The wire format for existing
|
||||
// events must stay byte-compatible: the relay must accept old payloads
|
||||
// and must not inject new fields old clients would misread.
|
||||
// =====================================================================
|
||||
|
||||
// --- BC-1: Old-client heartbeat (no `desynced` field) is accepted and
|
||||
// relayed without injecting `desynced`. Old clients never sent the
|
||||
// field; the relay must strip it from the wire so we don't surprise
|
||||
// them with unexpected keys. ---
|
||||
const bcrid = 'bc-'+Date.now();
|
||||
const bco = await c(), bcn = await c(); // bco = "old", bcn = "new"
|
||||
await j(bco, bcrid, 'oldp'); await j(bcn, bcrid, 'newp');
|
||||
bco._m.length = bcn._m.length = 0;
|
||||
// Old-client heartbeat: every field an old build would send, NO `desynced`.
|
||||
s(bco,'peer_status',{
|
||||
status:'heartbeat', username:'old', tabTitle:'t', mediaTitle:'m',
|
||||
playbackState:'playing', currentTime:42, volume:0.5, muted:false
|
||||
});
|
||||
let bcRelay = null; const bcStart = Date.now();
|
||||
while (Date.now()-bcStart < 800 && !bcRelay) {
|
||||
for (let i=0;i<bcn._m.length;i++){ const r=bcn._m[i]; if(r.startsWith('42')){ const [e,dd]=JSON.parse(r.substring(2)); if(e==='peer_status'){ bcn._m.splice(i,1); bcRelay=dd; break; } } }
|
||||
await new Promise(r=>setTimeout(r,30));
|
||||
}
|
||||
assert.ok(bcRelay, 'old-style heartbeat relayed');
|
||||
assert.ok(bcRelay.desynced === undefined, 'old-client heartbeat has no desynced on the wire (stripped)');
|
||||
assert.equal(bcRelay.currentTime, 42, 'old-client currentTime preserved');
|
||||
assert.equal(bcRelay.senderId, 'oldp', 'old-client senderId preserved');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- BC-2: Old client in a host-only room — server still gates its events
|
||||
// even though the client has no awareness of the mode. This is the
|
||||
// key guarantee for mixed rooms during rollout: an old client can't
|
||||
// drive a host-only room just because it ignores CONTROL_MODE. ---
|
||||
const hmrid = 'hcmix-'+Date.now();
|
||||
const hmo = await c(), hmg = await c(); // hmo = host (new), hmg = "old" guest
|
||||
await j(hmo, hmrid, 'hmixhost'); await j(hmg, hmrid, 'hmixold');
|
||||
hmo._m.length = hmg._m.length = 0;
|
||||
s(hmo,'set_control_mode',{controlMode:'host-only'});
|
||||
await w(hmo,'control_mode'); await w(hmg,'control_mode'); // old client's socket still receives it; old client would ignore
|
||||
hmo._m.length = hmg._m.length = 0;
|
||||
// "Old" guest tries to drive — server must drop. Host must NOT receive it.
|
||||
s(hmg,'pause',{currentTime:5});
|
||||
let oldGated=false; try { await w(hmo,'pause',600); } catch { oldGated=true; }
|
||||
assert.ok(oldGated, 'old-client pause dropped in host-only (server enforces regardless of client awareness)');
|
||||
// Host's own command still relays to the old client's socket — old client
|
||||
// applies it via its existing PLAY/PAUSE handler.
|
||||
s(hmo,'pause',{currentTime:7}); await w(hmg,'pause');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- BC-3: Mixed room with old + new client in 'everyone' mode — every
|
||||
// event flows identically to pre-HCM. Confirms no regression in the
|
||||
// default-mode relay path that could fragment a rolling-update room. ---
|
||||
const mxrid = 'mix-'+Date.now();
|
||||
const mxo = await c(), mxn = await c(); // mxo = old, mxn = new
|
||||
await j(mxo, mxrid, 'oldmx'); await j(mxn, mxrid, 'newmx');
|
||||
mxo._m.length = mxn._m.length = 0;
|
||||
// Old → new
|
||||
s(mxo,'play',{currentTime:1}); await w(mxn,'play');
|
||||
s(mxo,'seek',{currentTime:99}); await w(mxn,'seek');
|
||||
s(mxo,'force_sync_prepare',{targetTime:5}); await w(mxn,'force_sync_prepare');
|
||||
s(mxo,'episode_lobby',{expectedTitle:'S1E1'}); await w(mxn,'episode_lobby');
|
||||
// New → old
|
||||
mxo._m.length = mxn._m.length = 0;
|
||||
s(mxn,'pause',{currentTime:2}); await w(mxo,'pause');
|
||||
s(mxn,'seek',{currentTime:50}); await w(mxo,'seek');
|
||||
s(mxn,'force_sync_execute',{}); await w(mxo,'force_sync_execute');
|
||||
s(mxn,'episode_lobby_cancel',{}); await w(mxo,'episode_lobby_cancel');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- BC-4: New-client heartbeat WITH `desynced` does not break an old
|
||||
// client's receive path. The field is appended but old clients ignore
|
||||
// unknown keys — verify the relay preserves every pre-HCM field and
|
||||
// only adds `desynced`. ---
|
||||
const b4rid = 'bc4-'+Date.now();
|
||||
const b4old = await c(), b4new = await c();
|
||||
await j(b4old, b4rid, 'b4o'); await j(b4new, b4rid, 'b4n');
|
||||
b4old._m.length = b4new._m.length = 0;
|
||||
s(b4new,'peer_status',{status:'heartbeat', desynced:true, currentTime:7, playbackState:'paused', username:'newp'});
|
||||
let b4Relay = null; const b4Start = Date.now();
|
||||
while (Date.now()-b4Start < 800 && !b4Relay) {
|
||||
for (let i=0;i<b4old._m.length;i++){ const r=b4old._m[i]; if(r.startsWith('42')){ const [e,dd]=JSON.parse(r.substring(2)); if(e==='peer_status'){ b4old._m.splice(i,1); b4Relay=dd; break; } } }
|
||||
await new Promise(r=>setTimeout(r,30));
|
||||
}
|
||||
assert.ok(b4Relay, 'new-client heartbeat relayed to old client');
|
||||
assert.equal(b4Relay.desynced, true, 'desynced preserved for new recipients');
|
||||
assert.equal(b4Relay.currentTime, 7, 'old fields preserved on the same relay');
|
||||
assert.equal(b4Relay.senderId, 'b4n', 'senderId preserved');
|
||||
close();
|
||||
resetConnectionRate();
|
||||
|
||||
// --- Password room ---
|
||||
const prid = 'pw-'+Date.now();
|
||||
@@ -93,7 +408,7 @@ try {
|
||||
let d=''; res.on('data',c=>d+=c); res.on('end',()=>r([res.statusCode,JSON.parse(d)])); }));
|
||||
assert.equal(st,200); assert.equal(body.status,'online');
|
||||
|
||||
console.log('All 13 WebSocket integration tests passed');
|
||||
console.log('All WebSocket integration tests passed (incl. host control mode)');
|
||||
} catch(e) {
|
||||
console.error('FAILED:', e.message);
|
||||
process.exitCode=1;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
TITLE_PRIVACY_MODES,
|
||||
applyTitlePrivacyToPayload,
|
||||
normalizeSendTabTitle,
|
||||
normalizeTitlePrivacyMode,
|
||||
sanitizeSharedTitle,
|
||||
sanitizeTabTitle
|
||||
} from '../extension/title-privacy.js';
|
||||
|
||||
assert.equal(normalizeTitlePrivacyMode(undefined), TITLE_PRIVACY_MODES.FULL);
|
||||
assert.equal(normalizeTitlePrivacyMode('unknown'), TITLE_PRIVACY_MODES.FULL);
|
||||
assert.equal(normalizeTitlePrivacyMode(TITLE_PRIVACY_MODES.HIDDEN), TITLE_PRIVACY_MODES.HIDDEN);
|
||||
assert.equal(normalizeSendTabTitle(undefined, TITLE_PRIVACY_MODES.FULL), true);
|
||||
assert.equal(normalizeSendTabTitle(undefined, TITLE_PRIVACY_MODES.EPISODE), false);
|
||||
assert.equal(normalizeSendTabTitle(true, TITLE_PRIVACY_MODES.HIDDEN), true);
|
||||
assert.equal(normalizeSendTabTitle(false, TITLE_PRIVACY_MODES.FULL), false);
|
||||
|
||||
assert.equal(sanitizeTabTitle('Private Tab', true), 'Private Tab');
|
||||
assert.equal(sanitizeTabTitle('Private Tab', false), null);
|
||||
assert.equal(sanitizeTabTitle('', true), null);
|
||||
|
||||
assert.equal(sanitizeSharedTitle('Example Movie', 'full'), 'Example Movie');
|
||||
assert.equal(sanitizeSharedTitle('', 'full'), null);
|
||||
assert.equal(sanitizeSharedTitle(null, 'full'), null);
|
||||
|
||||
assert.equal(sanitizeSharedTitle('Show Name - S01/E04 - Title', 'episode'), 'S01E04');
|
||||
assert.equal(sanitizeSharedTitle('Folge 7 - Private Server', 'episode'), 'EP007');
|
||||
assert.equal(sanitizeSharedTitle('Example Movie', 'episode'), null);
|
||||
|
||||
assert.equal(sanitizeSharedTitle('Show Name - S01E04', 'hidden'), null);
|
||||
assert.equal(sanitizeSharedTitle('Private Tab Title', 'hidden'), null);
|
||||
|
||||
assert.deepEqual(
|
||||
applyTitlePrivacyToPayload({
|
||||
tabTitle: 'Private Jellyfin - S01E04',
|
||||
mediaTitle: 'Show Name - S01E04',
|
||||
currentTime: 42
|
||||
}, 'episode'),
|
||||
{
|
||||
tabTitle: 'Private Jellyfin - S01E04',
|
||||
mediaTitle: 'S01E04',
|
||||
currentTime: 42
|
||||
},
|
||||
'media privacy must not rewrite tabTitle'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
applyTitlePrivacyToPayload({
|
||||
tabTitle: 'Private Jellyfin - S01E04',
|
||||
status: 'heartbeat'
|
||||
}, 'episode'),
|
||||
{
|
||||
tabTitle: 'Private Jellyfin - S01E04',
|
||||
status: 'heartbeat'
|
||||
},
|
||||
'media privacy must not rewrite tabTitle or add absent media keys'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
applyTitlePrivacyToPayload({
|
||||
tabTitle: 'Private Tab',
|
||||
mediaTitle: 'Private Media',
|
||||
expectedTitle: 'S01E04',
|
||||
title: 'S01E04'
|
||||
}, 'hidden'),
|
||||
{
|
||||
tabTitle: 'Private Tab',
|
||||
mediaTitle: null,
|
||||
expectedTitle: null,
|
||||
title: null
|
||||
},
|
||||
'hidden media privacy must not clear tabTitle'
|
||||
);
|
||||
|
||||
console.log('title-privacy tests passed');
|
||||
@@ -13,6 +13,7 @@ const checks = [
|
||||
}],
|
||||
['rate-limiter unit tests', 'node', ['scripts/test-rate-limiter.mjs']],
|
||||
['episode-utils unit tests', 'node', ['scripts/test-episode-utils.mjs']],
|
||||
['title privacy unit tests', 'node', ['scripts/test-title-privacy.mjs']],
|
||||
['server WebSocket integration', 'node', ['scripts/test-server-ws.mjs']],
|
||||
['names generator', 'node', ['scripts/test-names.mjs']],
|
||||
['content video finder', 'node', ['scripts/test-content-video-finder.cjs']],
|
||||
|
||||
+231
-15
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
||||
import { Server } from 'socket.io';
|
||||
import crypto from 'crypto';
|
||||
import dotenv from 'dotenv';
|
||||
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js';
|
||||
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION, CONTROL_MODES, CAPABILITIES } from '../shared/constants.js';
|
||||
import {
|
||||
buildHealthPayload,
|
||||
checkCooldown,
|
||||
@@ -64,6 +64,10 @@ if (!isAdminMetricsTokenStrong(ADMIN_METRICS_TOKEN)) {
|
||||
console.warn('[SECURITY] ADMIN_METRICS_TOKEN is set but shorter than 32 characters. Use a long random token.');
|
||||
}
|
||||
|
||||
if (!process.env.SERVER_SALT) {
|
||||
console.warn('[SECURITY] SERVER_SALT is not set — using the built-in default salt (public in the repo). Set a unique SERVER_SALT so room-password hashes are not computed with a known salt.');
|
||||
}
|
||||
|
||||
export const app = express();
|
||||
app.set('trust proxy', 1); // For real client IP through reverse proxy
|
||||
|
||||
@@ -148,9 +152,42 @@ const peerToSocket = new Map(); // peerId -> socketId (Global lookup)
|
||||
const roomCreationLocks = new Map(); // roomId -> Promise (prevents race on room creation)
|
||||
const peerJoinLocks = new Map(); // peerId -> Promise (prevents race on same peerId joins)
|
||||
|
||||
// Host Control Mode: events a non-host guest may NOT initiate while a room is in
|
||||
// 'host-only' mode (they would move/disrupt everyone). Reactions like FORCE_SYNC_ACK,
|
||||
// EPISODE_READY, PEER_STATUS heartbeats remain allowed for all peers.
|
||||
const HOST_ONLY_GATED_EVENTS = new Set([
|
||||
EVENTS.PLAY,
|
||||
EVENTS.PAUSE,
|
||||
EVENTS.SEEK,
|
||||
EVENTS.FORCE_SYNC_PREPARE,
|
||||
EVENTS.FORCE_SYNC_EXECUTE,
|
||||
EVENTS.EPISODE_LOBBY,
|
||||
EVENTS.EPISODE_LOBBY_CANCEL
|
||||
]);
|
||||
|
||||
// Features this relay supports, advertised to clients in ROOM_DATA so they can
|
||||
// enable matching UI/behavior only when the server actually backs it. Append a
|
||||
// flag here when a new server-gated feature ships (e.g. co-host promotion).
|
||||
const SERVER_CAPABILITIES = [CAPABILITIES.HOST_CONTROL, CAPABILITIES.CO_HOST];
|
||||
|
||||
// M-4: minimum interval between CONTROL_MODE changes per room. Stops a rapidly
|
||||
// toggling host from thrashing every guest's UI (locked/unlocked/locked...) and
|
||||
// from generating one broadcast per toggle across all peers.
|
||||
const CONTROL_MODE_MIN_INTERVAL_MS = 500;
|
||||
|
||||
// Canonical control-mode/role snapshot sent to clients. controllers always includes
|
||||
// the owner (hostPeerId). Built in one place so every emit stays consistent.
|
||||
function controlModePayload(room) {
|
||||
return {
|
||||
controlMode: room.controlMode,
|
||||
hostPeerId: room.hostPeerId,
|
||||
controllers: room.controllers ? Array.from(room.controllers) : (room.hostPeerId ? [room.hostPeerId] : [])
|
||||
};
|
||||
}
|
||||
|
||||
function log(type, message, details = '') {
|
||||
const debugLogging = process.env.DEBUG_LOGGING === '1';
|
||||
const isVerbose = type === 'CONN' || type === 'ROOM' || type === 'DEDUPE' || type === 'CORS';
|
||||
const isVerbose = type === 'CONN' || type === 'ROOM' || type === 'DEDUPE' || type === 'CORS' || type === 'ACKDROP';
|
||||
if (!debugLogging && isVerbose) return;
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
@@ -201,6 +238,42 @@ function removePeerFromRoom(socketId, roomId, reason) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3.6. Host Control Mode: if the host left (and isn't still connected via another
|
||||
// socket), fall back to 'everyone' so the room never gets stuck locked, and
|
||||
// reassign host to the earliest remaining peer so the feature stays usable.
|
||||
// (v1: immediate fallback, no grace period — see host-control-mode docs.)
|
||||
// Skip while a join for this peerId is in flight (peerJoinLocks holds it):
|
||||
// that's the reconnect / second-tab case where the same peerId is being
|
||||
// re-added right after the dedupe kicked the old socket. Demoting there
|
||||
// would silently unlock the room on every second-tab open.
|
||||
// NOTE: this does *not* cover a true network blip where the old socket's
|
||||
// 'disconnect' fires before the new socket acquires its join lock — in that
|
||||
// window peerJoinLocks is empty, so the fallback fires. Documented v1
|
||||
// limitation (no host grace period); see KNOWN_LIMITATIONS.md.
|
||||
const peerRejoining = peerJoinLocks.has(peerId);
|
||||
const peerGone = !isPeerStillConnected && !peerRejoining;
|
||||
if (peerGone && room.controllers && room.peers.size > 0) {
|
||||
const wasController = room.controllers.has(peerId);
|
||||
room.controllers.delete(peerId);
|
||||
// H-1: a leaving initiator strands the room's force-sync — release the
|
||||
// slot so a future controller's PREPARE can take over cleanly.
|
||||
if (room.forceSyncInitiator === peerId) room.forceSyncInitiator = null;
|
||||
if (room.hostPeerId === peerId) {
|
||||
// Owner left → reassign owner + fall back to 'everyone' so the room is
|
||||
// never stuck locked, and reset the controller set to just the new owner.
|
||||
const nextPeerData = room.peerData.values().next().value;
|
||||
room.hostPeerId = nextPeerData ? nextPeerData.peerId : null;
|
||||
room.controlMode = CONTROL_MODES.EVERYONE;
|
||||
room.controllers = new Set(room.hostPeerId ? [room.hostPeerId] : []);
|
||||
io.to(roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
log('ROOM', `Owner left room ${roomId.substring(0, 3)}*** — fell back to 'everyone', new owner: ${room.hostPeerId}`);
|
||||
} else if (wasController) {
|
||||
// A co-host left → keep the mode, just broadcast the updated controller list.
|
||||
io.to(roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
log('ROOM', `Controller ${peerId} left room ${roomId.substring(0, 3)}***`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Delete empty room
|
||||
if (room.peers.size === 0) {
|
||||
rooms.delete(roomId);
|
||||
@@ -333,7 +406,19 @@ io.on('connection', (socket) => {
|
||||
peers: new Set(),
|
||||
peerIds: new Map(),
|
||||
peerData: new Map(),
|
||||
lastActivity: Date.now()
|
||||
lastActivity: Date.now(),
|
||||
// Host Control Mode: creator (first joiner) is the host/owner.
|
||||
hostPeerId: peerId,
|
||||
controlMode: CONTROL_MODES.EVERYONE,
|
||||
lastControlModeChangeAt: 0, // M-4: per-room debounce for control-mode toggles
|
||||
lastRoleChangeAt: 0, // M-4: per-room debounce for role promote/demote
|
||||
// Co-Host: peers allowed to drive in 'host-only'. Always includes the owner.
|
||||
controllers: new Set([peerId]),
|
||||
// H-1: peerId of the in-flight force-sync initiator. Lets a demoted
|
||||
// controller's FORCE_SYNC_EXECUTE through the host-only gate — without
|
||||
// it, demoting a co-host mid-force-sync would drop their EXECUTE and
|
||||
// leave every peer stuck paused.
|
||||
forceSyncInitiator: null
|
||||
};
|
||||
rooms.set(roomId, room);
|
||||
createdByMe = true;
|
||||
@@ -415,10 +500,14 @@ io.on('connection', (socket) => {
|
||||
peerToSocket.set(peerId, socket.id);
|
||||
|
||||
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, username: username || null, tabTitle: tabTitle || null, mediaTitle: mediaTitle || null, status: 'joined' });
|
||||
socket.emit(EVENTS.ROOM_DATA, {
|
||||
roomId,
|
||||
socket.emit(EVENTS.ROOM_DATA, {
|
||||
roomId,
|
||||
peers: Array.from(room.peers).map(sid => room.peerData.get(sid)),
|
||||
activeLobby: room.activeLobby || null
|
||||
activeLobby: room.activeLobby || null,
|
||||
hostPeerId: room.hostPeerId || null,
|
||||
controlMode: room.controlMode || CONTROL_MODES.EVERYONE,
|
||||
controllers: room.controllers ? Array.from(room.controllers) : [],
|
||||
capabilities: SERVER_CAPABILITIES
|
||||
});
|
||||
log('ROOM', `Peer ${peerId} joined: ${roomId.substring(0, 3)}***`);
|
||||
} finally {
|
||||
@@ -458,7 +547,35 @@ io.on('connection', (socket) => {
|
||||
const room = rooms.get(mapping.roomId);
|
||||
if (room) {
|
||||
room.lastActivity = Date.now();
|
||||
|
||||
|
||||
// --- Host Control Mode gate ---
|
||||
// In 'host-only' mode, drop room-moving events from anyone who is not
|
||||
// a controller (the owner + any promoted co-hosts). Robust chokepoint:
|
||||
// independent of client behavior, kills spam. Heartbeats/ACKs pass.
|
||||
//
|
||||
// H-1 exception: a demoted co-host's FORCE_SYNC_EXECUTE still has to
|
||||
// land — otherwise their already-relayed PREPARE would leave the whole
|
||||
// room stuck paused. Track the in-flight initiator on PREPARE and let
|
||||
// their matching EXECUTE through regardless of current controllers set.
|
||||
if (eventName === EVENTS.FORCE_SYNC_PREPARE &&
|
||||
room.controlMode === CONTROL_MODES.HOST_ONLY &&
|
||||
room.controllers && room.controllers.has(mapping.peerId)) {
|
||||
room.forceSyncInitiator = mapping.peerId;
|
||||
}
|
||||
const isOwnForceSyncExecute = eventName === EVENTS.FORCE_SYNC_EXECUTE &&
|
||||
room.forceSyncInitiator && mapping.peerId === room.forceSyncInitiator;
|
||||
if (!isOwnForceSyncExecute &&
|
||||
room.controlMode === CONTROL_MODES.HOST_ONLY &&
|
||||
!(room.controllers && room.controllers.has(mapping.peerId)) &&
|
||||
HOST_ONLY_GATED_EVENTS.has(eventName)) {
|
||||
log('ROOM', `Dropped ${eventName} from guest ${mapping.peerId} in host-only room ${mapping.roomId.substring(0, 3)}***`);
|
||||
return;
|
||||
}
|
||||
// Clear initiator tracking once the EXECUTE has been relayed.
|
||||
if (eventName === EVENTS.FORCE_SYNC_EXECUTE && room.forceSyncInitiator) {
|
||||
room.forceSyncInitiator = null;
|
||||
}
|
||||
|
||||
// --- S-2 & S-3: Sanitize ALL relay fields (strings, numbers, booleans) ---
|
||||
const clamp = (val, max) => typeof val === 'string' ? val.substring(0, max) : undefined;
|
||||
const clampNum = (val, min, max) => typeof val === 'number' && Number.isFinite(val) ? Math.max(min, Math.min(max, val)) : undefined;
|
||||
@@ -469,12 +586,13 @@ io.on('connection', (socket) => {
|
||||
room.peerData.set(socket.id, {
|
||||
...existing,
|
||||
username: data.username !== undefined ? (clamp(data.username, 30) ?? existing.username) : existing.username,
|
||||
tabTitle: data.tabTitle !== undefined ? (clamp(data.tabTitle, 100) ?? existing.tabTitle) : existing.tabTitle,
|
||||
mediaTitle: data.mediaTitle !== undefined ? (clamp(data.mediaTitle, 100) ?? existing.mediaTitle) : existing.mediaTitle,
|
||||
tabTitle: data.tabTitle === null ? null : (data.tabTitle !== undefined ? (clamp(data.tabTitle, 100) ?? existing.tabTitle) : existing.tabTitle),
|
||||
mediaTitle: data.mediaTitle === null ? null : (data.mediaTitle !== undefined ? (clamp(data.mediaTitle, 100) ?? existing.mediaTitle) : existing.mediaTitle),
|
||||
playbackState: data.playbackState !== undefined ? (validState(data.playbackState) ?? existing.playbackState) : existing.playbackState,
|
||||
currentTime: data.currentTime !== undefined ? (clampNum(data.currentTime, 0, 86400) ?? existing.currentTime) : existing.currentTime,
|
||||
currentTime: data.currentTime === null ? null : (data.currentTime !== undefined ? (clampNum(data.currentTime, 0, 86400) ?? existing.currentTime) : existing.currentTime),
|
||||
volume: data.volume !== undefined ? (clampNum(data.volume, 0, 1) ?? existing.volume) : existing.volume,
|
||||
muted: data.muted !== undefined ? (validBool(data.muted) ?? existing.muted) : existing.muted,
|
||||
desynced: data.desynced !== undefined ? (validBool(data.desynced) === true) : (existing.desynced || false),
|
||||
lastSeen: Date.now()
|
||||
});
|
||||
|
||||
@@ -482,14 +600,15 @@ io.on('connection', (socket) => {
|
||||
const relayPayload = {
|
||||
senderId: mapping.peerId,
|
||||
seq: clampNum(data.seq, 0, Number.MAX_SAFE_INTEGER),
|
||||
currentTime: clampNum(data.currentTime, 0, 86400),
|
||||
currentTime: data.currentTime === null ? null : clampNum(data.currentTime, 0, 86400),
|
||||
targetTime: clampNum(data.targetTime, 0, 86400),
|
||||
playbackState: validState(data.playbackState),
|
||||
username: clamp(data.username, 30),
|
||||
tabTitle: clamp(data.tabTitle, 100),
|
||||
mediaTitle: clamp(data.mediaTitle, 100),
|
||||
tabTitle: data.tabTitle === null ? null : clamp(data.tabTitle, 100),
|
||||
mediaTitle: data.mediaTitle === null ? null : clamp(data.mediaTitle, 100),
|
||||
volume: clampNum(data.volume, 0, 1),
|
||||
muted: validBool(data.muted),
|
||||
desynced: validBool(data.desynced),
|
||||
peerId: mapping.peerId,
|
||||
status: typeof data.status === 'string' ? data.status.substring(0, 16) : undefined,
|
||||
expectedTitle: clamp(data.expectedTitle, 100),
|
||||
@@ -553,6 +672,97 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(EVENTS.SET_CONTROL_MODE, (data) => {
|
||||
if (!checkEventRate(socket.id)) {
|
||||
log('SECURITY', `Event rate limit exceeded for socket (SET_CONTROL_MODE): ${socket.id}`);
|
||||
socket.disconnect(true);
|
||||
return;
|
||||
}
|
||||
if (!data || typeof data !== 'object') return;
|
||||
const mode = data.controlMode;
|
||||
if (mode !== CONTROL_MODES.EVERYONE && mode !== CONTROL_MODES.HOST_ONLY) return;
|
||||
|
||||
const mapping = socketToRoom.get(socket.id);
|
||||
if (!mapping) return;
|
||||
const room = rooms.get(mapping.roomId);
|
||||
if (!room) return;
|
||||
|
||||
// Only the owner may change the control mode.
|
||||
if (mapping.peerId !== room.hostPeerId) {
|
||||
log('AUTH', `Non-host ${mapping.peerId} tried to set control mode in ${mapping.roomId.substring(0, 3)}***`);
|
||||
// Re-sync the sender to the actual room state so any optimistic UI
|
||||
// (e.g. popup toggle) reverts — covers stale-local-amHost races (H-5).
|
||||
socket.emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
return;
|
||||
}
|
||||
if (room.controlMode === mode) return; // no-op, ignore (UI debounce backstop)
|
||||
|
||||
// M-4: per-room debounce — reject toggles faster than CONTROL_MODE_MIN_INTERVAL_MS.
|
||||
// The host still gets the final state via the next legit change; this just
|
||||
// kills the spam vector.
|
||||
const now = Date.now();
|
||||
if (now - room.lastControlModeChangeAt < CONTROL_MODE_MIN_INTERVAL_MS) {
|
||||
log('ROOM', `Control mode toggle debounced in ${mapping.roomId.substring(0, 3)}***`);
|
||||
// Re-sync the sender so any optimistic UI matches the actual (unchanged) state.
|
||||
socket.emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
return;
|
||||
}
|
||||
|
||||
room.controlMode = mode;
|
||||
room.lastControlModeChangeAt = now;
|
||||
room.lastActivity = now;
|
||||
io.to(mapping.roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
log('ROOM', `Control mode set to '${mode}' by host in room ${mapping.roomId.substring(0, 3)}***`);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.SET_PEER_ROLE, (data) => {
|
||||
if (!checkEventRate(socket.id)) {
|
||||
log('SECURITY', `Event rate limit exceeded for socket (SET_PEER_ROLE): ${socket.id}`);
|
||||
socket.disconnect(true);
|
||||
return;
|
||||
}
|
||||
if (!data || typeof data !== 'object') return;
|
||||
const targetPeerId = typeof data.peerId === 'string' ? data.peerId.substring(0, 16) : null;
|
||||
const makeController = data.controller === true;
|
||||
if (!targetPeerId) return;
|
||||
|
||||
const mapping = socketToRoom.get(socket.id);
|
||||
if (!mapping) return;
|
||||
const room = rooms.get(mapping.roomId);
|
||||
if (!room || !room.controllers) return;
|
||||
|
||||
// Only the owner may promote/demote controllers.
|
||||
if (mapping.peerId !== room.hostPeerId) {
|
||||
log('AUTH', `Non-owner ${mapping.peerId} tried to set peer role in ${mapping.roomId.substring(0, 3)}***`);
|
||||
socket.emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
return;
|
||||
}
|
||||
// The owner is always a controller and cannot be demoted.
|
||||
if (targetPeerId === room.hostPeerId) return;
|
||||
// Target must be a peer currently in the room.
|
||||
const targetPresent = Array.from(room.peerData.values()).some(d => d.peerId === targetPeerId);
|
||||
if (!targetPresent) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - (room.lastRoleChangeAt || 0) < CONTROL_MODE_MIN_INTERVAL_MS) {
|
||||
log('ROOM', `Role change debounced in ${mapping.roomId.substring(0, 3)}***`);
|
||||
socket.emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
return;
|
||||
}
|
||||
|
||||
if (makeController) {
|
||||
if (room.controllers.has(targetPeerId)) return; // no-op
|
||||
room.controllers.add(targetPeerId);
|
||||
} else {
|
||||
if (!room.controllers.has(targetPeerId)) return; // no-op
|
||||
room.controllers.delete(targetPeerId);
|
||||
}
|
||||
room.lastRoleChangeAt = now;
|
||||
room.lastActivity = now;
|
||||
io.to(mapping.roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||
log('ROOM', `Peer ${targetPeerId} ${makeController ? 'promoted to' : 'demoted from'} controller in ${mapping.roomId.substring(0, 3)}***`);
|
||||
});
|
||||
|
||||
socket.on(EVENTS.EVENT_ACK, (data) => {
|
||||
if (!checkEventRate(socket.id)) {
|
||||
log('SECURITY', `Event rate limit exceeded for socket (ACK): ${socket.id}`);
|
||||
@@ -569,12 +779,18 @@ io.on('connection', (socket) => {
|
||||
|
||||
// Security: Only relay ACK if both peers are in the same room
|
||||
if (senderMapping && targetMapping && senderMapping.roomId === targetMapping.roomId) {
|
||||
io.to(targetSocketId).emit(EVENTS.EVENT_ACK, {
|
||||
io.to(targetSocketId).emit(EVENTS.EVENT_ACK, {
|
||||
senderId: senderMapping.peerId,
|
||||
actionTimestamp: data.actionTimestamp
|
||||
});
|
||||
} else {
|
||||
} else if (senderMapping && targetMapping) {
|
||||
// Both peers exist but live in different rooms — genuinely suspicious.
|
||||
log('SECURITY', `Blocked cross-room ACK attempt from ${socket.id} to ${data.targetId}`);
|
||||
} else {
|
||||
// Benign + common: sender or target left/disconnected before the ACK
|
||||
// arrived (a command was in-flight when they went). Not an attack —
|
||||
// log quietly (verbose only) so it doesn't drown out real signals.
|
||||
log('ACKDROP', `Dropped ACK from ${socket.id} to absent peer ${data.targetId}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+18
-10
@@ -7,6 +7,14 @@ export const ROOM_LIST_COOLDOWN_MS = 10000;
|
||||
export const HEALTH_RATE_LIMIT_PER_MINUTE = 10;
|
||||
export const ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE = 5;
|
||||
|
||||
// --- Connection & event budgets (formerly inline magic numbers) ---
|
||||
export const CONNECTION_RATE_LIMIT = 10; // max new connections per IP per window
|
||||
export const CONNECTION_RATE_WINDOW_MS = 60000; // 1 minute
|
||||
export const EVENT_RATE_LIMIT = 50; // max relayed events per socket per window
|
||||
export const EVENT_RATE_WINDOW_MS = 10000; // 10 seconds
|
||||
export const HEALTH_RATE_WINDOW_MS = 60000; // 1 minute
|
||||
export const ADMIN_METRICS_AUTH_WINDOW_MS = 60000; // 1 minute
|
||||
|
||||
export const connectionCounts = new Map(); // ip -> { count, resetTime }
|
||||
export const failedAuthAttempts = new Map(); // Map<IP+RoomID, {count, lastAttempt}>
|
||||
export const eventCounts = new Map(); // socketId -> { count, resetTime }
|
||||
@@ -72,30 +80,30 @@ export function recordAuthFailure(ip, roomId) {
|
||||
|
||||
export function checkConnectionRate(ip) {
|
||||
const now = Date.now();
|
||||
const entry = connectionCounts.get(ip) || { count: 0, resetTime: now + 60000 };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
|
||||
const entry = connectionCounts.get(ip) || { count: 0, resetTime: now + CONNECTION_RATE_WINDOW_MS };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + CONNECTION_RATE_WINDOW_MS; }
|
||||
entry.count++;
|
||||
connectionCounts.set(ip, entry);
|
||||
if (entry.count <= 10) return true;
|
||||
if (entry.count <= CONNECTION_RATE_LIMIT) return true;
|
||||
rateLimitDenied.connections++;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function checkEventRate(socketId) {
|
||||
const now = Date.now();
|
||||
const entry = eventCounts.get(socketId) || { count: 0, resetTime: now + 10000 };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 10000; }
|
||||
const entry = eventCounts.get(socketId) || { count: 0, resetTime: now + EVENT_RATE_WINDOW_MS };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + EVENT_RATE_WINDOW_MS; }
|
||||
entry.count++;
|
||||
eventCounts.set(socketId, entry);
|
||||
if (entry.count <= 30) return true;
|
||||
if (entry.count <= EVENT_RATE_LIMIT) return true;
|
||||
rateLimitDenied.events++;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function checkHealthRate(ip) {
|
||||
const now = Date.now();
|
||||
const entry = healthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
|
||||
const entry = healthCounts.get(ip) || { count: 0, resetTime: now + HEALTH_RATE_WINDOW_MS };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + HEALTH_RATE_WINDOW_MS; }
|
||||
entry.count++;
|
||||
healthCounts.set(ip, entry);
|
||||
if (entry.count <= HEALTH_RATE_LIMIT_PER_MINUTE) return true;
|
||||
@@ -105,8 +113,8 @@ export function checkHealthRate(ip) {
|
||||
|
||||
export function checkAdminMetricsAuthRate(ip) {
|
||||
const now = Date.now();
|
||||
const entry = adminMetricsAuthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
|
||||
const entry = adminMetricsAuthCounts.get(ip) || { count: 0, resetTime: now + ADMIN_METRICS_AUTH_WINDOW_MS };
|
||||
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + ADMIN_METRICS_AUTH_WINDOW_MS; }
|
||||
entry.count++;
|
||||
adminMetricsAuthCounts.set(ip, entry);
|
||||
if (entry.count <= ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE) return true;
|
||||
|
||||
+24
-2
@@ -7,13 +7,12 @@
|
||||
*/
|
||||
|
||||
export const PROTOCOL_VERSION = "1.0.0";
|
||||
export const APP_VERSION = "2.4.1";
|
||||
export const APP_VERSION = "2.5.0";
|
||||
|
||||
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
|
||||
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
|
||||
export const OFFICIAL_SERVER_TOKEN = '62170b705234c4f4807a9b22420bb93cf1a2aacfa4c5d3b47804482babb8eb50';
|
||||
export const SUPPORT_URL = 'https://support.koalastuff.net';
|
||||
export const KOFI_URL = 'https://ko-fi.com/koaladev';
|
||||
export const GITHUB_URL = 'https://github.com/Shik3i/KoalaSync';
|
||||
|
||||
export function isFirefox() {
|
||||
@@ -34,6 +33,11 @@ export const EVENTS = {
|
||||
ROOM_DATA: "room_data", // Server -> Client: current room state
|
||||
ERROR: "error",
|
||||
|
||||
// Host Control Mode
|
||||
SET_CONTROL_MODE: "set_control_mode", // Client -> Server: host sets room control mode ('everyone' | 'host-only')
|
||||
CONTROL_MODE: "control_mode", // Server -> Client: control mode/role changed { controlMode, hostPeerId, controllers }
|
||||
SET_PEER_ROLE: "set_peer_role", // Client -> Server: owner promotes/demotes a peer { peerId, controller }
|
||||
|
||||
// Media Control
|
||||
PLAY: "play",
|
||||
PAUSE: "pause",
|
||||
@@ -58,6 +62,24 @@ export const EVENTS = {
|
||||
PONG: "pong" // server responds with same { t } for client RTT calculation
|
||||
};
|
||||
|
||||
// Room control modes (Host Control Mode feature).
|
||||
// NOTE: content.js does not import this module — it uses the string literals
|
||||
// 'everyone' / 'host-only' directly. Keep these values in sync there.
|
||||
export const CONTROL_MODES = {
|
||||
EVERYONE: 'everyone', // default: anyone can play/pause/seek for the room
|
||||
HOST_ONLY: 'host-only' // only the host drives the room
|
||||
};
|
||||
|
||||
// Server feature capabilities, advertised to clients in ROOM_DATA. Lets a client
|
||||
// detect what the relay actually supports instead of inferring it from the
|
||||
// presence of a data field — so new server features degrade cleanly on older
|
||||
// relays (unknown/absent list → feature treated as unavailable) and old clients
|
||||
// simply ignore the field. Add a flag here as each server-gated feature lands.
|
||||
export const CAPABILITIES = {
|
||||
HOST_CONTROL: 'host-control',
|
||||
CO_HOST: 'co-host' // owner promotes guests to additional controllers
|
||||
};
|
||||
|
||||
export const HEARTBEAT_INTERVAL = 15000; // 15s
|
||||
export const FORCE_SYNC_TIMEOUT = 8500; // 8.5s timeout for force sync ACKs (must be > content.js poll timeout of 8s)
|
||||
export const EPISODE_LOBBY_TIMEOUT = 60000; // 60s timeout for episode lobby
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{LANG_CODE}}" class="lang-{{LANG_CODE}}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ALT_INDEX_TITLE}}</title>
|
||||
<link rel="preload" href="{{ASSET_PATH}}style.min.css" as="style">
|
||||
<link rel="stylesheet" href="{{ASSET_PATH}}style.min.css">
|
||||
<link rel="icon" type="image/webp" href="{{ASSET_PATH}}assets/NewLogoIcon_64.webp">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives">
|
||||
|
||||
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives">
|
||||
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/alternatives">
|
||||
<link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/alternatives">
|
||||
<link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/alternatives">
|
||||
<link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/alternatives">
|
||||
<link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/alternatives">
|
||||
<link rel="alternate" hreflang="it" href="https://sync.koalastuff.net/it/alternatives">
|
||||
<link rel="alternate" hreflang="pl" href="https://sync.koalastuff.net/pl/alternatives">
|
||||
<link rel="alternate" hreflang="tr" href="https://sync.koalastuff.net/tr/alternatives">
|
||||
<link rel="alternate" hreflang="nl" href="https://sync.koalastuff.net/nl/alternatives">
|
||||
<link rel="alternate" hreflang="ja" href="https://sync.koalastuff.net/ja/alternatives">
|
||||
<link rel="alternate" hreflang="ko" href="https://sync.koalastuff.net/ko/alternatives">
|
||||
<link rel="alternate" hreflang="zh" href="https://sync.koalastuff.net/zh/alternatives">
|
||||
<link rel="alternate" hreflang="uk" href="https://sync.koalastuff.net/uk/alternatives">
|
||||
<link rel="alternate" hreflang="pt" href="https://sync.koalastuff.net/pt/alternatives">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/alternatives">
|
||||
|
||||
<meta name="description" content="{{ALT_INDEX_META_DESC}}">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives">
|
||||
<meta property="og:title" content="{{ALT_INDEX_TITLE}}">
|
||||
<meta property="og:description" content="{{ALT_INDEX_META_DESC}}">
|
||||
<meta property="og:image" content="https://sync.koalastuff.net/assets/RemoteProf.webp">
|
||||
<meta property="og:locale" content="{{OG_LOCALE}}">
|
||||
|
||||
<!-- Twitter / X -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ALT_INDEX_TITLE}}">
|
||||
<meta name="twitter:description" content="{{ALT_INDEX_META_DESC}}">
|
||||
<meta name="twitter:image" content="https://sync.koalastuff.net/assets/RemoteProf.webp">
|
||||
|
||||
<!-- Structured Data (Schema.org) for Breadcrumbs -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Alternatives",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Organization Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "KoalaSync",
|
||||
"url": "https://sync.koalastuff.net/",
|
||||
"logo": "https://sync.koalastuff.net/assets/NewLogoIcon_128.webp",
|
||||
"sameAs": [
|
||||
"https://github.com/Shik3i/KoalaSync",
|
||||
"https://mastodon.social/@koalastuff"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Mobile Browser Theme Styling -->
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
||||
<script src="{{ASSET_PATH}}lang-init.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-blobs">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<a href="{{ASSET_PATH}}" class="logo-area" style="text-decoration: none;">
|
||||
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
|
||||
<span>KoalaSync</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="{{ASSET_PATH}}" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" rel="noopener" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="lang-select-container">
|
||||
<select class="lang-dropdown" aria-label="Select Language">
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
<option value="fr">🇫🇷 Français</option>
|
||||
<option value="es">🇪🇸 Español</option>
|
||||
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
|
||||
<option value="ru">🇷🇺 Русский</option>
|
||||
<option value="it">🇮🇹 Italiano</option>
|
||||
<option value="pl">🇵🇱 Polski</option>
|
||||
<option value="tr">🇹🇷 Türkçe</option>
|
||||
<option value="nl">🇳🇱 Nederlands</option>
|
||||
<option value="ja">🇯🇵 日本語</option>
|
||||
<option value="ko">🇰🇷 한국어</option>
|
||||
<option value="zh">🇨🇳 中文</option>
|
||||
<option value="uk">🇺🇦 Українська</option>
|
||||
<option value="pt">🇵🇹 Português (Portugal)</option>
|
||||
</select>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</div>
|
||||
<button class="hamburger" aria-label="Menu" aria-expanded="false">☰</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2.5rem 2rem;">
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<img src="{{ASSET_PATH}}assets/RemoteProf.webp" alt="Koala teaching and explaining concepts" class="legal-mascot" width="180" height="180">
|
||||
</div>
|
||||
|
||||
<h1>
|
||||
<span lang="en">Guides & Comparisons</span>
|
||||
<span lang="de">Vergleiche & Anleitungen</span>
|
||||
</h1>
|
||||
<p style="text-align: center; font-size: 0.95rem; line-height: 1.6; max-width: 600px; margin: 0 auto 2rem auto; color: var(--text-muted);">
|
||||
<span lang="en">How does KoalaSync stack up against other watch party tools and streaming solutions? Read our honest, technical comparisons to find the best fit for your remote movie nights.</span>
|
||||
<span lang="de">Wie schlägt sich KoalaSync im Vergleich zu anderen Watch-Party-Tools und Streaming-Lösungen? Lies unsere ehrlichen, technischen Vergleiche, um die passende Lösung für deine Filmabende zu finden.</span>
|
||||
</p>
|
||||
|
||||
<!-- Cards Grid Layout -->
|
||||
<div class="guides-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 2rem; margin-bottom: 2rem;">
|
||||
<!-- Teleparty Card -->
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives/teleparty" class="guide-card" style="text-decoration: none; background: rgba(255, 255, 255, 0.03); border: 1px solid var(--glass-border); border-radius: 12px; padding: 1.5rem; display: flex; flex-direction: column; justify-content: space-between;">
|
||||
<div>
|
||||
<h3 style="margin-top: 0; margin-bottom: 0.75rem; color: #fff; font-size: 1.25rem;">{{ALT_TELEPARTY_TITLE}}</h3>
|
||||
<p style="font-size: 0.9rem; line-height: 1.5; color: var(--text-muted); margin-bottom: 1.5rem;">{{ALT_TELEPARTY_META_DESC}}</p>
|
||||
</div>
|
||||
<div class="btn btn-primary" style="align-self: flex-start; padding: 0.5rem 1rem; font-size: 0.85rem; border-radius: 6px;">
|
||||
<span lang="en">Read Comparison →</span>
|
||||
<span lang="de">Vergleich lesen →</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Screen Sharing Card -->
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives/screen-sharing" class="guide-card" style="text-decoration: none; background: rgba(255, 255, 255, 0.03); border: 1px solid var(--glass-border); border-radius: 12px; padding: 1.5rem; display: flex; flex-direction: column; justify-content: space-between;">
|
||||
<div>
|
||||
<h3 style="margin-top: 0; margin-bottom: 0.75rem; color: #fff; font-size: 1.25rem;">{{ALT_SCREEN_TITLE}}</h3>
|
||||
<p style="font-size: 0.9rem; line-height: 1.5; color: var(--text-muted); margin-bottom: 1.5rem;">{{ALT_SCREEN_META_DESC}}</p>
|
||||
</div>
|
||||
<div class="btn btn-primary" style="align-self: flex-start; padding: 0.5rem 1rem; font-size: 0.85rem; border-radius: 6px;">
|
||||
<span lang="en">Read Comparison →</span>
|
||||
<span lang="de">Vergleich lesen →</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- CTA block -->
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--glass-border);">
|
||||
<a href="{{ASSET_PATH}}" class="btn btn-primary" style="margin-bottom: 1.5rem;">{{ALT_TRY_KOALASYNC}}</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 KoalaSync. {{FOOTER_MIT}}</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">{{FOOTER_RAM}}</p>
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_LEGAL_LINK}}">{{FOOTER_LEGAL}}</a>
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_PRIVACY_LINK}}">{{FOOTER_PRIVACY}}</a>
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives"><span><span lang="en">Guides & comparisons</span><span lang="de">Vergleiche & Anleitungen</span></span></a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="{{ASSET_PATH}}app.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{LANG_CODE}}" class="lang-{{LANG_CODE}}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ALT_SCREEN_TITLE}}</title>
|
||||
<link rel="preload" href="{{ASSET_PATH}}style.min.css" as="style">
|
||||
<link rel="stylesheet" href="{{ASSET_PATH}}style.min.css">
|
||||
<link rel="icon" type="image/webp" href="{{ASSET_PATH}}assets/NewLogoIcon_64.webp">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives/screen-sharing">
|
||||
|
||||
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="it" href="https://sync.koalastuff.net/it/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="pl" href="https://sync.koalastuff.net/pl/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="tr" href="https://sync.koalastuff.net/tr/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="nl" href="https://sync.koalastuff.net/nl/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="ja" href="https://sync.koalastuff.net/ja/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="ko" href="https://sync.koalastuff.net/ko/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="zh" href="https://sync.koalastuff.net/zh/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="uk" href="https://sync.koalastuff.net/uk/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="pt" href="https://sync.koalastuff.net/pt/alternatives/screen-sharing">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/alternatives/screen-sharing">
|
||||
|
||||
<meta name="description" content="{{ALT_SCREEN_META_DESC}}">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives/screen-sharing">
|
||||
<meta property="og:title" content="{{ALT_SCREEN_TITLE}}">
|
||||
<meta property="og:description" content="{{ALT_SCREEN_META_DESC}}">
|
||||
<meta property="og:image" content="https://sync.koalastuff.net/assets/PopcornFriends.webp">
|
||||
<meta property="og:locale" content="{{OG_LOCALE}}">
|
||||
|
||||
<!-- Twitter / X -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ALT_SCREEN_TITLE}}">
|
||||
<meta name="twitter:description" content="{{ALT_SCREEN_META_DESC}}">
|
||||
<meta name="twitter:image" content="https://sync.koalastuff.net/assets/PopcornFriends.webp">
|
||||
|
||||
<!-- Structured Data (Schema.org) for Breadcrumbs -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Alternatives",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"name": "KoalaSync vs Screen Sharing",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives/screen-sharing"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Organization Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "KoalaSync",
|
||||
"url": "https://sync.koalastuff.net/",
|
||||
"logo": "https://sync.koalastuff.net/assets/NewLogoIcon_128.webp",
|
||||
"sameAs": [
|
||||
"https://github.com/Shik3i/KoalaSync",
|
||||
"https://mastodon.social/@koalastuff"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Mobile Browser Theme Styling -->
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
||||
<script src="{{ASSET_PATH}}lang-init.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-blobs">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<a href="{{ASSET_PATH}}" class="logo-area" style="text-decoration: none;">
|
||||
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
|
||||
<span>KoalaSync</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="{{ASSET_PATH}}" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" rel="noopener" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="lang-select-container">
|
||||
<select class="lang-dropdown" aria-label="Select Language">
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
<option value="fr">🇫🇷 Français</option>
|
||||
<option value="es">🇪🇸 Español</option>
|
||||
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
|
||||
<option value="ru">🇷🇺 Русский</option>
|
||||
<option value="it">🇮🇹 Italiano</option>
|
||||
<option value="pl">🇵🇱 Polski</option>
|
||||
<option value="tr">🇹🇷 Türkçe</option>
|
||||
<option value="nl">🇳🇱 Nederlands</option>
|
||||
<option value="ja">🇯🇵 日本語</option>
|
||||
<option value="ko">🇰🇷 한국어</option>
|
||||
<option value="zh">🇨🇳 中文</option>
|
||||
<option value="uk">🇺🇦 Українська</option>
|
||||
<option value="pt">🇵🇹 Português (Portugal)</option>
|
||||
</select>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</div>
|
||||
<button class="hamburger" aria-label="Menu" aria-expanded="false">☰</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<img src="{{ASSET_PATH}}assets/PopcornFriends.webp" alt="Koalas watching movie with popcorn" class="legal-mascot" width="180" height="180">
|
||||
</div>
|
||||
|
||||
<h1>{{ALT_SCREEN_TITLE}}</h1>
|
||||
|
||||
<!-- Intro -->
|
||||
<section>
|
||||
<p><em>{{ALT_SCREEN_INTRO}}</em></p>
|
||||
</section>
|
||||
|
||||
<!-- When screen sharing is enough -->
|
||||
<section>
|
||||
<h2>{{ALT_SCREEN_WHEN_ENOUGH_TITLE}}</h2>
|
||||
<p>{{ALT_SCREEN_WHEN_ENOUGH_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- Where screen sharing gets annoying -->
|
||||
<section>
|
||||
<h2>{{ALT_SCREEN_WHEN_ANNOYING_TITLE}}</h2>
|
||||
<p>{{ALT_SCREEN_WHEN_ANNOYING_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- Why KoalaSync can feel cleaner -->
|
||||
<section>
|
||||
<h2>{{ALT_SCREEN_WHY_CLEANER_TITLE}}</h2>
|
||||
<p>{{ALT_SCREEN_WHY_CLEANER_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- Honest limitations -->
|
||||
<section>
|
||||
<h2>{{ALT_SCREEN_LIMITS_TITLE}}</h2>
|
||||
<p>{{ALT_SCREEN_LIMITS_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- Summary -->
|
||||
<section>
|
||||
<h2>{{ALT_SCREEN_SUMMARY_TITLE}}</h2>
|
||||
<p>{{ALT_SCREEN_SUMMARY_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- CTA block -->
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--glass-border);">
|
||||
<a href="{{ASSET_PATH}}" class="btn btn-primary" style="margin-bottom: 1.5rem;">{{ALT_TRY_KOALASYNC}}</a>
|
||||
<p style="font-size: 0.9rem; text-align: center;">
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives/teleparty" style="color: var(--accent); text-decoration: none;">{{ALT_TELEPARTY_TITLE}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 KoalaSync. {{FOOTER_MIT}}</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">{{FOOTER_RAM}}</p>
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_LEGAL_LINK}}">{{FOOTER_LEGAL}}</a>
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_PRIVACY_LINK}}">{{FOOTER_PRIVACY}}</a>
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives"><span><span lang="en">Guides & comparisons</span><span lang="de">Vergleiche & Anleitungen</span></span></a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="{{ASSET_PATH}}app.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{LANG_CODE}}" class="lang-{{LANG_CODE}}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ALT_TELEPARTY_TITLE}}</title>
|
||||
<link rel="preload" href="{{ASSET_PATH}}style.min.css" as="style">
|
||||
<link rel="stylesheet" href="{{ASSET_PATH}}style.min.css">
|
||||
<link rel="icon" type="image/webp" href="{{ASSET_PATH}}assets/NewLogoIcon_64.webp">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives/teleparty">
|
||||
|
||||
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="it" href="https://sync.koalastuff.net/it/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="pl" href="https://sync.koalastuff.net/pl/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="tr" href="https://sync.koalastuff.net/tr/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="nl" href="https://sync.koalastuff.net/nl/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="ja" href="https://sync.koalastuff.net/ja/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="ko" href="https://sync.koalastuff.net/ko/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="zh" href="https://sync.koalastuff.net/zh/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="uk" href="https://sync.koalastuff.net/uk/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="pt" href="https://sync.koalastuff.net/pt/alternatives/teleparty">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/alternatives/teleparty">
|
||||
|
||||
<meta name="description" content="{{ALT_TELEPARTY_META_DESC}}">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives/teleparty">
|
||||
<meta property="og:title" content="{{ALT_TELEPARTY_TITLE}}">
|
||||
<meta property="og:description" content="{{ALT_TELEPARTY_META_DESC}}">
|
||||
<meta property="og:image" content="https://sync.koalastuff.net/assets/ProContraKoala.webp">
|
||||
<meta property="og:locale" content="{{OG_LOCALE}}">
|
||||
|
||||
<!-- Twitter / X -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ALT_TELEPARTY_TITLE}}">
|
||||
<meta name="twitter:description" content="{{ALT_TELEPARTY_META_DESC}}">
|
||||
<meta name="twitter:image" content="https://sync.koalastuff.net/assets/ProContraKoala.webp">
|
||||
|
||||
<!-- Structured Data (Schema.org) for Breadcrumbs -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Alternatives",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"name": "KoalaSync vs Teleparty",
|
||||
"item": "https://sync.koalastuff.net/{{LANG_PREFIX}}alternatives/teleparty"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Organization Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "KoalaSync",
|
||||
"url": "https://sync.koalastuff.net/",
|
||||
"logo": "https://sync.koalastuff.net/assets/NewLogoIcon_128.webp",
|
||||
"sameAs": [
|
||||
"https://github.com/Shik3i/KoalaSync",
|
||||
"https://mastodon.social/@koalastuff"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Mobile Browser Theme Styling -->
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
||||
<script src="{{ASSET_PATH}}lang-init.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-blobs">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<div class="container nav-content">
|
||||
<a href="{{ASSET_PATH}}" class="logo-area" style="text-decoration: none;">
|
||||
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
|
||||
<span>KoalaSync</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="{{ASSET_PATH}}" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width: 16px; height: 16px; display: block;"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" rel="noopener" style="display: inline-flex; align-items: center; gap: 6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="lang-select-container">
|
||||
<select class="lang-dropdown" aria-label="Select Language">
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
<option value="fr">🇫🇷 Français</option>
|
||||
<option value="es">🇪🇸 Español</option>
|
||||
<option value="pt-BR">🇧🇷 Português (Brasil)</option>
|
||||
<option value="ru">🇷🇺 Русский</option>
|
||||
<option value="it">🇮🇹 Italiano</option>
|
||||
<option value="pl">🇵🇱 Polski</option>
|
||||
<option value="tr">🇹🇷 Türkçe</option>
|
||||
<option value="nl">🇳🇱 Nederlands</option>
|
||||
<option value="ja">🇯🇵 日本語</option>
|
||||
<option value="ko">🇰🇷 한국어</option>
|
||||
<option value="zh">🇨🇳 中文</option>
|
||||
<option value="uk">🇺🇦 Українська</option>
|
||||
<option value="pt">🇵🇹 Português (Portugal)</option>
|
||||
</select>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</div>
|
||||
<button class="hamburger" aria-label="Menu" aria-expanded="false">☰</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="legal-content">
|
||||
<div class="legal-card" data-reveal style="padding: 2rem;">
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<img src="{{ASSET_PATH}}assets/ProContraKoala.webp" alt="Koala weighing pros and cons" class="legal-mascot" width="180" height="180">
|
||||
</div>
|
||||
|
||||
<h1>{{ALT_TELEPARTY_TITLE}}</h1>
|
||||
|
||||
<!-- Intro -->
|
||||
<section>
|
||||
<p><em>{{ALT_TELEPARTY_INTRO}}</em></p>
|
||||
</section>
|
||||
|
||||
<!-- When Teleparty may still make sense -->
|
||||
<section>
|
||||
<h2>{{ALT_TELEPARTY_WHEN_TELE_TITLE}}</h2>
|
||||
<p>{{ALT_TELEPARTY_WHEN_TELE_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- When KoalaSync may fit better -->
|
||||
<section>
|
||||
<h2>{{ALT_TELEPARTY_WHEN_KOALA_TITLE}}</h2>
|
||||
<p>{{ALT_TELEPARTY_WHEN_KOALA_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- The main difference -->
|
||||
<section>
|
||||
<h2>{{ALT_TELEPARTY_MAIN_DIFF_TITLE}}</h2>
|
||||
<p>{{ALT_TELEPARTY_MAIN_DIFF_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- Honest limitations -->
|
||||
<section>
|
||||
<h2>{{ALT_TELEPARTY_LIMITS_TITLE}}</h2>
|
||||
<p>{{ALT_TELEPARTY_LIMITS_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- Summary -->
|
||||
<section>
|
||||
<h2>{{ALT_TELEPARTY_SUMMARY_TITLE}}</h2>
|
||||
<p>{{ALT_TELEPARTY_SUMMARY_BODY}}</p>
|
||||
</section>
|
||||
|
||||
<!-- CTA block -->
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--glass-border);">
|
||||
<a href="{{ASSET_PATH}}" class="btn btn-primary" style="margin-bottom: 1.5rem;">{{ALT_TRY_KOALASYNC}}</a>
|
||||
<p style="font-size: 0.9rem; text-align: center;">
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives/screen-sharing" style="color: var(--accent); text-decoration: none;">{{ALT_SCREEN_TITLE}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 KoalaSync. {{FOOTER_MIT}}</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">{{FOOTER_RAM}}</p>
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_LEGAL_LINK}}">{{FOOTER_LEGAL}}</a>
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_PRIVACY_LINK}}">{{FOOTER_PRIVACY}}</a>
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives"><span><span lang="en">Guides & comparisons</span><span lang="de">Vergleiche & Anleitungen</span></span></a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="{{ASSET_PATH}}app.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+36
-5
@@ -331,7 +331,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const updateDynamicVersion = async () => {
|
||||
try {
|
||||
const versionPath = document.documentElement.lang === 'en' ? 'version.json' : '../version.json';
|
||||
const versionPath = '/version.json';
|
||||
const response = await fetch(versionPath);
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
@@ -480,14 +480,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const activeLang = safeGetLocalStorage('koala_lang') || (navigator.language.startsWith('de') ? 'de' : 'en');
|
||||
const path = window.location.pathname;
|
||||
const pathSegments = path.split('/');
|
||||
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'].includes(seg));
|
||||
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'].includes(seg));
|
||||
|
||||
// Only need to do this dynamic rewrite if we are NOT already inside a localized subdirectory
|
||||
if (!isSubdir) {
|
||||
const homeLinks = document.querySelectorAll('a[href="./"], a[href="de/"], a[href="fr/"], a[href="es/"], a[href="pt-BR/"], a[href="ru/"], a[href="it/"], a[href="pl/"], a[href="tr/"], a[href="nl/"], a[href="ja/"], a[href="ko/"], a[href="pt/"]');
|
||||
const homeLinks = document.querySelectorAll('a[href="./"], a[href="de/"], a[href="fr/"], a[href="es/"], a[href="it/"], a[href="nl/"], a[href="pl/"], a[href="pt/"], a[href="pt-BR/"], a[href="tr/"], a[href="ru/"], a[href="ja/"], a[href="ko/"], a[href="zh/"], a[href="uk/"]');
|
||||
homeLinks.forEach(link => {
|
||||
link.href = (activeLang === 'en') ? './' : `${activeLang}/`;
|
||||
});
|
||||
|
||||
const altLinks = document.querySelectorAll('a[href*="alternatives"]');
|
||||
altLinks.forEach(link => {
|
||||
const attr = link.getAttribute('href');
|
||||
if (attr === 'alternatives' || attr === 'alternatives/teleparty' || attr.endsWith('/alternatives')) {
|
||||
link.href = (activeLang === 'en') ? 'alternatives' : `${activeLang}/alternatives`;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -529,13 +537,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathSegments = path.split('/');
|
||||
const isAlternative = pathSegments.includes('alternatives');
|
||||
if (isAlternative) {
|
||||
let target;
|
||||
const firstSeg = pathSegments[1];
|
||||
const isLangSubdir = ['de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'].includes(firstSeg);
|
||||
|
||||
if (newLang === 'en') {
|
||||
if (isLangSubdir) {
|
||||
pathSegments.splice(1, 1);
|
||||
}
|
||||
} else {
|
||||
if (isLangSubdir) {
|
||||
pathSegments[1] = newLang;
|
||||
} else {
|
||||
pathSegments.splice(1, 0, newLang);
|
||||
}
|
||||
}
|
||||
target = pathSegments.join('/');
|
||||
window.location.href = target + window.location.hash;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if we are on a static landing page versus a dynamic utility page
|
||||
const isIndex = !path.includes('join');
|
||||
|
||||
if (isIndex) {
|
||||
// Static navigation: Route to correct subdirectory
|
||||
const pathSegments = path.split('/');
|
||||
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'].includes(seg));
|
||||
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'].includes(seg));
|
||||
|
||||
let targetPath;
|
||||
if (newLang === 'en') {
|
||||
@@ -558,7 +589,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else {
|
||||
// Dynamic page: Toggle classes and update elements dynamically without navigating away
|
||||
const html = document.documentElement;
|
||||
html.classList.remove('lang-en', 'lang-de', 'lang-fr', 'lang-es', 'lang-pt-br', 'lang-ru', 'lang-it', 'lang-pl', 'lang-tr', 'lang-nl', 'lang-ja', 'lang-ko', 'lang-pt');
|
||||
html.classList.remove('lang-en', 'lang-de', 'lang-fr', 'lang-es', 'lang-it', 'lang-nl', 'lang-pl', 'lang-pt', 'lang-pt-br', 'lang-tr', 'lang-ru', 'lang-ja', 'lang-ko', 'lang-zh', 'lang-uk');
|
||||
|
||||
// Fallback dynamic pages to 'en' if 'de' is not chosen (since fr/es markup is not present)
|
||||
const activeDisplayLang = (newLang === 'de') ? 'de' : 'en';
|
||||
|
||||
+94
-12
@@ -162,7 +162,7 @@ async function compile() {
|
||||
}
|
||||
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
||||
const localesDir = path.join(websiteDir, 'locales');
|
||||
const languages = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'];
|
||||
const languages = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'];
|
||||
|
||||
// Read version for build-time injection (SEO: crawlers see real version)
|
||||
const versionJson = JSON.parse(fs.readFileSync(path.join(websiteDir, 'version.json'), 'utf8'));
|
||||
@@ -174,6 +174,8 @@ async function compile() {
|
||||
let compiled = templateContent;
|
||||
compiled = compiled.replace(/\{\{ASSET_PATH\}\}/g, assetPath);
|
||||
compiled = compiled.replace(/\{\{VERSION\}\}/g, buildVersion);
|
||||
const langPrefix = lang === 'en' ? '' : `${lang}/`;
|
||||
compiled = compiled.replace(/\{\{LANG_PREFIX\}\}/g, langPrefix);
|
||||
languages.forEach(l => {
|
||||
compiled = compiled.replace(new RegExp(`\\{\\{SELECTED_${l.toUpperCase()}\\}\\}`, 'g'), l === lang ? 'selected' : '');
|
||||
});
|
||||
@@ -183,6 +185,53 @@ async function compile() {
|
||||
return compiled;
|
||||
}
|
||||
|
||||
// Read alternative page templates
|
||||
const telepartyTemplatePath = path.join(websiteDir, 'alternatives/teleparty.html');
|
||||
const screenSharingTemplatePath = path.join(websiteDir, 'alternatives/screen-sharing.html');
|
||||
const overviewTemplatePath = path.join(websiteDir, 'alternatives/index.html');
|
||||
const hasTelepartyTemplate = fs.existsSync(telepartyTemplatePath);
|
||||
const hasScreenSharingTemplate = fs.existsSync(screenSharingTemplatePath);
|
||||
const hasOverviewTemplate = fs.existsSync(overviewTemplatePath);
|
||||
const telepartyTemplate = hasTelepartyTemplate ? fs.readFileSync(telepartyTemplatePath, 'utf8') : '';
|
||||
const screenSharingTemplate = hasScreenSharingTemplate ? fs.readFileSync(screenSharingTemplatePath, 'utf8') : '';
|
||||
const overviewTemplate = hasOverviewTemplate ? fs.readFileSync(overviewTemplatePath, 'utf8') : '';
|
||||
|
||||
// Preload English locale for fallback
|
||||
const englishLocalePath = path.join(localesDir, 'en.json');
|
||||
const englishLocale = JSON.parse(fs.readFileSync(englishLocalePath, 'utf8'));
|
||||
|
||||
function compileAlternativePage(templateContent, locale, lang, assetPath, langPrefix) {
|
||||
let compiled = templateContent;
|
||||
compiled = compiled.replace(/\{\{LANG_CODE\}\}/g, lang);
|
||||
compiled = compiled.replace(/\{\{ASSET_PATH\}\}/g, assetPath);
|
||||
compiled = compiled.replace(/\{\{LANG_PREFIX\}\}/g, langPrefix);
|
||||
compiled = compiled.replace(/\{\{VERSION\}\}/g, buildVersion);
|
||||
|
||||
const indexTitle = lang === 'de' ? 'KoalaSync - Vergleiche & Anleitungen' : 'KoalaSync - Alternatives & Comparisons';
|
||||
const indexDesc = lang === 'de'
|
||||
? 'Entdecke ehrliche Vergleiche und detaillierte Leitfäden zwischen KoalaSync und anderen Watch-Party-Erweiterungen.'
|
||||
: 'Explore honest comparisons and detailed guides comparing KoalaSync with other watch party extensions and streaming solutions.';
|
||||
|
||||
compiled = compiled.replace(/\{\{ALT_INDEX_TITLE\}\}/g, indexTitle);
|
||||
compiled = compiled.replace(/\{\{ALT_INDEX_META_DESC\}\}/g, indexDesc);
|
||||
|
||||
// og:locale mapping for Facebook
|
||||
const ogLocales = {
|
||||
en: 'en_US', de: 'de_DE', fr: 'fr_FR', es: 'es_ES',
|
||||
'pt-BR': 'pt_BR', ru: 'ru_RU', it: 'it_IT', pl: 'pl_PL',
|
||||
tr: 'tr_TR', nl: 'nl_NL', ja: 'ja_JP', ko: 'ko_KR',
|
||||
zh: 'zh_CN', uk: 'uk_UA', pt: 'pt_PT'
|
||||
};
|
||||
compiled = compiled.replace(/\{\{OG_LOCALE\}\}/g, ogLocales[lang] || 'en_US');
|
||||
|
||||
// Merge locale with English fallback for keys not present in current locale
|
||||
const mergedLocale = { ...englishLocale, ...locale };
|
||||
for (const [key, value] of Object.entries(mergedLocale)) {
|
||||
compiled = compiled.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
for (const lang of languages) {
|
||||
const localePath = path.join(localesDir, `${lang}.json`);
|
||||
if (!fs.existsSync(localePath)) { console.warn(`Warning: Locale ${lang} not found.`); continue; }
|
||||
@@ -193,6 +242,26 @@ async function compile() {
|
||||
const html = compilePage(locale, '', lang);
|
||||
fs.writeFileSync(path.join(wwwDir, 'index.html'), html);
|
||||
englishHtml[''] = html;
|
||||
|
||||
// Compile alternatives for English (assets resolved using '../' prefix)
|
||||
if (hasTelepartyTemplate) {
|
||||
const altDir = path.join(wwwDir, 'alternatives');
|
||||
fs.mkdirSync(altDir, { recursive: true });
|
||||
const tpCompiled = compileAlternativePage(telepartyTemplate, locale, lang, '../', '');
|
||||
fs.writeFileSync(path.join(altDir, 'teleparty.html'), tpCompiled);
|
||||
}
|
||||
if (hasScreenSharingTemplate) {
|
||||
const altDir = path.join(wwwDir, 'alternatives');
|
||||
fs.mkdirSync(altDir, { recursive: true });
|
||||
const ssCompiled = compileAlternativePage(screenSharingTemplate, locale, lang, '../', '');
|
||||
fs.writeFileSync(path.join(altDir, 'screen-sharing.html'), ssCompiled);
|
||||
}
|
||||
if (hasOverviewTemplate) {
|
||||
const altDir = path.join(wwwDir, 'alternatives');
|
||||
fs.mkdirSync(altDir, { recursive: true });
|
||||
const ovCompiled = compileAlternativePage(overviewTemplate, locale, lang, '../', '');
|
||||
fs.writeFileSync(path.join(altDir, 'index.html'), ovCompiled);
|
||||
}
|
||||
} else {
|
||||
console.log(`Compiling ${lang.toUpperCase()} (${lang}/index.html)...`);
|
||||
const langDir = path.join(wwwDir, lang);
|
||||
@@ -200,6 +269,26 @@ async function compile() {
|
||||
const html = compilePage(locale, '../', lang);
|
||||
fs.writeFileSync(path.join(langDir, 'index.html'), html);
|
||||
englishHtml[lang] = html;
|
||||
|
||||
// Compile alternatives for other languages (assets resolved using '../../' prefix)
|
||||
if (hasTelepartyTemplate) {
|
||||
const langAltDir = path.join(langDir, 'alternatives');
|
||||
fs.mkdirSync(langAltDir, { recursive: true });
|
||||
const tpCompiled = compileAlternativePage(telepartyTemplate, locale, lang, '../../', lang + '/');
|
||||
fs.writeFileSync(path.join(langAltDir, 'teleparty.html'), tpCompiled);
|
||||
}
|
||||
if (hasScreenSharingTemplate) {
|
||||
const langAltDir = path.join(langDir, 'alternatives');
|
||||
fs.mkdirSync(langAltDir, { recursive: true });
|
||||
const ssCompiled = compileAlternativePage(screenSharingTemplate, locale, lang, '../../', lang + '/');
|
||||
fs.writeFileSync(path.join(langAltDir, 'screen-sharing.html'), ssCompiled);
|
||||
}
|
||||
if (hasOverviewTemplate) {
|
||||
const langAltDir = path.join(langDir, 'alternatives');
|
||||
fs.mkdirSync(langAltDir, { recursive: true });
|
||||
const ovCompiled = compileAlternativePage(overviewTemplate, locale, lang, '../../', lang + '/');
|
||||
fs.writeFileSync(path.join(langAltDir, 'index.html'), ovCompiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +308,7 @@ async function compile() {
|
||||
const src = path.join(websiteDir, mapping.src);
|
||||
const dest = path.join(wwwDir, mapping.dest);
|
||||
if (fs.existsSync(src)) {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` Copied: ${mapping.src} → ${mapping.dest}`);
|
||||
}
|
||||
@@ -278,23 +368,15 @@ async function compile() {
|
||||
let html = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 8a. Replace hashed asset refs
|
||||
html = html.replace(/href="(?:\.\.\/)?style\.min\.css"/g, (m) => {
|
||||
const prefix = m.includes('../') ? '../' : '';
|
||||
html = html.replace(/href="((?:\.\.\/)*)style\.min\.css"/g, (m, prefix) => {
|
||||
return `href="${prefix}${styleName}"`;
|
||||
});
|
||||
html = html.replace(/src="(?:\.\.\/)?app\.min\.js"/g, (m) => {
|
||||
const prefix = m.includes('../') ? '../' : '';
|
||||
html = html.replace(/src="((?:\.\.\/)*)app\.min\.js"/g, (m, prefix) => {
|
||||
return `src="${prefix}${appName}"`;
|
||||
});
|
||||
html = html.replace(/src="(?:\.\.\/)?lang-init\.min\.js"/g, (m) => {
|
||||
const prefix = m.includes('../') ? '../' : '';
|
||||
html = html.replace(/src="((?:\.\.\/)*)lang-init\.min\.js"/g, (m, prefix) => {
|
||||
return `src="${prefix}${langName}"`;
|
||||
});
|
||||
// Also update preload directives
|
||||
html = html.replace(/href="(?:\.\.\/)?style\.min\.css"/g, (m) => {
|
||||
const prefix = m.includes('../') ? '../' : '';
|
||||
return `href="${prefix}${styleName}"`;
|
||||
});
|
||||
|
||||
// 8b. Inject AVIF <picture> wrappers
|
||||
html = injectAvifPictures(html);
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<link rel="preload" href="../style.min.css" as="style">
|
||||
<link rel="stylesheet" href="../style.min.css">
|
||||
<link rel="icon" type="image/webp" href="../assets/NewLogoIcon_64.webp">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/de/datenschutz">
|
||||
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/privacy">
|
||||
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/datenschutz">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/privacy">
|
||||
<meta name="description" content="Datenschutzerklärung von KoalaSync - Wir nehmen Datenschutz ernst. Erfahre mehr über unseren Ansatz ohne Tracking, ohne Logs und ohne Cookies.">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<script type="application/ld+json">
|
||||
@@ -155,6 +160,7 @@
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="impressum" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
|
||||
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
|
||||
<a href="alternatives/teleparty">Vergleiche & Anleitungen</a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<link rel="preload" href="../style.min.css" as="style">
|
||||
<link rel="stylesheet" href="../style.min.css">
|
||||
<link rel="icon" type="image/webp" href="../assets/NewLogoIcon_64.webp">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/de/impressum">
|
||||
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/imprint">
|
||||
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/impressum">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/imprint">
|
||||
<meta name="description" content="Impressum (Legal Notice) von KoalaSync - Anbieterkennzeichnung und Kontaktinformationen für die Open-Source Video-Synchronisations-Erweiterung.">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<script type="application/ld+json">
|
||||
@@ -142,6 +147,7 @@
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="impressum" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
|
||||
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
|
||||
<a href="alternatives">Vergleiche & Anleitungen</a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<link rel="preload" href="style.min.css" as="style">
|
||||
<link rel="stylesheet" href="style.min.css">
|
||||
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/imprint">
|
||||
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/imprint">
|
||||
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/impressum">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/imprint">
|
||||
<meta name="description" content="Legal Notice (Impressum) of KoalaSync - Contact information and operator details for the open-source video synchronization extension.">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<script type="application/ld+json">
|
||||
@@ -140,6 +145,7 @@
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="imprint" style="color: var(--text-muted); text-decoration: none;">Legal Notice</a>
|
||||
<a href="privacy" style="color: var(--text-muted); text-decoration: none;">Privacy Policy</a>
|
||||
<a href="alternatives">Guides & comparisons</a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="impressum" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Legal Notice</span><span lang="de">Impressum</span></a>
|
||||
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Privacy Policy</span><span lang="de">Datenschutz</span></a>
|
||||
<a href="alternatives"><span lang="en">Guides & comparisons</span><span lang="de">Vergleiche & Anleitungen</span></a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
|
||||
@@ -31,11 +31,15 @@
|
||||
'tr': 'tr',
|
||||
'nl': 'nl',
|
||||
'ja': 'ja',
|
||||
'ko': 'ko'
|
||||
'ko': 'ko',
|
||||
'zh': 'zh',
|
||||
'uk': 'uk'
|
||||
};
|
||||
|
||||
var getBrowserLang = function() {
|
||||
var fullLang = (navigator.language || '').toLowerCase();
|
||||
if (fullLang.indexOf('zh') === 0) return 'zh';
|
||||
if (fullLang.indexOf('uk') === 0) return 'uk';
|
||||
if (fullLang.indexOf('pt-br') === 0) return 'pt-br';
|
||||
if (fullLang.indexOf('pt') === 0) return 'pt';
|
||||
return fullLang.split('-')[0];
|
||||
|
||||
+32
-2
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "de_DE",
|
||||
"META_TITLE": "KoalaSync | Netflix, YouTube & jedes Video mit Freunden synchronisieren",
|
||||
"META_DESCRIPTION": "Schaue Netflix, YouTube, Twitch und jedes HTML5-Video synchron mit Freunden. Kostenlose, quelloffene Browser-Erweiterung. Keine Anmeldung erforderlich.",
|
||||
"SCHEMA_APP_DESC": "Schaue Netflix, YouTube, Twitch und jedes HTML5-Video synchron mit Freunden. Kostenlose, quelloffene Browser-Erweiterung. Keine Anmeldung erforderlich.",
|
||||
"OG_TITLE": "KoalaSync | Netflix, Emby, Jellyfin & fast jedes Video mit Freunden synchronisieren",
|
||||
"OG_DESCRIPTION": "Schaue Netflix, Emby, Jellyfin, YouTube, Twitch und fast jedes HTML5-Video perfekt synchronisiert. Quelloffene, datenschutzfreundliche Browser-Erweiterung für Chrome und Firefox.",
|
||||
"TWITTER_TITLE": "KoalaSync | Netflix, Emby, Jellyfin & fast jedes Video mit Freunden synchronisieren – Browser-Erweiterung",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Nur unterstützte Seiten",
|
||||
"COMP_FEAT_6_NAME": "Lokalisierung / Übersetzung",
|
||||
"COMP_FEAT_6_DESC": "Verfügbare Oberflächensprachen und Übersetzungsunterstützung.",
|
||||
"COMP_FEAT_6_KOALA": "13 Sprachen (wachsend)",
|
||||
"COMP_FEAT_6_KOALA": "15 Sprachen (wachsend)",
|
||||
"COMP_FEAT_6_TELE": "Nur Englisch",
|
||||
"COMP_FEAT_7_NAME": "Audio Compressor / Lautstärke Angleichen",
|
||||
"COMP_FEAT_7_DESC": "Integrierte Audionormalisierung für leise Dialoge und laute Effekte",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Alles, was du über synchrones Videoschauen mit Freunden wissen musst.",
|
||||
"FAQ_Q1": "Kann ich Netflix, Emby oder Jellyfin synchron mit Freunden schauen?",
|
||||
"FAQ_A1": "Ja! KoalaSync funktioniert mit Netflix, YouTube, Twitch und jeder Website mit einem HTML5-Videoplayer — einschließlich Emby, Jellyfin und eigenen Medienservern. Installiere die Erweiterung, erstelle einen Raum, teile den Einladungslink und alle schauen perfekt synchron.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "Streamt KoalaSync das Video an meine Freunde?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Nein. KoalaSync streamt, hostet oder teilt keine Videoinhalte und umgeht keinen Zugang. Jede Person schaut das Video lokal im eigenen Browser und braucht selbst Zugriff auf die jeweilige Website oder den Streamingdienst, zum Beispiel ein eigenes Netflix-Konto. KoalaSync synchronisiert nur Aktionen wie Play, Pause und Spulen.",
|
||||
"FAQ_Q2": "Ist KoalaSync eine kostenlose Alternative zu Teleparty?",
|
||||
"FAQ_A2": "100 % kostenlos und Open Source unter der MIT-Lizenz. Keine Premium-Stufen, keine gesperrten Funktionen, keine Werbung. Anders als Teleparty läuft KoalaSyncs Relay-Server ausschließlich im RAM — ohne Datensammlung und ohne Benutzerkonten.",
|
||||
"FAQ_Q3": "Funktioniert KoalaSync mit Emby, Jellyfin und eigenen Medienservern?",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "de/impressum",
|
||||
"FOOTER_PRIVACY_LINK": "de/datenschutz",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync ist nicht mit Netflix, Disney+, Amazon, YouTube, Twitch oder anderen Streaming-Plattformen verbunden, wird von diesen nicht unterstützt oder steht in keiner Beziehung zu diesen. Alle Markenrechte liegen bei ihren jeweiligen Inhabern.",
|
||||
"FOOTER_SUPPORT": "Kauf mir einen Kaffee auf Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty: Welche Watch-Party-Lösung passt besser?",
|
||||
"ALT_TELEPARTY_META_DESC": "Ein ehrlicher Vergleich von KoalaSync und Teleparty für Watch-Partys, eingebauten Chat, Mobile-Support, Browser-Video-Sync, selbst gehostete Medienserver und Open Source.",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync und Teleparty lösen beide dasselbe nervige Problem: Du willst mit anderen Leuten online etwas schauen, ohne ständig „3, 2, 1, Play“ sagen zu müssen. Beide synchronisieren Videowiedergabe, aber sie gehen ziemlich unterschiedlich an die Sache ran.<br><br>Teleparty ist sinnvoll, wenn du einen eingebauten Chat möchtest oder am Handy schauen willst. KoalaSync ist eher für Desktop-Browser-Watch-Partys gedacht, bei denen du sowieso schon mit Freunden in Discord, Teamspeak oder einem anderen Voice-Call bist und einfach nur die Wiedergabe synchron halten möchtest.<br><br>Der Hauptgrund, warum ich KoalaSync gebaut habe, war Flexibilität. Es soll mit fast jeder Website funktionieren, die einen normalen Browser-Videoplayer nutzt, nicht nur mit einer festen Liste an Plattformen. Dazu gehören auch selbst gehostete Setups wie Emby, Jellyfin und Plex, lokale oder private Domains und viele normale Videoseiten im Browser.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Wann Teleparty trotzdem sinnvoller sein kann",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty kann trotzdem die bessere Wahl sein, wenn du den Chat direkt im Watch-Party-Tool haben möchtest. KoalaSync hat aktuell keinen eingebauten Chat. Es geht eher davon aus, dass du sowieso schon woanders mit deinen Freunden redest, zum Beispiel in Discord, Teamspeak, WhatsApp, Signal oder einem normalen Anruf.<br><br>Mobile ist der zweite große Unterschied. Teleparty hat eine App, während KoalaSync aktuell nur auf Desktop-Browsern läuft. Wenn du Netflix oder andere Inhalte am Handy oder Tablet synchron schauen möchtest, ist KoalaSync im Moment nicht das richtige Tool dafür.<br><br>Wenn dir also vor allem eingebauter Chat und Mobile-Support wichtig sind, passt Teleparty wahrscheinlich besser zu deinem Setup.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Wann KoalaSync besser passt",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync passt besser, wenn du ein flexibles Sync-Tool für den Desktop-Browser suchst und nicht auf eine kurze Plattformliste beschränkt sein möchtest.<br><br>Es funktioniert mit fast jeder Website, die einen normalen Browser-Videoplayer nutzt. Das ist besonders nützlich für Emby, Jellyfin, Plex, private Medienserver, lokale Domains oder kleinere Videoseiten, die klassische Watch-Party-Tools meistens gar nicht beachten.<br><br>Außerdem ist KoalaSync kostenlos, Open Source, braucht keine Accounts und kann bei Bedarf selbst gehostet werden. Ich habe es gebaut, weil ich etwas Privacy-freundliches wollte, das mit meinem eigenen Emby-Setup funktioniert, nicht noch eine geschlossene Plattform nur für ein paar große Streaming-Seiten.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "Der wichtigste Unterschied",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty ist eher ein Watch-Party-Produkt mit Chat und Mobile-Unterstützung. KoalaSync ist eher eine flexible Sync-Schicht für Browser-Video.<br><br>Das klingt erstmal nach einem kleinen Unterschied, ändert aber den Use Case ziemlich stark. Wenn du eine Watch-Party-App mit eingebautem Chat brauchst, ergibt Teleparty Sinn. Wenn du schon einen Voice-Call hast und nur möchtest, dass Play, Pause und Seek zwischen mehreren Browsern synchron bleiben, passt KoalaSync besser.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Ehrliche Grenzen",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync ist nicht magisch und versucht auch nicht, seine Grenzen zu verstecken. Es funktioniert aktuell nur auf Desktop-Browsern. Es gibt keinen eingebauten Chat und keine Mobile-App.<br><br>Die Kompatibilität hängt außerdem vom Browser und vom jeweiligen Videoplayer ab. KoalaSync sollte mit fast jedem normalen Browser-Videoplayer funktionieren, aber Websites können ihre Player, DRM-Logik oder Browser-Einschränkungen jederzeit ändern.<br><br>KoalaSync ist nicht mit Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex oder anderen hier genannten Marken verbunden. Die Namen werden nur verwendet, um typische Anwendungsfälle verständlich zu beschreiben.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Kurz gesagt",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Wenn du eingebauten Chat oder Mobile-Support brauchst, passt Teleparty wahrscheinlich besser. Wenn du kostenlose, Open-Source-Desktop-Browser-Synchronisierung möchtest, die mit fast jedem normalen Videoplayer funktioniert, inklusive selbst gehosteter Medienserver wie Emby, Jellyfin und Plex, ist KoalaSync wahrscheinlich die passendere Wahl.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync vs Bildschirmübertragung: Wann ist Video-Sync besser?",
|
||||
"ALT_SCREEN_META_DESC": "Ein ehrlicher Vergleich von KoalaSync und Bildschirmübertragung für Filmabende, Wiedergabekontrolle, Videoqualität, Privatsphäre, Untertitel und gemeinsames Schauen im Browser.",
|
||||
"ALT_SCREEN_INTRO": "Bildschirmübertragung funktioniert, und manchmal reicht das auch völlig. Eine Person teilt den Bildschirm, alle anderen schauen zu, fertig.<br><br>Bei längeren Filmabenden oder Serien kann das aber schnell nervig werden. Die Qualität hängt vom Upload, der CPU, dem Browser und der Komprimierung durch Discord, Teamspeak, Zoom oder andere Call-Apps ab. Außerdem kontrolliert nur eine Person wirklich die Wiedergabe.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Wann Bildschirmübertragung reicht",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Screen Sharing ist sinnvoll, wenn es möglichst schnell gehen soll. Wenn nur eine Person Zugriff auf das Video hat oder alle damit okay sind, dass eine Person Play, Pause und Spulen kontrolliert, ist es wahrscheinlich die einfachste Lösung.<br><br>Es reicht auch oft, wenn die Qualität nicht so wichtig ist, niemand eigene Untertitel oder Audio-Einstellungen braucht und die Person, die teilt, kein Problem damit hat, genau dieses Fenster oder diesen Bildschirm zu zeigen.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Wo Bildschirmübertragung nervig wird",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "Der größte Nachteil ist, dass nicht jeder im eigenen Player schaut. Eine Person streamt den Bildschirm, alle anderen sehen nur diesen Stream.<br><br>Das kann schlechtere Bildqualität, komprimierten Ton, Verzögerung und weniger Kontrolle für alle anderen bedeuten. Untertitel und Audiospur sind außerdem genau das, was die teilende Person eingestellt hat.<br><br>Dazu kommt der Privacy-Aspekt. Wenn du Fenster oder Tabs wechselst, können andere kurz deine Browser-Tabs, Mails, Reddit, Chats oder andere private Dinge sehen. Man kann das mit einem separaten Fenster umgehen, aber man muss trotzdem daran denken.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Warum KoalaSync sauberer sein kann",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync überträgt nicht deinen Bildschirm. Jeder schaut das Video lokal im eigenen Browser, und KoalaSync hält nur Aktionen wie Play, Pause und Spulen zwischen allen Teilnehmern synchron.<br><br>Dadurch kann jeder lokal Vollbild nutzen, eigene Untertitel oder Audio-Einstellungen verwenden, sofern der Player das unterstützt, und der Voice-Call bleibt separat in Discord, Teamspeak oder einer anderen App.<br><br>Für Filmabende fühlt sich das oft deutlich sauberer an, als wenn eine Person den ganzen Bildschirm streamt. Besonders praktisch ist das bei browserbasierten Setups wie Emby, Jellyfin, Plex, lokalen Dateien im Browser, privaten Domains oder anderen Websites mit normalen Browser-Videoplayern.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Ehrliche Grenzen",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync ersetzt keinen Voice-Call. Wenn ihr beim Schauen reden wollt, nutzt ihr weiterhin Discord, Teamspeak, Signal, WhatsApp oder was auch immer ihr normalerweise verwendet.<br><br>Außerdem braucht jede Person selbst Zugriff auf das Video oder die jeweilige Seite. KoalaSync umgeht keine Abos, Logins, DRM, regionale Einschränkungen oder Plattformregeln.<br><br>KoalaSync ist aktuell Desktop-Browser-only, hat keinen eingebauten Chat oder Voice-Chat und die Funktion hängt immer vom jeweiligen Browser und Videoplayer ab.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Kurz gesagt",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Bildschirmübertragung ist die einfachste Lösung, wenn es schnell gehen soll und Qualität, Privatsphäre oder individuelle Kontrolle nicht so wichtig sind. KoalaSync passt besser, wenn jeder lokal im eigenen Browser schauen soll und sich der Filmabend mehr wie wirklich gemeinsames Schauen anfühlen soll.",
|
||||
"ALT_TRY_KOALASYNC": "KoalaSync ausprobieren"
|
||||
}
|
||||
|
||||
+32
-2
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "en_US",
|
||||
"META_TITLE": "KoalaSync | Sync Netflix, YouTube & Any Video with Friends",
|
||||
"META_DESCRIPTION": "Watch Netflix, YouTube, Twitch & any HTML5 video in perfect sync with friends. Free, open-source browser extension for Chrome and Firefox. No sign-up needed.",
|
||||
"SCHEMA_APP_DESC": "Watch Netflix, YouTube, Twitch & any HTML5 video in perfect sync with friends. Free, open-source browser extension for Chrome and Firefox. No sign-up needed.",
|
||||
"OG_TITLE": "KoalaSync | Sync Netflix, Emby, Jellyfin & Almost Any Video with Friends",
|
||||
"OG_DESCRIPTION": "Watch Netflix, Emby, Jellyfin, YouTube, Twitch and almost any HTML5 video in perfect sync. Privacy-first, open-source browser extension for Chrome & Firefox.",
|
||||
"TWITTER_TITLE": "KoalaSync | Sync Netflix, Emby, Jellyfin & Almost Any Video with Friends – Browser Extension",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Supported sites only",
|
||||
"COMP_FEAT_6_NAME": "Localization / Translation",
|
||||
"COMP_FEAT_6_DESC": "Available interface languages and translation support.",
|
||||
"COMP_FEAT_6_KOALA": "13 Languages (and growing)",
|
||||
"COMP_FEAT_6_KOALA": "15 Languages (and growing)",
|
||||
"COMP_FEAT_6_TELE": "English only",
|
||||
"COMP_FEAT_7_NAME": "Audio Compressor / Volume Leveling",
|
||||
"COMP_FEAT_7_DESC": "Built-in audio normalization to balance quiet dialogue and loud effects",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Everything you need to know about watching videos together in perfect sync.",
|
||||
"FAQ_Q1": "Can I watch Netflix, Emby or Jellyfin together in sync with friends?",
|
||||
"FAQ_A1": "Yes! KoalaSync works on Netflix, YouTube, Twitch and any website with an HTML5 video player — including Emby, Jellyfin and self-hosted media servers. Install the extension, create a room, share the invite link, and everyone watches in perfect sync.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "Does KoalaSync stream the video to my friends?",
|
||||
"FAQ_A_VIDEO_ACCESS": "No. KoalaSync does not stream, host, share, or bypass access to video content. Everyone watches the video locally in their own browser and needs their own access to the website or streaming service, such as their own Netflix account. KoalaSync only synchronizes playback actions like play, pause, and seeking.",
|
||||
"FAQ_Q2": "Is KoalaSync a free alternative to Teleparty?",
|
||||
"FAQ_A2": "100% free and open source under the MIT License. No premium tiers, no locked features, no ads — everything is included. Unlike Teleparty, KoalaSync's relay server is RAM-only with zero data collection or user accounts required.",
|
||||
"FAQ_Q3": "Does KoalaSync work with Emby, Jellyfin and self-hosted media servers?",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync is not affiliated with, endorsed by, or associated with Netflix, Disney+, Amazon, YouTube, Twitch, or any other streaming platform. All trademarks are property of their respective owners.",
|
||||
"FOOTER_SUPPORT": "Buy me a coffee on Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty: which watch party tool should you use?",
|
||||
"ALT_TELEPARTY_META_DESC": "A practical comparison of KoalaSync and Teleparty for watch parties, built-in chat, mobile support, browser video sync, self-hosted media servers and open-source use.",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync and Teleparty both help with the same annoying problem: watching something together without doing the whole \"3, 2, 1, play\" routine. They just take a different approach.<br><br>Teleparty makes the most sense if you want a built-in chat or if you want to watch on mobile. KoalaSync is more for desktop browser watch parties where you are already talking on Discord, Teamspeak or another voice call and just want playback to stay in sync.<br><br>The main reason I built KoalaSync was flexibility. It is meant to work with almost any website that uses a normal browser video player, not just a fixed list of platforms. That includes self-hosted setups like Emby, Jellyfin and Plex, local/private domains, and a lot of normal video pages in the browser.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "When Teleparty may still make sense",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty can still be the better choice if you want the chat to be part of the watch party tool itself. KoalaSync does not currently have a built-in chat. It assumes you are already talking somewhere else, like Discord, Teamspeak, WhatsApp, Signal or a normal call.<br><br>Mobile is the other big reason. Teleparty has an app, while KoalaSync currently only works on desktop browsers. If you want to sync Netflix on a phone or tablet, KoalaSync is not the right tool for that right now.<br><br>So if chat and mobile support are the things you care about most, Teleparty may simply fit your setup better.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "When KoalaSync may fit better",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync fits better if you want a flexible desktop browser sync tool instead of something tied to a short platform list.<br><br>It works with almost any website that uses a normal browser video player. That is especially useful for Emby, Jellyfin, Plex, private media servers, local domains, or smaller video sites that classic watch party tools usually ignore.<br><br>It is also free, open source, does not require accounts, and can be self-hosted if you want to run your own relay. I built it because I wanted something privacy-friendly that worked with my own Emby setup, not another closed platform just for a few big streaming sites.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "The main difference",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty is more of a watch party product with chat and mobile support. KoalaSync is more of a flexible sync layer for browser video.<br><br>That sounds like a small difference, but it changes the use case a lot. If you need a full watch party app with chat inside it, Teleparty makes sense. If you already have voice chat and just want the video controls to stay synced across browsers, KoalaSync is the simpler fit.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Honest limitations",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync is not magic and it is not trying to hide its limits. It currently works only on desktop browsers. It has no built-in chat and no mobile app.<br><br>Compatibility also depends on the browser and the video player. KoalaSync should work with almost any normal browser video player, but websites can change their players, DRM behavior or browser restrictions at any time.<br><br>KoalaSync is not affiliated with Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex or any other brand mentioned here. The names are only used to explain typical use cases.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Summary",
|
||||
"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.<br><br>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_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.<br><br>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",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "The biggest downside is that not everyone is watching in their own player. One person streams their screen, and everyone else only sees that stream.<br><br>That can mean worse video quality, compressed audio, delayed playback, and less control for everyone else. Subtitles and audio tracks are also whatever the sharer picked.<br><br>There is also the privacy side. If you switch windows or tabs, people may briefly see your browser tabs, email, Reddit, chats or other private stuff. You can work around that with a separate window, but it is still something you have to think about.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Why KoalaSync can feel cleaner",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync does not stream your screen. Everyone watches the video locally in their own browser, and KoalaSync only keeps playback actions like play, pause and seeking in sync.<br><br>That means everyone can use fullscreen on their own device, use their own subtitle or audio settings if the player supports it, and keep the voice chat separate in Discord, Teamspeak or another call.<br><br>For movie nights, that can feel a lot cleaner than one person broadcasting their screen. It is especially useful for browser-based setups like Emby, Jellyfin, Plex, local files opened in the browser, private domains, or other websites with normal browser video players.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Honest limitations",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync is not a replacement for a voice call. If you want to talk while watching, you still use Discord, Teamspeak, Signal, WhatsApp or whatever you normally use.<br><br>Everyone also needs access to the video or page on their own device. KoalaSync does not bypass subscriptions, logins, DRM, regional restrictions or platform rules.<br><br>It is currently desktop-browser only, has no built-in chat or voice feature, and compatibility always depends on the browser and the video player.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Summary",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Screen sharing is the simplest option when you just want something quick and do not care much about quality, privacy or individual control. KoalaSync is a better fit when everyone should watch locally in their own browser and the session should feel more like actually watching together.",
|
||||
"ALT_TRY_KOALASYNC": "Try KoalaSync"
|
||||
}
|
||||
|
||||
+35
-5
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "es_ES",
|
||||
"META_TITLE": "KoalaSync | Sincroniza Netflix, YouTube y cualquier video con amigos",
|
||||
"META_DESCRIPTION": "Mira Netflix, YouTube, Twitch y cualquier video HTML5 en perfecta sincronización con amigos. Extensión de navegador gratuita, de código abierto y sin necesidad de registro.",
|
||||
"SCHEMA_APP_DESC": "Mira Netflix, YouTube, Twitch y cualquier video HTML5 en perfecta sincronización con amigos. Extensión de navegador gratuita, de código abierto y sin necesidad de registro.",
|
||||
"OG_TITLE": "KoalaSync | Sincroniza Netflix, Emby, Jellyfin y casi cualquier video con amigos",
|
||||
"OG_DESCRIPTION": "Mira Netflix, Emby, Jellyfin, YouTube, Twitch y casi cualquier video HTML5 en perfecta sincronización. Extensión de navegador de código abierto y respetuosa con la privacidad para Chrome y Firefox.",
|
||||
"TWITTER_TITLE": "KoalaSync | Sincroniza Netflix, Emby, Jellyfin y casi cualquier video con amigos – Extensión de navegador",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Solo sitios compatibles",
|
||||
"COMP_FEAT_6_NAME": "Localización / Traducción",
|
||||
"COMP_FEAT_6_DESC": "Idiomas de interfaz disponibles y soporte de traducción.",
|
||||
"COMP_FEAT_6_KOALA": "13 idiomas (y sumando)",
|
||||
"COMP_FEAT_6_KOALA": "15 idiomas (y sumando)",
|
||||
"COMP_FEAT_6_TELE": "Solo inglés",
|
||||
"COMP_FEAT_7_NAME": "Compresor de Audio / Nivelación de Volumen",
|
||||
"COMP_FEAT_7_DESC": "Normalización de audio integrada para equilibrar diálogos bajos y efectos fuertes",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Todo lo que necesitas saber para ver vídeos en perfecta sincronía.",
|
||||
"FAQ_Q1": "¿Puedo ver Netflix, Emby o Jellyfin sincronizado con amigos?",
|
||||
"FAQ_A1": "¡Sí! KoalaSync funciona en Netflix, YouTube, Twitch y cualquier web con reproductor HTML5, incluyendo Emby, Jellyfin y servidores multimedia propios. Instala la extensión, crea una sala, comparte el enlace y todos veréis lo mismo al mismo tiempo.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "¿KoalaSync transmite el video a mis amigos?",
|
||||
"FAQ_A_VIDEO_ACCESS": "No. KoalaSync no transmite, aloja, comparte ni evita el acceso al contenido de video. Cada persona ve el video localmente en su propio navegador y necesita su propio acceso al sitio web o servicio de streaming, por ejemplo su propia cuenta de Netflix. KoalaSync solo sincroniza acciones de reproducción como reproducir, pausar y avanzar o retroceder.",
|
||||
"FAQ_Q2": "¿Es KoalaSync una alternativa gratuita a Teleparty?",
|
||||
"FAQ_A2": "Es 100% gratuito y de código abierto (Licencia MIT). Sin suscripciones, sin funciones bloqueadas y sin anuncios. A diferencia de Teleparty, el servidor de KoalaSync solo usa RAM: no hay bases de datos ni cuentas de usuario.",
|
||||
"FAQ_Q3": "¿Funciona KoalaSync con Emby, Jellyfin y servidores propios?",
|
||||
@@ -150,9 +153,9 @@
|
||||
"MOCK_08": "Salir de la sala",
|
||||
"MOCK_09": "Seleccionar video",
|
||||
"MOCK_10": "Control remoto",
|
||||
"MOCK_11": "Reproducir",
|
||||
"MOCK_12": "Pausar",
|
||||
"MOCK_13": "SINCRO",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Reproducir",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausar",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SINCRO",
|
||||
"MOCK_14": "Ir a la posición de otros",
|
||||
"MOCK_15": "Estado de la última actividad",
|
||||
"MOCK_16": "Esperando a 1 participante (KoalaPC)...",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync no está afiliado, respaldado ni asociado con Netflix, Disney+, Amazon, YouTube, Twitch ni ninguna otra plataforma de streaming. Todas las marcas comerciales pertenecen a sus respectivos dueños.",
|
||||
"FOOTER_SUPPORT": "Invítame a un café en Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync frente a Teleparty: ¿qué herramienta de visionado conjunto deberías usar?",
|
||||
"ALT_TELEPARTY_META_DESC": "Una comparativa práctica entre KoalaSync y Teleparty para ver contenido en grupo, analizando aspectos como el chat integrado, la compatibilidad móvil, la sincronización de vídeo en el navegador, los servidores multimedia autohospedados y el uso de código abierto.",
|
||||
"ALT_TELEPARTY_INTRO": "Tanto KoalaSync como Teleparty ayudan a resolver el mismo problema molesto: ver algo juntos sin tener que recurrir a la típica rutina de «3, 2, 1, ¡reproducir!». Simplemente adoptan enfoques diferentes.<br><br>Teleparty es la opción más lógica si buscas un chat integrado o si quieres ver contenido desde un dispositivo móvil. KoalaSync está más orientado a sesiones de visionado en navegadores de escritorio, donde ya te comunicas a través de Discord, Teamspeak u otra aplicación de voz y solo necesitas que la reproducción se mantenga sincronizada.<br><br>La razón principal por la que creé KoalaSync fue la flexibilidad. Está diseñado para funcionar con casi cualquier sitio web que utilice un reproductor de vídeo estándar de navegador, no solo con una lista fija de plataformas. Esto incluye configuraciones autohospedadas como Emby, Jellyfin y Plex, dominios locales o privados y muchas páginas de vídeo convencionales.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Cuándo puede seguir siendo útil Teleparty",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty puede seguir siendo la mejor opción si prefieres que el chat forme parte de la propia herramienta de visionado. Actualmente, KoalaSync no cuenta con chat integrado; parte de la premisa de que ya te estás comunicando por otro medio, como Discord, Teamspeak, WhatsApp, Signal o una llamada convencional.<br><br>La compatibilidad móvil es otro factor clave. Teleparty dispone de aplicación propia, mientras que KoalaSync, por ahora, solo funciona en navegadores de escritorio. Si quieres sincronizar Netflix en un teléfono o tableta, KoalaSync no es la herramienta adecuada en este momento.<br><br>Por tanto, si el chat y la compatibilidad móvil son tus prioridades, es posible que Teleparty se adapte mejor a tus necesidades.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Cuándo es mejor usar KoalaSync",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync es una mejor opción si buscas una herramienta flexible para sincronizar la reproducción de vídeo en navegadores de escritorio, en lugar de algo limitado a una lista reducida de plataformas.<br><br>Funciona con casi cualquier sitio web que utilice el reproductor de vídeo estándar del navegador. Esto resulta especialmente útil para Emby, Jellyfin, Plex, servidores multimedia privados, dominios locales o sitios de vídeo más pequeños que las herramientas convencionales de visionado compartido suelen ignorar.<br><br>Además, es gratuito, de código abierto, no requiere crear cuentas y permite el autohospedaje si deseas gestionar tu propio servidor de retransmisión (relay). Lo creé porque buscaba una solución respetuosa con la privacidad que funcionara con mi propia configuración de Emby, y no otra plataforma cerrada limitada a unos pocos grandes servicios de streaming.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "La diferencia principal",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty está concebido principalmente como una herramienta para ver contenido en grupo, con funciones de chat y compatibilidad móvil. KoalaSync, en cambio, funciona más bien como una capa flexible de sincronización de vídeo para navegadores.<br><br>Puede parecer una diferencia menor, pero cambia considerablemente el caso de uso. Si necesitas una aplicación completa para ver contenido en grupo que incluya chat integrado, Teleparty es la opción lógica. Si ya dispones de chat de voz y solo quieres mantener sincronizados los controles de vídeo entre distintos navegadores, KoalaSync es la solución más sencilla.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Limitaciones reales",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync no hace milagros ni pretende ocultar sus limitaciones. Actualmente, solo funciona en navegadores de escritorio; no cuenta con chat integrado ni aplicación móvil.<br><br>La compatibilidad también depende del navegador y del reproductor de vídeo utilizado. Aunque KoalaSync debería funcionar con casi cualquier reproductor estándar de navegador, los sitios web pueden modificar sus reproductores, el comportamiento del DRM o las restricciones del navegador en cualquier momento.<br><br>KoalaSync no tiene ninguna relación con Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex ni ninguna otra marca mencionada aquí. Dichos nombres se utilizan únicamente para ilustrar casos de uso habituales.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Resumen",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Si buscas chat integrado o compatibilidad con dispositivos móviles, Teleparty podría ser la mejor opción. Si prefieres una solución gratuita y de código abierto para sincronizar navegadores de escritorio que funcione con casi cualquier reproductor de vídeo convencional —incluidos servidores multimedia de alojamiento propio como Emby, Jellyfin y Plex—, KoalaSync es probablemente la alternativa más adecuada.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync frente a compartir pantalla: ¿cuándo es mejor la sincronización de vídeo?",
|
||||
"ALT_SCREEN_META_DESC": "Una comparativa práctica entre KoalaSync y la función de compartir pantalla para noches de cine a distancia, analizando aspectos como el control de reproducción, la calidad de vídeo, la privacidad, los subtítulos y la visualización desde el navegador.",
|
||||
"ALT_SCREEN_INTRO": "Compartir pantalla funciona y, sinceramente, a veces es suficiente. Una persona comparte su pantalla, las demás la ven y listo.<br><br>Sin embargo, para sesiones de cine más largas o para ver episodios en grupo, puede resultar molesto. La calidad del vídeo depende de la velocidad de subida, la CPU y el navegador de quien comparte, así como de la compresión que apliquen aplicaciones como Discord, Teamspeak, Zoom u otras plataformas de videollamada. Además, solo una persona tiene el control real de la reproducción.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Cuándo basta con compartir pantalla",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Compartir pantalla es una buena opción si buscas una configuración lo más rápida posible. Si solo una persona tiene acceso al vídeo o si a todos les parece bien que una sola persona controle la reproducción, la pausa y el avance o retroceso, puede que sea la opción más sencilla.<br><br>También tiene sentido cuando la calidad no es una prioridad, cuando nadie necesita sus propios subtítulos o ajustes de audio, y cuando la persona que comparte se siente cómoda mostrando esa ventana del navegador o su pantalla.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Cuando compartir pantalla resulta molesto",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "La mayor desventaja es que no todos ven el contenido en su propio reproductor. Una persona transmite su pantalla y el resto solo ve esa transmisión.<br><br>Esto puede implicar una peor calidad de video, audio comprimido, retraso en la reproducción y menos control para los demás. Además, los subtítulos y las pistas de audio serán los que haya elegido la persona que comparte.<br><br>También está el aspecto de la privacidad. Si cambias de ventana o de pestaña, los demás podrían ver brevemente las pestañas de tu navegador, tu correo electrónico, Reddit, chats u otra información privada. Puedes evitar esto usando una ventana independiente, pero es algo que debes tener en cuenta.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Por qué KoalaSync puede resultar más limpio",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync no transmite tu pantalla. Cada persona ve el video localmente en su propio navegador, y KoalaSync solo sincroniza las acciones de reproducción, como iniciar, pausar y avanzar o retroceder.<br><br>Esto significa que todos pueden usar la pantalla completa en su propio dispositivo, utilizar sus propios ajustes de subtítulos o audio (si el reproductor lo permite) y mantener el chat de voz separado en Discord, Teamspeak o cualquier otra aplicación de llamadas.<br><br>Para las noches de cine, esto puede resultar mucho más limpio que tener a una persona transmitiendo su pantalla. Es especialmente útil para configuraciones basadas en navegador como Emby, Jellyfin, Plex, archivos locales abiertos en el navegador, dominios privados u otros sitios web que utilicen reproductores de video estándar del navegador.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Limitaciones reales",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync no sustituye a una llamada de voz. Si quieres hablar mientras ves el contenido, puedes seguir usando Discord, Teamspeak, Signal, WhatsApp o cualquier otra herramienta que utilices habitualmente.<br><br>Además, cada participante necesita acceder al vídeo o a la página web desde su propio dispositivo. KoalaSync no elude suscripciones, inicios de sesión, DRM, restricciones regionales ni las normas de las plataformas.<br><br>Actualmente solo funciona en navegadores de escritorio, no incluye funciones de chat o voz, y la compatibilidad depende siempre del navegador y del reproductor de vídeo.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Resumen",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Compartir pantalla es la opción más sencilla cuando buscas algo rápido y no te preocupa demasiado la calidad, la privacidad o el control individual. KoalaSync es una mejor alternativa cuando cada persona debe ver el contenido localmente en su propio navegador y se busca una experiencia que realmente se asemeje a verlo juntos.",
|
||||
"ALT_TRY_KOALASYNC": "Prueba KoalaSync"
|
||||
}
|
||||
|
||||
+36
-6
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "fr_FR",
|
||||
"META_TITLE": "KoalaSync | Synchronisez Netflix, YouTube et n'importe quelle vidéo avec vos amis",
|
||||
"META_DESCRIPTION": "Regardez Netflix, YouTube, Twitch et des vidéos HTML5 en synchro avec vos amis. Extension de navigateur gratuite et open-source. Aucune inscription requise.",
|
||||
"SCHEMA_APP_DESC": "Regardez Netflix, YouTube, Twitch et des vidéos HTML5 en synchro avec vos amis. Extension de navigateur gratuite et open-source. Aucune inscription requise.",
|
||||
"OG_TITLE": "KoalaSync | Synchronisez Netflix, Emby, Jellyfin et presque toutes les vidéos avec vos amis",
|
||||
"OG_DESCRIPTION": "Regardez Netflix, Emby, Jellyfin, YouTube, Twitch et presque n'importe quelle vidéo HTML5 en parfaite synchronisation. Extension de navigateur open-source et respectueuse de la vie privée pour Chrome et Firefox.",
|
||||
"TWITTER_TITLE": "KoalaSync | Synchronisez Netflix, Emby, Jellyfin et presque toutes les vidéos avec vos amis – Extension de navigateur",
|
||||
@@ -49,7 +50,7 @@
|
||||
"FEATURE_7_TITLE": "Égalisation du volume / Compresseur audio",
|
||||
"FEATURE_7_DESC": "Fini de jouer avec le bouton du volume. Le compresseur audio intégré booste les dialogues discrets et adoucit les explosions. Fonctionne sur Netflix, Prime Video, Disney+, YouTube et toute vidéo HTML5. Entièrement local — aucune donnée audio ne quitte votre ordinateur.",
|
||||
"FEATURE_8_TITLE": "Open Source (MIT License)",
|
||||
"FEATURE_8_DESC": "The entire codebase is publicly auditable on GitHub. No black boxes, no proprietary blobs — 100% transparency from the relay server to the browser extension.",
|
||||
"FEATURE_8_DESC": "Tout le code source est publiquement auditable sur GitHub. Pas de boite noire, pas de blob proprietaire - transparence totale du serveur relais a l'extension de navigateur.",
|
||||
"COMP_TITLE": "KoalaSync vs Teleparty",
|
||||
"COMP_SUBTITLE": "Découvrez pourquoi un outil open-source, sans publicité et respectueux de la vie privée est le meilleur choix pour regarder ensemble.",
|
||||
"COMP_COL_FEATURE": "Fonctionnalité",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Sites pris en charge uniquement",
|
||||
"COMP_FEAT_6_NAME": "Localisation / Traduction",
|
||||
"COMP_FEAT_6_DESC": "Langues d'interface disponibles et support de traduction.",
|
||||
"COMP_FEAT_6_KOALA": "13 langues (et plus à venir)",
|
||||
"COMP_FEAT_6_KOALA": "15 langues (et plus à venir)",
|
||||
"COMP_FEAT_6_TELE": "Anglais uniquement",
|
||||
"COMP_FEAT_7_NAME": "Compresseur Audio / Égalisation du Volume",
|
||||
"COMP_FEAT_7_DESC": "Normalisation audio intégrée pour équilibrer les dialogues silencieux et les effets sonores",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Tout ce que vous devez savoir pour regarder des vidéos en parfaite synchronisation.",
|
||||
"FAQ_Q1": "Puis-je regarder Netflix, Emby ou Jellyfin en synchronisation avec des amis ?",
|
||||
"FAQ_A1": "Oui ! KoalaSync fonctionne avec Netflix, YouTube, Twitch et tout site avec un lecteur vidéo HTML5 — y compris Emby, Jellyfin et les serveurs multimédia auto-hébergés. Installez l'extension, créez un salon, partagez le lien et regardez en parfaite synchronisation.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "KoalaSync diffuse-t-il la vidéo à mes amis ?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Non. KoalaSync ne diffuse, n’héberge, ne partage et ne contourne aucun accès aux contenus vidéo. Chaque personne regarde la vidéo localement dans son propre navigateur et doit disposer de son propre accès au site ou au service de streaming, par exemple son propre compte Netflix. KoalaSync synchronise uniquement les actions de lecture comme lecture, pause et recherche dans la vidéo.",
|
||||
"FAQ_Q2": "KoalaSync est-il une alternative gratuite à Teleparty ?",
|
||||
"FAQ_A2": "100 % gratuit et open source sous licence MIT. Aucun niveau premium, aucune fonctionnalité verrouillée, aucune publicité. Contrairement à Teleparty, le serveur relay de KoalaSync fonctionne uniquement en RAM — sans collecte de données ni comptes utilisateur.",
|
||||
"FAQ_Q3": "KoalaSync fonctionne-t-il avec Emby, Jellyfin et les serveurs multimédia auto-hébergés ?",
|
||||
@@ -150,9 +153,9 @@
|
||||
"MOCK_08": "Quitter",
|
||||
"MOCK_09": "Choisir la vidéo",
|
||||
"MOCK_10": "Télécommande",
|
||||
"MOCK_11": "Lire",
|
||||
"MOCK_12": "Pause",
|
||||
"MOCK_13": "Sauter vers les autres",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Lire",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pause",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
|
||||
"MOCK_14": "Dernière activité",
|
||||
"MOCK_15": "En attente de 1 participant (KoalaPC)...",
|
||||
"MOCK_16": "Passer & lire",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync n'est pas affilié à Netflix, Disney+, Amazon, YouTube, Twitch ou toute autre plateforme de streaming. Toutes les marques déposées appartiennent à leurs détenteurs respectifs.",
|
||||
"FOOTER_SUPPORT": "Offre-moi un café sur Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty : quel outil de visionnage en groupe choisir ?",
|
||||
"ALT_TELEPARTY_META_DESC": "Une comparaison pratique entre KoalaSync et Teleparty pour le visionnage en groupe : chat intégré, compatibilité mobile, synchronisation vidéo via navigateur, serveurs multimédias auto-hébergés et nature open-source.",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync et Teleparty répondent tous deux au même problème agaçant : regarder quelque chose ensemble sans avoir à passer par le rituel du « 3, 2, 1, lecture ». Ils adoptent simplement des approches différentes.<br><br>Teleparty est la solution idéale si vous souhaitez un chat intégré ou si vous voulez regarder du contenu sur mobile. KoalaSync est davantage conçu pour le visionnage sur navigateur (PC/Mac) lorsque vous communiquez déjà via Discord, Teamspeak ou un autre service d'appel vocal, et que vous voulez simplement que la lecture reste synchronisée.<br><br>La raison principale pour laquelle j'ai créé KoalaSync est la flexibilité. L'outil est conçu pour fonctionner avec presque tous les sites web utilisant un lecteur vidéo standard, et non avec une liste fixe de plateformes. Cela inclut les installations auto-hébergées comme Emby, Jellyfin et Plex, les domaines locaux ou privés, ainsi que de nombreuses pages vidéo classiques sur navigateur.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Quand Teleparty reste une bonne option",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty peut s'avérer être le meilleur choix si vous souhaitez que le chat soit directement intégré à l'outil de visionnage. KoalaSync ne propose pas de chat intégré pour le moment ; il part du principe que vous communiquez déjà par ailleurs, via Discord, Teamspeak, WhatsApp, Signal ou un appel classique.<br><br>L'aspect mobile constitue une autre raison majeure. Teleparty dispose d'une application, alors que KoalaSync ne fonctionne actuellement que sur les navigateurs de bureau. Si vous souhaitez synchroniser Netflix sur un téléphone ou une tablette, KoalaSync n'est pas l'outil adapté pour l'instant.<br><br>Ainsi, si le chat et la compatibilité mobile sont vos priorités, Teleparty pourrait tout simplement mieux convenir à votre configuration.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Quand choisir KoalaSync",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync est plus adapté si vous recherchez un outil flexible de synchronisation vidéo pour navigateur de bureau, plutôt qu'une solution limitée à une courte liste de plateformes.<br><br>Il fonctionne avec presque tous les sites web utilisant un lecteur vidéo standard de navigateur. C'est particulièrement utile pour Emby, Jellyfin, Plex, les serveurs multimédias privés, les domaines locaux ou les sites vidéo plus modestes que les outils classiques de visionnage en groupe ignorent généralement.<br><br>De plus, c'est un outil gratuit et open source qui ne nécessite pas de compte et peut être auto-hébergé si vous souhaitez gérer votre propre relais. Je l'ai créé parce que je voulais une solution respectueuse de la vie privée compatible avec ma propre installation Emby, et non une plateforme fermée supplémentaire réservée à quelques grands sites de streaming.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "La différence principale",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty est avant tout un outil de visionnage en groupe incluant une messagerie et une prise en charge mobile. KoalaSync se présente davantage comme une couche de synchronisation flexible pour la vidéo sur navigateur.<br><br>Cela peut sembler être un détail, mais cela change considérablement l'usage. Si vous avez besoin d'une application complète de visionnage en groupe avec messagerie intégrée, Teleparty est tout indiqué. Si vous disposez déjà d'un système de discussion vocale et que vous souhaitez simplement synchroniser les commandes vidéo entre différents navigateurs, KoalaSync est la solution la plus simple.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Limites en toute transparence",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync n'est pas magique et ne cherche pas à dissimuler ses limites. Actuellement, il ne fonctionne que sur les navigateurs de bureau. Il ne dispose ni de messagerie intégrée ni d'application mobile.<br><br>La compatibilité dépend également du navigateur et du lecteur vidéo. KoalaSync devrait fonctionner avec presque tous les lecteurs vidéo standard, mais les sites web peuvent modifier leurs lecteurs, la gestion des DRM ou les restrictions du navigateur à tout moment.<br><br>KoalaSync n'est affilié ni à Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex, ni à aucune autre marque mentionnée ici. Ces noms sont utilisés uniquement pour illustrer des cas d'utilisation courants.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Résumé",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Si vous recherchez une fonction de chat intégrée ou une prise en charge mobile, Teleparty pourrait être la meilleure option. Si vous préférez une solution gratuite et open source de synchronisation via navigateur (sur ordinateur) compatible avec presque tous les lecteurs vidéo classiques — y compris les serveurs multimédias auto-hébergés comme Emby, Jellyfin et Plex —, alors KoalaSync est probablement le choix idéal.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync vs partage d'écran : quand la synchronisation vidéo est-elle préférable ?",
|
||||
"ALT_SCREEN_META_DESC": "Une comparaison pratique entre KoalaSync et le partage d'écran pour les soirées cinéma à distance : contrôle de la lecture, qualité vidéo, confidentialité, sous-titres et visionnage via navigateur.",
|
||||
"ALT_SCREEN_INTRO": "Le partage d'écran fonctionne et, honnêtement, cela suffit parfois. Une personne partage son écran, les autres regardent, et le tour est joué.<br><br>Cependant, pour des soirées cinéma plus longues ou pour regarder des épisodes ensemble, cela peut devenir contraignant. La qualité vidéo dépend de la vitesse d'envoi (upload), du processeur et du navigateur de la personne qui partage, ainsi que de la compression appliquée par Discord, Teamspeak, Zoom ou toute autre application d'appel. De plus, une seule personne contrôle réellement la lecture.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Quand le partage d'écran suffit",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Le partage d'écran est une bonne solution lorsque vous souhaitez une mise en place ultra-rapide. Si une seule personne a accès à la vidéo ou si tout le monde accepte qu'une seule personne contrôle la lecture, la pause et la navigation dans la vidéo, c'est peut-être l'option la plus simple.<br><br>C'est aussi une solution pertinente lorsque la qualité n'est pas une priorité, que personne n'a besoin de paramètres de sous-titres ou audio personnalisés, et que la personne qui partage l'écran est à l'aise à l'idée de montrer sa fenêtre de navigateur ou son écran.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Les inconvénients du partage d'écran",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "Le principal inconvénient est que les participants ne regardent pas la vidéo via leur propre lecteur. Une personne diffuse son écran, et tous les autres ne voient que ce flux.<br><br>Cela peut entraîner une qualité vidéo moindre, une compression audio, un décalage de lecture et une perte de contrôle pour les autres spectateurs. De plus, les sous-titres et les pistes audio sont imposés par la personne qui partage.<br><br>Il y a aussi la question de la confidentialité. Si vous changez de fenêtre ou d'onglet, les autres peuvent brièvement voir vos onglets, vos e-mails, Reddit, vos discussions ou d'autres éléments privés. Vous pouvez contourner ce problème en utilisant une fenêtre séparée, mais c'est un aspect à prendre en compte.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Pourquoi KoalaSync offre une expérience plus fluide",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync ne diffuse pas votre écran. Chacun regarde la vidéo localement dans son propre navigateur, et KoalaSync se contente de synchroniser les actions de lecture telles que le lancement, la pause et la navigation dans la vidéo.<br><br>Ainsi, chacun peut passer en plein écran sur son appareil, utiliser ses propres paramètres de sous-titres ou audio (si le lecteur le permet) et gérer la discussion vocale séparément via Discord, Teamspeak ou un autre service d'appel.<br><br>Pour les soirées cinéma, cette approche est souvent plus agréable que la diffusion d'écran par une seule personne. C'est particulièrement utile pour les solutions basées sur un navigateur comme Emby, Jellyfin, Plex, les fichiers locaux ouverts dans le navigateur, les domaines privés ou tout autre site web utilisant des lecteurs vidéo standard.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Limites à connaître",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync ne remplace pas un appel vocal. Si vous souhaitez discuter tout en regardant, vous continuez à utiliser Discord, Teamspeak, Signal, WhatsApp ou tout autre outil habituel.<br><br>Chacun doit également pouvoir accéder à la vidéo ou à la page depuis son propre appareil. KoalaSync ne contourne ni les abonnements, ni les identifiants, ni les DRM, ni les restrictions géographiques ou les règles des plateformes.<br><br>L'outil fonctionne actuellement uniquement sur les navigateurs de bureau, ne dispose pas de fonction de chat ou de voix intégrée, et la compatibilité dépend toujours du navigateur et du lecteur vidéo utilisé.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Résumé",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Le partage d'écran est la solution la plus simple si vous cherchez une méthode rapide sans accorder une grande importance à la qualité, à la confidentialité ou au contrôle individuel. KoalaSync est plus adapté lorsque chaque participant doit visionner le contenu localement dans son propre navigateur, pour une expérience se rapprochant davantage d'un véritable visionnage en commun.",
|
||||
"ALT_TRY_KOALASYNC": "Essayer KoalaSync"
|
||||
}
|
||||
|
||||
+35
-5
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "it_IT",
|
||||
"META_TITLE": "KoalaSync | Sincronizza Netflix, YouTube e qualsiasi video con i tuoi amici",
|
||||
"META_DESCRIPTION": "Guarda Netflix, YouTube, Twitch e qualsiasi video HTML5 in perfetta sincronia. Estensione gratuita, open source e senza registrazione.",
|
||||
"SCHEMA_APP_DESC": "Guarda Netflix, YouTube, Twitch e qualsiasi video HTML5 in perfetta sincronia. Estensione gratuita, open source e senza registrazione.",
|
||||
"OG_TITLE": "KoalaSync | Sincronizza Netflix, Emby, Jellyfin e video web con i tuoi amici",
|
||||
"OG_DESCRIPTION": "Guarda video in perfetta sincronia con i tuoi amici. Estensione open source per Chrome e Firefox incentrata sulla privacy.",
|
||||
"TWITTER_TITLE": "KoalaSync | Sincronizza video in streaming con i tuoi amici",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Solo siti selezionati",
|
||||
"COMP_FEAT_6_NAME": "Traduzione",
|
||||
"COMP_FEAT_6_DESC": "Lingue disponibili per l'interfaccia.",
|
||||
"COMP_FEAT_6_KOALA": "13 Lingue (in crescita)",
|
||||
"COMP_FEAT_6_KOALA": "15 Lingue (in crescita)",
|
||||
"COMP_FEAT_6_TELE": "Solo Inglese",
|
||||
"COMP_FEAT_7_NAME": "Compressore Audio",
|
||||
"COMP_FEAT_7_DESC": "Bilanciamento automatico del suono per voci più chiare.",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Tutto quello che c'è da sapere per guardare video in perfetta sincronia.",
|
||||
"FAQ_Q1": "Posso guardare Netflix o YouTube in sincronia?",
|
||||
"FAQ_A1": "Certamente! KoalaSync funziona su Netflix, YouTube, Twitch e qualsiasi sito con video HTML5. Basta installare l'estensione, creare una stanza e condividere il link.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "KoalaSync trasmette il video ai miei amici?",
|
||||
"FAQ_A_VIDEO_ACCESS": "No. KoalaSync non trasmette, ospita, condivide o aggira l’accesso ai contenuti video. Ogni persona guarda il video localmente nel proprio browser e deve avere il proprio accesso al sito o al servizio di streaming, ad esempio un proprio account Netflix. KoalaSync sincronizza solo le azioni di riproduzione come play, pausa e avanzamento.",
|
||||
"FAQ_Q2": "KoalaSync è gratuito?",
|
||||
"FAQ_A2": "Sì, al 100%. È open source con licenza MIT. Non ci sono funzioni a pagamento né pubblicità. A differenza di altre app, non raccogliamo dati utente.",
|
||||
"FAQ_Q3": "Funziona con Emby o Jellyfin?",
|
||||
@@ -150,9 +153,9 @@
|
||||
"MOCK_08": "Esci dalla stanza",
|
||||
"MOCK_09": "Scegli Video",
|
||||
"MOCK_10": "Telecomando",
|
||||
"MOCK_11": "Play",
|
||||
"MOCK_12": "Pausa",
|
||||
"MOCK_13": "SINCRO",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Riproduci",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausa",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
|
||||
"MOCK_14": "Vai dagli altri",
|
||||
"MOCK_15": "Ultima attività",
|
||||
"MOCK_16": "In attesa di un amico (KoalaPC)...",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync non è affiliato a Netflix, YouTube o altre piattaforme. I marchi appartengono ai rispettivi proprietari.",
|
||||
"FOOTER_SUPPORT": "Offrimi un caffè su Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty: quale strumento scegliere per le watch party?",
|
||||
"ALT_TELEPARTY_META_DESC": "Un confronto pratico tra KoalaSync e Teleparty per le watch party: chat integrata, supporto mobile, sincronizzazione video nel browser, server multimediali self-hosted e natura open-source.",
|
||||
"ALT_TELEPARTY_INTRO": "Sia KoalaSync che Teleparty risolvono lo stesso fastidioso problema: guardare qualcosa insieme senza dover ricorrere alla classica procedura del '3, 2, 1, via'. Semplicemente, adottano approcci diversi.<br><br>Teleparty è la scelta ideale se cerchi una chat integrata o se vuoi guardare i contenuti da dispositivo mobile. KoalaSync è più indicato per le watch party su browser desktop, quando stai già comunicando tramite Discord, Teamspeak o un'altra chiamata vocale e vuoi semplicemente mantenere sincronizzata la riproduzione.<br><br>Il motivo principale per cui ho creato KoalaSync è la flessibilità. È progettato per funzionare con quasi tutti i siti web che utilizzano un normale lettore video del browser, non solo con una lista predefinita di piattaforme. Ciò include configurazioni self-hosted come Emby, Jellyfin e Plex, domini locali o privati e molte pagine web che ospitano video.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Quando Teleparty può ancora essere la scelta giusta",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty può rivelarsi la scelta migliore se desideri che la chat sia parte integrante dello strumento stesso per la watch party. Attualmente KoalaSync non dispone di una chat integrata; presuppone che tu stia già comunicando altrove, ad esempio su Discord, Teamspeak, WhatsApp, Signal o tramite una normale chiamata.<br><br>Il supporto mobile è un altro fattore chiave. Teleparty offre un'app dedicata, mentre KoalaSync funziona al momento solo su browser desktop. Se vuoi sincronizzare Netflix su smartphone o tablet, KoalaSync non è lo strumento adatto in questo momento.<br><br>Quindi, se la chat e il supporto mobile sono gli aspetti a cui tieni di più, Teleparty potrebbe adattarsi meglio alle tue esigenze.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Quando KoalaSync è la scelta ideale",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync è la soluzione ideale se cerchi uno strumento flessibile per sincronizzare la riproduzione video tra browser desktop, anziché una soluzione vincolata a una lista ristretta di piattaforme.<br><br>Funziona con quasi tutti i siti web che utilizzano i normali lettori video dei browser. È particolarmente utile per Emby, Jellyfin, Plex, server multimediali privati, domini locali o siti video minori che solitamente vengono ignorati dai classici strumenti per le sessioni di visione condivisa.<br><br>È inoltre gratuito, open source, non richiede la creazione di account e può essere ospitato autonomamente se desideri gestire il tuo relay. L'ho creato perché volevo una soluzione rispettosa della privacy che funzionasse con la mia configurazione Emby, non l'ennesima piattaforma chiusa limitata a pochi grandi servizi di streaming.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "La differenza principale",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty è concepito principalmente come strumento per la visione condivisa, completo di chat e supporto mobile. KoalaSync, invece, agisce più come un livello flessibile di sincronizzazione per i video riprodotti nel browser.<br><br>Può sembrare una differenza da poco, ma cambia notevolmente le modalità d'uso. Se cerchi un'applicazione completa per la visione condivisa con chat integrata, Teleparty è la scelta giusta. Se invece disponi già di una chat vocale e vuoi semplicemente mantenere sincronizzati i controlli video tra diversi browser, KoalaSync è la soluzione più semplice ed efficace.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Limiti dichiarati apertamente",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync non fa miracoli e non cerca di nascondere i propri limiti. Attualmente funziona solo su browser desktop; non dispone di chat integrata né di un'app per dispositivi mobili.<br><br>La compatibilità dipende anche dal browser e dal lettore video utilizzato. KoalaSync dovrebbe funzionare con quasi tutti i normali lettori video dei browser, ma i siti web possono modificare in qualsiasi momento i propri lettori, la gestione dei DRM o le restrizioni del browser.<br><br>KoalaSync non è affiliato a Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex o qualsiasi altro marchio qui menzionato. I nomi sono utilizzati esclusivamente per illustrare i tipici scenari di utilizzo.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Riepilogo",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Se cerchi una chat integrata o il supporto per dispositivi mobili, Teleparty potrebbe essere la scelta più adatta. Se invece preferisci una soluzione gratuita e open-source per la sincronizzazione tra browser desktop, compatibile con quasi tutti i comuni lettori video — inclusi i media server self-hosted come Emby, Jellyfin e Plex — KoalaSync è probabilmente l'opzione migliore.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync vs condivisione schermo: quando è meglio la sincronizzazione video?",
|
||||
"ALT_SCREEN_META_DESC": "Un confronto pratico tra KoalaSync e la condivisione dello schermo per serate film a distanza, analizzando controllo della riproduzione, qualità video, privacy, sottotitoli e visione tramite browser.",
|
||||
"ALT_SCREEN_INTRO": "La condivisione dello schermo funziona e, a dire il vero, a volte è sufficiente. Una persona condivide il proprio schermo, le altre guardano e il gioco è fatto.<br><br>Tuttavia, per serate film più lunghe o per guardare insieme degli episodi, può diventare scomoda. La qualità video dipende dalla velocità di upload, dalla CPU e dal browser di chi condivide, oltre che dalla compressione applicata da app come Discord, Teamspeak, Zoom o altre piattaforme di chiamata. Inoltre, solo una persona ha il controllo effettivo della riproduzione.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Quando la condivisione dello schermo è sufficiente",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "La condivisione dello schermo è una buona soluzione quando si cerca la configurazione più rapida possibile. Se solo una persona ha accesso al video o se a tutti va bene che una sola persona controlli la riproduzione, la pausa e lo scorrimento del video, potrebbe essere l'opzione più semplice.<br><br>È una soluzione valida anche quando la qualità non è una priorità assoluta, quando nessuno ha bisogno di impostazioni personalizzate per sottotitoli o audio e quando chi condivide lo schermo si sente a proprio agio nel mostrare quella specifica finestra del browser o l'intero schermo.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Quando la condivisione schermo diventa scomoda",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "Lo svantaggio principale è che non tutti guardano il video tramite il proprio lettore. Una persona trasmette il proprio schermo e tutti gli altri vedono solo quel flusso video.<br><br>Ciò può comportare una qualità video inferiore, audio compresso, ritardi nella riproduzione e un controllo limitato per gli altri partecipanti. Anche i sottotitoli e le tracce audio sono quelli scelti da chi effettua la condivisione.<br><br>C'è poi l'aspetto della privacy. Se cambi finestra o scheda, gli altri potrebbero vedere brevemente le schede del tuo browser, le email, Reddit, le chat o altri contenuti privati. È possibile aggirare il problema usando una finestra separata, ma è comunque un aspetto da tenere a mente.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Perché KoalaSync offre un'esperienza più pulita",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync non trasmette il tuo schermo. Ognuno guarda il video localmente sul proprio browser e KoalaSync si limita a sincronizzare le azioni di riproduzione, come avvio, pausa e scorrimento.<br><br>Questo permette a ciascuno di utilizzare la modalità a schermo intero sul proprio dispositivo, di impostare i sottotitoli o l'audio secondo le proprie preferenze (se il lettore lo consente) e di mantenere separata la chat vocale su Discord, Teamspeak o altre piattaforme.<br><br>Per le serate film, questa soluzione risulta molto più ordinata rispetto alla condivisione dello schermo da parte di una sola persona. È particolarmente utile per configurazioni basate su browser come Emby, Jellyfin, Plex, file locali aperti nel browser, domini privati o altri siti web dotati di lettori video standard.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Limiti reali",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync non sostituisce una chiamata vocale. Se volete parlare mentre guardate il contenuto, potete continuare a usare Discord, Teamspeak, Signal, WhatsApp o qualsiasi altro strumento abituale.<br><br>Ciascun partecipante deve inoltre poter accedere al video o alla pagina dal proprio dispositivo. KoalaSync non aggira abbonamenti, accessi, DRM, restrizioni geografiche o regole delle piattaforme.<br><br>Al momento è utilizzabile solo tramite browser desktop, non dispone di funzioni di chat o voce integrate e la compatibilità dipende sempre dal browser e dal lettore video.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Riepilogo",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "La condivisione dello schermo è l'opzione più semplice quando si cerca una soluzione rapida e non si dà molta importanza alla qualità, alla privacy o al controllo individuale. KoalaSync è la scelta ideale quando tutti devono guardare il contenuto localmente sul proprio browser, offrendo un'esperienza che ricorda più da vicino la visione condivisa reale.",
|
||||
"ALT_TRY_KOALASYNC": "Prova KoalaSync"
|
||||
}
|
||||
|
||||
+32
-2
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "ja_JP",
|
||||
"META_TITLE": "KoalaSync | Netflix、YouTube、あらゆる動画を友達と同期視聴",
|
||||
"META_DESCRIPTION": "Netflix、YouTube、Twitch、およびあらゆるHTML5動画を友達と完全に同期して視聴。ChromeおよびFirefox用の無料のオープンソースブラウザ拡張機能。登録不要。",
|
||||
"SCHEMA_APP_DESC": "Netflix、YouTube、Twitch、およびあらゆるHTML5動画を友達と完全に同期して視聴。ChromeおよびFirefox用の無料のオープンソースブラウザ拡張機能。登録不要。",
|
||||
"OG_TITLE": "KoalaSync | Netflix、Emby、Jellyfin、ほぼすべての動画を友達と同期",
|
||||
"OG_DESCRIPTION": "Netflix、Emby、Jellyfin、YouTube、Twitch、ほぼすべてのHTML5動画を完全に同期して視聴。ChromeおよびFirefox用のプライバシー優先のオープンソースブラウザ拡張機能。",
|
||||
"TWITTER_TITLE": "KoalaSync | Netflix、Emby、Jellyfin、ほぼすべての動画を友達と同期 – ブラウザ拡張機能",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "サポートされているサイトのみ",
|
||||
"COMP_FEAT_6_NAME": "ローカライズ / 翻訳",
|
||||
"COMP_FEAT_6_DESC": "利用可能なインターフェース言語と翻訳サポート。",
|
||||
"COMP_FEAT_6_KOALA": "13の言語(今後も追加予定)",
|
||||
"COMP_FEAT_6_KOALA": "15の言語(今後も追加予定)",
|
||||
"COMP_FEAT_6_TELE": "英語のみ",
|
||||
"COMP_FEAT_7_NAME": "オーディオコンプレッサー / 音量均等化",
|
||||
"COMP_FEAT_7_DESC": "静かな会話と大きな効果音のバランスを取る内蔵オーディオノーマライゼーション",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "完璧に同期して動画を一緒に楽しむために必要なすべての情報。",
|
||||
"FAQ_Q1": "友達と一緒にNetflix、Emby、Jellyfinを同期視聴できますか?",
|
||||
"FAQ_A1": "はい!KoalaSyncはNetflix, YouTube, TwitchおよびHTML5ビデオプレーヤーを備えたあらゆるウェブサイト(Emby, Jellyfin, 独自のメディアサーバーを含む)で動作します。拡張機能をインストールし、ルームを作成して招待リンクを共有するだけで、全員で完全に同期して視聴できます。",
|
||||
"FAQ_Q_VIDEO_ACCESS": "KoalaSync は友達に動画を配信しますか?",
|
||||
"FAQ_A_VIDEO_ACCESS": "いいえ。KoalaSync は動画コンテンツを配信、ホスト、共有したり、アクセス制限を回避したりしません。各参加者は自分のブラウザで動画をローカルに視聴し、Netflix アカウントなど、そのウェブサイトやストリーミングサービスへの自分自身のアクセス権が必要です。KoalaSync は再生、一時停止、シークなどの再生操作だけを同期します。",
|
||||
"FAQ_Q2": "KoalaSyncはTelepartyの無料の代替ツールですか?",
|
||||
"FAQ_A2": "MITライセンスのもとで100%無料かつオープンソース。有料プラン、制限された機能、広告などは一切ありません。Telepartyとは異 nel、KoalaSyncのリレーサーバーはメモリ上のみで動作し、データ収集やユーザーアカウント作成は不要です。",
|
||||
"FAQ_Q3": "KoalaSyncはEmby、Jellyfin、およびセルフホストのメディアサーバーで動作しますか?",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSyncは、Netflix、Disney+、Amazon、YouTube、Twitch、またはその他のストリーミングプラットフォームと提携、承認、または関連付けられているものではありません。すべての商標はそれぞれの所有者に帰属します。",
|
||||
"FOOTER_SUPPORT": "Ko-Fiでコーヒーをおごる"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty:どちらのウォッチパーティツールを使うべき?",
|
||||
"ALT_TELEPARTY_META_DESC": "ウォッチパーティ向けツール、KoalaSyncとTelepartyの実践的比較。チャット機能、モバイル対応、ブラウザでの動画同期、セルフホスト型メディアサーバー、オープンソース利用といった観点から解説します。",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSyncとTelepartyは、どちらも「3、2、1、再生!」と声を掛け合う手間をかけずに、一緒に動画を楽しむという共通の課題を解決してくれます。ただ、そのアプローチは異なります。<br><br>チャット機能が組み込まれていることや、モバイル端末で視聴できることを重視するなら、Telepartyが最適です。一方、KoalaSyncは、DiscordやTeamSpeakなどで既に通話しながら、ブラウザ上で動画の再生タイミングだけを同期させたいという、デスクトップブラウザでの利用に適しています。<br><br>私がKoalaSyncを開発した主な理由は「柔軟性」です。特定のプラットフォームだけでなく、一般的なブラウザ用動画プレーヤーを使用するほぼすべてのウェブサイトで動作するように設計されています。これには、Emby、Jellyfin、Plexといったセルフホスト環境や、ローカル/プライベートドメイン、そしてブラウザ上の一般的な動画ページなどが含まれます。",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Telepartyが適しているケース",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "ウォッチパーティツール自体にチャット機能が組み込まれていることを望むなら、Telepartyの方が良い選択肢となるでしょう。KoalaSyncには現在、チャット機能が搭載されていません。Discord、TeamSpeak、WhatsApp、Signal、あるいは通常の通話など、別の手段で既に会話していることを前提としています。<br><br>もう一つの大きな理由はモバイル対応です。Telepartyにはアプリがありますが、KoalaSyncは現時点ではデスクトップブラウザでしか動作しません。スマートフォンやタブレットでNetflixを同期再生したい場合、今のところKoalaSyncは適していません。<br><br>つまり、チャット機能やモバイル対応を何よりも重視するのであれば、Telepartyの方があなたの利用スタイルに合っているかもしれません。",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "KoalaSyncが適している場合",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "特定のプラットフォームに縛られるツールではなく、柔軟性の高いデスクトップブラウザ用同期ツールをお探しなら、KoalaSyncが適しています。<br><br>一般的なブラウザの動画プレーヤーを使用するほぼすべてのウェブサイトで動作します。これは特に、Emby、Jellyfin、Plex、プライベートメディアサーバー、ローカルドメイン、あるいは従来のウォッチパーティツールが見落としがちな小規模な動画サイトなどで役立ちます。<br><br>また、無料かつオープンソースであり、アカウント登録も不要です。独自のリレーサーバーを運用したい場合は、セルフホストも可能です。私がこのツールを開発したのは、一部の大手ストリーミングサイト専用の閉鎖的なプラットフォームではなく、自分のEmby環境で動作し、プライバシーに配慮したツールが欲しかったからです。",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "主な違い",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Telepartyは、チャットやモバイル対応を備えた「ウォッチパーティ(同時視聴)」向け製品です。一方、KoalaSyncは、ブラウザ上の動画再生を同期させるための、より柔軟な同期レイヤーと言えます。<br><br>些細な違いに聞こえるかもしれませんが、これによって利用シーンが大きく変わります。チャット機能が組み込まれた総合的なウォッチパーティアプリが必要なら、Telepartyが適しています。すでにボイスチャット環境があり、単にブラウザ間で動画の再生状態を同期させたいだけであれば、よりシンプルなKoalaSyncが適しています。",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "制限事項について",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSyncは魔法のツールではなく、その制限を隠そうともしていません。現時点ではデスクトップブラウザでのみ動作し、チャット機能やモバイルアプリは搭載されていません。<br><br>互換性はブラウザや動画プレーヤーにも依存します。KoalaSyncは一般的なブラウザの動画プレーヤーのほとんどで動作するはずですが、ウェブサイト側がプレーヤー、DRMの挙動、あるいはブラウザへの制限などをいつでも変更する可能性があります。<br><br>KoalaSyncは、Netflix、Disney+、Prime Video、YouTube、Teleparty、Emby、Jellyfin、Plex、その他ここで言及されているブランドとは一切関係ありません。これらの名称は、一般的な利用シーンを説明するために使用されているに過ぎません。",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "概要",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "チャット機能やモバイル対応を重視するなら、Telepartyの方が適しているかもしれません。一方、無料かつオープンソースで、デスクトップブラウザでの同期が可能、さらにEmby、Jellyfin、Plexといったセルフホスト型メディアサーバーを含む、一般的な動画プレーヤーのほとんどで動作するツールをお探しなら、KoalaSyncの方が適しているでしょう。",
|
||||
"ALT_SCREEN_TITLE": "KoalaSyncと画面共有:動画同期の方が優れているのはどんな時?",
|
||||
"ALT_SCREEN_META_DESC": "リモートでの映画鑑賞会におけるKoalaSyncと画面共有の実践的な比較。再生制御、画質、プライバシー、字幕、ブラウザでの視聴といった観点から解説します。",
|
||||
"ALT_SCREEN_INTRO": "画面共有は有効な手段であり、正直なところ、それで十分な場合もあります。一人が画面を共有し、他の全員がそれを見るだけで済みますから。<br><br>しかし、長時間の映画鑑賞やシリーズ作品を一緒に見るとなると、煩わしさを感じることもあります。画質は、共有者のアップロード速度、CPU、ブラウザ、そしてDiscord、TeamSpeak、Zoomなどの通話アプリによる圧縮処理に左右されます。また、再生を実際に操作できるのは一人のみです。",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "画面共有で十分な場合",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "とにかく手早く準備を済ませたいなら、画面共有で問題ありません。もし一人だけが動画にアクセスできる場合……あるいは、一人が再生・一時停止・シーク(再生位置の移動)を操作することに全員が納得しているなら、それが最もシンプルな選択肢かもしれません。<br><br>画質がそれほど重要ではなく、個別の字幕や音声設定を必要とする人がおらず、かつ共有者がブラウザウィンドウや画面をそのまま見せても構わない場合には、理にかなった方法と言えます。",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "画面共有が煩わしくなる場合",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "最大の欠点は、全員がそれぞれのプレイヤーで視聴するわけではないという点です。一人が画面をストリーミングし、他の全員はそのストリーム映像を見るだけになります。<br><br>その結果、画質の低下、音声の圧縮、再生の遅延が生じたり、他の参加者が操作できる範囲が狭まったりする可能性があります。字幕や音声トラックも、共有者が選択したものに固定されます。<br><br>プライバシーの問題もあります。ウィンドウやタブを切り替える際、ブラウザのタブ、メール、Reddit、チャット、その他の個人的な情報が一時的に見えてしまう可能性があります。別のウィンドウを使うことで回避はできますが、やはり考慮すべき点ではあります。",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "KoalaSyncがすっきり感じられる理由",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSyncは画面をストリーミングしません。全員がそれぞれのブラウザでローカルに動画を視聴し、KoalaSyncは再生・一時停止・シークといった操作の同期のみを行います。<br><br>つまり、全員が自分のデバイスでフルスクリーン表示を利用したり、プレイヤーが対応していれば個別の字幕や音声設定を使ったりできるほか、DiscordやTeamSpeakなどの通話ツールで音声チャットを別に行うことも可能です。<br><br>映画鑑賞会などでは、一人が画面を配信するよりも、はるかにすっきりとした体験になります。特に、Emby、Jellyfin、Plex、ブラウザで開いたローカルファイル、プライベートドメイン、あるいは一般的なブラウザ用動画プレイヤーを備えたウェブサイトなど、ブラウザベースの環境で利用する場合に非常に便利です。",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "正直なところ、制限事項も",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSyncは音声通話の代わりになるものではありません。視聴しながら会話をする場合は、Discord、TeamSpeak、Signal、WhatsAppなど、普段お使いのツールをそのまま利用します。<br><br>また、参加者全員がそれぞれのデバイスでその動画やページにアクセスできる必要があります。KoalaSyncは、サブスクリプション、ログイン、DRM(デジタル著作権管理)、地域制限、プラットフォームの利用規約などを回避するものではありません。<br><br>現在はデスクトップ版ブラウザのみに対応しており、チャットや音声通話機能は内蔵されていません。また、動作の可否は使用するブラウザや動画プレーヤーに依存します。",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "概要",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "画質やプライバシー、個別の操作性をあまり気にせず、手軽に済ませたい場合は「画面共有」が最も簡単な選択肢です。一方、参加者それぞれが自身のブラウザで動画を再生し、実際に同じ空間で一緒に視聴しているような感覚を重視するなら、KoalaSyncの方が適しています。",
|
||||
"ALT_TRY_KOALASYNC": "KoalaSyncを試す"
|
||||
}
|
||||
|
||||
+32
-2
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "ko_KR",
|
||||
"META_TITLE": "KoalaSync | Netflix, YouTube 및 모든 비디오를 친구와 실시간 동기화 시청",
|
||||
"META_DESCRIPTION": "Netflix, YouTube, Twitch 및 모든 HTML5 비디오를 친구들과 완벽하게 동기화하여 시청하세요. Chrome 및 Firefox용 무료 오픈 소스 브라우저 확장 프로그램. 가입 필요 없음.",
|
||||
"SCHEMA_APP_DESC": "Netflix, YouTube, Twitch 및 모든 HTML5 비디오를 친구들과 완벽하게 동기화하여 시청하세요. Chrome 및 Firefox용 무료 오픈 소스 브라우저 확장 프로그램. 가입 필요 없음.",
|
||||
"OG_TITLE": "KoalaSync | Netflix, Emby, Jellyfin 및 거의 모든 비디오를 친구와 동기화 시청",
|
||||
"OG_DESCRIPTION": "Netflix, Emby, Jellyfin, YouTube, Twitch 및 거의 모든 HTML5 비디오를 완벽하게 동기화하여 시청하세요. Chrome 및 Firefox용 개인정보 보호 중심 오픈 소스 브라우저 확장 프로그램.",
|
||||
"TWITTER_TITLE": "KoalaSync | Netflix, Emby, Jellyfin 및 거의 모든 비디오를 친구와 동기화 시청 – 브라우저 확장 프로그램",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "지원되는 사이트만 가능",
|
||||
"COMP_FEAT_6_NAME": "로컬라이징 / 다국어 지원",
|
||||
"COMP_FEAT_6_DESC": "제공되는 인터페이스 언어 및 번역 지원 여부.",
|
||||
"COMP_FEAT_6_KOALA": "13개 언어 지원 (추가 중)",
|
||||
"COMP_FEAT_6_KOALA": "15개 언어 지원 (추가 중)",
|
||||
"COMP_FEAT_6_TELE": "영어만 지원",
|
||||
"COMP_FEAT_7_NAME": "오디오 압축기 / 볼륨 레벨링",
|
||||
"COMP_FEAT_7_DESC": "조용한 대화와 큰 효과음의 균형을 맞추는 내장 오디오 노멀라이제이션",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "완벽한 동기화로 비디오를 함께 시청하는 방법에 대해 알아야 할 모든 것.",
|
||||
"FAQ_Q1": "친구들과 Netflix, Emby 또는 Jellyfin을 함께 동기화하여 시청할 수 있나요?",
|
||||
"FAQ_A1": "네! KoalaSync는 Netflix, YouTube, Twitch 및 HTML5 비디오 플레이어가 있는 모든 웹사이트(Emby, Jellyfin 및 자체 호스팅 미디어 서버 포함)에서 작동합니다. 확장 프로그램을 설치하고 방을 만든 후 초대 링크를 공유하면 모두가 완벽하게 동기화되어 시청할 수 있습니다.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "KoalaSync가 친구들에게 동영상을 스트리밍하나요?",
|
||||
"FAQ_A_VIDEO_ACCESS": "아니요. KoalaSync는 동영상 콘텐츠를 스트리밍, 호스팅, 공유하거나 접근 권한을 우회하지 않습니다. 모든 참가자는 각자의 브라우저에서 동영상을 로컬로 시청하며, Netflix 계정처럼 해당 웹사이트나 스트리밍 서비스에 대한 본인만의 접근 권한이 필요합니다. KoalaSync는 재생, 일시정지, 탐색 같은 재생 동작만 동기화합니다.",
|
||||
"FAQ_Q2": "KoalaSync는 Teleparty의 무료 대체 서비스인가요?",
|
||||
"FAQ_A2": "MIT 라이선스에 따라 100% 무료이며 오픈 소스입니다. 프리미ウム 등급도, 제한된 기능도, 광고도 없습니다. 모든 것이 포함되어 있습니다. Teleparty와 달리 KoalaSync의 릴레이 서버는 가입이나 개인정보 수집 없이 RAM에서만 실행됩니다.",
|
||||
"FAQ_Q3": "KoalaSync는 Emby, Jellyfin 및 자체 호스팅 미디어 서버에서 작동하나요?",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync는 Netflix, Disney+, Amazon, YouTube, Twitch 또는 기타 스트리밍 플랫폼과 제휴, 보증 또는 연관되어 있지 않습니다. 모든 상표는 해당 소유자의 재산입니다.",
|
||||
"FOOTER_SUPPORT": "Ko-Fi에서 커피 한 잔 사주세요"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync 대 Teleparty: 어떤 '워치 파티(watch party)' 도구를 선택해야 할까요?",
|
||||
"ALT_TELEPARTY_META_DESC": "워치 파티를 위한 KoalaSync와 Teleparty의 실용적 비교: 내장 채팅, 모바일 지원, 브라우저 영상 동기화, 자체 호스팅 미디어 서버 및 오픈 소스 활용 여부 등을 다룹니다.",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync와 Teleparty는 모두 '3, 2, 1, 재생' 같은 번거로운 절차 없이 함께 영상을 시청할 수 있게 해준다는 점에서 같은 문제를 해결해 줍니다. 다만 접근 방식에는 차이가 있습니다.<br><br>내장 채팅 기능이 필요하거나 모바일에서 시청하고 싶다면 Teleparty가 가장 합리적인 선택입니다. 반면 KoalaSync는 Discord, Teamspeak 또는 기타 음성 통화 서비스를 통해 이미 대화를 나누고 있으면서, 단순히 영상 재생만 동기화하고 싶은 데스크톱 브라우저 기반의 워치 파티에 더 적합합니다.<br><br>제가 KoalaSync를 개발한 주된 이유는 유연성 때문입니다. 특정 플랫폼 목록에만 국한되지 않고, 일반적인 브라우저 비디오 플레이어를 사용하는 거의 모든 웹사이트에서 작동하도록 설계되었습니다. 여기에는 Emby, Jellyfin, Plex와 같은 자체 호스팅 환경, 로컬/비공개 도메인, 그리고 브라우저상의 수많은 일반 동영상 페이지가 포함됩니다.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Teleparty가 여전히 좋은 선택이 될 수 있는 경우",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "워치 파티 도구 자체에 채팅 기능이 포함되기를 원한다면 Teleparty가 더 나은 선택일 수 있습니다. KoalaSync에는 현재 내장 채팅 기능이 없습니다. Discord, Teamspeak, WhatsApp, Signal 또는 일반 통화 등 다른 수단을 통해 이미 대화하고 있다는 것을 전제로 하기 때문입니다.<br><br>모바일 지원 여부도 중요한 이유 중 하나입니다. Teleparty는 전용 앱을 제공하지만, KoalaSync는 현재 데스크톱 브라우저에서만 작동합니다. 스마트폰이나 태블릿에서 Netflix 영상을 동기화하고 싶다면, 현재로서는 KoalaSync가 적합한 도구가 아닙니다.<br><br>따라서 채팅과 모바일 지원이 가장 중요한 요소라면, Teleparty가 여러분의 환경에 더 잘 맞을 것입니다.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "KoalaSync가 더 적합한 경우",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "지원 플랫폼이 제한적인 도구 대신 유연한 데스크톱 브라우저 동기화 도구를 원하신다면 KoalaSync가 더 나은 선택입니다.<br><br>일반적인 브라우저 비디오 플레이어를 사용하는 거의 모든 웹사이트에서 작동합니다. 이는 특히 Emby, Jellyfin, Plex, 개인 미디어 서버, 로컬 도메인 또는 기존 '워치 파티(watch party)' 도구들이 흔히 지원하지 않는 소규모 동영상 사이트에서 유용합니다.<br><br>또한 무료이자 오픈 소스이며, 계정 생성도 필요하지 않습니다. 직접 릴레이 서버를 운영하고 싶다면 자체 호스팅도 가능합니다. 저는 소수의 대형 스트리밍 사이트만을 위한 폐쇄적인 플랫폼이 아니라, 제 Emby 환경에서도 잘 작동하면서 개인정보 보호를 중시하는 도구를 만들고 싶어 이 프로그램을 개발했습니다.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "주요 차이점",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty는 채팅과 모바일 지원 기능을 갖춘 '워치 파티' 서비스에 가깝습니다. 반면 KoalaSync는 브라우저 비디오를 위한 유연한 동기화 도구(레이어)에 가깝습니다.<br><br>사소한 차이처럼 들릴 수 있지만, 실제 사용 방식에는 큰 차이가 있습니다. 채팅 기능이 포함된 종합적인 워치 파티 앱이 필요하다면 Teleparty가 적합합니다. 하지만 이미 음성 채팅 수단이 있고 단순히 브라우저 간 비디오 재생 상태만 동기화하고 싶다면, 더 간편한 KoalaSync가 알맞은 선택입니다.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "솔직하게 밝히는 한계점",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync는 마법 같은 도구가 아니며, 그 한계를 숨기지도 않습니다. 현재 데스크톱 브라우저에서만 작동하며, 자체 채팅 기능이나 모바일 앱은 없습니다.<br><br>호환성은 브라우저와 비디오 플레이어의 영향을 받습니다. KoalaSync는 일반적인 브라우저 비디오 플레이어라면 대부분 작동하지만, 웹사이트 측에서 플레이어, DRM 방식, 또는 브라우저 관련 제한 사항을 언제든지 변경할 수 있습니다.<br><br>KoalaSync는 Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex 또는 여기에 언급된 다른 어떤 브랜드와도 제휴 관계가 아닙니다. 해당 명칭들은 단지 일반적인 사용 사례를 설명하기 위해 사용되었습니다.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "요약",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "내장 채팅이나 모바일 지원 기능이 필요하다면 Teleparty가 더 적합할 수 있습니다. 반면, Emby, Jellyfin, Plex와 같은 자체 호스팅 미디어 서버를 포함해 거의 모든 일반적인 동영상 플레이어와 호환되는 무료 오픈 소스 데스크톱 브라우저 동기화 도구를 원하신다면 KoalaSync가 더 나은 선택일 것입니다.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync 대 화면 공유: 동영상 동기화가 더 나은 경우는 언제일까요?",
|
||||
"ALT_SCREEN_META_DESC": "원격 영화 감상, 재생 제어, 화질, 개인정보 보호, 자막 및 브라우저 기반 시청 측면에서 KoalaSync와 화면 공유를 실용적으로 비교해 봅니다.",
|
||||
"ALT_SCREEN_INTRO": "화면 공유도 훌륭한 방법이며, 솔직히 때로는 그것만으로도 충분합니다. 한 사람이 화면을 공유하고 나머지는 시청하기만 하면 되니까요.<br><br>하지만 긴 영화를 보거나 여러 에피소드를 함께 시청할 때는 다소 번거로울 수 있습니다. 화질은 화면을 공유하는 사람의 업로드 속도, CPU, 브라우저, 그리고 Discord, Teamspeak, Zoom 등의 통화 앱이 적용하는 압축 방식에 따라 달라집니다. 또한, 재생을 실제로 제어할 수 있는 사람도 한 명뿐입니다.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "화면 공유로 충분한 경우",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "가장 빠르게 준비를 마치고 싶을 때는 화면 공유가 좋은 선택입니다. 만약 한 사람만 동영상에 접근할 수 있거나...\"또는 한 사람이 재생, 일시 정지, 탐색(seek)을 제어하는 방식에 모두가 동의한다면, 이것이 가장 간단한 선택지가 될 수 있습니다.<br><br>또한 화질이 크게 중요하지 않거나, 개별적인 자막이나 오디오 설정이 필요 없으며, 화면을 공유하는 사람이 자신의 브라우저 창이나 화면을 보여주는 데 거부감이 없는 경우에도 합리적인 방법입니다.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "화면 공유가 번거로울 수 있는 상황",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "가장 큰 단점은 각자의 플레이어로 영상을 시청하는 것이 아니라는 점입니다. 한 사람이 자신의 화면을 스트리밍하면, 다른 사람들은 그 스트리밍 화면만 보게 됩니다.<br><br>이로 인해 화질 저하, 오디오 압축, 재생 지연이 발생할 수 있으며, 다른 사람들의 제어 권한도 제한됩니다. 자막이나 오디오 트랙 또한 화면 공유자가 선택한 설정으로 고정됩니다.<br><br>개인정보 보호 문제도 있습니다. 창이나 탭을 전환할 때 브라우저 탭, 이메일, Reddit, 채팅 내용 등 사적인 정보가 일시적으로 노출될 수 있습니다. 별도의 창을 사용하여 이를 피할 수는 있지만, 여전히 고려해야 할 사항입니다.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "KoalaSync가 더 깔끔하게 느껴지는 이유",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync는 화면을 직접 스트리밍하지 않습니다. 각자 자신의 브라우저에서 영상을 시청하며, KoalaSync는 재생, 일시 정지, 탐색과 같은 재생 관련 동작만 동기화합니다.<br><br>따라서 각자 자신의 기기에서 전체 화면으로 시청하거나, 플레이어가 지원하는 경우 개별 자막 및 오디오 설정을 사용할 수 있으며, 음성 채팅은 Discord, Teamspeak 또는 기타 통화 서비스를 통해 별도로 진행할 수 있습니다.<br><br>영화 감상 모임의 경우, 한 사람이 화면을 송출하는 방식보다 훨씬 깔끔한 경험을 제공합니다. 특히 Emby, Jellyfin, Plex와 같은 브라우저 기반 서비스나 브라우저에서 연 로컬 파일, 비공개 도메인, 또는 일반적인 브라우저 비디오 플레이어를 사용하는 웹사이트에서 활용하기에 매우 유용합니다.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "솔직한 한계점",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync는 음성 통화를 대체하는 서비스가 아닙니다. 영상을 보면서 대화하고 싶다면 평소처럼 Discord, Teamspeak, Signal, WhatsApp 등을 그대로 사용하면 됩니다.<br><br>또한 모든 참가자가 각자의 기기에서 해당 영상이나 페이지에 접속할 수 있어야 합니다. KoalaSync는 구독, 로그인, DRM, 지역 제한 또는 플랫폼 규정을 우회하지 않습니다.<br><br>현재 데스크톱 브라우저에서만 지원되며, 자체 채팅이나 음성 기능은 없습니다. 호환성은 항상 브라우저 및 동영상 플레이어의 영향을 받습니다.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "요약",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "화질, 개인정보 보호, 개별 제어 기능 등을 크게 신경 쓰지 않고 간편하게 진행하고 싶을 때는 화면 공유가 가장 간단한 방법입니다. 반면, 모든 참가자가 각자의 브라우저에서 직접 영상을 시청하며 실제로 함께 보는 듯한 느낌을 원한다면 KoalaSync가 더 적합합니다.",
|
||||
"ALT_TRY_KOALASYNC": "KoalaSync 사용해 보기"
|
||||
}
|
||||
|
||||
+33
-3
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "nl_NL",
|
||||
"META_TITLE": "KoalaSync | Synchroniseer Netflix, YouTube & elke video met vrienden",
|
||||
"META_DESCRIPTION": "Bekijk Netflix, YouTube, Twitch en elke HTML5-video in perfecte synchronisatie met vrienden. Gratis, open-source browserextensie voor Chrome en Firefox. Geen registratie nodig.",
|
||||
"SCHEMA_APP_DESC": "Bekijk Netflix, YouTube, Twitch en elke HTML5-video in perfecte synchronisatie met vrienden. Gratis, open-source browserextensie voor Chrome en Firefox. Geen registratie nodig.",
|
||||
"OG_TITLE": "KoalaSync | Synchroniseer Netflix, Emby, Jellyfin & bijna elke video met vrienden",
|
||||
"OG_DESCRIPTION": "Bekijk Netflix, Emby, Jellyfin, YouTube, Twitch en bijna elke HTML5-video in perfecte synchronisatie. Privacy-first, open-source browserextensie voor Chrome & Firefox.",
|
||||
"TWITTER_TITLE": "KoalaSync | Synchroniseer Netflix, Emby, Jellyfin & bijna elke video met vrienden – Browserextensie",
|
||||
@@ -49,7 +50,7 @@
|
||||
"FEATURE_7_TITLE": "Volume egaliseren / Audiocompressor",
|
||||
"FEATURE_7_DESC": "Niet langer aan de volumeknop zitten. De ingebouwde audiocompressor versterkt stille dialogen en dempt harde explosies. Werkt op Netflix, Prime Video, Disney+, YouTube en elke HTML5-video. Volledig lokaal — geen audiogegevens verlaten uw computer.",
|
||||
"FEATURE_8_TITLE": "Open Source (MIT License)",
|
||||
"FEATURE_8_DESC": "The entire codebase is publicly auditable on GitHub. No black boxes, no proprietary blobs — 100% transparency from the relay server to the browser extension.",
|
||||
"FEATURE_8_DESC": "De volledige codebase is openbaar controleerbaar op GitHub. Geen zwarte dozen, geen propri?taire blobs - 100% transparantie van de relayserver tot de browserextensie.",
|
||||
"COMP_TITLE": "KoalaSync vs. Teleparty",
|
||||
"COMP_SUBTITLE": "Ontdek waarom open-source, reclamevrij en privacy-first de superieure manier is om samen te kijken.",
|
||||
"COMP_COL_FEATURE": "Functie",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Alleen ondersteunde sites",
|
||||
"COMP_FEAT_6_NAME": "Lokalisatie / Vertaling",
|
||||
"COMP_FEAT_6_DESC": "Beschikbare interfacetalen en vertaalondersteuning.",
|
||||
"COMP_FEAT_6_KOALA": "13 talen (en groeiend)",
|
||||
"COMP_FEAT_6_KOALA": "15 talen (en groeiend)",
|
||||
"COMP_FEAT_6_TELE": "Alleen Engels",
|
||||
"COMP_FEAT_7_NAME": "Audiocompressor / Volume Egaliseren",
|
||||
"COMP_FEAT_7_DESC": "Ingebouwde audionormalisatie om stille dialogen en luide effecten in balans te brengen",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Alles wat u moet weten over het samen bekijken van video's in perfecte synchronisatie.",
|
||||
"FAQ_Q1": "Kan ik Netflix, Emby of Jellyfin samen in sync kijken met vrienden?",
|
||||
"FAQ_A1": "Ja! KoalaSync werkt op Netflix, YouTube, Twitch en elke website met een HTML5-videospeler — inclusief Emby, Jellyfin en zelfgehoste mediaservers. Installeer de extensie, maak een kamer, deel de uitnodigingslink en iedereen kijkt in perfecte sync.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "Streamt KoalaSync de video naar mijn vrienden?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Nee. KoalaSync streamt, host of deelt geen videocontent en omzeilt geen toegang tot video’s. Iedereen kijkt de video lokaal in de eigen browser en heeft zelf toegang nodig tot de website of streamingdienst, bijvoorbeeld een eigen Netflix-account. KoalaSync synchroniseert alleen afspeelacties zoals afspelen, pauzeren en spoelen.",
|
||||
"FAQ_Q2": "Is KoalaSync een gratis alternatief voor Teleparty?",
|
||||
"FAQ_A2": "100% gratis en open-source onder de MIT-licentie. Geen premium-niveaus, geen vergrendelde functies, geen advertenties — alles is inbegrepen. In tegenstelling tot Teleparty is de relayserver van KoalaSync alleen in het RAM-geheugen met nul gegevensverzameling of gebruikersaccounts vereist.",
|
||||
"FAQ_Q3": "Werkt KoalaSync met Emby, Jellyfin en zelfgehoste mediaservers?",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync is niet verbonden aan, goedgekeurd door of geassocieerd met Netflix, Disney+, Amazon, YouTube, Twitch of enig ander streamingplatform. Alle handelsmerken zijn eigendom van hun respectieve eigenaren.",
|
||||
"FOOTER_SUPPORT": "Trakteer op een koffie via Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs. Teleparty: welke tool voor 'watch parties' moet je kiezen?",
|
||||
"ALT_TELEPARTY_META_DESC": "Een praktische vergelijking tussen KoalaSync en Teleparty voor watch parties, met aandacht voor ingebouwde chat, mobiele ondersteuning, videosynchronisatie in de browser, self-hosted mediaservers en open-sourcegebruik.",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync en Teleparty bieden allebei een oplossing voor hetzelfde vervelende probleem: samen iets kijken zonder dat gedoe met '3, 2, 1, afspelen'. Ze pakken het alleen anders aan.<br><br>Teleparty is de logische keuze als je een ingebouwde chatfunctie wilt of op mobiel wilt kijken. KoalaSync is meer geschikt voor watch parties via de desktopbrowser, waarbij je al met elkaar praat via Discord, Teamspeak of een andere spraakverbinding en alleen wilt dat het afspelen synchroon loopt.<br><br>De belangrijkste reden waarom ik KoalaSync heb ontwikkeld, is flexibiliteit. Het is bedoeld om te werken met vrijwel elke website die een standaard videospeler in de browser gebruikt, en niet slechts met een vaste lijst platforms. Dit omvat ook self-hosted systemen zoals Emby, Jellyfin en Plex, lokale of privé-domeinen en talloze reguliere videopagina's in de browser.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Wanneer Teleparty toch een goede keuze kan zijn",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty kan de betere keuze zijn als je wilt dat de chatfunctie deel uitmaakt van de watch party-tool zelf. KoalaSync heeft momenteel geen ingebouwde chat. Er wordt vanuit gegaan dat je al ergens anders met elkaar praat, bijvoorbeeld via Discord, Teamspeak, WhatsApp, Signal of een gewoon telefoongesprek.<br><br>Mobiel gebruik is een andere belangrijke factor. Teleparty heeft een app, terwijl KoalaSync momenteel alleen in desktopbrowsers werkt. Als je Netflix wilt synchroniseren op een telefoon of tablet, is KoalaSync daarvoor op dit moment niet de juiste tool.<br><br>Dus als chat en mobiele ondersteuning voor jou het belangrijkst zijn, past Teleparty wellicht beter bij jouw situatie.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Wanneer KoalaSync wellicht beter past",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync is een betere keuze als je op zoek bent naar een flexibele tool voor het synchroniseren van video's in desktopbrowsers, in plaats van een oplossing die gebonden is aan een beperkt aantal platforms.<br><br>Het werkt met vrijwel elke website die gebruikmaakt van een standaard videospeler in de browser. Dit is vooral handig voor Emby, Jellyfin, Plex, privémediaservers, lokale domeinen of kleinere videosites die doorgaans worden overgeslagen door traditionele 'watch party'-tools.<br><br>Bovendien is het gratis en open source, vereist het geen account en kun je het zelf hosten als je je eigen relay wilt draaien. Ik heb het ontwikkeld omdat ik een privacyvriendelijke oplossing wilde die samenwerkte met mijn eigen Emby-opstelling, en niet weer een gesloten platform dat alleen geschikt is voor een paar grote streamingdiensten.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "Het belangrijkste verschil",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty is meer een 'watch party'-product, inclusief chatfunctie en ondersteuning voor mobiele apparaten. KoalaSync fungeert eerder als een flexibele synchronisatielaag voor video's in de browser.<br><br>Dat lijkt misschien een klein verschil, maar het heeft grote gevolgen voor de manier waarop je de tool gebruikt. Als je een complete 'watch party'-app met ingebouwde chat nodig hebt, is Teleparty een logische keuze. Als je al over voicechat beschikt en alleen wilt dat de videobediening synchroon blijft lopen tussen verschillende browsers, is KoalaSync de eenvoudigere en geschiktere oplossing.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Eerlijke blik op de beperkingen",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync is geen wondermiddel en doet niet geheimzinnig over zijn beperkingen. Op dit moment werkt de tool alleen in desktopbrowsers. Er is geen ingebouwde chatfunctie en geen mobiele app.<br><br>De werking hangt ook af van de browser en de videospeler. KoalaSync zou moeten werken met vrijwel elke standaard videospeler in de browser, maar websites kunnen op elk moment hun speler, DRM-implementatie of browserbeperkingen wijzigen.<br><br>KoalaSync is niet verbonden met Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex of enig ander hier genoemd merk. Deze namen worden uitsluitend gebruikt om typische gebruiksscenario's toe te lichten.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Samenvatting",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Als je behoefte hebt aan een ingebouwde chatfunctie of ondersteuning voor mobiele apparaten, is Teleparty wellicht de betere keuze. Als je echter op zoek bent naar gratis, open-source synchronisatie via desktopbrowsers die werkt met vrijwel elke standaard videospeler – inclusief zelfgehoste mediaservers zoals Emby, Jellyfin en Plex – dan is KoalaSync waarschijnlijk de betere optie.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync vs. scherm delen: wanneer is videosynchronisatie beter?",
|
||||
"ALT_SCREEN_META_DESC": "Een praktische vergelijking tussen KoalaSync en scherm delen voor filmavonden op afstand, met aandacht voor bediening van het afspelen, videokwaliteit, privacy, ondertiteling en kijken via de browser.",
|
||||
"ALT_SCREEN_INTRO": "Scherm delen werkt prima en is soms ook echt voldoende. Eén persoon deelt het scherm, de rest kijkt mee, en klaar ben je.<br><br>Maar bij langere filmavonden of het samen kijken van afleveringen kan het vervelend worden. De videokwaliteit hangt af van de uploadsnelheid, CPU en browser van degene die het scherm deelt, evenals de compressie die wordt toegepast door apps als Discord, Teamspeak, Zoom of andere belprogramma's. Bovendien heeft slechts één persoon echt controle over het afspelen.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Wanneer scherm delen voldoende is",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Scherm delen is een prima oplossing als je de snelst mogelijke opzet wilt. Als slechts één persoon toegang heeft tot de video of als iedereen ermee akkoord gaat dat één persoon de bediening (afspelen, pauzeren, vooruit-/terugspoelen) in handen heeft, is dit wellicht de eenvoudigste optie.<br><br>Het is ook een logische keuze als de beeldkwaliteit minder belangrijk is, niemand specifieke ondertiteling of audio-instellingen nodig heeft en de persoon die het beeld deelt er geen bezwaar tegen heeft om dat browservenster of scherm te laten zien.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Wanneer scherm delen vervelend kan zijn",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "Het grootste nadeel is dat niet iedereen in zijn eigen speler kijkt. Eén persoon streamt het scherm en alle anderen zien alleen die stream.<br><br>Dit kan leiden tot een lagere beeldkwaliteit, gecomprimeerde audio, vertraging bij het afspelen en minder controle voor de andere kijkers. Ook de ondertiteling en audiosporen zijn afhankelijk van wat de deler heeft gekozen.<br><br>Daarnaast speelt privacy een rol. Als je van venster of tabblad wisselt, kunnen anderen mogelijk kortstondig je browsertabbladen, e-mail, Reddit, chats of andere privégegevens zien. Dit is te omzeilen door een apart venster te gebruiken, maar het is wel iets om rekening mee te houden.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Waarom KoalaSync 'netter' kan aanvoelen",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync streamt je scherm niet. Iedereen bekijkt de video lokaal in de eigen browser; KoalaSync zorgt er alleen voor dat acties zoals afspelen, pauzeren en vooruit-/terugspoelen synchroon blijven lopen.<br><br>Hierdoor kan iedereen op het eigen apparaat in volledig scherm kijken, eigen instellingen voor ondertiteling of audio gebruiken (mits de speler dit ondersteunt) en de voicechat gescheiden houden via Discord, Teamspeak of een andere beldienst.<br><br>Voor filmavonden kan dit veel prettiger aanvoelen dan wanneer één persoon het scherm deelt. Het is vooral handig voor browsergebaseerde systemen zoals Emby, Jellyfin, Plex, lokale bestanden die in de browser worden geopend, privédomeinen of andere websites met standaard videospelers in de browser.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Eerlijke beperkingen",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync is geen vervanging voor een spraakgesprek. Als je tijdens het kijken wilt praten, gebruik je gewoon Discord, Teamspeak, Signal, WhatsApp of wat je normaal ook gebruikt.<br><br>Iedereen heeft ook toegang tot de video of pagina op het eigen apparaat nodig. KoalaSync omzeilt geen abonnementen, inloggegevens, DRM, regionale beperkingen of platformregels.<br><br>Het werkt momenteel alleen in desktopbrowsers, heeft geen ingebouwde chat- of spraakfunctie en de compatibiliteit hangt altijd af van de browser en de videospeler.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Samenvatting",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Scherm delen is de eenvoudigste optie als je snel iets wilt doen en niet veel waarde hecht aan kwaliteit, privacy of individuele controle. KoalaSync is een betere keuze wanneer iedereen lokaal in de eigen browser moet kijken en de sessie meer moet aanvoelen als echt samen kijken.",
|
||||
"ALT_TRY_KOALASYNC": "Probeer KoalaSync"
|
||||
}
|
||||
|
||||
+32
-2
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "pl_PL",
|
||||
"META_TITLE": "KoalaSync | Synchronizuj Netflix, YouTube i dowolne wideo ze znajomymi",
|
||||
"META_DESCRIPTION": "Oglądaj Netflix, YouTube, Twitch i dowolne wideo HTML5 w idealnej synchronizacji ze znajomymi. Darmowe rozszerzenie o otwartym kodzie dla Chrome i Firefox. Bez rejestracji.",
|
||||
"SCHEMA_APP_DESC": "Oglądaj Netflix, YouTube, Twitch i dowolne wideo HTML5 w idealnej synchronizacji ze znajomymi. Darmowe rozszerzenie o otwartym kodzie dla Chrome i Firefox. Bez rejestracji.",
|
||||
"OG_TITLE": "KoalaSync | Synchronizuj Netflix, Emby, Jellyfin i prawie każde wideo ze znajomymi",
|
||||
"OG_DESCRIPTION": "Oglądaj Netflix, Emby, Jellyfin, YouTube, Twitch i prawie każde wideo HTML5 w idealnej synchronizacji. Zorientowane na prywatność rozszerzenie o otwartym kodzie dla Chrome i Firefox.",
|
||||
"TWITTER_TITLE": "KoalaSync | Synchronizuj Netflix, Emby, Jellyfin i prawie każde wideo ze znajomymi – Rozszerzenie przeglądarki",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Tylko obsługiwane strony",
|
||||
"COMP_FEAT_6_NAME": "Lokalizacja / Tłumaczenie",
|
||||
"COMP_FEAT_6_DESC": "Dostępne języki interfejsu i wsparcie tłumaczeń.",
|
||||
"COMP_FEAT_6_KOALA": "13 języków (i rośnie)",
|
||||
"COMP_FEAT_6_KOALA": "15 języków (i rośnie)",
|
||||
"COMP_FEAT_6_TELE": "Tylko angielski",
|
||||
"COMP_FEAT_7_NAME": "Kompresor Audio / Wyrównywanie Głośności",
|
||||
"COMP_FEAT_7_DESC": "Wbudowana normalizacja dźwięku do równoważenia cichych dialogów i głośnych efektów",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Wszystko, co musisz wiedzieć o wspólnym oglądaniu filmów w idealnej synchronizacji.",
|
||||
"FAQ_Q1": "Czy mogę oglądać Netflix, Emby lub Jellyfin w synchronizacji ze znajomymi?",
|
||||
"FAQ_A1": "Tak! KoalaSync działa na Netflix, YouTube, Twitch i każdej stronie z odtwarzaczem wideo HTML5 — w tym Emby, Jellyfin i własnych serwerach mediów. Zainstaluj rozszerzenie, utwórz pokój, udostępnij link i wszyscy oglądają w idealnej synchronizacji.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "Czy KoalaSync przesyła wideo do moich znajomych?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Nie. KoalaSync nie przesyła, nie hostuje, nie udostępnia ani nie omija dostępu do treści wideo. Każda osoba ogląda wideo lokalnie we własnej przeglądarce i potrzebuje własnego dostępu do danej strony lub usługi streamingowej, na przykład własnego konta Netflix. KoalaSync synchronizuje tylko akcje odtwarzania, takie jak odtwarzanie, pauza i przewijanie.",
|
||||
"FAQ_Q2": "Czy KoalaSync to darmowa alternatywa dla Teleparty?",
|
||||
"FAQ_A2": "W 100% darmowy i o otwartym kodzie na licencji MIT. Brak planów premium, brak zablokowanych funkcji, brak reklam — wszystko jest w cenie. W przeciwieństwie do Teleparty, serwer KoalaSync działa tylko w pamięci RAM, bez zbierania danych i bez kont użytkowników.",
|
||||
"FAQ_Q3": "Czy KoalaSync działa z Emby, Jellyfin i własnymi serwerami mediów?",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync nie jest powiązany, wspierany ani stowarzyszony z Netflix, Disney+, Amazon, YouTube, Twitch ani żadną inną platformą streamingową. Wszystkie znaki towarowe są własnością ich odpowiednich właścicieli.",
|
||||
"FOOTER_SUPPORT": "Postaw kawę na Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync czy Teleparty: którego narzędzia do wspólnego oglądania użyć?",
|
||||
"ALT_TELEPARTY_META_DESC": "Praktyczne porównanie narzędzi KoalaSync i Teleparty pod kątem wspólnego oglądania: wbudowany czat, obsługa urządzeń mobilnych, synchronizacja wideo w przeglądarce, własne serwery multimediów i charakter open-source.",
|
||||
"ALT_TELEPARTY_INTRO": "Zarówno KoalaSync, jak i Teleparty rozwiązują ten sam uciążliwy problem: jak oglądać coś wspólnie bez konieczności odliczania „3, 2, 1, start”. Różnią się jednak podejściem do tego zadania.<br><br>Teleparty sprawdza się najlepiej, jeśli zależy Ci na wbudowanym czacie lub oglądaniu na urządzeniach mobilnych. KoalaSync jest raczej rozwiązaniem do wspólnego oglądania w przeglądarce na komputerze – gdy rozmawiacie już przez Discorda, Teamspeaka lub inny komunikator głosowy i chcecie jedynie zsynchronizować odtwarzanie.<br><br>Głównym powodem stworzenia KoalaSync była elastyczność. Narzędzie to ma współpracować z niemal każdą stroną wykorzystującą standardowy odtwarzacz wideo w przeglądarce, a nie tylko z wybraną listą platform. Obejmuje to własne serwery multimediów, takie jak Emby, Jellyfin czy Plex, domeny lokalne lub prywatne oraz wiele typowych stron z materiałami wideo.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Kiedy Teleparty wciąż może być dobrym wyborem",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty może okazać się lepszym rozwiązaniem, jeśli chcesz, aby czat był częścią samego narzędzia do wspólnego oglądania. KoalaSync nie posiada obecnie wbudowanego czatu; zakłada, że rozmawiacie za pośrednictwem innych kanałów, takich jak Discord, Teamspeak, WhatsApp, Signal czy zwykłe połączenie głosowe.<br><br>Kolejną istotną kwestią jest obsługa urządzeń mobilnych. Teleparty posiada dedykowaną aplikację, podczas gdy KoalaSync działa obecnie tylko w przeglądarkach na komputerach stacjonarnych. Jeśli chcesz zsynchronizować Netflixa na telefonie lub tablecie, KoalaSync nie będzie w tej chwili odpowiednim narzędziem.<br><br>Zatem, jeśli czat i obsługa urządzeń mobilnych są dla Ciebie najważniejsze, Teleparty może po prostu lepiej pasować do Twoich potrzeb.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Kiedy warto wybrać KoalaSync",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync to lepszy wybór, jeśli szukasz elastycznego narzędzia do synchronizacji odtwarzania wideo w przeglądarce, a nie rozwiązania ograniczonego do wąskiej listy platform.<br><br>Narzędzie współpracuje z niemal każdą stroną internetową wykorzystującą standardowy odtwarzacz wideo w przeglądarce. Jest to szczególnie przydatne w przypadku serwisów takich jak Emby, Jellyfin, Plex, prywatnych serwerów multimedialnych, domen lokalnych czy mniejszych serwisów wideo, które zazwyczaj są pomijane przez klasyczne narzędzia do wspólnego oglądania.<br><br>Co więcej, KoalaSync jest darmowe, ma otwarty kod źródłowy, nie wymaga zakładania konta i pozwala na samodzielne hostowanie (jeśli chcesz uruchomić własny serwer pośredniczący). Stworzyłem je, ponieważ zależało mi na rozwiązaniu szanującym prywatność, które współpracowałoby z moją własną konfiguracją Emby, a nie na kolejnej zamkniętej platformie ograniczonej do kilku największych serwisów streamingowych.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "Główna różnica",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty to produkt nastawiony na wspólne oglądanie, oferujący wbudowany czat i obsługę urządzeń mobilnych. KoalaSync pełni raczej funkcję elastycznej warstwy synchronizującej odtwarzanie wideo w przeglądarce.<br><br>Może się to wydawać drobną różnicą, ale znacząco wpływa na sposób użytkowania. Jeśli potrzebujesz kompletnej aplikacji do wspólnego oglądania z wbudowanym czatem, Teleparty będzie dobrym wyborem. Jeśli natomiast korzystasz już z czatu głosowego i zależy Ci jedynie na synchronizacji sterowania odtwarzaniem między przeglądarkami, prostszym i lepszym rozwiązaniem będzie KoalaSync.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Szczerze o ograniczeniach",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync nie jest magicznym rozwiązaniem i nie ukrywa swoich ograniczeń. Obecnie działa tylko w przeglądarkach na komputerach stacjonarnych. Nie posiada wbudowanego czatu ani aplikacji mobilnej.<br><br>Kompatybilność zależy również od przeglądarki i odtwarzacza wideo. KoalaSync powinno współpracować z niemal każdym standardowym odtwarzaczem przeglądarkowym, jednak właściciele stron mogą w każdej chwili zmienić odtwarzacz, sposób działania zabezpieczeń DRM czy ograniczenia przeglądarki.<br><br>KoalaSync nie jest powiązane z serwisami Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex ani żadną inną wymienioną tu marką. Nazwy te zostały użyte wyłącznie w celu zilustrowania typowych scenariuszy użycia.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Podsumowanie",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Jeśli zależy Ci na wbudowanym czacie lub obsłudze urządzeń mobilnych, lepszym wyborem może okazać się Teleparty. Jeśli natomiast szukasz darmowego, otwartoźródłowego rozwiązania do synchronizacji w przeglądarce na komputerze, które współpracuje z niemal każdym standardowym odtwarzaczem wideo – w tym z własnymi serwerami multimedialnymi, takimi jak Emby, Jellyfin czy Plex – lepszym rozwiązaniem będzie prawdopodobnie KoalaSync.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync a udostępnianie ekranu: kiedy synchronizacja wideo sprawdza się lepiej?",
|
||||
"ALT_SCREEN_META_DESC": "Praktyczne porównanie KoalaSync i udostępniania ekranu pod kątem wspólnych seansów filmowych online: sterowanie odtwarzaniem, jakość obrazu, prywatność, napisy i oglądanie w przeglądarce.",
|
||||
"ALT_SCREEN_INTRO": "Udostępnianie ekranu działa i – szczerze mówiąc – czasem to wystarczy. Jedna osoba udostępnia swój ekran, pozostali oglądają i gotowe.<br><br>Jednak w przypadku dłuższych seansów czy wspólnego oglądania odcinków seriali może to stać się uciążliwe. Jakość obrazu zależy od szybkości wysyłania danych (uploadu), procesora i przeglądarki osoby udostępniającej, a także od kompresji stosowanej przez aplikacje takie jak Discord, Teamspeak, Zoom czy inne komunikatory. Co więcej, tylko jedna osoba ma faktyczną kontrolę nad odtwarzaniem.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Kiedy udostępnianie ekranu wystarczy",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Udostępnianie ekranu sprawdza się dobrze, gdy zależy Ci na jak najszybszym rozpoczęciu seansu. Jeśli tylko jedna osoba ma dostęp do materiału wideo lub jeśli wszystkim odpowiada, że jedna osoba steruje odtwarzaniem (start, pauza, przewijanie), może to być najprostsze rozwiązanie.<br><br>Ma to również sens, gdy jakość nie jest kluczowa, nikt nie potrzebuje własnych ustawień napisów czy dźwięku, a osoba udostępniająca nie ma nic przeciwko pokazywaniu okna przeglądarki lub całego ekranu.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Kiedy udostępnianie ekranu bywa uciążliwe",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "Największą wadą jest to, że nie każdy ogląda materiał we własnym odtwarzaczu. Jedna osoba przesyła obraz swojego ekranu, a wszyscy pozostali widzą tylko ten przekaz.<br><br>Może to oznaczać gorszą jakość wideo, skompresowany dźwięk, opóźnienia w odtwarzaniu oraz mniejszą kontrolę dla pozostałych osób. Napisy i ścieżki dźwiękowe są również narzucone przez osobę udostępniającą.<br><br>Istnieje też kwestia prywatności. Jeśli przełączysz okna lub karty, inni mogą na chwilę zobaczyć zawartość Twoich kart w przeglądarce, e-maile, Reddita, czaty czy inne prywatne treści. Można temu zaradzić, używając osobnego okna, ale wciąż jest to kwestia, o której trzeba pamiętać.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Dlaczego KoalaSync może wydawać się lepszym rozwiązaniem",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync nie przesyła obrazu Twojego ekranu. Każdy ogląda wideo lokalnie we własnej przeglądarce, a KoalaSync jedynie synchronizuje działania takie jak start, pauza czy przewijanie.<br><br>Oznacza to, że każdy może korzystać z trybu pełnoekranowego na własnym urządzeniu, używać własnych ustawień napisów lub dźwięku (jeśli odtwarzacz na to pozwala) oraz prowadzić rozmowę głosową oddzielnie – np. przez Discorda, TeamSpeaka czy inne komunikatory.<br><br>W przypadku wspólnych seansów filmowych rozwiązanie to może wydawać się znacznie bardziej uporządkowane i wygodne niż udostępnianie ekranu przez jedną osobę. Jest to szczególnie przydatne w przypadku rozwiązań przeglądarkowych, takich jak Emby, Jellyfin, Plex, plików lokalnych otwieranych w przeglądarce, prywatnych domen czy innych stron korzystających ze standardowych odtwarzaczy wideo.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Szczerze o ograniczeniach",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync nie zastępuje rozmowy głosowej. Jeśli chcesz rozmawiać podczas oglądania, nadal możesz korzystać z Discorda, TeamSpeaka, Signala, WhatsAppa lub innych narzędzi, których używasz na co dzień.<br><br>Każda osoba musi również mieć dostęp do filmu lub strony na własnym urządzeniu. KoalaSync nie omija subskrypcji, konieczności logowania, zabezpieczeń DRM, ograniczeń regionalnych ani regulaminów platform.<br><br>Obecnie rozwiązanie to działa tylko w przeglądarkach na komputerach stacjonarnych i nie posiada wbudowanego czatu ani funkcji komunikacji głosowej, a kompatybilność zależy od używanej przeglądarki oraz odtwarzacza wideo.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Podsumowanie",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Udostępnianie ekranu to najprostsze rozwiązanie, gdy zależy Ci na szybkości, a kwestie jakości, prywatności czy indywidualnej kontroli nad odtwarzaniem nie są priorytetem. KoalaSync sprawdza się lepiej, gdy uczestnicy oglądają materiał lokalnie we własnych przeglądarkach, a sesja ma bardziej przypominać wspólny seans.",
|
||||
"ALT_TRY_KOALASYNC": "Wypróbuj KoalaSync"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "pt_BR",
|
||||
"META_TITLE": "KoalaSync | Sincronize Netflix, YouTube e qualquer vídeo com amigos",
|
||||
"META_DESCRIPTION": "Assista Netflix, YouTube, Twitch e qualquer vídeo HTML5 em perfeita sincronia. Extensão de navegador gratuita e open source. Sem cadastro.",
|
||||
"SCHEMA_APP_DESC": "Assista Netflix, YouTube, Twitch e qualquer vídeo HTML5 em perfeita sincronia. Extensão de navegador gratuita e open source. Sem cadastro.",
|
||||
"OG_TITLE": "KoalaSync | Sincronize Netflix, Emby, Jellyfin e vídeos com amigos",
|
||||
"OG_DESCRIPTION": "Assista a vídeos em perfeita sincronia com seus amigos. Extensão open source e focada em privacidade.",
|
||||
"TWITTER_TITLE": "KoalaSync | Sincronize vídeos online com seus amigos",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Apenas sites selecionados",
|
||||
"COMP_FEAT_6_NAME": "Traduzido",
|
||||
"COMP_FEAT_6_DESC": "Idiomas suportados.",
|
||||
"COMP_FEAT_6_KOALA": "13 Idiomas",
|
||||
"COMP_FEAT_6_KOALA": "15 Idiomas",
|
||||
"COMP_FEAT_6_TELE": "Apenas Inglês",
|
||||
"COMP_FEAT_7_NAME": "Compressor de Áudio",
|
||||
"COMP_FEAT_7_DESC": "Melhora a clareza das vozes nos vídeos.",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Tudo o que você precisa saber.",
|
||||
"FAQ_Q1": "Posso assistir Netflix ou YouTube em sincronia com meus amigos?",
|
||||
"FAQ_A1": "Sim! O KoalaSync funciona no Netflix, YouTube, Twitch e qualquer site com vídeo HTML5. Instale a extensão, compartilhe o link e pronto.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "O KoalaSync transmite o vídeo para meus amigos?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Não. O KoalaSync não transmite, hospeda, compartilha nem contorna o acesso a conteúdos de vídeo. Cada pessoa assiste ao vídeo localmente no próprio navegador e precisa ter seu próprio acesso ao site ou serviço de streaming, como sua própria conta da Netflix. O KoalaSync sincroniza apenas ações de reprodução, como reproduzir, pausar e avançar ou voltar.",
|
||||
"FAQ_Q2": "É gratuito?",
|
||||
"FAQ_A2": "Sim, é 100% gratuito e open source. Sem recursos pagos e sem anúncios.",
|
||||
"FAQ_Q3": "Funciona com Emby ou Jellyfin?",
|
||||
@@ -150,9 +153,9 @@
|
||||
"MOCK_08": "Sair da Sala",
|
||||
"MOCK_09": "Selecionar Vídeo",
|
||||
"MOCK_10": "Controle Remoto",
|
||||
"MOCK_11": "Play",
|
||||
"MOCK_12": "Pausa",
|
||||
"MOCK_13": "SINCRO",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Play",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausa",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SINCRO",
|
||||
"MOCK_14": "Ir para a posição dos outros",
|
||||
"MOCK_15": "Última Atividade",
|
||||
"MOCK_16": "Aguardando 1 amigo (KoalaPC)...",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync não é afiliado à Netflix, Disney+, YouTube, Twitch, etc. Todas as marcas registradas são de propriedade de seus respectivos donos.",
|
||||
"FOOTER_SUPPORT": "Me pague um café"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs. Teleparty: qual ferramenta de watch party você deve usar?",
|
||||
"ALT_TELEPARTY_META_DESC": "Uma comparação prática entre KoalaSync e Teleparty para watch parties, abordando chat integrado, suporte para dispositivos móveis, sincronização de vídeo no navegador, servidores de mídia hospedados pelo próprio usuário e uso de código aberto.",
|
||||
"ALT_TELEPARTY_INTRO": "Tanto o KoalaSync quanto o Teleparty ajudam a resolver o mesmo problema incômodo: assistir a algo em conjunto sem precisar daquela rotina de \"3, 2, 1, play\". A diferença é a abordagem que cada um utiliza.<br><br>O Teleparty é a opção mais indicada se você deseja um chat integrado ou quer assistir pelo celular. O KoalaSync é mais voltado para watch parties em navegadores de desktop, onde você já conversa via Discord, Teamspeak ou outra chamada de voz e só precisa que a reprodução do vídeo permaneça sincronizada.<br><br>O principal motivo pelo qual criei o KoalaSync foi a flexibilidade. Ele foi projetado para funcionar com praticamente qualquer site que utilize um reprodutor de vídeo padrão de navegador, e não apenas com uma lista fixa de plataformas. Isso inclui sistemas hospedados pelo próprio usuário, como Emby, Jellyfin e Plex, domínios locais ou privados e muitas páginas de vídeo comuns na web.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Quando o Teleparty ainda pode ser uma boa opção",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "O Teleparty ainda pode ser a melhor escolha se você quiser que o chat faça parte da própria ferramenta de watch party. Atualmente, o KoalaSync não possui chat integrado; ele parte do princípio de que você já está conversando por outro meio, como Discord, Teamspeak, WhatsApp, Signal ou uma chamada convencional.<br><br>O suporte para dispositivos móveis é outro fator importante. O Teleparty possui um aplicativo, enquanto o KoalaSync, no momento, funciona apenas em navegadores de desktop. Se você deseja sincronizar a Netflix em um celular ou tablet, o KoalaSync não é a ferramenta ideal para isso agora.<br><br>Portanto, se chat e suporte para dispositivos móveis são as suas prioridades, o Teleparty pode simplesmente se adequar melhor à sua configuração.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Quando o KoalaSync pode ser a melhor opção",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "O KoalaSync é mais adequado se você busca uma ferramenta flexível de sincronização de vídeo em navegadores para desktop, em vez de algo restrito a uma lista limitada de plataformas.<br><br>Ele funciona com praticamente qualquer site que utilize o reprodutor de vídeo padrão do navegador. Isso é especialmente útil para Emby, Jellyfin, Plex, servidores de mídia privados, domínios locais ou sites de vídeo menores que as ferramentas tradicionais de 'watch party' costumam ignorar.<br><br>Além disso, é gratuito, de código aberto, não exige criação de conta e permite auto-hospedagem caso você queira executar seu próprio servidor de retransmissão (relay). Eu o criei porque queria algo que respeitasse a privacidade e funcionasse com minha própria configuração do Emby, e não outra plataforma fechada restrita a alguns grandes serviços de streaming.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "A principal diferença",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "O Teleparty é mais voltado para a experiência de 'watch party' (sessão de visualização conjunta), oferecendo chat e suporte para dispositivos móveis. Já o KoalaSync funciona mais como uma camada flexível de sincronização de vídeo entre navegadores.<br><br>Pode parecer uma diferença pequena, mas ela altera significativamente a forma de uso. Se você precisa de um aplicativo completo de 'watch party' com chat integrado, o Teleparty é a escolha ideal. Se você já utiliza chat de voz e quer apenas manter os controles de vídeo sincronizados entre navegadores, o KoalaSync é a solução mais simples.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Limitações reais",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "O KoalaSync não faz mágica e não tenta esconder suas limitações. Atualmente, ele funciona apenas em navegadores para desktop. Não possui chat integrado nem aplicativo para dispositivos móveis.<br><br>A compatibilidade também depende do navegador e do reprodutor de vídeo utilizado. O KoalaSync deve funcionar com praticamente qualquer reprodutor de vídeo padrão de navegador, mas os sites podem alterar seus reprodutores, o comportamento de DRM ou as restrições do navegador a qualquer momento.<br><br>O KoalaSync não tem vínculo com Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex ou qualquer outra marca mencionada aqui. Os nomes são utilizados apenas para ilustrar casos de uso comuns.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Resumo",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Se você busca chat integrado ou suporte para dispositivos móveis, o Teleparty pode ser a melhor opção. Se você prefere uma solução gratuita e de código aberto para sincronização em navegadores de desktop — que funcione com praticamente qualquer reprodutor de vídeo comum, incluindo servidores de mídia auto-hospedados como Emby, Jellyfin e Plex —, o KoalaSync é provavelmente a escolha ideal.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync vs. compartilhamento de tela: quando a sincronização de vídeo é melhor?",
|
||||
"ALT_SCREEN_META_DESC": "Uma comparação prática entre o KoalaSync e o compartilhamento de tela para sessões de cinema remotas, abordando controle de reprodução, qualidade de vídeo, privacidade, legendas e visualização via navegador.",
|
||||
"ALT_SCREEN_INTRO": "O compartilhamento de tela funciona e, honestamente, às vezes é o suficiente. Uma pessoa compartilha a tela, as outras assistem e pronto.<br><br>Mas, para sessões de cinema mais longas ou para assistir a episódios em conjunto, isso pode se tornar incômodo. A qualidade do vídeo depende da velocidade de upload, da CPU e do navegador de quem está compartilhando, além da compressão aplicada por aplicativos como Discord, Teamspeak, Zoom ou outros. Além disso, apenas uma pessoa realmente controla a reprodução.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Quando o compartilhamento de tela é suficiente",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "O compartilhamento de tela é uma boa opção quando você quer uma configuração rápida. Se apenas uma pessoa tem acesso ao vídeo ou se todos concordam que uma única pessoa controle a reprodução, a pausa e a navegação pelo vídeo, essa pode ser a opção mais simples.<br><br>Também faz sentido quando a qualidade não é uma prioridade, quando ninguém precisa de legendas ou configurações de áudio personalizadas e quando a pessoa que está compartilhando se sente confortável em exibir aquela janela do navegador ou a tela do computador.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Quando o compartilhamento de tela pode ser incômodo",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "A maior desvantagem é que nem todos assistem através de seu próprio reprodutor de vídeo. Uma pessoa transmite a tela, e todas as outras apenas visualizam essa transmissão.<br><br>Isso pode resultar em qualidade de vídeo inferior, áudio comprimido, atraso na reprodução e menos controle para os demais participantes. As legendas e faixas de áudio também serão aquelas escolhidas por quem está compartilhando.<br><br>Há também a questão da privacidade. Se você alternar entre janelas ou abas, as pessoas poderão ver brevemente suas abas do navegador, e-mail, Reddit, chats ou outros conteúdos privados. É possível contornar isso usando uma janela separada, mas ainda é algo que você precisa levar em consideração.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Por que o KoalaSync pode parecer uma solução mais organizada",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "O KoalaSync não transmite a sua tela. Todos assistem ao vídeo localmente em seus próprios navegadores, e o KoalaSync mantém sincronizadas apenas as ações de reprodução, como iniciar, pausar e avançar ou retroceder o vídeo.<br><br>Isso significa que cada pessoa pode usar o modo de tela cheia em seu próprio dispositivo, utilizar suas configurações de legenda ou áudio (se o reprodutor oferecer suporte) e manter o chat de voz separado, via Discord, Teamspeak ou outra ferramenta de chamada.<br><br>Para noites de filmes, essa abordagem pode parecer muito mais organizada do que uma pessoa transmitindo a tela. É especialmente útil para configurações baseadas em navegador, como Emby, Jellyfin, Plex, arquivos locais abertos no navegador, domínios privados ou outros sites que utilizam reprodutores de vídeo padrão do navegador.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Limitações reais",
|
||||
"ALT_SCREEN_LIMITS_BODY": "O KoalaSync não substitui uma chamada de voz. Se você quiser conversar enquanto assiste, continue usando Discord, Teamspeak, Signal, WhatsApp ou qualquer ferramenta que utilize normalmente.<br><br>Todos também precisam ter acesso ao vídeo ou à página em seus próprios dispositivos. O KoalaSync não contorna assinaturas, logins, DRM, restrições regionais ou regras da plataforma.<br><br>Atualmente, funciona apenas em navegadores para desktop, não possui recursos integrados de chat ou voz, e a compatibilidade depende sempre do navegador e do reprodutor de vídeo.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Resumo",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "O compartilhamento de tela é a opção mais simples quando você quer algo rápido e não se preocupa muito com qualidade, privacidade ou controle individual. O KoalaSync é mais adequado quando todos devem assistir localmente em seus próprios navegadores e a sessão deve proporcionar uma experiência mais próxima de realmente assistir juntos.",
|
||||
"ALT_TRY_KOALASYNC": "Experimente o KoalaSync"
|
||||
}
|
||||
|
||||
+35
-5
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "pt_PT",
|
||||
"META_TITLE": "KoalaSync | Sincronize Netflix, YouTube e vídeos com os amigos",
|
||||
"META_DESCRIPTION": "Veja Netflix, YouTube, Twitch e qualquer vídeo HTML5 em sincronia perfeita. Extensão gratuita e de código aberto. Sem registo.",
|
||||
"SCHEMA_APP_DESC": "Veja Netflix, YouTube, Twitch e qualquer vídeo HTML5 em sincronia perfeita. Extensão gratuita e de código aberto. Sem registo.",
|
||||
"OG_TITLE": "KoalaSync | Sincronize Netflix, Emby, Jellyfin e vídeos com os amigos",
|
||||
"OG_DESCRIPTION": "Veja vídeos em sincronia perfeita com os amigos. Extensão de código aberto e focada na privacidade.",
|
||||
"TWITTER_TITLE": "KoalaSync | Sincronize vídeos em streaming com os amigos",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Apenas sites selecionados",
|
||||
"COMP_FEAT_6_NAME": "Traduzido",
|
||||
"COMP_FEAT_6_DESC": "Idiomas suportados.",
|
||||
"COMP_FEAT_6_KOALA": "13 Idiomas",
|
||||
"COMP_FEAT_6_KOALA": "15 Idiomas",
|
||||
"COMP_FEAT_6_TELE": "Apenas Inglês",
|
||||
"COMP_FEAT_7_NAME": "Compressor de Áudio",
|
||||
"COMP_FEAT_7_DESC": "Melhora a clareza das vozes nos vídeos.",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Tudo o que precisa de saber.",
|
||||
"FAQ_Q1": "Posso ver a Netflix ou YouTube com os meus amigos?",
|
||||
"FAQ_A1": "Sim! O KoalaSync funciona no Netflix, YouTube, Twitch e qualquer site com vídeo HTML5. Instale a extensão, partilhe o link e está feito.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "O KoalaSync transmite o vídeo para os meus amigos?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Não. O KoalaSync não transmite, aloja, partilha nem contorna o acesso a conteúdos de vídeo. Cada pessoa vê o vídeo localmente no seu próprio navegador e precisa do seu próprio acesso ao site ou serviço de streaming, por exemplo a sua própria conta Netflix. O KoalaSync apenas sincroniza ações de reprodução, como reproduzir, pausar e avançar ou recuar.",
|
||||
"FAQ_Q2": "É gratuito?",
|
||||
"FAQ_A2": "Sim, é 100% gratuito e de código aberto. Sem funções premium bloqueadas e sem publicidade.",
|
||||
"FAQ_Q3": "Funciona com Emby ou Jellyfin?",
|
||||
@@ -150,9 +153,9 @@
|
||||
"MOCK_08": "Sair da Sala",
|
||||
"MOCK_09": "Selecionar Vídeo",
|
||||
"MOCK_10": "Controlo Remoto",
|
||||
"MOCK_11": "Play",
|
||||
"MOCK_12": "Pausa",
|
||||
"MOCK_13": "SINCRO",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Play",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pausa",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SINCRO",
|
||||
"MOCK_14": "Ir para a posição dos outros",
|
||||
"MOCK_15": "Última Atividade",
|
||||
"MOCK_16": "À espera de 1 amigo (KoalaPC)...",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync não é afiliado à Netflix, Disney+, YouTube, Twitch, etc. Todas as marcas são propriedade dos seus titulares.",
|
||||
"FOOTER_SUPPORT": "Oferece-me um café"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync vs Teleparty: que ferramenta de festa de exibição deve utilizar?",
|
||||
"ALT_TELEPARTY_META_DESC": "Uma comparação prática entre o KoalaSync e o Teleparty para festas de exibição, chat integrado, suporte para dispositivos móveis, sincronização de vídeo no browser, servidores de media auto-hospedados e utilização de código aberto.",
|
||||
"ALT_TELEPARTY_INTRO": "O KoalaSync e o Teleparty resolvem o mesmo problema irritante: ver algo em conjunto sem ter de fazer toda aquela rotina de \"3, 2, 1, reproduzir\". Apenas usam abordagens diferentes.<br><br>O Teleparty é mais indicado se pretende um chat integrado ou se pretende ver em dispositivos móveis. O KoalaSync está mais virado para sessões de vídeo em browsers de desktop, onde já estão a conversar no Discord, Teamspeak ou noutra chamada de voz e apenas querem que a reprodução permaneça sincronizada.<br><br>A principal razão para eu ter criado o KoalaSync foi a flexibilidade. Foi concebido para funcionar com praticamente qualquer site que utilize um leitor de vídeo comum de browser, e não apenas com uma lista fixa de plataformas.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Quando o Teleparty ainda pode fazer sentido",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "O Teleparty pode ainda ser a melhor opção se quiser que o chat faça parte da própria ferramenta de visualização em grupo. O KoalaSync não tem um chat integrado de momento. Assume que já está a falar noutro local, como o Discord, Teamspeak, WhatsApp, Signal ou numa chamada telefónica normal.<br><br>A compatibilidade com dispositivos móveis é outro factor importante. O Teleparty tem uma aplicação, enquanto o KoalaSync funciona apenas em browsers de desktop. Se pretende sincronizar a Netflix num telemóvel ou tablet, o KoalaSync não é a ferramenta certa para isso agora.<br><br>Portanto, se o chat e a compatibilidade com dispositivos móveis são os aspetos mais importantes para si, o Teleparty pode simplesmente adequar-se melhor à sua configuração.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Quando o KoalaSync pode ser a melhor opção",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "O KoalaSync é a melhor opção se procura uma ferramenta flexível de sincronização para navegadores de desktop, em vez de algo limitado a uma lista restrita de plataformas.<br><br>Funciona com praticamente qualquer site que utilize um leitor de vídeo padrão de navegador. Isto é especialmente útil para Emby, Jellyfin, Plex, servidores de media privados, domínios locais ou sites de vídeo mais pequenos do que as ferramentas clássicas de watch party geralmente ignoram.<br><br>Além disso, é gratuito, de código aberto, não requer contas e pode ser alojado no seu próprio servidor, caso queira executar o seu próprio relay.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "A principal diferença",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "O Teleparty é mais um produto para ver vídeos em grupo, com chat e suporte para dispositivos móveis. O KoalaSync é mais uma camada de sincronização flexível para vídeos nos browsers.<br><br>Isto pode parecer uma pequena diferença, mas muda bastante o caso de utilização. Se precisa de uma aplicação completa para ver vídeos em grupo, com chat integrado, o Teleparty faz sentido. Se já tem chat de voz e apenas quer que os controlos de vídeo permaneçam sincronizados entre browsers, o KoalaSync é a opção mais simples.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Limitações honestas",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "O KoalaSync não é mágico e não tenta esconder as suas limitações. Atualmente, funciona apenas em navegadores de desktop. Não possui chat integrado nem aplicação para dispositivos móveis.<br><br>A compatibilidade também depende do navegador e do reprodutor de vídeo. O KoalaSync deve funcionar com quase qualquer reprodutor de vídeo comum de navegador, mas os sites podem alterar os seus reprodutores, comportamento de DRM ou restrições de navegador a qualquer momento.<br><br>O KoalaSync não tem qualquer ligação com Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex ou qualquer outra marca aqui mencionada.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Resumo",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Se pretende um chat integrado ou suporte para dispositivos móveis, o Teleparty pode ser a melhor opção. Se procura uma sincronização gratuita e de código aberto para browsers de desktop que funcione com praticamente qualquer leitor de vídeo comum, incluindo servidores de multimédia auto-hospedados como o Emby, Jellyfin e Plex, o KoalaSync é provavelmente a melhor escolha.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync vs. partilha de ecrã: quando é que a sincronização de vídeo é melhor?",
|
||||
"ALT_SCREEN_META_DESC": "Uma comparação prática entre o KoalaSync e a partilha de ecrã para noites de cinema remotas, controlo de reprodução, qualidade de vídeo, privacidade, legendas e visualização através do browser.",
|
||||
"ALT_SCREEN_INTRO": "Partilhar o ecrã funciona e, por vezes, é suficiente. Uma pessoa partilha o ecrã, todos os outros vêem e pronto.<br><br>Mas para longas noites de cinema ou para ver episódios juntos, pode tornar-se irritante. A qualidade do vídeo depende da velocidade de upload de quem partilha, do CPU, do browser e da compressão aplicada pelo Discord, Teamspeak, Zoom ou outra aplicação de chamadas. Além disso, apenas uma pessoa controla a reprodução.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Quando a partilha de ecrã é suficiente",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Partilhar o ecrã é bom quando se pretende a configuração mais rápida possível. Se apenas uma pessoa tiver acesso ao vídeo ou se todos concordarem que uma pessoa controla a reprodução, a pausa e o avanço/retrocesso, esta pode ser a opção mais simples.<br><br>Também faz sentido quando a qualidade não é um fator crucial, quando ninguém precisa das suas próprias legendas ou definições de áudio e quando a pessoa que está a partilhar se sente confortável em mostrar a janela do browser ou o ecrã.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Onde a partilha de ecrã se torna irritante",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "A maior desvantagem é que nem todos estão a ver nos seus próprios leitores. Uma pessoa transmite o seu ecrã e todos os outros apenas vêem essa transmissão.<br><br>Isto pode significar pior qualidade de vídeo, áudio comprimido, reprodução diferida e menos controlo para todos os outros. As legendas e as faixas de áudio também são definidas pela pessoa que está a partilhar.<br><br>Há também a questão da privacidade. Se alternar entre janelas ou separadores, as pessoas podem ver brevemente os seus separadores do browser, e-mails, Reddit, chats ou outros conteúdos privados. Pode contornar isto com uma janela separada, mas ainda é algo que tem de considerar.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Porque é que o KoalaSync pode parecer mais limpo",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "O KoalaSync não transmite o seu ecrã. Cada pessoa vê o vídeo localmente no seu próprio browser, e o KoalaSync mantém sincronizadas apenas as ações de reprodução, como reproduzir, pausar e avançar/retroceder.<br><br>Isto significa que todos podem utilizar o modo de ecrã inteiro nos seus próprios dispositivos, utilizar as suas próprias definições de legendas ou áudio, se o leitor for compatível, e manter o chat de voz separado no Discord, Teamspeak ou outra chamada.<br><br>Para as noites de cinema, isto pode parecer muito mais limpo do que uma pessoa a transmitir o seu ecrã. É especialmente útil para configurações baseadas no browser, como Emby, Jellyfin, Plex, ficheiros locais abertos no browser, domínios privados ou outros sites com leitores de vídeo comuns do browser.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Limitações honestas",
|
||||
"ALT_SCREEN_LIMITS_BODY": "O KoalaSync não substitui uma chamada de voz. Se quiser conversar enquanto assiste, utilize ainda o Discord, Teamspeak, Signal, WhatsApp ou qualquer outra aplicação que utilize normalmente.<br><br>Todos também precisam de ter acesso ao vídeo ou à página nos seus próprios dispositivos. O KoalaSync não ignora as subscrições, logins, DRM, restrições regionais ou regras de plataforma.<br><br>Atualmente, funciona apenas em navegadores de desktop, não tem chat ou recurso de voz integrados e a compatibilidade depende sempre do navegador e do leitor de vídeo.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Resumo",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Partilhar o ecrã é a opção mais simples quando se quer algo rápido e não se liga muito à qualidade, privacidade ou controlo individual. O KoalaSync é mais adequado quando todos devem assistir localmente nos seus próprios browsers e a sessão deve parecer mais como assistir em conjunto.",
|
||||
"ALT_TRY_KOALASYNC": "Experimente o KoalaSync"
|
||||
}
|
||||
|
||||
+35
-5
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "ru_RU",
|
||||
"META_TITLE": "KoalaSync | Синхронизация Netflix, YouTube и любого видео с друзьями",
|
||||
"META_DESCRIPTION": "Смотрите Netflix, YouTube, Twitch и любые HTML5-видео в синхронизации с друзьями. Бесплатное расширение браузера с открытым кодом. Регистрация не требуется.",
|
||||
"SCHEMA_APP_DESC": "Смотрите Netflix, YouTube, Twitch и любые HTML5-видео в синхронизации с друзьями. Бесплатное расширение браузера с открытым кодом. Регистрация не требуется.",
|
||||
"OG_TITLE": "KoalaSync | Синхронизация Netflix, Emby, Jellyfin и почти любого видео с друзьями",
|
||||
"OG_DESCRIPTION": "Смотрите Netflix, Emby, Jellyfin, YouTube, Twitch и почти любое HTML5-видео в идеальной синхронизации. Открытое и ориентированное на конфиденциальность расширение для Chrome и Firefox.",
|
||||
"TWITTER_TITLE": "KoalaSync | Синхронизация Netflix, Emby, Jellyfin и почти любого видео с друзьями – Расширение",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Только поддерживаемые сайты",
|
||||
"COMP_FEAT_6_NAME": "Локализация / Перевод",
|
||||
"COMP_FEAT_6_DESC": "Доступные языки интерфейса и поддержка перевода.",
|
||||
"COMP_FEAT_6_KOALA": "13 языков (и расширяется)",
|
||||
"COMP_FEAT_6_KOALA": "15 языков (и расширяется)",
|
||||
"COMP_FEAT_6_TELE": "Только английский",
|
||||
"COMP_FEAT_7_NAME": "Аудиокомпрессор / Выравнивание Громкости",
|
||||
"COMP_FEAT_7_DESC": "Встроенная нормализация звука для баланса тихих диалогов и громких эффектов",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Всё, что нужно знать о синхронном просмотре видео с друзьями.",
|
||||
"FAQ_Q1": "Могу ли я смотреть Netflix, Emby или Jellyfin синхронно с друзьями?",
|
||||
"FAQ_A1": "Да! KoalaSync работает с Netflix, YouTube, Twitch и любым сайтом с HTML5-видеоплеером — включая Emby, Jellyfin и собственные медиасерверы. Установите расширение, создайте комнату, поделитесь ссылкой и смотрите в идеальной синхронизации.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "KoalaSync транслирует видео моим друзьям?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Нет. KoalaSync не транслирует, не размещает, не передает видеоконтент и не обходит доступ к нему. Каждый участник смотрит видео локально в своем браузере и должен иметь собственный доступ к сайту или стриминговому сервису, например собственный аккаунт Netflix. KoalaSync синхронизирует только действия воспроизведения: запуск, паузу и перемотку.",
|
||||
"FAQ_Q2": "KoalaSync — бесплатная альтернатива Teleparty?",
|
||||
"FAQ_A2": "100% бесплатно и с открытым исходным кодом под лицензией MIT. Без премиум-уровней, без заблокированных функций, без рекламы. В отличие от Teleparty, сервер KoalaSync работает только в ОЗУ — без сбора данных и учётных записей.",
|
||||
"FAQ_Q3": "Работает ли KoalaSync с Emby, Jellyfin и собственными медиасерверами?",
|
||||
@@ -150,9 +153,9 @@
|
||||
"MOCK_08": "Покинуть комнату",
|
||||
"MOCK_09": "Выбрать видео",
|
||||
"MOCK_10": "Пульт управления",
|
||||
"MOCK_11": "Воспроизвести",
|
||||
"MOCK_12": "Пауза",
|
||||
"MOCK_13": "Перейти к другим",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Воспроизвести",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Пауза",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
|
||||
"MOCK_14": "Последняя активность",
|
||||
"MOCK_15": "Ожидание 1 участника (KoalaPC)...",
|
||||
"MOCK_16": "Пропустить и играть",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync не связан, не спонсируется и не аффилирован с Netflix, Disney+, Amazon, YouTube, Twitch или любой другой стриминговой платформой. Все товарные знаки являются собственностью их соответствующих владельцев.",
|
||||
"FOOTER_SUPPORT": "Угости кофе на Ko-Fi"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync или Teleparty: какой инструмент выбрать для совместного просмотра?",
|
||||
"ALT_TELEPARTY_META_DESC": "Практическое сравнение KoalaSync и Teleparty: встроенный чат, поддержка мобильных устройств, синхронизация видео в браузере, использование собственных медиасерверов и открытый исходный код.",
|
||||
"ALT_TELEPARTY_INTRO": "И KoalaSync, и Teleparty решают одну и ту же надоедливую проблему: как смотреть что-либо вместе, не прибегая к команде «3, 2, 1, начали!». Просто они используют разные подходы.<br><br>Teleparty — отличный выбор, если вам нужен встроенный чат или возможность просмотра на мобильном устройстве. KoalaSync больше подходит для совместного просмотра через браузер на компьютере: вы общаетесь, например, в Discord, TeamSpeak или другом голосовом чате, а программа просто обеспечивает синхронизацию воспроизведения.<br><br>Главной причиной создания KoalaSync стала гибкость. Инструмент задуман так, чтобы работать практически с любым сайтом, использующим стандартный браузерный видеоплеер, а не только с фиксированным списком платформ. Это касается и решений для самостоятельного хостинга (например, Emby, Jellyfin и Plex), и локальных или частных доменов, и множества обычных веб-страниц с видео.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Когда Teleparty все же может быть полезен",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty может оказаться лучшим выбором, если вам важно наличие чата непосредственно в самом инструменте для просмотра. В KoalaSync встроенного чата пока нет; предполагается, что вы общаетесь на другой платформе — например, в Discord, TeamSpeak, WhatsApp, Signal или по обычной телефонной связи.<br><br>Еще один важный фактор — поддержка мобильных устройств. У Teleparty есть приложение, тогда как KoalaSync пока работает только в браузерах на ПК. Если вы хотите синхронизировать просмотр Netflix на телефоне или планшете, KoalaSync для этого сейчас не подойдет.<br><br>Таким образом, если наличие чата и поддержка мобильных устройств для вас в приоритете, Teleparty может лучше вписаться в ваш сценарий использования.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Когда лучше выбрать KoalaSync",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync — отличный выбор, если вам нужен гибкий инструмент для синхронизации видео в десктопном браузере, а не решение, привязанное к ограниченному списку платформ.<br><br>Сервис работает практически с любым сайтом, использующим стандартный браузерный видеоплеер. Это особенно удобно для Emby, Jellyfin, Plex, частных медиасерверов, локальных доменов или небольших видеосайтов, которые обычно игнорируются классическими инструментами для совместного просмотра.<br><br>Кроме того, это бесплатный проект с открытым исходным кодом; он не требует создания учетной записи и позволяет развернуть собственный сервер-посредник (relay), если вы хотите полного контроля. Я создал его, потому что искал решение, уважающее конфиденциальность и совместимое с моей собственной системой Emby, а не очередную закрытую платформу, работающую лишь с несколькими крупными стриминговыми сервисами.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "Главное отличие",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty — это полноценный сервис для совместного просмотра со встроенным чатом и поддержкой мобильных устройств. KoalaSync — это скорее гибкий инструмент для синхронизации воспроизведения видео в браузере.<br><br>Разница кажется незначительной, но она существенно меняет сценарий использования. Если вам нужно комплексное приложение для совместного просмотра со встроенным чатом, Teleparty — отличный вариант. Если же у вас уже есть голосовая связь и вам просто нужно синхронизировать управление видео в разных браузерах, KoalaSync подойдет лучше и проще.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Честно об ограничениях",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync не творит чудес и не скрывает своих ограничений. На данный момент сервис работает только в десктопных браузерах. В нем нет встроенного чата или мобильного приложения.<br><br>Совместимость также зависит от браузера и видеоплеера. KoalaSync должен работать практически с любым стандартным браузерным плеером, однако сайты могут в любой момент изменить свои плееры, механизмы DRM или ограничения браузера.<br><br>KoalaSync не связан с Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex или любыми другими упомянутыми здесь брендами. Названия используются исключительно для иллюстрации типичных сценариев использования.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Краткий обзор",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Если вам нужен встроенный чат или поддержка мобильных устройств, Teleparty может подойти лучше. Если же вы ищете бесплатное решение с открытым исходным кодом для синхронизации в настольном браузере, работающее практически с любым стандартным видеоплеером — включая собственные медиасерверы, такие как Emby, Jellyfin и Plex, — то, вероятно, лучшим выбором станет KoalaSync.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync против демонстрации экрана: когда лучше использовать синхронизацию видео?",
|
||||
"ALT_SCREEN_META_DESC": "Практическое сравнение KoalaSync и демонстрации экрана для совместного просмотра фильмов на расстоянии: управление воспроизведением, качество видео, конфиденциальность, субтитры и просмотр через браузер.",
|
||||
"ALT_SCREEN_INTRO": "Демонстрация экрана — рабочий вариант, и порой его вполне достаточно. Один человек транслирует свой экран, остальные смотрят — и всё готово.<br><br>Однако при длительных киносеансах или совместном просмотре сериалов это может начать раздражать. Качество видео зависит от скорости исходящего соединения, мощности процессора и браузера того, кто ведет трансляцию, а также от алгоритмов сжатия, используемых в Discord, TeamSpeak, Zoom или других приложениях для связи. Кроме того, полноценно управлять воспроизведением может только один человек.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Когда достаточно демонстрации экрана",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Демонстрация экрана отлично подходит, если нужно максимально быстро всё настроить. Если доступ к видео есть только у одного человека или если всех устраивает, что один человек управляет воспроизведением (запуск, пауза, перемотка), то это может быть самым простым вариантом.<br><br>Этот способ также подходит, когда качество не критично, никому не нужны индивидуальные настройки субтитров или звука, а ведущему удобно демонстрировать окно браузера или экран.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Когда демонстрация экрана может раздражать",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "Главный недостаток в том, что зрители не смотрят видео в собственных плеерах. Один человек транслирует свой экран, а остальные видят только эту трансляцию.<br><br>Это может привести к снижению качества видео, сжатию звука, задержкам воспроизведения и ограничению возможностей управления для остальных. Субтитры и звуковые дорожки также будут такими, какие выбрал ведущий.<br><br>Не стоит забывать и о конфиденциальности. Если вы переключите окно или вкладку, зрители могут на мгновение увидеть ваши вкладки браузера, электронную почту, Reddit, чаты или другую личную информацию. Эту проблему можно решить, используя отдельное окно, но об этом всё равно приходится помнить.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Почему KoalaSync может быть удобнее",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync не транслирует ваш экран. Каждый смотрит видео локально в своем браузере, а KoalaSync лишь синхронизирует действия: запуск, паузу и перемотку.<br><br>Это позволяет каждому развернуть видео на весь экран своего устройства, использовать собственные настройки субтитров или звука (если плеер это поддерживает) и отдельно общаться в голосовом чате через Discord, TeamSpeak или другой сервис.<br><br>Для совместного просмотра фильмов это решение может быть гораздо удобнее, чем трансляция экрана одним человеком. Оно особенно полезно при использовании веб-сервисов (например, Emby, Jellyfin, Plex), локальных файлов, открытых в браузере, закрытых ресурсов или любых других сайтов со стандартными браузерными видеоплеерами.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Честный взгляд на ограничения",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync не заменяет голосовую связь. Если во время просмотра вы хотите общаться, используйте Discord, TeamSpeak, Signal, WhatsApp или любой другой привычный вам сервис.<br><br>Каждому участнику также необходим доступ к видео или веб-странице на собственном устройстве. KoalaSync не обходит требования к подписке, авторизации, DRM-защите, региональные ограничения или правила платформ.<br><br>На данный момент сервис работает только в браузерах на ПК, не имеет встроенного чата или функции голосовой связи, а совместимость зависит от конкретного браузера и видеоплеера.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Краткий обзор",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Демонстрация экрана — самый простой вариант, если нужно быстро всё организовать и вас не особо волнуют качество, конфиденциальность или возможность индивидуального управления. KoalaSync лучше подходит для ситуаций, когда каждый участник смотрит видео локально в своем браузере, а сам сеанс больше напоминает реальный совместный просмотр.",
|
||||
"ALT_TRY_KOALASYNC": "Попробовать KoalaSync"
|
||||
}
|
||||
|
||||
+32
-2
@@ -7,6 +7,7 @@
|
||||
"OG_LOCALE": "tr_TR",
|
||||
"META_TITLE": "KoalaSync | Netflix, YouTube ve Herhangi Bir Videoyu Arkadaşlarınızla Eşitleyin",
|
||||
"META_DESCRIPTION": "Netflix, YouTube, Twitch ve herhangi bir HTML5 videoyu arkadaşlarınızla mükemmel uyum içinde izleyin. Chrome ve Firefox için ücretsiz, açık kaynaklı tarayıcı eklentisi. Kayıt gerekmez.",
|
||||
"SCHEMA_APP_DESC": "Netflix, YouTube, Twitch ve herhangi bir HTML5 videoyu arkadaşlarınızla mükemmel uyum içinde izleyin. Chrome ve Firefox için ücretsiz, açık kaynaklı tarayıcı eklentisi. Kayıt gerekmez.",
|
||||
"OG_TITLE": "KoalaSync | Netflix, Emby, Jellyfin ve Neredeyse Her Videoyu Arkadaşlarınızla Eşitleyin",
|
||||
"OG_DESCRIPTION": "Netflix, Emby, Jellyfin, YouTube, Twitch ve neredeyse tüm HTML5 videolarını mükemmel bir senkronizasyonla izleyin. Chrome ve Firefox için gizlilik öncelikli, açık kaynaklı tarayıcı eklentisi.",
|
||||
"TWITTER_TITLE": "KoalaSync | Netflix, Emby, Jellyfin ve Neredeyse Her Videoyu Arkadaşlarla Eşitle – Tarayıcı Eklentisi",
|
||||
@@ -75,7 +76,7 @@
|
||||
"COMP_FEAT_5_TELE": "Sadece desteklenen siteler",
|
||||
"COMP_FEAT_6_NAME": "Yerelleştirme / Çeviri",
|
||||
"COMP_FEAT_6_DESC": "Mevcut arayüz dilleri ve çeviri desteği.",
|
||||
"COMP_FEAT_6_KOALA": "13 Dil (ve artıyor)",
|
||||
"COMP_FEAT_6_KOALA": "15 Dil (ve artıyor)",
|
||||
"COMP_FEAT_6_TELE": "Sadece İngilizce",
|
||||
"COMP_FEAT_7_NAME": "Ses Kompresörü / Ses Seviyesi Dengeleme",
|
||||
"COMP_FEAT_7_DESC": "Sessiz diyaloglar ve yüksek efektler arasında denge sağlayan yerleşik ses normalizasyonu",
|
||||
@@ -116,6 +117,8 @@
|
||||
"FAQ_SUBTITLE": "Videoları mükemmel bir uyum içinde birlikte izleme hakkında bilmeniz gereken her şey.",
|
||||
"FAQ_Q1": "Arkadaşlarımla Netflix, Emby veya Jellyfin'i eş zamanlı olarak izleyebilir miyim?",
|
||||
"FAQ_A1": "Evet! KoalaSync; Netflix, YouTube, Twitch ve HTML5 video oynatıcısı olan tüm web sitelerinde (Emby, Jellyfin ve kendi barındırdığınız medya sunucuları dahil) çalışır. Eklentiyi kurun, bir oda oluşturun, davet bağlantısını paylaşın; herkes mükemmel bir uyum içinde izlesin.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "KoalaSync videoyu arkadaşlarıma aktarır mı?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Hayır. KoalaSync video içeriğini yayınlamaz, barındırmaz, paylaşmaz veya erişim kısıtlamalarını aşmaz. Herkes videoyu kendi tarayıcısında yerel olarak izler ve ilgili web sitesine ya da yayın hizmetine kendi erişimine, örneğin kendi Netflix hesabına, ihtiyaç duyar. KoalaSync yalnızca oynat, duraklat ve ileri geri sarma gibi oynatma eylemlerini senkronize eder.",
|
||||
"FAQ_Q2": "KoalaSync, Teleparty'ye ücretsiz bir alternatif mi?",
|
||||
"FAQ_A2": "MIT Lisansı kapsamında %100 ücretsiz ve açık kaynaklıdır. Premium katmanlar yok, kilitli özellikler yok, reklam yok — her şey dahil. Teleparty'nin aksine, KoalaSync'in aktarım sunucusu yalnızca RAM üzerindedir; sıfır veri toplama ve kullanıcı hesabı gereksinimi vardır.",
|
||||
"FAQ_Q3": "KoalaSync; Emby, Jellyfin ve kendi barındırdığınız medya sunucularıyla çalışır mı?",
|
||||
@@ -175,5 +178,32 @@
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync; Netflix, Disney+, Amazon, YouTube, Twitch veya başka herhangi bir yayın platformuna bağlı değildir, bunlar tarafından desteklenmemektedir ve bunlarla ilişkili değildir. Tüm ticari markalar ilgili sahiplerinin mülkiyetindedir.",
|
||||
"FOOTER_SUPPORT": "Ko-Fi'de bana kahve ısmarla"
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync ve Teleparty: Hangi ortak izleme aracını kullanmalısınız?",
|
||||
"ALT_TELEPARTY_META_DESC": "Ortak izleme etkinlikleri (watch party) için KoalaSync ve Teleparty'nin; yerleşik sohbet, mobil desteği, tarayıcı tabanlı video senkronizasyonu, kendi kendine barındırılan (self-hosted) medya sunucuları ve açık kaynak kullanımı açısından pratik bir karşılaştırması.",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync ve Teleparty, aynı can sıkıcı sorunu çözmeye yardımcı oluyor: O meşhur \"3, 2, 1, başlat\" ritüeliyle uğraşmadan birlikte bir şeyler izlemek. Ancak bunu yaparken farklı yaklaşımlar benimsiyorlar.<br><br>Yerleşik bir sohbet özelliği istiyorsanız veya mobilde izlemek istiyorsanız Teleparty en mantıklı seçenek olabilir. KoalaSync ise daha çok; Discord, Teamspeak veya başka bir sesli görüşme üzerinden zaten konuştuğunuz ve sadece oynatmanın senkronize kalmasını istediğiniz, masaüstü tarayıcı tabanlı izleme etkinlikleri için uygundur.<br><br>KoalaSync'i geliştirmemin temel nedeni esneklikti. Sadece belirli platformlar listesiyle sınırlı kalmayıp, standart bir tarayıcı video oynatıcısı kullanan hemen hemen her web sitesiyle çalışacak şekilde tasarlandı. Buna Emby, Jellyfin ve Plex gibi kendi kendine barındırılan sistemler, yerel/özel alan adları ve tarayıcıdaki pek çok sıradan video sayfası da dahildir.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Teleparty'nin hala mantıklı olabileceği durumlar",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Sohbetin doğrudan izleme aracının bir parçası olmasını istiyorsanız Teleparty hala daha iyi bir seçenek olabilir. KoalaSync'te şu an için yerleşik bir sohbet özelliği bulunmuyor; uygulama, Discord, Teamspeak, WhatsApp, Signal veya normal bir telefon görüşmesi gibi başka bir kanal üzerinden zaten konuştuğunuz varsayımıyla çalışıyor.<br><br>Bir diğer önemli neden ise mobil desteği. Teleparty'nin bir uygulaması varken, KoalaSync şu anda yalnızca masaüstü tarayıcılarda çalışıyor. Eğer telefon veya tablette Netflix senkronizasyonu yapmak istiyorsanız, KoalaSync şu an için bunun doğru aracı değildir.<br><br>Özetle; sohbet ve mobil desteği sizin için en önemli unsurlarsa, Teleparty kurulumunuza daha uygun olabilir.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "KoalaSync ne zaman daha iyi bir seçenek olabilir?",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "Sınırlı sayıda platformla kısıtlı bir çözüm yerine esnek bir masaüstü tarayıcı senkronizasyon aracı istiyorsanız KoalaSync daha iyi bir tercihtir.<br><br>Standart tarayıcı video oynatıcısını kullanan hemen hemen her web sitesiyle çalışır. Bu özellik; özellikle Emby, Jellyfin, Plex, özel medya sunucuları, yerel alan adları veya klasik 'birlikte izleme' (watch party) araçlarının genellikle göz ardı ettiği daha küçük video siteleri için oldukça kullanışlıdır.<br><br>Ayrıca ücretsiz ve açık kaynaklıdır; hesap gerektirmez ve kendi aktarım sunucunuzu (relay) çalıştırmak isterseniz kendi sunucunuzda barındırılabilir. Bunu geliştirme sebebim, sadece birkaç büyük yayın platformuna özel kapalı bir sistem yerine, kendi Emby kurulumumla uyumlu ve gizlilik dostu bir çözüm istememdi.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "Temel fark",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty daha çok sohbet ve mobil desteği sunan bir 'birlikte izleme' ürünüdür. KoalaSync ise tarayıcı videoları için daha esnek bir senkronizasyon katmanı işlevi görür.<br><br>Bu küçük bir fark gibi görünebilir ancak kullanım şeklini büyük ölçüde değiştirir. Eğer içinde sohbet özelliği de bulunan kapsamlı bir 'birlikte izleme' uygulamasına ihtiyacınız varsa Teleparty mantıklı bir seçimdir. Ancak sesli sohbet imkanınız zaten varsa ve sadece video kontrollerinin tarayıcılar arasında senkronize kalmasını istiyorsanız, KoalaSync daha basit ve uygun bir çözümdür.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Dürüstçe belirtilen sınırlamalar",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync sihirli bir çözüm değildir ve sınırlarını gizlemeye çalışmaz. Şu anda yalnızca masaüstü tarayıcılarda çalışır; yerleşik sohbet özelliği veya mobil uygulaması yoktur.<br><br>Uyumluluk durumu tarayıcıya ve video oynatıcısına da bağlıdır. KoalaSync'in standart tarayıcı video oynatıcılarının neredeyse tamamıyla çalışması beklenir; ancak web siteleri oynatıcılarını, DRM (Dijital Hak Yönetimi) işleyişlerini veya tarayıcı kısıtlamalarını diledikleri zaman değiştirebilirler.<br><br>KoalaSync; Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex veya burada adı geçen diğer hiçbir markayla bağlantılı değildir. Bu isimler yalnızca tipik kullanım senaryolarını açıklamak amacıyla kullanılmıştır.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Özet",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Yerleşik sohbet veya mobil desteği istiyorsanız, Teleparty daha uygun bir seçenek olabilir. Emby, Jellyfin ve Plex gibi kendi barındırdığınız medya sunucuları da dahil olmak üzere hemen hemen her standart video oynatıcıyla çalışan, ücretsiz ve açık kaynaklı bir masaüstü tarayıcı senkronizasyon aracı arıyorsanız, KoalaSync muhtemelen daha iyi bir tercihtir.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync ve ekran paylaşımı karşılaştırması: Video senkronizasyonu ne zaman daha iyidir?",
|
||||
"ALT_SCREEN_META_DESC": "Uzaktan film geceleri, oynatma kontrolü, video kalitesi, gizlilik, altyazılar ve tarayıcı tabanlı izleme konularında KoalaSync ile ekran paylaşımının pratik bir karşılaştırması.",
|
||||
"ALT_SCREEN_INTRO": "Ekran paylaşımı işe yarar ve dürüst olmak gerekirse bazen yeterlidir. Bir kişi ekranını paylaşır, diğerleri izler ve işlem tamamlanır.<br><br>Ancak daha uzun film gecelerinde veya dizi bölümlerini birlikte izlerken bu durum can sıkıcı bir hal alabilir. Video kalitesi; paylaşımı yapan kişinin yükleme hızına, işlemci (CPU) gücüne, tarayıcısına ve Discord, Teamspeak, Zoom ya da diğer görüşme uygulamalarının uyguladığı sıkıştırma işlemine bağlıdır. Ayrıca, oynatmayı gerçekten kontrol eden tek bir kişi vardır.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Ekran paylaşımı ne zaman yeterlidir?",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Mümkün olan en hızlı kurulumu istediğinizde ekran paylaşımı gayet uygundur. Eğer videoya yalnızca bir kişi erişebiliyorsa ya da herkes oynatma, duraklatma ve ileri/geri sarma işlemlerinin tek bir kişi tarafından kontrol edilmesini sorun etmiyorsa, bu en basit seçenek olabilir.<br><br>Ayrıca görüntü kalitesinin çok önemli olmadığı, kimsenin kendine özel altyazı veya ses ayarlarına ihtiyaç duymadığı ve paylaşımı yapan kişinin o tarayıcı penceresini veya ekranını göstermekte bir sakınca görmediği durumlarda da mantıklı bir yöntemdir.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Ekran paylaşımının can sıkıcı olabildiği durumlar",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "En büyük dezavantaj, herkesin videoyu kendi oynatıcısında izlemiyor olmasıdır. Bir kişi ekranını yayınlar ve diğer herkes sadece bu yayını görür.<br><br>Bu durum; daha düşük görüntü kalitesi, sıkıştırılmış ses, gecikmeli oynatma ve diğerleri için daha az kontrol anlamına gelebilir. Altyazı ve ses seçenekleri de paylaşımı yapan kişinin seçtikleriyle sınırlı kalır.<br><br>Bir de gizlilik boyutu var. Pencereler veya sekmeler arasında geçiş yaparsanız, insanlar tarayıcı sekmelerinizi, e-postalarınızı, Reddit'i, sohbetlerinizi veya diğer özel içerikleri anlık olarak görebilir. Ayrı bir pencere kullanarak bunun önüne geçebilirsiniz ancak yine de dikkat etmeniz gereken bir husustur.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "KoalaSync neden daha düzenli bir deneyim sunar?",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync ekranınızı yayınlamaz. Herkes videoyu kendi tarayıcısında yerel olarak izler; KoalaSync ise yalnızca oynatma, duraklatma ve ileri/geri sarma gibi eylemleri senkronize halde tutar.<br><br>Bu sayede herkes kendi cihazında tam ekran modunu kullanabilir, oynatıcı destekliyorsa kendi altyazı veya ses ayarlarını seçebilir ve sesli sohbeti Discord, Teamspeak veya başka bir görüşme üzerinden ayrı olarak sürdürebilir.<br><br>Film geceleri için bu yöntem, bir kişinin ekranını yayınlamasına kıyasla çok daha düzenli ve temiz bir deneyim sunabilir. Özellikle Emby, Jellyfin, Plex gibi tarayıcı tabanlı sistemlerde, tarayıcıda açılan yerel dosyalarda, özel alan adlarında veya standart tarayıcı video oynatıcılarını kullanan diğer web sitelerinde oldukça kullanışlıdır.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Dürüstçe belirtilen sınırlamalar",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync, sesli görüşmenin yerini tutmaz. İzleme sırasında konuşmak isterseniz, yine Discord, Teamspeak, Signal, WhatsApp veya normalde kullandığınız herhangi bir aracı kullanmaya devam edebilirsiniz.<br><br>Ayrıca herkesin ilgili videoya veya sayfaya kendi cihazından erişebilmesi gerekir. KoalaSync; abonelikleri, oturum açma işlemlerini, DRM korumalarını, bölgesel kısıtlamaları veya platform kurallarını devre dışı bırakmaz.<br><br>Şu anda yalnızca masaüstü tarayıcılarında çalışır; yerleşik bir sohbet veya sesli görüşme özelliği yoktur ve uyumluluk her zaman kullanılan tarayıcıya ve video oynatıcıya bağlıdır.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Özet",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Hızlı bir çözüm aradığınızda ve kalite, gizlilik ya da bireysel kontrol konularına çok önem vermediğinizde, ekran paylaşımı en basit seçenektir. Herkesin içeriği kendi tarayıcısında yerel olarak izlemesi gereken ve oturumun gerçekten birlikte izliyormuş hissi verdiği durumlarda ise KoalaSync daha uygun bir tercihtir.",
|
||||
"ALT_TRY_KOALASYNC": "KoalaSync'i Deneyin"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"LANG_CODE": "uk",
|
||||
"HTML_CLASS": "lang-uk",
|
||||
"CANONICAL_PATH": "uk/",
|
||||
"LANG_TOGGLE_URL": "../",
|
||||
"LANG_TOGGLE_TEXT": "EN",
|
||||
"OG_LOCALE": "uk_UA",
|
||||
"META_TITLE": "KoalaSync | Синхронізуйте Netflix, YouTube і будь-яке відео з друзями",
|
||||
"META_DESCRIPTION": "Дивіться Netflix, YouTube, Twitch і будь-яке HTML5 відео в ідеальній синхронізації з друзями. Безкоштовне розширення для браузера з відкритим кодом для Chrome і Firefox. Реєстрація не потрібна.",
|
||||
"SCHEMA_APP_DESC": "Дивіться Netflix, YouTube, Twitch і будь-яке HTML5 відео в ідеальній синхронізації з друзями. Безкоштовне розширення для браузера з відкритим кодом для Chrome і Firefox. Реєстрація не потрібна.",
|
||||
"OG_TITLE": "KoalaSync | Синхронізація Netflix, Emby, Jellyfin і майже будь-якого відео з друзями",
|
||||
"OG_DESCRIPTION": "Дивіться Netflix, Emby, Jellyfin, YouTube, Twitch і майже будь-яке відео HTML5 в ідеальній синхронізації. Розширення веб-переглядача з відкритим вихідним кодом для Chrome і Firefox, перш за все конфіденційність.",
|
||||
"TWITTER_TITLE": "KoalaSync | Синхронізація Netflix, Emby, Jellyfin і майже будь-якого відео з друзями – розширення браузера",
|
||||
"TWITTER_DESCRIPTION": "Дивіться Netflix, Emby, Jellyfin, YouTube, Twitch і майже будь-яке відео HTML5 в ідеальній синхронізації з друзями. Розширення веб-переглядача з відкритим вихідним кодом для Chrome і Firefox, перш за все конфіденційність.",
|
||||
"NAV_FEATURES": "особливості",
|
||||
"NAV_HOW_IT_WORKS": "Як це працює",
|
||||
"HERO_TITLE": "Дивіться разом.<span class='hero-line2'>Ідеальна синхронізація.</span>",
|
||||
"HERO_SUBTITLE": "Ваш віддалений кіновечір без лагів. Ні реєстрації, ні збору даних. Просто поділіться посиланням і дивіться разом.",
|
||||
"HERO_MASCOT_ALT": "Мила коала стоїть і дивиться вниз на кнопки завантаження",
|
||||
"ADD_TO_CHROME": "Додати до Chrome",
|
||||
"ADD_TO_FIREFOX": "Додати до Firefox",
|
||||
"COMPAT_HEADING": "Працює на ваших улюблених платформах",
|
||||
"COMPAT_MORE": "і багато іншого",
|
||||
"COMPAT_TOOLTIP": "Працює майже на будь-якому сайті з елементом відео",
|
||||
"USE_CASES_TITLE": "Ідеально підходить для будь-якого випадку",
|
||||
"USE_CASES_SUBTITLE": "Незалежно від того, поблизу чи далеко, KoalaSync об’єднує людей навколо їхніх улюблених відео.",
|
||||
"USE_CASE_1_ALT": "Дві милі коали сидять разом і діляться відерцем попкорну",
|
||||
"USE_CASE_1_TITLE": "Вечір кіно з друзями",
|
||||
"USE_CASE_1_DESC": "Синхронізуйте свої фільми в режимі реального часу та спілкуйтеся через Discord, Zoom або улюблену програму для голосових викликів. Таке відчуття, ніби сидиш в одній кімнаті.",
|
||||
"USE_CASE_2_ALT": "Професор-коала презентує на екрані двом віддаленим студентам-коалам",
|
||||
"USE_CASE_2_TITLE": "Дистанційне навчання",
|
||||
"USE_CASE_2_DESC": "Аналізуйте онлайн-уроки, лекції чи навчальні потоки для розробників разом з однокласниками чи колегами. Зробіть паузу та обговоріть складні кроки в ідеальній гармонії.",
|
||||
"USE_CASE_3_ALT": "Мила пара коал дивиться разом віддалено; один сидить за комп’ютером, а інший у ліжку з ноутбуком, а між ними літають серця",
|
||||
"USE_CASE_3_TITLE": "Відносини на відстані",
|
||||
"USE_CASE_3_DESC": "Подолайте відстань і насолоджуйтесь побаченнями. Переживайте кожен поворот сюжету, смійтеся над тими самими жартами та діліться емоційними моментами в ту саму мілісекунду.",
|
||||
"WHY_TITLE": "Чому KoalaSync?",
|
||||
"WHY_SUBTITLE": "Створено для надійної синхронізації, конфіденційності та простого налаштування.",
|
||||
"FEATURE_1_TITLE": "Повний контроль/миттєва синхронізація",
|
||||
"FEATURE_1_DESC": "Один робить паузу, всі зупиняються. Один шукає, всі йдуть. Наш двофазний протокол синхронізації координує відтворення в режимі реального часу для всіх учасників.",
|
||||
"FEATURE_2_TITLE": "Нескінченне споглядання",
|
||||
"FEATURE_2_DESC": "Автоматичне відтворення синхронізовано. KoalaSync автоматично визначає переходи епізодів і утримує відтворення, доки всі однорангові пристрої успішно не завантажать наступне відео.",
|
||||
"FEATURE_3_TITLE": "Нуль облікових записів / чиста конфіденційність",
|
||||
"FEATURE_3_DESC": "Ні реєстрації, ні відстеження, ні слідів постійності. Сервер повністю працює в ефемерному RAM і очищає вашу кімнату, коли ви залишаєте її.",
|
||||
"FEATURE_4_TITLE": "Універсальна підтримка HTML5",
|
||||
"FEATURE_4_DESC": "Працює на YouTube, Twitch, Netflix, Jellyfin, Emby і майже будь-якій стандартній веб-сторінці, що містить відеоелемент HTML5. Також сумісний із користувальницькими налаштуваннями, розміщеними на власному хості. На наші сервери ніколи не надсилаються відеодані — лише метадані синхронізації.",
|
||||
"FEATURE_5_TITLE": "Саморозміщення та готовність до Docker",
|
||||
"FEATURE_5_DESC": "Наші офіційні сервери безкоштовні та швидкі, але ви можете отримати повне право власності, якщо хочете. Запустіть власний приватний сервер ретрансляції за лічені секунди через Docker.",
|
||||
"FEATURE_6_TITLE": "Миттєві запрошення / Приєднання в 1 клік",
|
||||
"FEATURE_6_DESC": "Немає IP-адрес або паролів для обміну. Поділіться створеним посиланням із запрошенням із друзями, щоб вони могли автоматично приєднатися до вашої кімнати одним клацанням миші.",
|
||||
"FEATURE_7_TITLE": "Вирівнювання гучності / Аудіокомпресор",
|
||||
"FEATURE_7_DESC": "Більше не потрібно натискати на ручку гучності. Вбудований аудіокомпресор посилює тихий діалог і приборкує гучні вибухи. Працює на Netflix, Prime Video, Disney+, YouTube і будь-якому відео HTML5. Повністю локально — аудіодані не залишають ваш комп’ютер.",
|
||||
"FEATURE_8_TITLE": "Відкритий код (ліцензія MIT)",
|
||||
"FEATURE_8_DESC": "Вся кодова база доступна для публічного аудиту на GitHub. Жодних чорних скриньок, жодних власних блобів — 100% прозорість від сервера ретрансляції до розширення браузера.",
|
||||
"COMP_TITLE": "KoalaSync проти Teleparty",
|
||||
"COMP_SUBTITLE": "Дізнайтеся, чому відкритий вихідний код, без реклами та конфіденційність – це найкращий спосіб дивитися разом.",
|
||||
"COMP_COL_FEATURE": "Особливість",
|
||||
"COMP_FEAT_1_NAME": "Вартість / Paywalls",
|
||||
"COMP_FEAT_1_DESC": "Преміум-підписки, приховані комісії або заблоковані функції.",
|
||||
"COMP_FEAT_1_KOALA": "100% безкоштовно",
|
||||
"COMP_FEAT_1_TELE": "Платні рівні / преміум-блокування",
|
||||
"COMP_FEAT_2_NAME": "Ліцензія/Код підлягає аудиту",
|
||||
"COMP_FEAT_2_DESC": "Чи є вихідний код відкритим і доступним для вільного аудиту.",
|
||||
"COMP_FEAT_2_KOALA": "Відкритий код (MIT)",
|
||||
"COMP_FEAT_2_TELE": "Власний",
|
||||
"COMP_FEAT_3_NAME": "Самостійне розміщення",
|
||||
"COMP_FEAT_3_DESC": "Можливість розгорнути власний приватний сервер ретрансляції.",
|
||||
"COMP_FEAT_3_KOALA": "Так (готовий до Docker)",
|
||||
"COMP_FEAT_3_TELE": "немає",
|
||||
"COMP_FEAT_4_NAME": "Конфіденційність і відстеження",
|
||||
"COMP_FEAT_4_DESC": "Вимоги до реєстрації, відстеження файлів cookie та аналітика.",
|
||||
"COMP_FEAT_4_KOALA": "Нульова стійкість (лише для RAM)",
|
||||
"COMP_FEAT_4_TELE": "Google Analytics і файли cookie",
|
||||
"COMP_FEAT_5_NAME": "Сумісність сайту",
|
||||
"COMP_FEAT_5_DESC": "Підтримувані веб-сайти та сумісність із плеєрами.",
|
||||
"COMP_FEAT_5_KOALA": "Майже будь-яке відео HTML5",
|
||||
"COMP_FEAT_5_TELE": "Лише підтримувані сайти",
|
||||
"COMP_FEAT_6_NAME": "Локалізація / Переклад",
|
||||
"COMP_FEAT_6_DESC": "Доступні мови інтерфейсу та підтримка перекладу.",
|
||||
"COMP_FEAT_6_KOALA": "15 мов (і зростає)",
|
||||
"COMP_FEAT_6_TELE": "лише англійською мовою",
|
||||
"COMP_FEAT_7_NAME": "Аудіокомпресор / Вирівнювання гучності",
|
||||
"COMP_FEAT_7_DESC": "Вбудована нормалізація аудіо для балансування тихих діалогів і гучних ефектів",
|
||||
"COMP_FEAT_7_KOALA": "Так (4 попередні налаштування, сухий/мокрий контроль)",
|
||||
"COMP_FEAT_7_TELE": "немає",
|
||||
"COMP_FOOTNOTE_1": "Стан для порівняння: травень 2026 року.",
|
||||
"COMP_FOOTNOTE_2": "Офіційні ціни на Teleparty Premium і відомості про підтримувані мережі:",
|
||||
"COMP_FOOTNOTE_3": "Політика конфіденційності Teleparty і збір даних відстеження:",
|
||||
"COMP_FOOTNOTE_4": "Працює на веб-сайтах, які дозволяють ін’єкції сценаріїв у стандартні відеотеги HTML5. Веб-сайти з дуже суворими політиками безпеки вмісту (CSP), захистом від копіювання DRM або сильно затуманеними оболонками програвачів (як-от складні тіньові DOM) можуть обмежувати автоматизований контроль або впровадження.",
|
||||
"STEPS_TITLE": "Початок роботи",
|
||||
"STEP_1_TITLE": "Встановити розширення",
|
||||
"STEP_1_DESC": "Додайте KoalaSync до свого браузера з веб-магазину Chrome, доповнень Firefox або завантажте найновіший розробник ZIP із GitHub.",
|
||||
"STEP_1_ILLUS_DESC": "Синхронізатор відео насамперед із конфіденційністю",
|
||||
"STEP_1_ILLUS_DL_CHROME": "Завантажити для Chrome",
|
||||
"STEP_1_ILLUS_DL_FIREFOX": "Завантажити для Firefox",
|
||||
"STEP_1_ILLUS_ACTIVE": "Розширення активне",
|
||||
"STEP_1_ILLUS_READY": "Готові дивитися разом!",
|
||||
"STEP_2_TITLE": "Створіть кімнату",
|
||||
"STEP_2_DESC": "Відкрийте спливаюче вікно розширення та натисніть «+ Створити нову кімнату». KoalaSync автоматично генерує безпечний Room ID і пароль, приєднується до нього та копіює посилання для запрошення в буфер обміну.",
|
||||
"STEP_2_ILLUS_ROOM": "Кімната",
|
||||
"STEP_2_ILLUS_SYNC": "Синхронізувати",
|
||||
"STEP_2_ILLUS_SETTINGS": "Налаштування",
|
||||
"STEP_2_ILLUS_CREATE": "+ Створити нову кімнату",
|
||||
"STEP_2_ILLUS_MANUAL": "Ручне підключення / Додатково",
|
||||
"STEP_2_ILLUS_COPIED": "Посилання на запрошення скопійовано!",
|
||||
"STEP_3_TITLE": "Поділитися та синхронізувати",
|
||||
"STEP_3_DESC": "Надішліть запрошення своїм друзям. Коли вони приєднаються, виберіть вкладку відео та насолоджуйтеся синхронізованим відтворенням.",
|
||||
"STEP_3_ILLUS_IN_SYNC": "СИНХРОНІЗОВАНО",
|
||||
"SELF_TITLE": "Для самостійних господарів",
|
||||
"SELF_SUBTITLE": "Не довіряєте нашому серверу? Підтримуйте повний суверенітет даних. Розгорніть власний приватний сервер ретрансляції за лічені хвилини.",
|
||||
"SELF_MASCOT_ALT": "Мила коала сидить за ноутбуком і розгортає контейнер Docker для самостійного розміщення",
|
||||
"SELF_COPY_CODE": "Копіювати код",
|
||||
"SELF_GITHUB_PACKAGES": "Переглянути всі теги зображень на пакетах GitHub",
|
||||
"BOTTOM_TITLE": "Не переконали? Подивіться самі.",
|
||||
"BOTTOM_SUBTITLE": "KoalaSync є на 100% відкритим кодом, без реклами та відстеження. Перевірте нашу кодову базу на GitHub або безпосередньо встановіть розширення для браузера.",
|
||||
"BOTTOM_MASCOT_ALT": "Мила коала тримає сторінку репозиторію GitHub поруч із талісманом GitHub Octocat",
|
||||
"FAQ_TITLE": "Часті запитання",
|
||||
"FAQ_SUBTITLE": "Все, що вам потрібно знати про спільний перегляд відео в ідеальній синхронізації.",
|
||||
"FAQ_Q1": "Чи можу я дивитися Netflix, Emby або Jellyfin разом із друзями?",
|
||||
"FAQ_A1": "так! KoalaSync працює на Netflix, YouTube, Twitch і будь-якому веб-сайті з відеопрогравачем HTML5, включаючи Emby, Jellyfin і медіасервери, розміщені на власному хості. Установіть розширення, створіть кімнату, поділіться посиланням для запрошення, і всі дивитимуться в повній синхронізації.",
|
||||
"FAQ_Q_VIDEO_ACCESS": "Чи транслює KoalaSync відео моїм друзям?",
|
||||
"FAQ_A_VIDEO_ACCESS": "Ні. KoalaSync не транслює, не розміщує, не поширює відеоконтент і не обходить доступ до нього. Кожен учасник дивиться відео локально у власному браузері та повинен мати власний доступ до сайту або стримінгового сервісу, наприклад власний акаунт Netflix. KoalaSync синхронізує лише дії відтворення, як-от запуск, паузу та перемотування.",
|
||||
"FAQ_Q2": "Чи є KoalaSync безкоштовною альтернативою Teleparty?",
|
||||
"FAQ_A2": "100% безкоштовне та з відкритим кодом за ліцензією MIT. Без преміум-рівнів, без заблокованих функцій, без реклами — все включено. На відміну від Teleparty, ретрансляційний сервер KoalaSync призначений лише для RAM і не вимагає збору даних або облікових записів користувачів.",
|
||||
"FAQ_Q3": "Чи працює KoalaSync із Emby, Jellyfin і власними медіа-серверами?",
|
||||
"FAQ_A3": "так KoalaSync виявляє будь-який стандартний відеоелемент HTML5, що означає, що він ідеально працює з Emby, Jellyfin, Plex та будь-якими спеціальними налаштуваннями потокового передавання. Ви також можете самостійно розмістити сервер ретрансляції KoalaSync через Docker для повного суверенітету даних.",
|
||||
"FAQ_Q4": "Які браузери та платформи підтримують KoalaSync?",
|
||||
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave та всі браузери на основі Chromium. Встановіть його безпосередньо з веб-магазину Chrome або додатків Firefox — ручне налаштування не потрібне.",
|
||||
"FAQ_Q5": "Чи всім моїм друзям потрібно встановити розширення?",
|
||||
"FAQ_A5": "Так, кожному учаснику потрібно встановити KoalaSync — але немає реєстрації чи створення облікового запису. Просто клацніть посилання запрошення, установіть розширення, якщо ви ще цього не зробили, і ви автоматично приєднаєтесь до кімнати за лічені секунди.",
|
||||
"FAQ_Q6": "Чи мої дані в безпеці? Що робити, якщо я не довіряю офіційному серверу?",
|
||||
"FAQ_A6": "Повністю безпечний. Сервер ретрансляції KoalaSync призначений лише для RAM — усі дані сеансу назавжди видаляються, коли ви залишаєте кімнату. Ні бази даних, ні реєстрації, ні аналітики, ні облікових записів. А якщо ви віддаєте перевагу повному контролю: ви можете самостійно розмістити свій приватний сервер ретрансляції через Docker за лічені хвилини. Уся кодова база є відкритим кодом і доступна д��я перевірки на GitHub.",
|
||||
"FAQ_Q7": "Чи можу я виправити тихий діалог і гучні вибухи під час перегляду відео в браузері?",
|
||||
"FAQ_A7": "Так. KoalaSync 2.2.0 містить вбудований аудіокомпресор, який зменшує різницю між тихими та гучними частинами відео. Він працює локально через Web Audio API - аудіодані ніколи не залишають ваш комп'ютер. Відкрийте спливне вікно, створіть кімнату, перейдіть до «Налаштування» → «Обробка аудіо», увімкніть компресор і виберіть пресет. Працює з Netflix, Prime Video, Disney+, YouTube і будь-яким HTML5-відеосайтом.",
|
||||
"HOWTO_TITLE": "Як синхронізувати відео з друзями за допомогою KoalaSync",
|
||||
"HOWTO_DESC": "Дивіться будь-яке відео в ідеальній синхронізації з друзями на YouTube, Netflix, Twitch тощо. Без реєстрації, без відстеження.",
|
||||
"HOWTO_STEP_1_NAME": "Встановіть розширення для браузера",
|
||||
"HOWTO_STEP_1_TEXT": "Додайте KoalaSync до свого браузера з Chrome Web Store або Firefox Add-ons. Він працює на будь-якому веб-сайті з елементом відео.",
|
||||
"HOWTO_STEP_2_NAME": "Створіть кімнату",
|
||||
"HOWTO_STEP_2_TEXT": "Відкрийте спливаюче вікно розширення та натисніть «Створити нову кімнату». Посилання для запрошення автоматично копіюється в буфер обміну.",
|
||||
"HOWTO_STEP_3_NAME": "Поділіться та синхронізуйте",
|
||||
"HOWTO_STEP_3_TEXT": "Надішліть запрошення своїм друзям. Виберіть вкладку відео та насолоджуйтеся синхронізованим відтворенням у реальному часі.",
|
||||
"FOOTER_MIT": "Відкритий код за ліцензією MIT.",
|
||||
"FOOTER_RAM": "На наших серверах дані не зберігаються. Чисте реле на основі RAM.",
|
||||
"FOOTER_LEGAL": "Юридична інформація",
|
||||
"FOOTER_PRIVACY": "Політика конфіденційності",
|
||||
"MOCK_01": "Активна кімната",
|
||||
"MOCK_02": "Офіційний сервер",
|
||||
"MOCK_03": "Посилання на запрошення",
|
||||
"MOCK_04": "Однолітки в кімнаті",
|
||||
"MOCK_05": "ВИ",
|
||||
"MOCK_06": "Граючи",
|
||||
"MOCK_07": "Одноліток",
|
||||
"MOCK_08": "Вийти з кімнати",
|
||||
"MOCK_09": "Виберіть Відео",
|
||||
"MOCK_10": "Пульт дистанційного керування",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Грати",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Пауза",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> СИНХ",
|
||||
"MOCK_14": "Перейти до інших",
|
||||
"MOCK_15": "Статус останньої дії",
|
||||
"MOCK_16": "Очікування 1 партнера (KoalaPC)...",
|
||||
"MOCK_17": "Все одно пропустіть і грайте",
|
||||
"MOCK_18": "Ваше ім'я користувача",
|
||||
"MOCK_19": "Приховати безладні вкладки",
|
||||
"MOCK_20": "Автоматична синхронізація наступного епізоду",
|
||||
"MOCK_21": "Автоматичне копіювання посилання для запрошення",
|
||||
"MOCK_22": "Сповіщення браузера",
|
||||
"MOCK_23": "Усунення несправностей",
|
||||
"MOCK_24": "Регенерувати Peer ID",
|
||||
"MOCK_25": "Використовуйте це, якщо ви бачите помилку \"Дублікати ідентифікатора\".",
|
||||
"MOCK_26": "Статус підключення",
|
||||
"MOCK_27": "Підключено",
|
||||
"MOCK_28": "Копіювати журнали",
|
||||
"MOCK_29": "Інформація про налагодження відео",
|
||||
"MOCK_30": "Повна історія дій",
|
||||
"MOCK_31": "Журнали (останні 50)",
|
||||
"MOCK_32": "ЯСНО",
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync не пов’язаний із Netflix, Disney+, Amazon, YouTube, Twitch або будь-якою іншою потоковою платформою, не схвалений і не пов’язаний з ними. Усі торгові марки є власністю відповідних власників.",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync проти Teleparty: який інструмент для спільного перегляду відео слід використовувати?",
|
||||
"ALT_TELEPARTY_META_DESC": "Практичне порівняння KoalaSync та Teleparty для спільного перегляду відео, вбудованого чату, підтримки мобільних пристроїв, синхронізації відео в браузері, власних медіасерверів та використання програмного забезпечення з відкритим вихідним кодом.",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync та Teleparty допомагають вирішити ту саму надокучливу проблему: перегляд чогось разом без виконання всієї процедури «3, 2, 1, грати». Вони просто використовують інший підхід.<br><br>Teleparty має найбільший сенс, якщо вам потрібен вбудований чат або якщо ви хочете дивитися на мобільному пристрої. KoalaSync більше підходить для вечірок у браузері на комп'ютері, де ви вже спілкуєтеся через Discord, Teamspeak або інший голосовий дзвінок і просто хочете, щоб відтворення залишалося синхронізованим.<br><br>Головною причиною, чому я створив KoalaSync, була гнучкість. Він призначений для роботи майже з будь-яким веб-сайтом, який використовує звичайний відеоплеєр браузера, а не лише з фіксованим списком платформ. Це включає самостійно розміщені налаштування, такі як Emby, Jellyfin та Plex, локальні/приватні домени та багато звичайних відеосторінок у браузері.",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Коли Teleparty все ще може мати сенс",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "Teleparty все ще може бути кращим вибором, якщо ви хочете, щоб чат був частиною самого інструменту спільного перегляду. KoalaSync наразі не має вбудованого чату. Він передбачає, що ви вже розмовляєте деінде, наприклад, через Discord, Teamspeak, WhatsApp, Signal або звичайний дзвінок.<br><br>Мобільні пристрої – ще одна важлива причина. Teleparty має додаток, тоді як KoalaSync наразі працює лише у браузерах на комп'ютері. Якщо ви хочете синхронізувати Netflix на телефоні або планшеті, KoalaSync зараз не є для цього правильним інструментом.<br><br>Тож, якщо чат і мобільна підтримка – це те, що вас найбільше хвилює, Teleparty може просто краще підійти для вашої конфігурації.",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "Коли KoalaSync може підійти краще",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "KoalaSync краще підходить, якщо вам потрібен гнучкий інструмент синхронізації браузера на робочому столі, а не щось, прив'язане до короткого списку платформ.<br><br>Він працює майже з будь-яким веб-сайтом, який використовує звичайний відеоплеєр браузера. Це особливо корисно для Emby, Jellyfin, Plex, приватних медіасерверів, локальних доменів або невеликих відеосайтів, які класичні інструменти для спільного перегляду зазвичай ігнорують.<br><br>Він також безкоштовний, з відкритим кодом, не вимагає облікових записів і може бути самостійно розміщений, якщо ви хочете запустити власне реле. Я створив його, бо хотів щось конфіденційне, що працювало б з моєю власною системою Emby, а не ще одну закриту платформу лише для кількох великих стрімінгових сайтів.",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "Основна відмінність",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty — це радше продукт для спільного перегляду з чатом та підтримкою мобільних пристроїв. KoalaSync — це радше гнучкий рівень синхронізації для відео в браузері.<br><br>Це звучить як невелика різниця, але вона значно змінює варіант використання. Якщо вам потрібен повноцінний додаток для спільного перегляду з чатом всередині, Teleparty має сенс. Якщо у вас вже є голосовий чат і ви просто хочете, щоб елементи керування відео синхронізувалися в різних браузерах, KoalaSync — це простіший варіант.",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "Чесні обмеження",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync — це не магія, і він не намагається приховати свої обмеження. Наразі він працює лише у браузерах для комп'ютерів. У нього немає вбудованого чату та мобільного додатку.<br><br>Сумісність також залежить від браузера та відеоплеєра. KoalaSync має працювати майже з будь-яким звичайним відеоплеєром браузера, але веб-сайти можуть будь-коли змінювати свої плеєри, поведінку DRM або обмеження браузера.<br><br>KoalaSync не пов'язаний з Netflix, Disney+, Prime Video, YouTube, Teleparty, Emby, Jellyfin, Plex або будь-яким іншим брендом, згаданим тут. Назви використовуються лише для пояснення типових випадків використання.",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "Короткий опис",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "Якщо вам потрібен вбудований чат або мобільна підтримка, Teleparty може бути кращим варіантом. Якщо вам потрібна безкоштовна синхронізація браузера з відкритим кодом для комп'ютера, яка працює майже з будь-яким звичайним відеоплеєром, включаючи власні медіасервери, такі як Emby, Jellyfin та Plex, KoalaSync, ймовірно, кращий варіант.",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync проти спільного використання екрана: коли синхронізація відео краща?",
|
||||
"ALT_SCREEN_META_DESC": "Практичне порівняння KoalaSync та спільного використання екрана для віддалених кіновечорів, керування відтворенням, якості відео, конфіденційності, субтитрів та перегляду в браузері.",
|
||||
"ALT_SCREEN_INTRO": "Спільний доступ до екрана працює, і іноді цього, чесно кажучи, достатньо. Одна людина показує свій екран, усі інші дивляться, і все.<br><br>Але для триваліших кіновечорів або перегляду епізодів разом це може дратувати. Якість відео залежить від завантаження користувача, процесора, браузера та будь-якого стиснення, яке застосовується в Discord, Teamspeak, Zoom або іншому додатку для дзвінків. Крім того, лише одна людина дійсно контролює відтворення.",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "Коли достатньо спільного доступу до екрана",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "Спільний доступ до екрана підходить, коли потрібно максимально швидко налаштувати відео. Якщо лише одна людина має доступ до нього, або всіх влаштовує одна людина, яка контролює відтворення, паузу та перемотування, це може бути найпростішим варіантом.<br><br>Це також має сенс, коли якість не має великого значення, коли нікому не потрібні власні субтитри чи налаштування звуку, і коли особа, яка надає доступ, почувається комфортно, показуючи це вікно браузера чи екран.",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "Де спільний доступ до екрана стає дратуючим",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "Найбільшим недоліком є те, що не всі дивляться у своєму власному плеєрі. Одна людина транслює свій екран, а всі інші бачать лише цей потік.<br><br>Це може означати гіршу якість відео, стиснуте аудіо, затримку відтворення та менший контроль для всіх інших. Субтитри та аудіодоріжки також залежать від того, що вибрав користувач, який надає доступ.<br><br>Існує також питання конфіденційності. Якщо ви перемикаєте вікна або вкладки, люди можуть ненадовго побачити ваші вкладки браузера, електронну пошту, Reddit, чати чи інші приватні речі. Ви можете обійти це за допомогою окремого вікна, але це все одно те, про що вам доведеться подумати.",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "Чому KoalaSync може здаватися чистішим",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync не транслює ваш екран. Кожен дивиться відео локально у своєму браузері, і KoalaSync синхронізує лише дії відтворення, такі як відтворення, пауза та пошук.<br><br>Це означає, що кожен може використовувати повноекранний режим на своєму пристрої, використовувати власні налаштування субтитрів або аудіо, якщо плеєр їх підтримує, та зберігати голосовий чат окремо в Discord, Teamspeak або іншому дзвінку.<br><br>Для кіновечорів це може здаватися набагато чистішим, ніж коли одна людина транслює свій екран. Це особливо корисно для браузерних налаштувань, таких як Emby, Jellyfin, Plex, локальних файлів, відкритих у браузері, приватних доменів або інших веб-сайтів зі звичайними відеоплеєрами браузера.",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "Чесні обмеження",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync не замінює голосовий дзвінок. Якщо ви хочете розмовляти під час перегляду, ви все одно використовуєте Discord, Teamspeak, Signal, WhatsApp або будь-який інший додаток, який ви зазвичай використовуєте.<br><br>Кожному також потрібен доступ до відео або сторінки на власному пристрої. KoalaSync не обходить підписки, вхід, DRM, регіональні обмеження чи правила платформи.<br><br>Наразі він доступний лише для браузера на комп'ютері, не має вбудованої функції чату чи голосового зв'язку, а сумісність завжди залежить від браузера та відеоплеєра.",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "Підсумок",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "Спільний доступ до екрана – це найпростіший варіант, коли ви просто хочете чогось швидкого і не дуже переймаєтеся якістю, конфіденційністю чи індивідуальним контролем. KoalaSync краще підходить, коли кожен має дивитися локально у своєму власному браузері, і сеанс має більше нагадувати спільний перегляд.",
|
||||
"ALT_TRY_KOALASYNC": "Спробуйте KoalaSync"
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"LANG_CODE": "zh",
|
||||
"HTML_CLASS": "lang-zh",
|
||||
"CANONICAL_PATH": "zh/",
|
||||
"LANG_TOGGLE_URL": "../",
|
||||
"LANG_TOGGLE_TEXT": "EN",
|
||||
"OG_LOCALE": "zh_CN",
|
||||
"META_TITLE": "KoalaSync | 与朋友同步 Netflix、YouTube 和任何视频",
|
||||
"META_DESCRIPTION": "与朋友完美同步观看 Netflix、YouTube、Twitch 和任何 HTML5 视频。适用于 Chrome 和 Firefox 的免费开源浏览器扩展。无需注册。",
|
||||
"SCHEMA_APP_DESC": "与朋友完美同步观看 Netflix、YouTube、Twitch 和任何 HTML5 视频。适用于 Chrome 和 Firefox 的免费开源浏览器扩展。无需注册。",
|
||||
"OG_TITLE": "KoalaSync |与朋友同步 Netflix、Emby、Jellyfin 和几乎任何视频",
|
||||
"OG_DESCRIPTION": "完美同步观看 Netflix、Emby、Jellyfin、YouTube、Twitch 和几乎任何 HTML5 视频。适用于 Chrome 和 Firefox 的隐私第一开源浏览器扩展。",
|
||||
"TWITTER_TITLE": "KoalaSync |与朋友同步 Netflix、Emby、Jellyfin 和几乎所有视频 – 浏览器扩展",
|
||||
"TWITTER_DESCRIPTION": "与朋友完美同步观看 Netflix、Emby、Jellyfin、YouTube、Twitch 和几乎任何 HTML5 视频。适用于 Chrome 和 Firefox 的隐私第一开源浏览器扩展。",
|
||||
"NAV_FEATURES": "特征",
|
||||
"NAV_HOW_IT_WORKS": "它是如何运作的",
|
||||
"HERO_TITLE": "一起观看.<span class='hero-line2'> 完美同步.</span>",
|
||||
"HERO_SUBTITLE": "您的远程电影之夜没有延迟。无需注册,无需收集数据。只需分享链接即可一起观看。",
|
||||
"HERO_MASCOT_ALT": "一只可爱的考拉站着,低头看着下载按钮",
|
||||
"ADD_TO_CHROME": "添加到Chrome",
|
||||
"ADD_TO_FIREFOX": "添加到Firefox",
|
||||
"COMPAT_HEADING": "适用于您最喜欢的平台",
|
||||
"COMPAT_MORE": "还有更多",
|
||||
"COMPAT_TOOLTIP": "适用于几乎所有具有视频元素的网站",
|
||||
"USE_CASES_TITLE": "适合任何场合",
|
||||
"USE_CASES_SUBTITLE": "无论远近,KoalaSync 将人们聚集在他们最喜欢的视频周围。",
|
||||
"USE_CASE_1_ALT": "两只可爱的考拉坐在一起分享一桶爆米花",
|
||||
"USE_CASE_1_TITLE": "与朋友的电影之夜",
|
||||
"USE_CASE_1_DESC": "实时同步您的电影并通过 Discord、Zoom 或您最喜欢的语音通话应用程序进行交谈。感觉就像坐在同一个房间里一样。",
|
||||
"USE_CASE_2_ALT": "一位考拉教授在屏幕上向两只远程学生考拉进行演示",
|
||||
"USE_CASE_2_TITLE": "远程学习",
|
||||
"USE_CASE_2_DESC": "与同学或同事一起分析在线教程、讲座或开发人员培训流。暂停并完美和谐地讨论复杂的步骤。",
|
||||
"USE_CASE_3_ALT": "一对可爱的考拉情侣一起远程观看;一个坐在电脑前,另一个拿着笔记本电脑躺在床上,两人的心在飞舞",
|
||||
"USE_CASE_3_TITLE": "远距离关系",
|
||||
"USE_CASE_3_DESC": "拉近距离,享受约会之夜。体验每一个情节转折,嘲笑同样的笑话,并在同一毫秒内分享情感时刻。",
|
||||
"WHY_TITLE": "为什么是KoalaSync?",
|
||||
"WHY_SUBTITLE": "专为可靠的同步、隐私优先的设计和轻松的设置而打造。",
|
||||
"FEATURE_1_TITLE": "完全控制/即时同步",
|
||||
"FEATURE_1_DESC": "一个人停顿,所有人都停顿。一追寻,众人追随。我们的两阶段同步协议协调所有参与者的实时播放。",
|
||||
"FEATURE_2_TITLE": "无尽的狂欢",
|
||||
"FEATURE_2_DESC": "自动同步播放。 KoalaSync 自动检测剧集转换并保持播放,直到所有对等方都成功加载下一个视频。",
|
||||
"FEATURE_3_TITLE": "零账户/纯粹隐私",
|
||||
"FEATURE_3_DESC": "没有注册,没有跟踪,没有持久性足迹。服务器完全在临时 RAM 中运行,并在您离开时清理您的房间。",
|
||||
"FEATURE_4_TITLE": "通用 HTML5 支持",
|
||||
"FEATURE_4_DESC": "适用于 YouTube、Twitch、Netflix、Jellyfin、Emby 以及几乎任何包含 HTML5 视频元素的标准网页。还与自定义自托管设置兼容。不会将任何视频数据发送到我们的服务器,仅发送同步元数据。",
|
||||
"FEATURE_5_TITLE": "自托管和 Docker 就绪",
|
||||
"FEATURE_5_DESC": "我们的官方服务器是免费且快速的,但如果您愿意,您可以完全拥有所有权。通过 Docker 在几秒钟内启动您自己的私有中继服务器。",
|
||||
"FEATURE_6_TITLE": "即时邀请/一键加入",
|
||||
"FEATURE_6_DESC": "无需交换 IP 地址或密码。与您的朋友分享生成的邀请链接,让他们只需单击一下即可自动加入您的房间。",
|
||||
"FEATURE_7_TITLE": "音量调节/音频压缩器",
|
||||
"FEATURE_7_DESC": "不再需要调节音量旋钮。内置音频压缩器可增强安静的对话并抑制大声的爆炸声。适用于 Netflix、Prime Video、Disney+、YouTube 和任何 HTML5 视频。完全本地化——音频数据不会离开您的计算机。",
|
||||
"FEATURE_8_TITLE": "开源(MIT 许可证)",
|
||||
"FEATURE_8_DESC": "整个代码库可在 GitHub 上公开审核。没有黑匣子,没有专有 blob——从中继服务器到浏览器扩展 100% 透明。",
|
||||
"COMP_TITLE": "KoalaSync 与 Teleparty",
|
||||
"COMP_SUBTITLE": "了解为什么开源、无广告和隐私优先是一起观看的最佳方式。",
|
||||
"COMP_COL_FEATURE": "特征",
|
||||
"COMP_FEAT_1_NAME": "成本/付费专区",
|
||||
"COMP_FEAT_1_DESC": "高级订阅、隐藏费用或锁定功能。",
|
||||
"COMP_FEAT_1_KOALA": "100% 免费",
|
||||
"COMP_FEAT_1_TELE": "付费等级/高级锁",
|
||||
"COMP_FEAT_2_NAME": "许可证/代码可审核",
|
||||
"COMP_FEAT_2_DESC": "源代码是否开放并可自由审核。",
|
||||
"COMP_FEAT_2_KOALA": "开源(MIT)",
|
||||
"COMP_FEAT_2_TELE": "所有权",
|
||||
"COMP_FEAT_3_NAME": "自托管",
|
||||
"COMP_FEAT_3_DESC": "能够部署您自己的私有中继服务器。",
|
||||
"COMP_FEAT_3_KOALA": "是(Docker-就绪)",
|
||||
"COMP_FEAT_3_TELE": "不",
|
||||
"COMP_FEAT_4_NAME": "隐私和追踪",
|
||||
"COMP_FEAT_4_DESC": "注册要求、跟踪 cookie 和分析。",
|
||||
"COMP_FEAT_4_KOALA": "零持久性(仅限 RAM)",
|
||||
"COMP_FEAT_4_TELE": "谷歌分析和 Cookie",
|
||||
"COMP_FEAT_5_NAME": "站点兼容性",
|
||||
"COMP_FEAT_5_DESC": "支持的网站和播放器兼容性。",
|
||||
"COMP_FEAT_5_KOALA": "几乎任何 HTML5 视频",
|
||||
"COMP_FEAT_5_TELE": "仅支持的网站",
|
||||
"COMP_FEAT_6_NAME": "本地化/翻译",
|
||||
"COMP_FEAT_6_DESC": "可用的界面语言和翻译支持。",
|
||||
"COMP_FEAT_6_KOALA": "15 种语言(并且还在不断增加)",
|
||||
"COMP_FEAT_6_TELE": "仅英语",
|
||||
"COMP_FEAT_7_NAME": "音频压缩器/音量调节",
|
||||
"COMP_FEAT_7_DESC": "内置音频标准化,以平衡安静的对话和响亮的效果",
|
||||
"COMP_FEAT_7_KOALA": "有(4 个预设,干/湿控制)",
|
||||
"COMP_FEAT_7_TELE": "不",
|
||||
"COMP_FOOTNOTE_1": "比较状态:2026 年 5 月。",
|
||||
"COMP_FOOTNOTE_2": "官方 Teleparty Premium 定价和支持的网络详细信息:",
|
||||
"COMP_FOOTNOTE_3": "Teleparty 隐私政策和跟踪数据收集:",
|
||||
"COMP_FOOTNOTE_4": "适用于允许将脚本注入标准 HTML5 视频标签的网站。具有高度严格的内容安全策略 (CSP)、DRM 复制保护或严重混淆的播放器包装器(如复杂的影子 DOM)的网站可能会限制自动控制或注入。",
|
||||
"STEPS_TITLE": "入门",
|
||||
"STEP_1_TITLE": "安装扩展",
|
||||
"STEP_1_DESC": "从 Chrome Web Store、Firefox 附加组件将 KoalaSync 添加到您的浏览器,或从 GitHub 下载最新的开发人员 ZIP。",
|
||||
"STEP_1_ILLUS_DESC": "隐私第一的视频同步器",
|
||||
"STEP_1_ILLUS_DL_CHROME": "下载 Chrome",
|
||||
"STEP_1_ILLUS_DL_FIREFOX": "下载 Firefox",
|
||||
"STEP_1_ILLUS_ACTIVE": "分机激活",
|
||||
"STEP_1_ILLUS_READY": "准备好一起观看吧!",
|
||||
"STEP_2_TITLE": "创建房间",
|
||||
"STEP_2_DESC": "打开扩展弹出窗口并单击“+ 创建新房间”。 KoalaSync 自动生成安全的 Room ID 和密码,加入它,并将邀请链接复制到剪贴板。",
|
||||
"STEP_2_ILLUS_ROOM": "房间",
|
||||
"STEP_2_ILLUS_SYNC": "同步",
|
||||
"STEP_2_ILLUS_SETTINGS": "设置",
|
||||
"STEP_2_ILLUS_CREATE": "+ 创建新房间",
|
||||
"STEP_2_ILLUS_MANUAL": "手动连接/高级",
|
||||
"STEP_2_ILLUS_COPIED": "邀请链接已复制!",
|
||||
"STEP_3_TITLE": "分享与同步",
|
||||
"STEP_3_DESC": "将邀请链接发送给您的朋友。他们加入后,选择您的视频选项卡并享受同步播放。",
|
||||
"STEP_3_ILLUS_IN_SYNC": "同步",
|
||||
"SELF_TITLE": "对于自托管者",
|
||||
"SELF_SUBTITLE": "不信任我们的服务器?维护完整的数据主权。只需几分钟即可部署您自己的私有中继服务器。",
|
||||
"SELF_MASCOT_ALT": "一只可爱的考拉坐在笔记本电脑前,部署 Docker 容器进行自托管",
|
||||
"SELF_COPY_CODE": "复制代码",
|
||||
"SELF_GITHUB_PACKAGES": "查看 GitHub 套餐上的所有图像标签",
|
||||
"BOTTOM_TITLE": "不相信?你自己看看吧。",
|
||||
"BOTTOM_SUBTITLE": "KoalaSync 100%开源、无广告、无跟踪。在 GitHub 上审核我们的代码库或直接安装浏览器扩展。",
|
||||
"BOTTOM_MASCOT_ALT": "一只可爱的考拉在 GitHub 吉祥物 Octocat 旁边拿着 GitHub 存储库页面",
|
||||
"FAQ_TITLE": "常见问题解答",
|
||||
"FAQ_SUBTITLE": "关于完美同步地一起观看视频所需了解的一切。",
|
||||
"FAQ_Q1": "我可以与朋友同步观看 Netflix、Emby 或 Jellyfin 吗?",
|
||||
"FAQ_A1": "是的! KoalaSync 适用于 Netflix、YouTube、Twitch 以及任何具有 HTML5 视频播放器的网站 - 包括 Emby、Jellyfin 和自托管媒体服务器。安装扩展程序、创建房间、共享邀请链接,每个人都可以完美同步观看。",
|
||||
"FAQ_Q_VIDEO_ACCESS": "KoalaSync 会把视频串流给我的朋友吗?",
|
||||
"FAQ_A_VIDEO_ACCESS": "不会。KoalaSync 不会串流、托管、共享视频内容,也不会绕过任何访问权限。每个人都在自己的浏览器中本地观看视频,并且需要自己拥有该网站或流媒体服务的访问权限,例如自己的 Netflix 账号。KoalaSync 只同步播放、暂停和拖动进度等播放操作。",
|
||||
"FAQ_Q2": "KoalaSync 是 Teleparty 的免费替代品吗?",
|
||||
"FAQ_A2": "在 MIT 许可证下 100% 免费和开源。没有高级套餐、没有锁定功能、没有广告——一切都包含在内。与 Teleparty 不同,KoalaSync 的中继服务器仅是 RAM,需要零数据收集或用户帐户。",
|
||||
"FAQ_Q3": "KoalaSync 是否可以与 Emby、Jellyfin 和自托管媒体服务器配合使用?",
|
||||
"FAQ_A3": "是的。 KoalaSync 可检测任何标准 HTML5 视频元素,这意味着它可以与 Emby、Jellyfin、Plex 和任何自定义自托管流设置完美配合。您还可以通过 Docker 自托管 KoalaSync 中继服务器,以实现完整的数据主权。",
|
||||
"FAQ_Q4": "哪些浏览器和平台支持KoalaSync?",
|
||||
"FAQ_A4": "Google Chrome、Mozilla Firefox、Microsoft Edge、Opera、Brave 以及所有基于 Chromium 的浏览器。直接从 Chrome Web Store 或 Firefox 附加组件安装 - 无需手动配置。",
|
||||
"FAQ_Q5": "我所有的朋友都需要安装该扩展吗?",
|
||||
"FAQ_A5": "是的,每个参与者都需要安装 KoalaSync - 但无需注册或创建帐户。只需单击邀请链接,安装扩展程序(如果尚未安装),您将在几秒钟内自动加入房间。",
|
||||
"FAQ_Q6": "我的数据安全吗?如果我不信任官方服务器怎么办?",
|
||||
"FAQ_A6": "完全安全。 KoalaSync 的中继服务器仅限 RAM — 所有会话数据在您离开房间时都会永久删除。没有数据库,没有日志记录,没有分析,没有帐户。如果您喜欢完全控制:您可以在几分钟内通过 Docker 自行托管您自己的私人中继服务器。整个代码库是开源的,并且可以在 GitHub 上进行审核。",
|
||||
"FAQ_Q7": "我可以修复在浏览器中观看视频时安静的对话和大声的爆炸声吗?",
|
||||
"FAQ_A7": "是的。 KoalaSync 2.2.0 包含一个内置音频压缩器,可减少视频安静部分和大声部分之间的差距。它通过 Web Audio API 在本地运行 — 任何音频数据都不会离开您的计算机。打开弹出窗口,创建一个房间,转到“设置”→“音频处理”,打开压缩器,然后选择一个预设。适用于 Netflix、Prime Video、Disney+、YouTube 和任何 HTML5 视频网站。",
|
||||
"HOWTO_TITLE": "如何使用KoalaSync与好友同步视频",
|
||||
"HOWTO_DESC": "与 YouTube、Netflix、Twitch 等平台上的朋友完美同步观看任何视频。无需注册,无需跟踪。",
|
||||
"HOWTO_STEP_1_NAME": "安装浏览器扩展",
|
||||
"HOWTO_STEP_1_TEXT": "从 Chrome Web Store 或 Firefox 附加组件将 KoalaSync 添加到您的浏览器。它适用于任何带有视频元素的网站。",
|
||||
"HOWTO_STEP_2_NAME": "创建一个房间",
|
||||
"HOWTO_STEP_2_TEXT": "打开扩展弹出窗口并单击“创建新房间”。邀请链接会自动复制到您的剪贴板。",
|
||||
"HOWTO_STEP_3_NAME": "分享和同步",
|
||||
"HOWTO_STEP_3_TEXT": "将邀请链接发送给您的朋友。选择一个视频选项卡并实时享受同步播放。",
|
||||
"FOOTER_MIT": "根据 MIT 许可证开源。",
|
||||
"FOOTER_RAM": "我们的服务器上不存储任何数据。纯RAM型继电器。",
|
||||
"FOOTER_LEGAL": "法律声明",
|
||||
"FOOTER_PRIVACY": "隐私政策",
|
||||
"MOCK_01": "活动室",
|
||||
"MOCK_02": "官方服务器",
|
||||
"MOCK_03": "邀请链接",
|
||||
"MOCK_04": "房间里的同伴",
|
||||
"MOCK_05": "你",
|
||||
"MOCK_06": "演奏",
|
||||
"MOCK_07": "同行",
|
||||
"MOCK_08": "留出空间",
|
||||
"MOCK_09": "选择视频",
|
||||
"MOCK_10": "遥控",
|
||||
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> 玩",
|
||||
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> 暂停",
|
||||
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> 同步",
|
||||
"MOCK_14": "跳转到其他",
|
||||
"MOCK_15": "上次活动状态",
|
||||
"MOCK_16": "正在等待 1 个同伴 (KoalaPC)...",
|
||||
"MOCK_17": "仍然跳过并播放",
|
||||
"MOCK_18": "您的用户名",
|
||||
"MOCK_19": "隐藏杂乱标签",
|
||||
"MOCK_20": "自动同步下一集",
|
||||
"MOCK_21": "自动复制邀请链接",
|
||||
"MOCK_22": "浏览器通知",
|
||||
"MOCK_23": "故障排除",
|
||||
"MOCK_24": "再生Peer ID",
|
||||
"MOCK_25": "如果您看到“重复身份”错误,请使用此选项。",
|
||||
"MOCK_26": "连接状态",
|
||||
"MOCK_27": "已连接",
|
||||
"MOCK_28": "复制日志",
|
||||
"MOCK_29": "视频调试信息",
|
||||
"MOCK_30": "完整的行动历史",
|
||||
"MOCK_31": "日志(最后 50 条)",
|
||||
"MOCK_32": "清除",
|
||||
"FOOTER_LEGAL_LINK": "imprint",
|
||||
"FOOTER_PRIVACY_LINK": "privacy",
|
||||
"FOOTER_DISCLAIMER": "KoalaSync 不隶属于 Netflix、Disney+、Amazon、YouTube、Twitch 或任何其他流媒体平台,也不受其认可或关联。所有商标均为其各自所有者的财产。",
|
||||
"FOOTER_SUPPORT": "Support KoalaSync",
|
||||
"ALT_TELEPARTY_TITLE": "KoalaSync 与 Teleparty:你应该选择哪款在线同步观影工具?",
|
||||
"ALT_TELEPARTY_META_DESC": "针对 KoalaSync 和 Teleparty 的实用对比,涵盖内置聊天功能、移动端支持、浏览器视频同步、自建媒体服务器以及开源特性等方面。",
|
||||
"ALT_TELEPARTY_INTRO": "KoalaSync 和 Teleparty 都能解决同一个令人头疼的问题:如何一起观看视频,而无需经历\u201c3、2、1,开始播放\u201d这种繁琐的同步步骤。只不过,它们采取了不同的实现方式。<br><br>如果你需要内置聊天功能或希望在移动设备上观看,Teleparty 是更合理的选择。KoalaSync 则更适合桌面浏览器观影场景——即你已经通过 Discord、TeamSpeak 或其他语音通话软件进行交流,只需确保视频播放保持同步即可。<br><br>我开发 KoalaSync 的主要初衷是为了灵活性。它旨在支持几乎所有使用标准浏览器视频播放器的网站,而不仅仅局限于特定的平台列表。这包括 Emby、Jellyfin 和 Plex 等自建媒体服务,本地/私有域名,以及浏览器中的各类常规视频页面。",
|
||||
"ALT_TELEPARTY_WHEN_TELE_TITLE": "Teleparty 依然适用的场景",
|
||||
"ALT_TELEPARTY_WHEN_TELE_BODY": "如果你希望聊天功能集成在观影工具本身之中,Teleparty 仍然是更好的选择。KoalaSync 目前没有内置聊天功能;它默认你已经在其他平台(如 Discord、TeamSpeak、WhatsApp、Signal 或常规通话)上进行交流。<br><br>移动端支持是另一个关键因素。Teleparty 拥有专属应用程序,而 KoalaSync 目前仅支持桌面浏览器。如果你想在手机或平板电脑上同步观看 Netflix,KoalaSync 暂时并不适用。<br><br>因此,如果你最看重聊天功能和移动端支持,Teleparty 可能会更适合你的使用需求。",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_TITLE": "KoalaSync 适用场景",
|
||||
"ALT_TELEPARTY_WHEN_KOALA_BODY": "如果您想要一款灵活的桌面浏览器同步工具,而不是受限于特定平台列表的软件,那么 KoalaSync 是更合适的选择。<br><br>它适用于几乎所有使用标准浏览器视频播放器的网站。对于 Emby、Jellyfin、Plex、私有媒体服务器、本地域或传统\u201c在线观影派对\u201d工具通常忽略的小型视频网站来说,这一点尤为实用。<br><br>此外,它完全免费且开源,无需注册账号;如果您希望运行自己的中继服务器,还可以选择自托管。我开发它的初衷是寻找一款既注重隐私又能与我自己的 Emby 系统兼容的工具,而不是那种仅针对少数几家大型流媒体平台的封闭式平台。",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_TITLE": "主要区别",
|
||||
"ALT_TELEPARTY_MAIN_DIFF_BODY": "Teleparty 更侧重于提供包含聊天和移动端支持的\u201c观影派对\u201d体验;而 KoalaSync 则更像是一个针对浏览器视频的灵活同步层。<br><br>这听起来似乎差别不大,但实际上极大地改变了使用场景。如果您需要一个内置聊天功能的完整观影派对应用,Teleparty 是合理的选择;如果您已有语音聊天渠道,仅需保持各浏览器间的视频播放控制同步,那么 KoalaSync 则是更简洁适用的方案。",
|
||||
"ALT_TELEPARTY_LIMITS_TITLE": "局限性说明",
|
||||
"ALT_TELEPARTY_LIMITS_BODY": "KoalaSync 并非万能,我们也坦诚其局限性。目前它仅支持桌面端浏览器,没有内置聊天功能,也没有移动端应用。<br><br>兼容性也取决于浏览器和视频播放器。虽然 KoalaSync 理论上支持几乎所有标准浏览器视频播放器,但网站随时可能更改其播放器、DRM(数字版权管理)机制或浏览器限制策略。<br><br>KoalaSync 与 Netflix、Disney+、Prime Video、YouTube、Teleparty、Emby、Jellyfin、Plex 或本文提及的任何其他品牌均无关联。提及这些名称仅为说明典型的使用场景。",
|
||||
"ALT_TELEPARTY_SUMMARY_TITLE": "总结",
|
||||
"ALT_TELEPARTY_SUMMARY_BODY": "如果您需要内置聊天功能或移动端支持,Teleparty 可能是更合适的选择。如果您想要一款免费、开源且基于桌面浏览器的同步工具,并希望它能兼容几乎所有常规视频播放器(包括 Emby、Jellyfin 和 Plex 等自建媒体服务器),那么 KoalaSync 恐怕更适合您。",
|
||||
"ALT_SCREEN_TITLE": "KoalaSync 与屏幕共享:何时使用视频同步更佳?",
|
||||
"ALT_SCREEN_META_DESC": "针对远程观影之夜,从播放控制、视频质量、隐私保护、字幕支持及浏览器观看体验等方面,对 KoalaSync 与屏幕共享进行实用对比。",
|
||||
"ALT_SCREEN_INTRO": "屏幕共享确实可行,而且老实说,有时这就足够了。一人共享屏幕,其他人观看,搞定。<br><br>但在长时间观影或一起追剧时,它可能会让人感到烦恼。视频质量取决于共享者的上传带宽、CPU、浏览器,以及 Discord、Teamspeak、Zoom 或其他通话应用所采用的压缩算法。此外,实际上只有一个人能控制播放。",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_TITLE": "何时使用屏幕共享就足够了",
|
||||
"ALT_SCREEN_WHEN_ENOUGH_BODY": "如果您追求最快的设置速度,屏幕共享是个不错的选择。如果只有一个人能访问该视频……\u201c或者,如果大家都能接受由一个人来控制播放、暂停和进度跳转,那这可能是最简单的方案。<br><br>此外,如果画质要求不高,大家不需要各自独立的字幕或音频设置,且分享者也不介意展示自己的浏览器窗口或屏幕,那么这种方式也很合理。",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_TITLE": "屏幕共享可能带来的不便",
|
||||
"ALT_SCREEN_WHEN_ANNOYING_BODY": "最大的弊端在于,并非每个人都在自己的播放器中观看。由一人流式传输屏幕画面,其他人只能观看该画面。<br><br>这意味着视频画质可能较差、音频经过压缩、播放存在延迟,且其他人缺乏控制权。字幕和音轨也完全取决于分享者的选择。<br><br>此外还涉及隐私问题。如果你切换窗口或标签页,其他人可能会短暂看到你的浏览器标签页、电子邮件、Reddit 页面、聊天记录或其他私密内容。虽然可以通过使用独立窗口来规避这一问题,但这仍是需要考虑的因素。",
|
||||
"ALT_SCREEN_WHY_CLEANER_TITLE": "为何 KoalaSync 体验更清爽",
|
||||
"ALT_SCREEN_WHY_CLEANER_BODY": "KoalaSync 不会流式传输你的屏幕画面。每个人都在自己的浏览器中本地观看视频,KoalaSync 仅负责同步播放、暂停和进度跳转等操作。<br><br>这意味着每个人都可以在自己的设备上使用全屏模式,在播放器支持的情况下使用各自偏好的字幕或音频设置,并保持语音聊天(如 Discord、TeamSpeak 或其他通话工具)的独立性。<br><br>对于观影聚会而言,这种方式比单人广播屏幕画面的体验要清爽得多。它特别适用于基于浏览器的方案,例如 Emby、Jellyfin、Plex、在浏览器中打开的本地文件、私有域网站,或其他使用标准浏览器视频播放器的网站。",
|
||||
"ALT_SCREEN_LIMITS_TITLE": "客观存在的局限性",
|
||||
"ALT_SCREEN_LIMITS_BODY": "KoalaSync 无法替代语音通话。如果您想在观看的同时进行交流,仍可使用 Discord、Teamspeak、Signal、WhatsApp 或您常用的任何通讯工具。<br><br>此外,每位参与者都需要在各自的设备上访问相应的视频或网页。KoalaSync 无法绕过订阅、登录验证、DRM(数字版权管理)、地域限制或平台规则。<br><br>该工具目前仅支持桌面端浏览器,不包含内置聊天或语音功能;其兼容性取决于具体的浏览器和视频播放器。",
|
||||
"ALT_SCREEN_SUMMARY_TITLE": "摘要",
|
||||
"ALT_SCREEN_SUMMARY_BODY": "如果您追求快速便捷,且不太在意画质、隐私或个性化控制,那么屏幕共享是最简单的选择。而当您希望每位参与者都在各自的浏览器中观看,并营造出一种真正\u201c共同观看\u201d的氛围时,KoalaSync 则是更理想的方案。",
|
||||
"ALT_TRY_KOALASYNC": "试用 KoalaSync"
|
||||
}
|
||||
@@ -7,6 +7,11 @@
|
||||
<link rel="preload" href="style.min.css" as="style">
|
||||
<link rel="stylesheet" href="style.min.css">
|
||||
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
|
||||
<link rel="canonical" href="https://sync.koalastuff.net/privacy">
|
||||
<link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/privacy">
|
||||
<link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/datenschutz">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/privacy">
|
||||
<meta name="description" content="Privacy Policy of KoalaSync - We take data security and transparency seriously. Learn about our zero-tracking, zero-log, and zero-cookie approach.">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<script type="application/ld+json">
|
||||
@@ -158,6 +163,7 @@
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="imprint" style="color: var(--text-muted); text-decoration: none;">Legal Notice</a>
|
||||
<a href="privacy" style="color: var(--text-muted); text-decoration: none;">Privacy Policy</a>
|
||||
<a href="alternatives">Guides & comparisons</a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
Mastodon
|
||||
|
||||
+1007
-17
File diff suppressed because it is too large
Load Diff
+36
-6
@@ -1828,6 +1828,17 @@ footer {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
||||
/* --- Animations --- */
|
||||
[data-reveal] {
|
||||
opacity: 0;
|
||||
@@ -2564,12 +2575,9 @@ html:not(.lang-de) [lang="de"] {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.comparison-table td.check::before {
|
||||
content: "KoalaSync";
|
||||
}
|
||||
|
||||
.comparison-table td.check::before,
|
||||
.comparison-table td.cross::before {
|
||||
content: "Teleparty";
|
||||
content: attr(data-label);
|
||||
}
|
||||
|
||||
/* Keep the KoalaSync row subtly highlighted, like on desktop. */
|
||||
@@ -2930,7 +2938,29 @@ header[id] {
|
||||
.status-ring.active-pulse,
|
||||
.hero-mockup-wrapper,
|
||||
.cta-group .btn {
|
||||
will-change: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Alternatives Hub Styles --- */
|
||||
.guide-card {
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.25s ease, border-color 0.25s ease !important;
|
||||
}
|
||||
.guide-card:hover {
|
||||
transform: translateY(-4px);
|
||||
background-color: rgba(255, 255, 255, 0.07) !important;
|
||||
border-color: rgba(255, 255, 255, 0.22) !important;
|
||||
box-shadow: 0 12px 24px -10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.guide-card .btn-primary {
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
}
|
||||
.guide-card .btn-primary:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.guides-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+37
-19
@@ -25,6 +25,8 @@
|
||||
<link rel="alternate" hreflang="nl" href="https://sync.koalastuff.net/nl/">
|
||||
<link rel="alternate" hreflang="ja" href="https://sync.koalastuff.net/ja/">
|
||||
<link rel="alternate" hreflang="ko" href="https://sync.koalastuff.net/ko/">
|
||||
<link rel="alternate" hreflang="zh" href="https://sync.koalastuff.net/zh/">
|
||||
<link rel="alternate" hreflang="uk" href="https://sync.koalastuff.net/uk/">
|
||||
<link rel="alternate" hreflang="pt" href="https://sync.koalastuff.net/pt/">
|
||||
<link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/">
|
||||
<meta name="robots" content="index, follow">
|
||||
@@ -48,6 +50,8 @@
|
||||
<meta property="og:locale:alternate" content="nl_NL">
|
||||
<meta property="og:locale:alternate" content="ja_JP">
|
||||
<meta property="og:locale:alternate" content="ko_KR">
|
||||
<meta property="og:locale:alternate" content="zh_CN">
|
||||
<meta property="og:locale:alternate" content="uk_UA">
|
||||
<meta property="og:locale:alternate" content="pt_PT">
|
||||
<meta property="og:site_name" content="KoalaSync">
|
||||
|
||||
@@ -85,7 +89,7 @@
|
||||
"@type": "WebSite",
|
||||
"name": "KoalaSync",
|
||||
"url": "https://sync.koalastuff.net/",
|
||||
"inLanguage": ["en", "de", "fr", "es", "pt-BR", "ru", "it", "pl", "tr", "nl", "ja", "ko", "pt"]
|
||||
"inLanguage": ["en", "de", "fr", "es", "it", "nl", "pl", "pt", "pt-BR", "tr", "ru", "ja", "ko", "zh", "uk"]
|
||||
}
|
||||
</script>
|
||||
<script type="application/ld+json" id="schema-software">
|
||||
@@ -101,8 +105,8 @@
|
||||
"price": "0",
|
||||
"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.4.1",
|
||||
"description": "{{SCHEMA_APP_DESC}}",
|
||||
"softwareVersion": "2.5.0",
|
||||
"license": "https://opensource.org/licenses/MIT",
|
||||
"sameAs": "https://github.com/Shik3i/KoalaSync",
|
||||
"image": "https://sync.koalastuff.net/assets/NewLogoIcon.webp",
|
||||
@@ -150,6 +154,11 @@
|
||||
"name": "{{FAQ_Q1}}",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "{{FAQ_A1}}" }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "{{FAQ_Q_VIDEO_ACCESS}}",
|
||||
"acceptedAnswer": { "@type": "Answer", "text": "{{FAQ_A_VIDEO_ACCESS}}" }
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "{{FAQ_Q2}}",
|
||||
@@ -231,6 +240,8 @@
|
||||
<a href="https://sync.koalastuff.net/nl/" hreflang="nl">Nederlands</a>
|
||||
<a href="https://sync.koalastuff.net/ja/" hreflang="ja">日本語</a>
|
||||
<a href="https://sync.koalastuff.net/ko/" hreflang="ko">한국어</a>
|
||||
<a href="https://sync.koalastuff.net/zh/" hreflang="zh">中文</a>
|
||||
<a href="https://sync.koalastuff.net/uk/" hreflang="uk">Українська</a>
|
||||
<a href="https://sync.koalastuff.net/pt/" hreflang="pt">Português (Portugal)</a>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -249,6 +260,8 @@
|
||||
<option value="nl" {{SELECTED_NL}}>🇳🇱 Nederlands</option>
|
||||
<option value="ja" {{SELECTED_JA}}>🇯🇵 日本語</option>
|
||||
<option value="ko" {{SELECTED_KO}}>🇰🇷 한국어</option>
|
||||
<option value="zh" {{SELECTED_ZH}}>🇨🇳 中文</option>
|
||||
<option value="uk" {{SELECTED_UK}}>🇺🇦 Українська</option>
|
||||
<option value="pt" {{SELECTED_PT}}>🇵🇹 Português (Portugal)</option>
|
||||
</select>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
@@ -775,8 +788,8 @@
|
||||
<strong><span>{{COMP_FEAT_1_NAME}}</span></strong>
|
||||
<span class="feat-desc"><span>{{COMP_FEAT_1_DESC}}</span></span>
|
||||
</td>
|
||||
<td class="check highlight">✔ <span>{{COMP_FEAT_1_KOALA}}</span></td>
|
||||
<td class="cross">
|
||||
<td class="check highlight" data-label="KoalaSync">✔ <span>{{COMP_FEAT_1_KOALA}}</span></td>
|
||||
<td class="cross" data-label="Teleparty">
|
||||
✘ <span>{{COMP_FEAT_1_TELE}}</span>
|
||||
<a href="https://www.teleparty.com/premium" target="_blank" rel="noopener noreferrer" class="source-link" title="See Teleparty Premium Pricing"><sup>[1]</sup></a>
|
||||
</td>
|
||||
@@ -786,8 +799,8 @@
|
||||
<strong><span>{{COMP_FEAT_2_NAME}}</span></strong>
|
||||
<span class="feat-desc"><span>{{COMP_FEAT_2_DESC}}</span></span>
|
||||
</td>
|
||||
<td class="check highlight">✔ <span>{{COMP_FEAT_2_KOALA}}</span></td>
|
||||
<td class="cross">
|
||||
<td class="check highlight" data-label="KoalaSync">✔ <span>{{COMP_FEAT_2_KOALA}}</span></td>
|
||||
<td class="cross" data-label="Teleparty">
|
||||
✘ <span>{{COMP_FEAT_2_TELE}}</span>
|
||||
<a href="https://www.teleparty.com/privacy" target="_blank" rel="noopener noreferrer" class="source-link" title="See Teleparty Privacy Policy"><sup>[2]</sup></a>
|
||||
</td>
|
||||
@@ -797,8 +810,8 @@
|
||||
<strong><span>{{COMP_FEAT_3_NAME}}</span></strong>
|
||||
<span class="feat-desc"><span>{{COMP_FEAT_3_DESC}}</span></span>
|
||||
</td>
|
||||
<td class="check highlight">✔ <span>{{COMP_FEAT_3_KOALA}}</span></td>
|
||||
<td class="cross">
|
||||
<td class="check highlight" data-label="KoalaSync">✔ <span>{{COMP_FEAT_3_KOALA}}</span></td>
|
||||
<td class="cross" data-label="Teleparty">
|
||||
✘ <span>{{COMP_FEAT_3_TELE}}</span>
|
||||
<a href="https://www.teleparty.com/privacy" target="_blank" rel="noopener noreferrer" class="source-link" title="See Teleparty Privacy Policy"><sup>[2]</sup></a>
|
||||
</td>
|
||||
@@ -808,8 +821,8 @@
|
||||
<strong><span>{{COMP_FEAT_4_NAME}}</span></strong>
|
||||
<span class="feat-desc"><span>{{COMP_FEAT_4_DESC}}</span></span>
|
||||
</td>
|
||||
<td class="check highlight">✔ <span>{{COMP_FEAT_4_KOALA}}</span></td>
|
||||
<td class="cross">
|
||||
<td class="check highlight" data-label="KoalaSync">✔ <span>{{COMP_FEAT_4_KOALA}}</span></td>
|
||||
<td class="cross" data-label="Teleparty">
|
||||
✘ <span>{{COMP_FEAT_4_TELE}}</span>
|
||||
<a href="https://www.teleparty.com/privacy" target="_blank" rel="noopener noreferrer" class="source-link" title="See Teleparty Privacy Policy"><sup>[2]</sup></a>
|
||||
</td>
|
||||
@@ -819,8 +832,8 @@
|
||||
<strong><span>{{COMP_FEAT_5_NAME}}</span></strong>
|
||||
<span class="feat-desc"><span>{{COMP_FEAT_5_DESC}}</span></span>
|
||||
</td>
|
||||
<td class="check highlight">✔ <span>{{COMP_FEAT_5_KOALA}}</span><a href="#footnote-3" class="source-link" title="Technical Compatibility Details"><sup>[3]</sup></a></td>
|
||||
<td class="cross">
|
||||
<td class="check highlight" data-label="KoalaSync">✔ <span>{{COMP_FEAT_5_KOALA}}</span><a href="#footnote-3" class="source-link" title="Technical Compatibility Details"><sup>[3]</sup></a></td>
|
||||
<td class="cross" data-label="Teleparty">
|
||||
✘ <span>{{COMP_FEAT_5_TELE}}</span>
|
||||
<a href="https://www.teleparty.com/premium" target="_blank" rel="noopener noreferrer" class="source-link" title="See Teleparty Premium Platforms"><sup>[1]</sup></a>
|
||||
</td>
|
||||
@@ -830,8 +843,8 @@
|
||||
<strong><span>{{COMP_FEAT_6_NAME}}</span></strong>
|
||||
<span class="feat-desc"><span>{{COMP_FEAT_6_DESC}}</span></span>
|
||||
</td>
|
||||
<td class="check highlight">✔ <span>{{COMP_FEAT_6_KOALA}}</span></td>
|
||||
<td class="cross">
|
||||
<td class="check highlight" data-label="KoalaSync">✔ <span>{{COMP_FEAT_6_KOALA}}</span></td>
|
||||
<td class="cross" data-label="Teleparty">
|
||||
✘ <span>{{COMP_FEAT_6_TELE}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -840,8 +853,8 @@
|
||||
<strong><span>{{COMP_FEAT_7_NAME}}</span></strong>
|
||||
<span class="feat-desc"><span>{{COMP_FEAT_7_DESC}}</span></span>
|
||||
</td>
|
||||
<td class="check highlight">✔ <span>{{COMP_FEAT_7_KOALA}}</span></td>
|
||||
<td class="cross">
|
||||
<td class="check highlight" data-label="KoalaSync">✔ <span>{{COMP_FEAT_7_KOALA}}</span></td>
|
||||
<td class="cross" data-label="Teleparty">
|
||||
✘ <span>{{COMP_FEAT_7_TELE}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1189,6 +1202,10 @@
|
||||
<summary><span>{{FAQ_Q1}}</span></summary>
|
||||
<p><span>{{FAQ_A1}}</span></p>
|
||||
</details>
|
||||
<details class="faq-item" data-reveal>
|
||||
<summary><span>{{FAQ_Q_VIDEO_ACCESS}}</span></summary>
|
||||
<p><span>{{FAQ_A_VIDEO_ACCESS}}</span></p>
|
||||
</details>
|
||||
<details class="faq-item" data-reveal>
|
||||
<summary><span>{{FAQ_Q2}}</span></summary>
|
||||
<p><span>{{FAQ_A2}}</span></p>
|
||||
@@ -1257,9 +1274,10 @@
|
||||
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_LEGAL_LINK}}" style="color: var(--text-muted); text-decoration: none;"><span>{{FOOTER_LEGAL}}</span></a>
|
||||
<a href="{{ASSET_PATH}}{{FOOTER_PRIVACY_LINK}}" style="color: var(--text-muted); text-decoration: none;"><span>{{FOOTER_PRIVACY}}</span></a>
|
||||
<a href="https://ko-fi.com/koaladev" target="_blank" rel="noopener" title="{{FOOTER_SUPPORT}}" style="color: #fff; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; background: #ff5e5b; border: 1px solid rgba(255,255,255,0.18); border-radius: 999px; padding: 3px 9px; font-size: 0.72rem; font-weight: 700; line-height: 1;">
|
||||
<a href="{{ASSET_PATH}}{{LANG_PREFIX}}alternatives"><span><span lang="en">Guides & comparisons</span><span lang="de">Vergleiche & Anleitungen</span></span></a>
|
||||
<a href="https://support.koalastuff.net" target="_blank" rel="noopener" title="{{FOOTER_SUPPORT}}" style="color: #fff; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; background: #ff5e5b; border: 1px solid rgba(255,255,255,0.18); border-radius: 999px; padding: 3px 9px; font-size: 0.72rem; font-weight: 700; line-height: 1;">
|
||||
<span aria-hidden="true">♥</span>
|
||||
<span>Ko-Fi</span>
|
||||
<span>{{FOOTER_SUPPORT}}</span>
|
||||
</a>
|
||||
<a href="https://mastodon.social/@koalastuff" rel="me noopener" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "2.4.1",
|
||||
"date": "2026-06-19T08:50:27Z"
|
||||
"version": "2.5.0",
|
||||
"date": "2026-06-29T10:58:06Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user