Compare commits

...

44 Commits

Author SHA1 Message Date
Koala 595ea297f5 chore(release): release v2.0.5 2026-06-03 11:33:54 +02:00
GitHub Action a948780745 chore(release): update versions to v2.0.4 [skip ci] 2026-06-03 09:07:20 +00:00
Koala 8c899a6469 Update changelog for v2.0.4 2026-06-03 11:06:44 +02:00
Koala 4f4cdcd8c0 Cache health responses server-side 2026-06-03 11:04:45 +02:00
Koala 602be3a724 Tighten health endpoint hardening 2026-06-03 10:59:08 +02:00
Koala bb7bd21102 Update release assets for v2.0.3 2026-06-03 10:51:47 +02:00
GitHub Action 543bfe074d chore(release): update versions to v2.0.3 [skip ci] 2026-06-03 08:49:25 +00:00
Koala 81c50eff16 Harden room discovery and health metrics 2026-06-03 10:48:49 +02:00
Koala bf0fb5741f fix: version.json path for all non-English locales
Changed condition from lang === 'de' to lang === 'en' so that all
non-English language subdirectories (fr, es, pt-BR, ru, de) correctly
resolve version.json via ../version.json instead of only German.
2026-06-02 14:31:13 +02:00
Timo 6d2355f404 Store Assets added 2026-06-02 09:26:20 +02:00
Koala 732d585273 docs: add v2.0.2 changelog and update README badge 2026-06-02 08:23:49 +02:00
GitHub Action 666808f876 chore(release): update versions to v2.0.2 [skip ci] 2026-06-02 06:17:21 +00:00
Koala 3ec9812265 fix: use server-trusted peerId in relay payload to prevent spoofing 2026-06-02 08:17:08 +02:00
Koala 64f3c5eefd fix: use future-proof regex for Amazon domain matching 2026-06-02 08:06:35 +02:00
Koala 3fe074b308 fix: use orteil.dashnet.org instead of cookieclicker.com in blacklist 2026-06-02 08:04:31 +02:00
Koala a06cca8b9d fix: apply boundary-safe hostname matching to remaining platform and blacklist checks 2026-06-02 08:02:47 +02:00
KoalaDev c4fc3ab53b Merge pull request #4 from Shik3i/alert-autofix-3
Potential fix for code scanning alert no. 3: Incomplete URL substring sanitization
2026-06-02 07:55:08 +02:00
KoalaDev 3f3fea58ed Merge pull request #5 from Shik3i/alert-autofix-2
Potential fix for code scanning alert no. 2: DOM text reinterpreted as HTML
2026-06-02 07:54:51 +02:00
KoalaDev 014169f84e Potential fix for code scanning alert no. 2: DOM text reinterpreted as HTML
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-02 07:51:01 +02:00
KoalaDev 9c61abee03 Potential fix for code scanning alert no. 3: Incomplete URL substring sanitization
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-02 07:50:35 +02:00
KoalaDev 2f11c60307 Merge pull request #2 from Shik3i/dependabot/npm_and_yarn/server/npm_and_yarn-e5a46ec0e1
chore(deps): bump ws from 8.18.3 to 8.20.1 in /server in the npm_and_yarn group across 1 directory
2026-06-02 07:41:19 +02:00
KoalaDev 27301f8746 Merge pull request #3 from Shik3i/dependabot/npm_and_yarn/server/npm_and_yarn-05b1f1d78b
chore(deps): bump qs from 6.15.1 to 6.15.2 in /server in the npm_and_yarn group across 1 directory
2026-06-02 07:40:26 +02:00
dependabot[bot] 030b839b12 chore(deps): bump qs
Bumps the npm_and_yarn group with 1 update in the /server directory: [qs](https://github.com/ljharb/qs).


Updates `qs` from 6.15.1 to 6.15.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.1...v6.15.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 05:39:05 +00:00
dependabot[bot] 5c805bcafa chore(deps): bump ws
Bumps the npm_and_yarn group with 1 update in the /server directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 8.18.3 to 8.20.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.20.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 05:39:00 +00:00
Koala eb83d9148e Update contact email in CODE_OF_CONDUCT 2026-06-02 07:23:38 +02:00
Koala 290360387f Add CODE_OF_CONDUCT and PULL_REQUEST_TEMPLATE
- Add CODE_OF_CONDUCT.md based on Contributor Covenant 2.1
- Add .github/PULL_REQUEST_TEMPLATE.md with structured sections
- Link CODE_OF_CONDUCT in README.md and CONTRIBUTING.md
2026-06-02 07:22:37 +02:00
Koala c56e404c13 Overhaul issue templates
- Rewrite bug_report.md with professional structure and Copy Logs requirement
- Rewrite feature_request.md to focus on problem, use cases, and benefits
2026-06-02 07:19:49 +02:00
KoalaDev e519ea2302 Update issue templates 2026-06-02 07:16:17 +02:00
Koala 5a75bae8d0 mobile: nav container padding auf 0.5rem reduziert 2026-06-02 06:43:57 +02:00
Koala fc3f4f38ff mobile: Header noch weiter nach links gerückt
- Nav Container Padding auf Mobile reduziert (0.75rem statt 2rem)
- Logo-Area Gap auf 0.25rem reduziert
- Icon auf 40x40 verkleinert
- Schriftgröße auf 1.1rem reduziert
- Vertical margin vom Icon entfernt
2026-06-02 06:41:46 +02:00
Koala f69da7542e mobile: Sprachauswahl immer sichtbar, Hamburger entfernt
- Sprachauswahl aus nav-links in neuen nav-right Container verschoben
- nav-right ist auf Mobile rechtsbündig und immer sichtbar
- Hamburger-Menü entfernt (Links sind Same-Page-Anchors)
- Logo-Bereich auf Mobile kompakter (kleineres Icon, weniger Gap)
- Lang-Selector auf Mobile kompakter (kleinere Icons, weniger Padding)
- Gleiches Restructuring für join.html, impressum.html, datenschutz.html
2026-06-02 06:35:39 +02:00
Timo aba05060b9 feat: add localization comparison row to website comparison table 2026-06-01 17:11:55 +02:00
Timo 0b986e6e13 fix: build-time VERSION injection via build.js instead of JS runtime 2026-06-01 16:27:45 +02:00
Timo b23ce8ee7d fix: dynamic mockup version from version.json instead of hardcoded 1.9.3 2026-06-01 16:25:18 +02:00
Timo 3195bd0089 docs: move older changes into v2.0.0 where they belong 2026-06-01 16:21:35 +02:00
Timo 1c1778d265 docs: remove older changelog entries - attribution unclear 2026-06-01 16:20:36 +02:00
Timo 0262d9c1ad docs: fix changelog dates to match git tag timestamps 2026-06-01 16:19:11 +02:00
Timo f23c329709 docs: move release notice to top of README 2026-06-01 16:18:25 +02:00
Timo 78a165d368 docs: clarify Copy Debug Report description in v2.0.0 2026-06-01 16:16:40 +02:00
Timo 63b5ef0ffd docs: remove website-only entries from changelog, keep extension+server only 2026-06-01 16:16:23 +02:00
Timo a26b07cb5c docs: highlight extension i18n as biggest v2.0.0 feature 2026-06-01 16:15:32 +02:00
Timo a445759dd7 docs: add CHANGELOG.md, link from README, version badge -> clickable changelog 2026-06-01 16:14:30 +02:00
Timo 1e883d8ee4 chore: update assets - add icon folder, remove old SOCIAL_PREVIEW.png 2026-06-01 16:03:40 +02:00
GitHub Action be574e0e25 chore(release): update versions to v2.0.1 [skip ci] 2026-06-01 13:59:31 +00:00
95 changed files with 1616 additions and 280 deletions
+56
View File
@@ -0,0 +1,56 @@
---
name: 🐛 Bug Report
about: Create a report to help us improve KoalaSync
title: ''
labels: bug
assignees: ''
---
> **⚠️ Required:** Before submitting, open the KoalaSync **Status** tab in the extension popup and click **"Copy Logs"**. Paste the full output below — it contains essential system info, connection state, and debug data needed to diagnose your issue.
<details>
<summary><b>📋 Copy Logs Output</b></summary>
<!-- Paste the copied logs here -->
</details>
---
### Describe the Bug
A clear and concise description of what the bug is.
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
### Expected Behavior
A clear and concise description of what you expected to happen.
### Actual Behavior
A clear and concise description of what actually happened.
### Screenshots / Screen Recordings
If applicable, add screenshots or recordings to help explain your problem.
### Environment
- **Browser:** (e.g. Chrome 125, Firefox 128)
- **Extension Version:** (visible at the bottom of the Settings tab)
- **OS:** (e.g. Windows 11, macOS 14.5)
- **Self-Hosted Server?:** (yes / no — if yes, provide server version)
- **Website/Platform:** (e.g. YouTube, Netflix, Twitch, Jellyfin, Emby)
### Additional Context
Add any other context about the problem here (e.g. network setup, VPN usage, multiple monitors, etc.).
+31
View File
@@ -0,0 +1,31 @@
---
name: 🚀 Feature Request
about: Suggest a new feature or enhancement for KoalaSync
title: ''
labels: enhancement
assignees: ''
---
### Problem Description
A clear and concise description of the problem or limitation you're encountering. What's missing or what could be improved?
*As a user, I want to ... so that ...*
### Proposed Solution
Describe the feature you'd like to see. How should it work? Be as specific as possible.
### Use Cases & Benefits
- **When would you use this?** (e.g., specific websites, workflows, setups)
- **What value does it add?** (e.g., saves time, enables new functionality, improves UX)
### Alternatives Considered
What workarounds or alternative approaches have you tried? Are there other ways to achieve a similar result?
### Additional Context
Add any other context, sketches, mockups, or references (e.g., links to similar features in other projects).
+45
View File
@@ -0,0 +1,45 @@
<!--
Thanks for contributing to KoalaSync!
Please read the CONTRIBUTING.md and CODE_OF_CONDUCT.md before submitting.
By submitting this PR, you agree to abide by our code of conduct.
Use conventional commits for the title: feat:, fix:, docs:, refactor:, chore:
-->
### Description
<!-- What does this PR do and why? Link to relevant issue or motivation. -->
Closes #
### Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that alters existing behavior)
- [ ] Refactoring (no functional changes)
- [ ] Documentation update
- [ ] Build, dependencies, or CI
### How Has This Been Tested?
<!-- Describe the tests you ran and the environments (browsers, OS, etc.) -->
- [ ] Tested on Chrome
- [ ] Tested on Firefox
- [ ] `npm run lint` passes with zero errors and zero warnings
- [ ] `node -c` passes on all modified `.js` files
### Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have added/updated tests if needed
- [ ] I have updated documentation if needed (`docs/`, README, etc.)
- [ ] Protocol changes: I ran `node scripts/build-extension.js` and updated relevant docs
- [ ] No new warnings, secrets, or hardcoded credentials introduced
### Additional Context
<!-- Screenshots, migration notes, performance data, etc. -->
+95
View File
@@ -0,0 +1,95 @@
# KoalaSync Changelog
All notable changes to the KoalaSync browser extension and relay server.
---
## [v2.0.5] — 2026-06-03
### Security & Hardening
- Hardened extension room idle auto-leave detection to correctly recognize when the target tab's video heartbeat goes stale (e.g., after tab navigation or media closure).
- Exported cleaner graceful shutdown and lifecycle methods (`stopServerForTests`) from the relay server to prevent socket leaks and port-binding conflicts during verify checks.
### Added
- Added a validation step in `test-locales.js` to ensure the supported language list in `extension/i18n.js` is perfectly synchronized with the actual JSON translation files in the locales directory.
- Added a robust route verification test suite (`scripts/test-server-routes.mjs`) covering rate limit throttling, caching headers, and admin metrics access control.
---
## [v2.0.4] — 2026-06-03
### Security & Hardening
- Hardened relay health endpoints against simple flood traffic: `GET /` and `GET /health` are now limited to 10 requests per minute per client IP.
- Added lazy 60-second server-side caching for `GET /`, basic `/health`, and admin `/health` JSON responses to reduce repeated health-check work under noisy polling.
- Added stricter brute-force throttling for invalid admin metrics bearer attempts.
- Added startup warning for short `ADMIN_METRICS_TOKEN` values and documented that production Node ports must stay private behind Caddy or another trusted reverse proxy.
- Lowered the default maximum peers per room to 25.
### Added
- Optional privacy-preserving admin metrics on `/health` when `ADMIN_METRICS_TOKEN` is configured and a valid bearer token is supplied. Metrics are aggregate-only and exclude room IDs, peer IDs, usernames, IP addresses, media titles, passwords, and other user-level data.
### Changed
- Removed `bcryptjs`; temporary room passwords continue to use keyed SHA-256/HMAC hashing as documented.
- Public room discovery is now rate-limited server-side to one refresh every 10 seconds per socket, with the extension refresh button locked for 11 seconds.
### Fixed
- Improved Shadow DOM video detection so real embedded players are not hidden by smaller light-DOM preview or placeholder videos.
- Fixed join-button timeout cleanup after join status responses.
---
## [v2.0.2] — 2026-06-02
### Fixed
- Peer identity spoofing in relay server: client-supplied `peerId` could be used to impersonate other peers in PEER_STATUS events. Server now always stamps `peerId` with the authenticated sender's identity.
- Amazon domain detection: replaced broad `includes('amazon.')` substring check with boundary-safe regex that correctly matches all Amazon storefronts (`amazon.com`, `amazon.de`, `amazon.co.uk`, etc.) while rejecting lookalike domains.
---
## [v2.0.1] — 2026-06-01
### Fixed
- Video detection on Prime Video: `findVideo()` now scores all video elements by size, duration, and mute state instead of picking the first one. Fixes 0×0 placeholder being selected over the actual player.
- History entries in debug report showing `?` instead of action names.
- Prime Video status in compatibility matrix updated to reflect partial support.
### Added
- Multi-video overview table in Copy Debug Report when a page has more than one `<video>` element. Shows resolution, mute state, playback state, readyState, duration, and marks the currently targeted video.
---
## [v2.0.0] — 2026-06-01
### 🌍 Multi-Language Extension (Biggest Feature!)
- **6-Language UI**: The browser extension is now fully translated into **English, German, French, Spanish, Portuguese (Brazilian), and Russian**. Switch languages instantly in Settings without reload.
- **Real-Time i18n**: Every label, button, tooltip, toast notification, empty state, and onboarding guide updates dynamically when the language changes.
### New Features
- **Copy Debug Report (Markdown)**: The *Copy Logs* button in the Status tab now copies a fully formatted Markdown debug report — system info, connection status, video diagnostics, action history, and logs. One click, paste into a GitHub issue, all debugging data ready.
- **Platform Auto-Detection**: The Dev tab now identifies streaming platforms (YouTube, Netflix, Twitch, Prime Video, Disney+, HBO Max, Vimeo, Dailymotion) and displays the detected platform.
- **Enhanced Video Debug Info**: 20+ new fields in the Status tab including network state, buffered ranges, dimensions (with 0×0 warning), media error codes, shadow DOM status, seeking/ended/loop flags, volume, playback speed, and data attributes.
- **No-Video Diagnostic Mode**: When no video is found, the Status tab shows platform, page title, video count, shadow DOM presence, and MediaSession data to help troubleshoot.
### Changed
- **New TwoPointZero Branding**: Updated extension icons (16/32/48/96/128px).
- **Larger Popup Logo**: Extension popup icon increased to 48px.
- **Prime Video Unblocked**: Removed `amazon.` from the tab blacklist so Amazon/Prime Video tabs appear in the video selector.
- **Improved Debug Report**: Full User-Agent string for accurate browser identification, UTC timestamp, connection details including server URL and room info.
- **Smart Disconnect**: Improved disconnect handling when leaving rooms.
- **Human-Readable Room IDs**: Expanded word lists for friendlier room names.
- **Custom Server Support**: WEB_JOIN_REQUEST and join button for custom server invite flows.
- **Reconnection Strategy**: Custom server reconnection improvements.
- **Episode-Aware Sync**: Command sequencing with smarter episode transition detection and echo suppression for smoother series binges.
- **Sync Status Refinements**: YouTube and Twitch sync behavior improved.
- **No External Dependencies**: Extension remains dependency-free with no library overhead.
### Fixed
- Hardcoded strings, missing translation keys, and Service Worker notification race conditions.
---
## Versioning Policy
- **MAJOR** (x.0.0): Breaking protocol changes, architecture rewrites, or major feature milestones.
- **MINOR** (0.x.0): New features, significant enhancements, new translations, or UI redesigns.
- **PATCH** (0.0.x): Bug fixes, minor improvements, and documentation updates. PATCH releases may not receive individual changelog entries if bundled with a MINOR release.
+131
View File
@@ -0,0 +1,131 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Project maintainers are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the project maintainer at **koaladev@koalamail.rocks**.
All complaints will be reviewed and investigated promptly and fairly.
All project maintainers are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Project maintainers will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from project maintainers, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+2
View File
@@ -2,6 +2,8 @@
Thanks for your interest in improving KoalaSync. All contributions are welcome — from bug reports and translations to core protocol changes.
Please note that by participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
---
## Ways to Contribute
+1 -1
View File
@@ -7,7 +7,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.
- **Room Passwords**: If you set a room password, it is stored only as a secure **bcrypt hash** in RAM. The server never sees or stores your plaintext password.
- **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.
### Data Retention
+4
View File
@@ -12,6 +12,8 @@
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
</p>
<p align="center"><a href="CHANGELOG.md"><b>New v2.0.2 Release!</b> — See what's changed</a></p>
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
### 🌟 Why KoalaSync?
@@ -104,10 +106,12 @@ Both the official KoalaSync website and the **v2.0 Browser Extension** feature f
### 📖 Documentation & Links
- **[CHANGELOG.md](CHANGELOG.md)**: Full version history for the extension and relay server.
- **[TESTED_SERVICES.md](TESTED_SERVICES.md)**: Detailed compatibility matrix of tested streaming platforms and known limitations.
- **[TRANSLATION.md](website/TRANSLATION.md)**: Translation and localization guide for contributors.
- **[PRIVACY.md](PRIVACY.md)**: Data Handling and Privacy Policy.
- **[CONTRIBUTING.md](CONTRIBUTING.md)**: How to help make KoalaSync better.
- **[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)**: Our community standards and expectations.
- **[HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)**: Step-by-step walkthrough of the complete user flow.
- **[ARCHITECTURE.md](docs/ARCHITECTURE.md)**: Deep-dive into the two-phase sync and heartbeat logic.
- **[SECURITY.md](SECURITY.md)**: Disclosure policy and security practices.
+3 -2
View File
@@ -73,8 +73,9 @@ Encrypt sensitive findings with our PGP key (available on request).
KoalaSync's security is grounded in its architecture:
- **RAM-only relay**: No database, no persistent logs. All session data evaporates on disconnect.
- **bcrypt room passwords**: Plaintext passwords never stored. Brute-force protection: 5 attempts → 15-minute IP lockout.
- **Rate limiting**: Connection rate (IP-based, 60s window) and event rate (per-socket, 10s window).
- **Keyed SHA-256 room password hashes**: Plaintext passwords are never stored. Room passwords are held only as in-memory HMAC-SHA256 hashes for the short room lifetime, with brute-force protection: 5 attempts → 15-minute IP lockout.
- **Rate limiting**: Connection rate (IP-based, 60s window), health endpoint rate (10 requests/minute/IP), wrong admin-metrics bearer attempts (5 requests/minute/IP), and event rate (per-socket, 10s window). Health-style JSON responses are cached server-side for 60 seconds and refreshed lazily on request.
- **Reverse proxy boundary**: The relay trusts one proxy hop for client IP detection. In production, keep the Node server reachable only through Caddy or another trusted reverse proxy.
- **URL-hash credential isolation**: Invitation credentials live in the URL fragment (`#join:...`) — never sent to the web server.
- **Strict CSP**: `default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'`.
- **No third-party requests**: Zero CDNs, fonts, analytics, or external scripts.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+2 -1
View File
@@ -8,7 +8,8 @@ services:
- PORT=3000 # Port KoalaSync listens on inside the container
- MIN_VERSION=1.0.0 # Minimum client version allowed to connect
- MAX_ROOMS=100 # Maximum number of rooms that can exist
- MAX_PEERS_PER_ROOM=50 # Maximum number of peers allowed per room
- MAX_PEERS_PER_ROOM=25 # Maximum number of peers allowed per room
- ADMIN_METRICS_TOKEN= # Optional: 32+ char random token for aggregate-only /health metrics
pids_limit: 2048 # Limits the container to 2048 process IDs for safety
networks: # Attaches the service to the networks listed below
- caddy_net # Joins the pre-existing Caddy network for reverse proxying
+2 -1
View File
@@ -10,7 +10,8 @@ services:
- PORT=3000 # Port KoalaSync listens on inside the container
- MIN_VERSION=1.0.0 # Minimum client version allowed to connect
- MAX_ROOMS=100 # Maximum number of rooms that can exist
- MAX_PEERS_PER_ROOM=50 # Maximum number of peers allowed per room
- MAX_PEERS_PER_ROOM=25 # Maximum number of peers allowed per room
- ADMIN_METRICS_TOKEN= # Optional: 32+ char random token for aggregate-only /health metrics
pids_limit: 2048 # Limits the container to 2048 process IDs for safety
networks: # Attaches the service to the networks listed below
bond0_network: # Network name as referenced by the service
+1 -1
View File
@@ -58,7 +58,7 @@ KoalaSync uses a megaphone routing approach to minimize server logic:
## 7. Security & Stability
- **Service Worker Lifecycle**: Uses `chrome.alarms` (30s interval) to prevent the Manifest V3 service worker from suspending while in an active room. On wake, runtime state is restored from `chrome.storage.session` via `ensureState()`.
- **Reconnect Visualization**: Badge shows "..." (orange) during reconnect. Popup displays "Reconnecting..." with attempt counter.
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or DoS. Real client IP extracted via `x-forwarded-for` header behind proxies/CDNs.
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or simple DoS. Public health endpoints are limited to 10 requests/minute/IP and cached server-side for 60 seconds, wrong admin-metrics bearer attempts to 5 requests/minute/IP, and room discovery to one request every 10 seconds per socket. Real client IP is taken from the trusted reverse proxy hop, so the Node port must stay private behind Caddy or another trusted proxy.
- **Room Creation Lock**: Per-room mutex prevents race conditions when multiple peers join a new room simultaneously.
- **CORS**: Allows `chrome-extension://` origins for WebSocket fallback compatibility.
- **Message Buffer**: `maxHttpBufferSize` set to 4KB to accommodate large `JOIN_ROOM` payloads.
+2 -2
View File
@@ -51,7 +51,7 @@ Click **"Create Room"** in the popup's Room tab:
4. **Server-side processing**:
- All fields are **sanitized**: `roomId` is stripped of invalid characters and clamped to 64 chars; `peerId` clamped to 16 chars; `password` clamped to 128 chars; `username` clamped to 30 chars.
- The server **hashes the password** with bcrypt and stores the hash in RAM (the plaintext is never stored).
- The server **hashes the password** with a keyed SHA-256 HMAC and stores only that hash in RAM (the plaintext is never stored).
- A new room object is created in memory with the peer's data.
- The server responds with `ROOM_DATA` containing the list of peers in the room.
@@ -89,7 +89,7 @@ When your friend opens the link in their browser:
- After 500ms, the page dispatches a `KOALASYNC_JOIN_REQUEST` custom DOM event with `{ roomId, password, useCustomServer, serverUrl }`.
- `bridge.js` catches this event and forwards it to `background.js` via `chrome.runtime.sendMessage`.
- `background.js` stores the credentials in `chrome.storage.sync` and emits `JOIN_ROOM` to the server.
- The server validates the password against the stored bcrypt hash.
- The server validates the password against the stored keyed SHA-256 HMAC hash.
- On success, the server responds with `ROOM_DATA` and broadcasts `PEER_STATUS { status: 'joined' }` to all existing peers.
- The join page updates to show "✅ Successfully joined!".
+2 -1
View File
@@ -36,7 +36,8 @@ export default [
URL: "readonly",
URLSearchParams: "readonly",
WebSocket: "readonly",
self: "readonly"
self: "readonly",
process: "readonly"
}
},
rules: {
+101 -10
View File
@@ -56,7 +56,7 @@ function ensureState() {
'logs', 'history', 'currentRoom', 'lastActionState',
'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks',
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle',
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount'
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt'
], (data) => {
clearTimeout(storageTimeout);
if (data.expectedAcksCount !== undefined) expectedAcksCount = data.expectedAcksCount;
@@ -80,6 +80,8 @@ function ensureState() {
if (data.reconnectFailed !== undefined) reconnectFailed = data.reconnectFailed;
if (data.reconnectStartTime) reconnectStartTime = data.reconnectStartTime;
if (data.reconnectAttempts !== undefined) reconnectAttempts = data.reconnectAttempts;
if (data.roomIdleSince !== undefined) roomIdleSince = data.roomIdleSince;
if (data.lastContentHeartbeatAt !== undefined) lastContentHeartbeatAt = data.lastContentHeartbeatAt;
// Recover Force Sync Timeout
if (data.forceSyncDeadline) {
@@ -139,9 +141,12 @@ let reconnectStartTime = null;
let reconnectFailed = false;
let reconnectAttempts = 0;
let currentServerUrl = null;
let roomIdleSince = null;
let lastContentHeartbeatAt = null;
const MAX_RECONNECT_ATTEMPTS = 20;
const _RECONNECT_BASE_DELAY = 500;
const _RECONNECT_MAX_DELAY = 5000;
const ROOM_IDLE_AUTO_LEAVE_MS = 2 * 60 * 60 * 1000;
// Force Sync Coordination
let isForceSyncInitiator = false;
@@ -304,6 +309,8 @@ function forceDisconnect() {
isNamespaceJoined = false;
isForceSyncInitiator = false;
expectedAcksCount = 0;
roomIdleSince = null;
lastContentHeartbeatAt = null;
forceSyncAcks.clear();
eventQueue = [];
chrome.storage.session.set({
@@ -312,7 +319,9 @@ function forceDisconnect() {
forceSyncDeadline: null,
expectedAcksCount: 0,
eventQueue: [],
episodeLobby: null
episodeLobby: null,
roomIdleSince: null,
lastContentHeartbeatAt: null
}).catch(() => {});
if (currentRoom) {
currentRoom.peers = [];
@@ -322,6 +331,63 @@ function forceDisconnect() {
broadcastConnectionStatus('disconnected');
}
function persistRoomIdleState() {
chrome.storage.session.set({ roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
}
function markRoomUseful() {
roomIdleSince = null;
lastContentHeartbeatAt = Date.now();
persistRoomIdleState();
}
function markRoomPotentiallyIdle() {
if (!currentRoom) {
roomIdleSince = null;
lastContentHeartbeatAt = null;
persistRoomIdleState();
return;
}
if (!roomIdleSince) {
roomIdleSince = Date.now();
persistRoomIdleState();
}
}
function clearTargetTabForIdle() {
currentTabId = null;
currentTabTitle = null;
lastContentHeartbeatAt = null;
if (currentRoom) {
roomIdleSince = Date.now();
}
chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
updateBadgeStatus();
}
async function leaveRoomAfterIdleGrace(reason) {
if (!currentRoom) return;
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
currentTabId = null;
currentTabTitle = null;
roomIdleSince = null;
lastContentHeartbeatAt = null;
clearEpisodeLobbyState();
await chrome.storage.session.set({
currentRoom: null,
currentTabId: null,
currentTabTitle: null,
roomIdleSince: null,
lastContentHeartbeatAt: null,
episodeLobby: null
}).catch(() => {});
await chrome.storage.sync.set({ roomId: '', password: '' }).catch(() => {});
addLog(reason, 'info');
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
updateBadgeStatus();
}
async function connect() {
if (isConnecting) return;
isConnecting = true;
@@ -482,12 +548,15 @@ async function connect() {
socket.onerror = () => {
broadcastConnectionStatus('disconnected');
addLog('WebSocket Error: Connection failed', 'error');
const logType = reconnectAttempts > 1 ? 'error' : 'warn';
addLog('WebSocket Error: Connection failed', logType);
};
} catch (e) {
isConnecting = false;
addLog(e.message, 'error');
const logType = reconnectAttempts > 1 ? 'error' : 'warn';
const errMsg = (e && e.message) ? e.message : String(e || 'Unknown connection error');
addLog(errMsg, logType);
broadcastConnectionStatus('disconnected');
scheduleReconnect();
}
@@ -628,6 +697,7 @@ function handleServerEvent(event, data) {
switch (event) {
case EVENTS.ROOM_DATA:
currentRoom = data;
markRoomPotentiallyIdle();
if (currentRoom && Array.isArray(currentRoom.peers)) {
currentRoom.peers = currentRoom.peers.map(p => typeof p === 'object' ? createPeerData(p) : { peerId: p, username: null, tabTitle: null, mediaTitle: null, playbackState: null, currentTime: null, volume: null, muted: null, lastHeartbeat: Date.now() });
@@ -1149,8 +1219,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
}).catch(err => {
if (retries >= 3) {
addLog(`Content Script not responding in tab ${tabId} after ${retries} retries`, 'warn');
currentTabId = null;
updateBadgeStatus();
clearTargetTabForIdle();
return;
}
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
@@ -1164,8 +1233,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
});
} else {
addLog(`Content Script not responding in tab ${tabId}`, 'warn');
currentTabId = null;
updateBadgeStatus();
clearTargetTabForIdle();
}
});
}
@@ -1181,6 +1249,15 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
connect();
}
} else if (currentRoom) {
const now = Date.now();
const heartbeatAge = lastContentHeartbeatAt ? (now - lastContentHeartbeatAt) : Infinity;
if (!currentTabId || heartbeatAge > 45000) {
markRoomPotentiallyIdle();
}
if (roomIdleSince && Date.now() - roomIdleSince >= ROOM_IDLE_AUTO_LEAVE_MS) {
await leaveRoomAfterIdleGrace('Left room after 2 hours without a selected video heartbeat.');
return;
}
// Heartbeat Logic: Always include identity metadata
const settings = await getSettings();
emit(EVENTS.PEER_STATUS, {
@@ -1284,6 +1361,9 @@ async function handleAsyncMessage(message, sender, sendResponse) {
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
currentTabId = null;
currentTabTitle = null;
roomIdleSince = null;
lastContentHeartbeatAt = null;
updateBadgeStatus();
@@ -1297,6 +1377,10 @@ async function handleAsyncMessage(message, sender, sendResponse) {
chrome.storage.session.set({
currentRoom: null,
currentTabId: null,
currentTabTitle: null,
roomIdleSince: null,
lastContentHeartbeatAt: null,
isForceSyncInitiator: false,
forceSyncAcks: [],
forceSyncDeadline: null,
@@ -1489,6 +1573,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
updateBadgeStatus();
}
markRoomUseful();
getSettings().then(settings => {
const statusPayload = { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle };
emit(EVENTS.PEER_STATUS, statusPayload);
@@ -1516,7 +1601,11 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} else if (message.type === 'SET_TARGET_TAB') {
currentTabId = message.tabId;
currentTabTitle = message.tabTitle;
chrome.storage.session.set({ currentTabId, currentTabTitle });
lastContentHeartbeatAt = null;
if (currentRoom) {
roomIdleSince = Date.now();
}
chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
if (currentTabId) {
@@ -1642,7 +1731,9 @@ chrome.tabs.onRemoved.addListener(async (tabId) => {
const wasInRoom = !!currentRoom;
currentTabId = null;
currentTabTitle = null;
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null });
lastContentHeartbeatAt = null;
roomIdleSince = Date.now();
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
addLog('Target tab closed.', 'warn');
+24 -25
View File
@@ -80,27 +80,26 @@
// --- Helper: find the best video element on the page ---
// Prefers larger, visible videos over tiny preview/trailer elements.
function findVideo(root = document) {
const allVideos = root.querySelectorAll('video');
if (allVideos.length === 0) {
// Optimize: scan only potential player, video, media, and stream hosts by matching typical keywords (case-insensitive)
// or common custom element tags. This prevents recursive scanning of thousands of standard DOM nodes (div, span, a, etc.)
// while guaranteeing 100% airtight compatibility with all video web components in the wild.
const potentialHosts = root.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i], [id*="media" i], [class*="media" i], [id*="stream" i], [class*="stream" i], ytd-player, netflix-player, emby-player, jellyfin-player, video-player');
for (const el of potentialHosts) {
if (el.shadowRoot) {
const found = findVideo(el.shadowRoot);
if (found) return found;
}
const candidates = Array.from(root.querySelectorAll('video'));
// Scan likely media hosts even when light-DOM videos exist; many players
// expose a tiny preview/ad video outside Shadow DOM and the real player inside.
const potentialHosts = root.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i], [id*="media" i], [class*="media" i], [id*="stream" i], [class*="stream" i], ytd-player, netflix-player, emby-player, jellyfin-player, video-player');
for (const el of potentialHosts) {
if (el.shadowRoot) {
const found = findVideo(el.shadowRoot);
if (found) candidates.push(found);
}
return null;
}
if (candidates.length === 0) return null;
// Multiple videos found → pick the best one
if (allVideos.length === 1) return allVideos[0];
if (candidates.length === 1) return candidates[0];
let best = null;
let bestScore = -1;
for (const v of allVideos) {
for (const v of candidates) {
if (v.tagName !== 'VIDEO') continue;
// Score: visible area + bonus for unmuted + bonus for longer duration
const area = (v.videoWidth || v.offsetWidth || 0) * (v.videoHeight || v.offsetHeight || 0);
@@ -262,8 +261,8 @@
try {
const host = window.location.hostname.toLowerCase();
const isYouTube = host.includes('youtube.com');
const isTwitch = host.includes('twitch.tv');
const isYouTube = host === 'youtube.com' || host.endsWith('.youtube.com');
const isTwitch = host === 'twitch.tv' || host.endsWith('.twitch.tv');
if (isYouTube) {
const ytButton = document.querySelector('.ytp-play-button');
@@ -456,15 +455,15 @@
const platform = (() => {
const h = window.location.hostname.toLowerCase();
if (h.includes('youtube.com')) return 'YouTube';
if (h.includes('twitch.tv')) return 'Twitch';
if (h.includes('netflix.com')) return 'Netflix';
if (h.includes('primevideo.com') || h.includes('amazon.')) return 'Prime Video';
if (h.includes('disneyplus.com')) return 'Disney+';
if (h.includes('hulu.com')) return 'Hulu';
if (h.includes('max.com') || h.includes('hbomax.com')) return 'Max/HBO';
if (h.includes('vimeo.com')) return 'Vimeo';
if (h.includes('dailymotion.com')) return 'Dailymotion';
if (h === 'youtube.com' || h.endsWith('.youtube.com')) return 'YouTube';
if (h === 'twitch.tv' || h.endsWith('.twitch.tv')) return 'Twitch';
if (h === 'netflix.com' || h.endsWith('.netflix.com')) return 'Netflix';
if (h === 'primevideo.com' || h.endsWith('.primevideo.com') || /(^|\.)amazon\.(com\.[a-z]{2}|co\.[a-z]{2}|[a-z]{2,})$/.test(h)) return 'Prime Video';
if (h === 'disneyplus.com' || h.endsWith('.disneyplus.com')) return 'Disney+';
if (h === 'hulu.com' || h.endsWith('.hulu.com')) return 'Hulu';
if (h === 'hbomax.com' || h.endsWith('.hbomax.com') || h === 'max.com' || h.endsWith('.max.com')) return 'Max/HBO';
if (h === 'vimeo.com' || h.endsWith('.vimeo.com')) return 'Vimeo';
if (h === 'dailymotion.com' || h.endsWith('.dailymotion.com')) return 'Dailymotion';
return 'Generic';
})();
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "AKTUALISIEREN",
"BTN_REFRESH_TOOLTIP": "Die Liste der öffentlichen Räume aktualisieren",
"PUBLIC_ROOMS_REFRESHING": "Aktualisiere...",
"BTN_REFRESH_COOLDOWN": "WARTE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Die Raumliste kühlt ab. Versuche es in {seconds}s erneut.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Aktualisiere öffentliche Räume. Nächste Aktualisierung in {seconds}s verfügbar.",
"LABEL_ACTIVE_ROOM": "Aktiver Raum",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Der Raum, mit dem du gerade verbunden bist",
"ACTIVE_ROOM_NONE": "KEINER",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "REFRESH",
"BTN_REFRESH_TOOLTIP": "Refresh the list of public rooms",
"PUBLIC_ROOMS_REFRESHING": "Refreshing...",
"BTN_REFRESH_COOLDOWN": "WAIT {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Room list refresh is cooling down. Try again in {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Refreshing public rooms. Next refresh available in {seconds}s.",
"LABEL_ACTIVE_ROOM": "Active Room",
"LABEL_ACTIVE_ROOM_TOOLTIP": "The room you are currently connected to",
"ACTIVE_ROOM_NONE": "NONE",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ACTUALIZAR",
"BTN_REFRESH_TOOLTIP": "Actualizar la lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Actualizando...",
"BTN_REFRESH_COOLDOWN": "ESPERA {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "La lista de salas está en espera. Inténtalo de nuevo en {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Actualizando salas públicas. Próxima actualización en {seconds}s.",
"LABEL_ACTIVE_ROOM": "Sala activa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "La sala a la que estás conectado actualmente",
"ACTIVE_ROOM_NONE": "NINGUNA",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ACTUALISER",
"BTN_REFRESH_TOOLTIP": "Actualiser la liste des salons publics",
"PUBLIC_ROOMS_REFRESHING": "Actualisation...",
"BTN_REFRESH_COOLDOWN": "ATTENDRE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "La liste des salons est en pause. Réessayez dans {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Actualisation des salons publics. Prochaine actualisation dans {seconds}s.",
"LABEL_ACTIVE_ROOM": "Salon actif",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Le salon auquel vous êtes actuellement connecté",
"ACTIVE_ROOM_NONE": "AUCUN",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ATUALIZAR",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Atualizando...",
"BTN_REFRESH_COOLDOWN": "AGUARDE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "A lista de salas está em espera. Tente novamente em {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Atualizando salas públicas. Próxima atualização em {seconds}s.",
"LABEL_ACTIVE_ROOM": "Sala ativa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "A sala à qual você está conectado no momento",
"ACTIVE_ROOM_NONE": "NENHUMA",
+3
View File
@@ -33,6 +33,9 @@
"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": "НЕТ",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "2.0.0",
"version": "2.0.5",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
+113 -8
View File
@@ -66,21 +66,58 @@ let popupIntervals = [];
let populateTabsToken = null;
let errorToken = 0;
let forceSyncDone = false;
let connectionErrorTimer = null;
let pendingConnectionErrorMsg = null;
let roomListRefreshTimer = null;
let roomListRefreshInterval = null;
const ROOM_LIST_REFRESH_COOLDOWN_MS = 11000;
// --- Helpers ---
function clearConnectionErrorTimer() {
if (connectionErrorTimer) {
clearTimeout(connectionErrorTimer);
connectionErrorTimer = null;
}
pendingConnectionErrorMsg = null;
}
function setRoomRefreshCooldown() {
if (roomListRefreshTimer) clearTimeout(roomListRefreshTimer);
if (roomListRefreshInterval) clearInterval(roomListRefreshInterval);
const originalLabel = getMessage('BTN_REFRESH');
const updateLabel = () => {
const secondsLeft = Math.max(1, Math.ceil((cooldownEndsAt - Date.now()) / 1000));
elements.refreshRooms.textContent = getMessage('BTN_REFRESH_COOLDOWN', { seconds: secondsLeft });
elements.refreshRooms.title = getMessage('BTN_REFRESH_COOLDOWN_TOOLTIP', { seconds: secondsLeft });
};
const cooldownEndsAt = Date.now() + ROOM_LIST_REFRESH_COOLDOWN_MS;
elements.refreshRooms.disabled = true;
updateLabel();
roomListRefreshInterval = setInterval(updateLabel, 250);
roomListRefreshTimer = setTimeout(() => {
elements.refreshRooms.disabled = false;
elements.refreshRooms.textContent = originalLabel;
elements.refreshRooms.title = getMessage('BTN_REFRESH_TOOLTIP');
clearInterval(roomListRefreshInterval);
roomListRefreshInterval = null;
roomListRefreshTimer = null;
}, ROOM_LIST_REFRESH_COOLDOWN_MS);
}
// --- Initialization ---
async function init() {
// Load Settings
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale']);
let activeLang = data.locale;
if (!activeLang) {
const systemLang = (navigator.language || chrome.i18n.getUILanguage()).split('-')[0];
activeLang = ['en', 'de', 'fr', 'es', 'pt', 'ru'].includes(systemLang) ? (systemLang === 'pt' ? 'pt-BR' : systemLang) : 'en';
chrome.storage.sync.set({ locale: activeLang });
}
await loadLocale(activeLang);
translateDOM();
@@ -148,8 +185,8 @@ async function init() {
// Check for invite link on landing page
checkInviteLink();
// Initial room list fetch
chrome.runtime.sendMessage({ type: 'GET_ROOM_LIST' });
// Initialize public rooms placeholder
renderEmpty(elements.publicRooms, 'rooms');
// Debug Info Refresh
popupIntervals.push(setInterval(refreshDebugInfo, 2000));
@@ -813,7 +850,7 @@ function updateRoomList(rooms) {
function checkInviteLink() {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs[0];
if (tab && tab.url && tab.url.includes(OFFICIAL_LANDING_PAGE_URL) && tab.url.includes('#join:')) {
if (tab && tab.url && tab.url.startsWith(OFFICIAL_LANDING_PAGE_URL + '/') && tab.url.includes('#join:')) {
try {
const rawHash = tab.url.split('#join:')[1];
if (!rawHash) return;
@@ -952,7 +989,12 @@ function showToast(message, type = 'info', duration = 3000) {
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
toast.textContent = message || '';
toast.style.whiteSpace = 'pre-wrap';
const delay = Math.max(0, duration - 600);
toast.style.animation = `toastSlideIn 0.3s ease-out, toastFadeOut 0.3s ease-in ${delay}ms forwards`;
container.appendChild(toast);
setTimeout(() => toast.remove(), duration);
}
@@ -981,6 +1023,7 @@ elements.roomId.addEventListener('input', () => {
});
elements.joinBtn.addEventListener('click', async () => {
clearConnectionErrorTimer();
if (elements.joinBtn.disabled) return;
const roomIdInput = elements.roomId.value.trim();
const isCreating = !roomIdInput;
@@ -1040,6 +1083,7 @@ elements.joinBtn.addEventListener('click', async () => {
});
elements.leaveBtn.addEventListener('click', async () => {
clearConnectionErrorTimer();
chrome.runtime.sendMessage({ type: 'LEAVE_ROOM' });
await chrome.storage.sync.set({ roomId: '', password: '' });
elements.roomId.value = '';
@@ -1080,10 +1124,13 @@ if (syncTabCreateRoomBtn) syncTabCreateRoomBtn.addEventListener('click', () => {
});
elements.refreshRooms.addEventListener('click', () => {
if (elements.refreshRooms.disabled) return;
setRoomRefreshCooldown();
elements.publicRooms.replaceChildren();
const el = document.createElement('div');
el.style.cssText = 'text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;';
el.textContent = getMessage('PUBLIC_ROOMS_REFRESHING');
el.textContent = getMessage('PUBLIC_ROOMS_REFRESHING_COOLDOWN', { seconds: Math.ceil(ROOM_LIST_REFRESH_COOLDOWN_MS / 1000) });
elements.publicRooms.appendChild(el);
chrome.runtime.sendMessage({ type: 'GET_ROOM_LIST' });
});
@@ -1310,7 +1357,25 @@ chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'LOG_UPDATE') {
refreshLogs();
if (msg.log && msg.log.type === 'error') {
showError(msg.log.message);
const errMsg = msg.log.message || '';
const isConnErr = typeof errMsg === 'string' && (
errMsg.toLowerCase().includes('connection') ||
errMsg.toLowerCase().includes('websocket')
);
if (isConnErr) {
pendingConnectionErrorMsg = msg.log.message;
if (!connectionErrorTimer) {
connectionErrorTimer = setTimeout(() => {
if (pendingConnectionErrorMsg) {
showError(pendingConnectionErrorMsg);
}
connectionErrorTimer = null;
pendingConnectionErrorMsg = null;
}, 5000);
}
} else {
showError(msg.log.message);
}
}
} else if (msg.type === 'ACTION_UPDATE') {
const state = msg.state;
@@ -1366,6 +1431,9 @@ chrome.runtime.onMessage.addListener((msg) => {
updatePeerList(msg.peers);
if (msg.peers) detectPeerChanges(msg.peers);
} else if (msg.type === 'CONNECTION_STATUS') {
if (msg.status === 'connected') {
clearConnectionErrorTimer();
}
if (msg.status !== 'reconnecting') {
applyConnectionStatus(msg.status);
reconnectSlowMode = false;
@@ -1397,6 +1465,13 @@ chrome.runtime.onMessage.addListener((msg) => {
} else if (msg.type === 'ROOM_LIST') {
updateRoomList(msg.rooms);
} else if (msg.type === 'JOIN_STATUS') {
if (joinBtnTimeout) {
clearTimeout(joinBtnTimeout);
joinBtnTimeout = null;
}
elements.joinBtn.disabled = false;
elements.joinBtn.textContent = getMessage('BTN_JOIN_ROOM');
if (msg.success) {
// Final confirmation of join from background
chrome.storage.sync.get(['roomId', 'password', 'useCustomServer', 'serverUrl'], (data) => {
@@ -1589,6 +1664,28 @@ elements.copyLogs.addEventListener('click', () => {
const original = elements.copyLogs.textContent;
elements.copyLogs.textContent = getMessage('TOAST_LOGS_COPIED');
setTimeout(() => { elements.copyLogs.textContent = original; }, 2000);
// Construct rich multiline toast summary
const verStr = safe(status.version, '?');
const logsCount = logs.length;
const historyCount = history.length;
const peersCount = Array.isArray(status.peers) ? status.peers.length : 0;
let summary = `📋 Debug Report Copied!\n`;
summary += `• Version: v${verStr}\n`;
summary += `• System Logs: ${logsCount} entries\n`;
summary += `• Sync Actions: ${historyCount} captured\n`;
if (status.roomId) {
summary += `• Peers in Room: ${peersCount}\n`;
}
if (rawVideo && vs.found) {
const stateStr = vs.paused === true ? 'Paused' : (vs.paused === false ? 'Playing' : 'Unknown');
const timeStr = typeof vs.currentTime === 'number' ? `${Math.round(vs.currentTime)}s` : '?';
summary += `• Video: ${stateStr} at ${timeStr}\n`;
}
summary += `Paste it in markdown to view!`;
showToast(summary, 'success', 5000);
}).catch(() => {
showToast(getMessage('TOAST_COPY_FAILED'), 'error');
});
@@ -1753,6 +1850,14 @@ window.addEventListener('unload', () => {
clearTimeout(forceSyncResetTimer);
forceSyncResetTimer = null;
}
if (roomListRefreshTimer) {
clearTimeout(roomListRefreshTimer);
roomListRefreshTimer = null;
}
if (roomListRefreshInterval) {
clearInterval(roomListRefreshInterval);
roomListRefreshInterval = null;
}
});
// --- Episode Lobby UI ---
+2 -5
View File
@@ -1,12 +1,12 @@
{
"name": "koalasync",
"version": "1.9.3",
"version": "2.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "koalasync",
"version": "1.9.3",
"version": "2.0.5",
"devDependencies": {
"archiver": "^7.0.1",
"esbuild": "^0.28.0",
@@ -1240,7 +1240,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1374,7 +1373,6 @@
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"bare-abort-controller": "*"
},
@@ -1897,7 +1895,6 @@
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
+3 -2
View File
@@ -1,12 +1,13 @@
{
"name": "koalasync",
"version": "2.0.0",
"version": "2.0.5",
"description": "KoalaSync Build Scripts",
"private": true,
"scripts": {
"build:extension": "node scripts/build-extension.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"verify": "node scripts/verify-release.mjs"
},
"devDependencies": {
"archiver": "^7.0.1",
+64
View File
@@ -0,0 +1,64 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const contentPath = path.join(__dirname, '..', 'extension', 'content.js');
const source = fs.readFileSync(contentPath, 'utf8');
function extractFunction(name, text) {
const start = text.indexOf(`function ${name}`);
assert.notStrictEqual(start, -1, `${name} not found`);
const bodyStart = text.indexOf('{', start);
let depth = 0;
for (let i = bodyStart; i < text.length; i++) {
if (text[i] === '{') depth++;
if (text[i] === '}') depth--;
if (depth === 0) return text.slice(start, i + 1);
}
throw new Error(`${name} body did not terminate`);
}
function makeVideo(name, width, height, options = {}) {
return {
name,
tagName: 'VIDEO',
videoWidth: width,
videoHeight: height,
offsetWidth: width,
offsetHeight: height,
muted: options.muted ?? true,
duration: options.duration ?? 0
};
}
const lightPreview = makeVideo('light-preview', 160, 90, { muted: false, duration: 30 });
const shadowPlayer = makeVideo('shadow-player', 1920, 1080, { muted: false, duration: 3600 });
const shadowRoot = {
querySelectorAll(selector) {
if (selector === 'video') return [shadowPlayer];
return [];
}
};
const shadowHost = { shadowRoot };
const fakeDocument = {
querySelectorAll(selector) {
if (selector === 'video') return [lightPreview];
return [shadowHost];
}
};
const fnSource = extractFunction('findVideo', source);
const findVideo = Function('document', `${fnSource}; return findVideo;`)(fakeDocument);
const selected = findVideo(fakeDocument);
assert.strictEqual(
selected,
shadowPlayer,
'findVideo should score Shadow DOM videos together with light DOM videos'
);
console.log('content video finder tests passed');
+36 -2
View File
@@ -9,13 +9,47 @@ if (!fs.existsSync(enPath)) {
process.exit(1);
}
let hasError = false;
// Verify SUPPORTED_LANGUAGES in extension/i18n.js matches JSON files
const i18nPath = path.join(__dirname, '..', 'extension', 'i18n.js');
try {
if (fs.existsSync(i18nPath)) {
const i18nContent = fs.readFileSync(i18nPath, 'utf8');
const langMatch = i18nContent.match(/export const SUPPORTED_LANGUAGES = \[(.*?)\];/);
if (!langMatch) {
hasError = true;
console.error('❌ Could not parse SUPPORTED_LANGUAGES from extension/i18n.js');
} else {
const supportedLangs = langMatch[1].split(',').map(s => s.trim().replace(/['"]/g, ''));
const fileLangs = fs.readdirSync(localesDir)
.filter(file => file.endsWith('.json'))
.map(file => file.replace('.json', ''));
for (const lang of fileLangs) {
if (!supportedLangs.includes(lang)) {
hasError = true;
console.error(`${lang}.json exists in extension/locales but is missing from SUPPORTED_LANGUAGES in extension/i18n.js`);
}
}
for (const lang of supportedLangs) {
if (!fileLangs.includes(lang)) {
hasError = true;
console.error(`${lang} is in SUPPORTED_LANGUAGES in extension/i18n.js but ${lang}.json is missing from extension/locales`);
}
}
}
}
} catch (err) {
hasError = true;
console.error('❌ Failed to verify SUPPORTED_LANGUAGES synchronization:', err.message);
}
const enDict = JSON.parse(fs.readFileSync(enPath, 'utf8'));
const enKeys = Object.keys(enDict);
const localeFiles = fs.readdirSync(localesDir).filter(file => file.endsWith('.json') && file !== 'en.json');
let hasError = false;
console.log(`Auditing i18n locales using ${enKeys.length} baseline keys from en.json...\n`);
for (const file of localeFiles) {
+27
View File
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { cwd } from 'node:process';
const popupPath = path.join(cwd(), 'extension', 'popup.js');
const source = fs.readFileSync(popupPath, 'utf8');
assert.match(
source,
/const ROOM_LIST_REFRESH_COOLDOWN_MS\s*=\s*11000;/,
'popup should define an 11 second room-list refresh cooldown'
);
assert.match(
source,
/elements\.refreshRooms\.disabled\s*=\s*true;/,
'refresh button should be disabled while cooldown is active'
);
assert.match(
source,
/setTimeout\(\(\)\s*=>\s*{\s*elements\.refreshRooms\.disabled\s*=\s*false;/s,
'refresh button should be re-enabled after the cooldown'
);
console.log('popup refresh cooldown tests passed');
+85
View File
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict';
import {
buildHealthPayload,
checkCooldown,
getCachedPayload,
isAdminMetricsAuthorized,
isAdminMetricsTokenStrong
} from '../server/ops.js';
const missingAuth = isAdminMetricsAuthorized(undefined, 'secret-token');
assert.equal(missingAuth, false, 'missing Authorization header must not authorize metrics');
const wrongAuth = isAdminMetricsAuthorized('Bearer wrong-token', 'secret-token');
assert.equal(wrongAuth, false, 'wrong bearer token must not authorize metrics');
const correctAuth = isAdminMetricsAuthorized('Bearer secret-token', 'secret-token');
assert.equal(correctAuth, true, 'correct bearer token should authorize metrics');
const disabledAuth = isAdminMetricsAuthorized('Bearer secret-token', '');
assert.equal(disabledAuth, false, 'empty admin token disables admin metrics');
assert.equal(isAdminMetricsTokenStrong(''), true, 'empty admin token is allowed because metrics stay disabled');
assert.equal(isAdminMetricsTokenStrong('short-token'), false, 'short admin token should be reported as weak');
assert.equal(
isAdminMetricsTokenStrong('a'.repeat(32)),
true,
'admin token with at least 32 characters should be considered strong'
);
const cooldowns = new Map();
assert.equal(checkCooldown(cooldowns, 'socket-1', 10_000, 100_000), true, 'first cooldown check passes');
assert.equal(checkCooldown(cooldowns, 'socket-1', 10_000, 105_000), false, 'second cooldown check inside window fails');
assert.equal(checkCooldown(cooldowns, 'socket-1', 10_000, 110_000), true, 'cooldown check after window passes');
const cache = new Map();
let buildCalls = 0;
const firstCached = getCachedPayload(cache, 'basic-health', 60_000, () => ({ value: ++buildCalls }), 1_000);
const secondCached = getCachedPayload(cache, 'basic-health', 60_000, () => ({ value: ++buildCalls }), 30_000);
const expiredCached = getCachedPayload(cache, 'basic-health', 60_000, () => ({ value: ++buildCalls }), 61_001);
assert.deepEqual(firstCached, { value: 1 }, 'cache should return the builder payload on first request');
assert.strictEqual(secondCached, firstCached, 'cache should reuse payloads inside the ttl');
assert.deepEqual(expiredCached, { value: 2 }, 'cache should rebuild payloads after ttl expiry');
const roomA = { peers: new Set(['a', 'b']), activeLobby: null };
const roomB = { peers: new Set(['c', 'd', 'e']), activeLobby: { expectedTitle: 'Episode 2' } };
const rooms = new Map([['room-a', roomA], ['room-b', roomB]]);
const basicHealth = buildHealthPayload({
rooms,
connections: 5,
includeMetrics: false,
now: 1234,
uptime: 99,
memoryUsage: () => ({ rss: 10, heapUsed: 5, heapTotal: 8 }),
rateLimitSizes: { connections: 1, events: 2, health: 3, adminMetricsAuth: 4, authFailures: 5, roomList: 6 }
});
assert.deepEqual(
Object.keys(basicHealth).sort(),
['connections', 'rooms', 'status', 'timestamp', 'uptime'].sort(),
'basic health should not expose extended metrics'
);
const adminHealth = buildHealthPayload({
rooms,
connections: 5,
includeMetrics: true,
now: 1234,
uptime: 99,
memoryUsage: () => ({ rss: 10, heapUsed: 5, heapTotal: 8 }),
rateLimitSizes: { connections: 1, events: 2, health: 3, adminMetricsAuth: 4, authFailures: 5, roomList: 6 }
});
assert.equal(adminHealth.peers, 5, 'admin metrics should include aggregate peer count');
assert.equal(adminHealth.roomsWithLobby, 1, 'admin metrics should count active lobbies');
assert.equal(adminHealth.avgPeersPerRoom, 2.5, 'admin metrics should include average room size');
assert.equal(adminHealth.maxPeersInRoom, 3, 'admin metrics should include max room size');
assert.deepEqual(adminHealth.memory, { rss: 10, heapUsed: 5, heapTotal: 8 }, 'admin metrics should expose process memory');
assert.deepEqual(
adminHealth.rateLimitEntries,
{ connections: 1, events: 2, health: 3, adminMetricsAuth: 4, authFailures: 5, roomList: 6 },
'admin metrics should expose aggregate rate-limit map sizes'
);
console.log('server ops tests passed');
+94
View File
@@ -0,0 +1,94 @@
import assert from 'node:assert/strict';
import {
ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE,
HEALTH_RATE_LIMIT_PER_MINUTE,
healthCounts,
adminMetricsAuthCounts,
healthResponseCache,
httpServer,
rooms,
startServer,
stopServerForTests
} from '../server/index.js';
const adminToken = process.env.ADMIN_METRICS_TOKEN || 'test-admin-token-with-more-than-32-chars';
const baseHeaders = { 'x-forwarded-for': '203.0.113.10' };
function url(path) {
const address = httpServer.address();
return `http://127.0.0.1:${address.port}${path}`;
}
async function request(path, options = {}) {
return fetch(url(path), {
...options,
headers: {
...baseHeaders,
...(options.headers || {})
}
});
}
try {
await startServer(0, '127.0.0.1');
let res = await request('/');
assert.equal(res.status, 200, 'root health endpoint should respond');
assert.equal(res.headers.get('cache-control'), 'no-store', 'root response should disable HTTP caching');
assert.deepEqual(await res.json(), { status: 'online', service: 'KoalaSync Relay' });
res = await request('/health');
assert.equal(res.status, 200, 'basic health endpoint should respond');
assert.equal(res.headers.get('cache-control'), 'no-store', 'basic health response should disable HTTP caching');
const basicHealth = await res.json();
assert.equal(basicHealth.status, 'ok', 'basic health should report ok');
assert.equal(basicHealth.rooms, 0, 'basic health should include room count');
assert.equal('peers' in basicHealth, false, 'basic health should not expose admin metrics');
assert.equal('memory' in basicHealth, false, 'basic health should not expose memory metrics');
rooms.set('route-test-room', {
peers: new Set(['socket-a', 'socket-b']),
peerData: new Map(),
peerIds: new Map(),
activeLobby: { expectedTitle: 'Episode 1', initiatorPeerId: 'peer-a', readyPeers: ['peer-a'] }
});
healthResponseCache.clear();
healthCounts.clear();
res = await request('/health', {
headers: { authorization: `Bearer ${adminToken}`, 'x-forwarded-for': '203.0.113.20' }
});
assert.equal(res.status, 200, 'authorized admin health endpoint should respond');
const adminHealth = await res.json();
assert.equal(adminHealth.rooms, 1, 'admin health should include room count');
assert.equal(adminHealth.peers, 2, 'admin health should include aggregate peer count');
assert.equal(adminHealth.roomsWithLobby, 1, 'admin health should include aggregate lobby count');
assert.equal(typeof adminHealth.memory?.rss, 'number', 'admin health should include aggregate memory metrics');
assert.equal('route-test-room' in adminHealth, false, 'admin health should not expose room identifiers');
healthCounts.clear();
adminMetricsAuthCounts.clear();
for (let i = 0; i < ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE; i++) {
res = await request('/health', {
headers: { authorization: 'Bearer wrong-token', 'x-forwarded-for': '203.0.113.30' }
});
assert.equal(res.status, 200, `wrong admin bearer attempt ${i + 1} should still return basic health`);
}
res = await request('/health', {
headers: { authorization: 'Bearer wrong-token', 'x-forwarded-for': '203.0.113.30' }
});
assert.equal(res.status, 429, 'wrong admin bearer attempts should be throttled after the limit');
healthCounts.clear();
for (let i = 0; i < HEALTH_RATE_LIMIT_PER_MINUTE; i++) {
const path = i % 2 === 0 ? '/' : '/health';
res = await request(path, { headers: { 'x-forwarded-for': '203.0.113.40' } });
assert.equal(res.status, 200, `shared health request ${i + 1} should be allowed`);
}
res = await request('/', { headers: { 'x-forwarded-for': '203.0.113.40' } });
assert.equal(res.status, 429, 'root and health should share the public health rate limit');
console.log('server route tests passed');
} finally {
await stopServerForTests();
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const checks = [
['server ops', 'node', ['scripts/test-server-ops.mjs']],
['server routes', 'node', ['scripts/test-server-routes.mjs'], {
env: { ADMIN_METRICS_TOKEN: 'verify-admin-token-with-more-than-32-chars' }
}],
['content video finder', 'node', ['scripts/test-content-video-finder.js']],
['popup refresh cooldown', 'node', ['scripts/test-popup-refresh-cooldown.mjs']],
['server syntax index', 'node', ['-c', 'server/index.js']],
['server syntax ops', 'node', ['-c', 'server/ops.js']],
['content syntax', 'node', ['-c', 'extension/content.js']],
['popup syntax', 'node', ['-c', 'extension/popup.js']],
['background syntax', 'node', ['-c', 'extension/background.js']],
['locale coverage', 'node', ['scripts/test-locales.js']],
['lint', 'npm', ['run', 'lint']],
['root production audit', 'npm', ['audit', '--omit=dev']],
['server production audit', 'npm', ['audit', '--omit=dev'], { cwd: path.join(repoRoot, 'server') }],
['extension build', 'npm', ['run', 'build:extension']],
['website build', 'node', ['website/build.js']]
];
function runCheck([label, command, args, options = {}]) {
return new Promise((resolve, reject) => {
console.log(`\n==> ${label}`);
const child = spawn(command, args, {
cwd: options.cwd || repoRoot,
env: { ...process.env, ...(options.env || {}) },
stdio: 'inherit'
});
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${label} failed with exit code ${code}`));
});
});
}
for (const check of checks) {
await runCheck(check);
}
console.log('\nRelease verification passed');
+4 -1
View File
@@ -1,4 +1,7 @@
PORT=3000
MIN_VERSION=1.0.0
MAX_ROOMS=1000
MAX_PEERS_PER_ROOM=50
MAX_PEERS_PER_ROOM=25
# Optional: enables aggregate-only admin metrics on /health with Authorization: Bearer <token>
# Use a long random token, 32+ characters recommended.
ADMIN_METRICS_TOKEN=
+28 -1
View File
@@ -14,10 +14,34 @@ Copy `.env.example` to `.env` and configure your settings.
```bash
PORT=3000
MAX_ROOMS=1000
MAX_PEERS_PER_ROOM=50
MAX_PEERS_PER_ROOM=25
MIN_VERSION=1.0.0
# Optional: enables aggregate-only admin metrics on /health with Authorization: Bearer <token>
# Use a long random token, 32+ characters recommended.
ADMIN_METRICS_TOKEN=
```
### Health & Metrics
`GET /` and `GET /health` are IP-rate-limited to 10 requests per minute per client IP. These health-style responses are cached server-side for 60 seconds and refreshed lazily on request. By default `/health` returns only basic service status, uptime, room count, connection count, and a timestamp.
If `ADMIN_METRICS_TOKEN` is set, requests with `Authorization: Bearer <token>` receive additional aggregate metrics such as total peers, average peers per room, max room size, active lobby count, rate-limit map sizes, and process memory usage. Wrong admin bearer attempts are separately limited to 5 requests per minute per client IP. The metrics response does not include room IDs, peer IDs, usernames, IP addresses, media titles, or other user-level data.
Use a long random `ADMIN_METRICS_TOKEN` of at least 32 characters. Shorter configured tokens still work, but the server logs a startup warning.
Generate a token with one of these commands:
```bash
openssl rand -base64 32
# or
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
```
For Docker Compose deployments, set the generated value in `server/.env`:
```bash
ADMIN_METRICS_TOKEN=replace-with-a-long-random-token
```
When polling `/health` from Prometheus, Uptime Kuma, cron, or a load balancer, keep the interval comfortably below the public limit of 10 requests per minute per client IP. A 30-60 second interval is recommended for routine monitoring. Use the admin bearer token only from trusted monitoring hosts, and keep the Node server private behind Caddy or another trusted reverse proxy because IP-based limits depend on the configured proxy boundary.
### Docker (Recommended)
The server is available as a pre-built image on GHCR.
```bash
@@ -38,6 +62,9 @@ npm start
## Security
- **Rate Limiting**: IP-based connection limits and socket-based event limits.
- **Health Endpoint Throttle**: `GET /` and `GET /health` are limited to 10 requests per minute per IP, with 60-second lazy server-side response caching and stricter throttling for wrong admin bearer attempts.
- **Room Discovery Throttle**: Room-list refreshes are rate-limited server-side to one request every 10 seconds per socket.
- **Token Handshake**: Requires a valid token defined in the root `shared/constants.js`.
- **Single Source of Truth**: The server imports constants directly from the root `shared/` directory.
- **In-Memory**: Rooms are automatically pruned after 2 hours of inactivity.
- **Reverse Proxy Boundary**: The server trusts one reverse proxy hop for client IP detection. Keep the Node port private/firewalled so clients can only reach it through Caddy or another trusted proxy.
+152 -34
View File
@@ -1,9 +1,17 @@
import express from 'express';
import { createServer } from 'http';
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 {
buildHealthPayload,
checkCooldown,
getCachedPayload,
isAdminMetricsAuthorized,
isAdminMetricsTokenStrong
} from './ops.js';
dotenv.config();
@@ -15,19 +23,36 @@ function hashPassword(password) {
const PORT = process.env.PORT || 3000;
const MAX_ROOMS = parseInt(process.env.MAX_ROOMS) || 1000;
const MAX_PEERS_PER_ROOM = parseInt(process.env.MAX_PEERS_PER_ROOM) || 50;
const MAX_PEERS_PER_ROOM = parseInt(process.env.MAX_PEERS_PER_ROOM) || 25;
const MIN_VERSION = process.env.MIN_VERSION || '1.0.0';
const ADMIN_METRICS_TOKEN = process.env.ADMIN_METRICS_TOKEN || '';
const ROOM_LIST_COOLDOWN_MS = 10000;
export const HEALTH_RATE_LIMIT_PER_MINUTE = 10;
export const ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE = 5;
const HEALTH_RESPONSE_CACHE_TTL_MS = 60000;
const app = express();
if (!isAdminMetricsTokenStrong(ADMIN_METRICS_TOKEN)) {
console.warn('[SECURITY] ADMIN_METRICS_TOKEN is set but shorter than 32 characters. Use a long random token.');
}
export const app = express();
app.set('trust proxy', 1); // For real client IP through reverse proxy
export const healthResponseCache = new Map();
// Health Check with Rate Limiting
app.get('/', (req, res) => {
const clientIp = req.ip;
if (!checkHealthRate(clientIp)) {
return res.status(429).json({ error: 'Too many requests. Try again later.' });
}
res.json({ status: 'online', service: 'KoalaSync Relay' });
res.set('Cache-Control', 'no-store');
res.json(getCachedPayload(
healthResponseCache,
'root',
HEALTH_RESPONSE_CACHE_TTL_MS,
() => ({ status: 'online', service: 'KoalaSync Relay' })
));
});
app.get('/health', (req, res) => {
@@ -35,19 +60,37 @@ app.get('/health', (req, res) => {
if (!checkHealthRate(clientIp)) {
return res.status(429).json({ error: 'Rate limited' });
}
res.json({
status: 'ok',
uptime: process.uptime(),
rooms: rooms.size,
connections: io.engine?.clientsCount ?? 0,
timestamp: Date.now()
});
const authHeader = req.get('authorization');
const includeMetrics = isAdminMetricsAuthorized(authHeader, ADMIN_METRICS_TOKEN);
if (ADMIN_METRICS_TOKEN && authHeader && !includeMetrics && !checkAdminMetricsAuthRate(clientIp)) {
return res.status(429).json({ error: 'Rate limited' });
}
res.set('Cache-Control', 'no-store');
res.json(getCachedPayload(
healthResponseCache,
includeMetrics ? 'health-admin' : 'health-basic',
HEALTH_RESPONSE_CACHE_TTL_MS,
() => buildHealthPayload({
rooms,
connections: io.engine?.clientsCount ?? 0,
includeMetrics,
uptime: process.uptime(),
rateLimitSizes: {
connections: connectionCounts.size,
events: eventCounts.size,
health: healthCounts.size,
adminMetricsAuth: adminMetricsAuthCounts.size,
authFailures: failedAuthAttempts.size,
roomList: roomListCooldowns.size
}
})
));
});
const httpServer = createServer(app);
export const httpServer = createServer(app);
// Socket.IO setup with security constraints
const io = new Server(httpServer, {
export const io = new Server(httpServer, {
cors: {
origin: (origin, callback) => {
if (!origin || origin === 'https://sync.koalastuff.net' || origin.startsWith('chrome-extension://') || origin.startsWith('moz-extension://')) {
@@ -67,7 +110,7 @@ const io = new Server(httpServer, {
/**
* In-memory storage
*/
const rooms = new Map();
export const rooms = new Map();
const socketToRoom = new Map();
const peerToSocket = new Map(); // peerId -> socketId (Global lookup)
const roomCreationLocks = new Map(); // roomId -> Promise (prevents race on room creation)
@@ -78,7 +121,7 @@ function log(type, message, details = '') {
}
// Rate Limiting & Security
const connectionCounts = new Map(); // ip -> { count, resetTime }
export const connectionCounts = new Map(); // ip -> { count, resetTime }
const failedAuthAttempts = new Map(); // Map<IP+RoomID, {count, lastAttempt}>
function checkAuthRate(ip, roomId) {
@@ -128,7 +171,7 @@ function recordAuthFailure(ip, roomId) {
}
// Periodically clean up old auth failure records (every 15 minutes)
setInterval(() => {
const authFailureCleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, record] of failedAuthAttempts.entries()) {
if (now - record.lastAttempt > 15 * 60 * 1000) {
@@ -137,11 +180,13 @@ setInterval(() => {
}
}, 15 * 60 * 1000);
const eventCounts = new Map(); // socketId -> { count, resetTime }
const healthCounts = new Map(); // ip -> { count, resetTime }
export const eventCounts = new Map(); // socketId -> { count, resetTime }
export const healthCounts = new Map(); // ip -> { count, resetTime }
export const adminMetricsAuthCounts = new Map(); // ip -> { count, resetTime }
const roomListCooldowns = new Map(); // socketId -> last allowed timestamp
// Clean up connection counts and event counts to prevent memory leak
setInterval(() => {
const rateLimitCleanupInterval = setInterval(() => {
const now = Date.now();
for (const [ip, entry] of connectionCounts.entries()) {
if (now > entry.resetTime) {
@@ -158,6 +203,16 @@ setInterval(() => {
healthCounts.delete(ip);
}
}
for (const [ip, entry] of adminMetricsAuthCounts.entries()) {
if (now > entry.resetTime) {
adminMetricsAuthCounts.delete(ip);
}
}
for (const [socketId] of roomListCooldowns.entries()) {
if (!io.sockets.sockets.has(socketId)) {
roomListCooldowns.delete(socketId);
}
}
}, 60000);
function checkConnectionRate(ip) {
@@ -184,7 +239,16 @@ function checkHealthRate(ip) {
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
entry.count++;
healthCounts.set(ip, entry);
return entry.count <= 60;
return entry.count <= HEALTH_RATE_LIMIT_PER_MINUTE;
}
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; }
entry.count++;
adminMetricsAuthCounts.set(ip, entry);
return entry.count <= ADMIN_METRICS_AUTH_RATE_LIMIT_PER_MINUTE;
}
/**
@@ -503,7 +567,7 @@ io.on('connection', (socket) => {
mediaTitle: clamp(data.mediaTitle, 100),
volume: clampNum(data.volume, 0, 1),
muted: validBool(data.muted),
peerId: typeof data.peerId === 'string' ? data.peerId.substring(0, 16) : undefined,
peerId: mapping.peerId,
status: typeof data.status === 'string' ? data.status.substring(0, 16) : undefined,
expectedTitle: clamp(data.expectedTitle, 100),
title: clamp(data.title, 100),
@@ -541,6 +605,10 @@ io.on('connection', (socket) => {
socket.disconnect(true);
return;
}
if (!checkCooldown(roomListCooldowns, socket.id, ROOM_LIST_COOLDOWN_MS)) {
socket.emit(EVENTS.ERROR, { message: 'Room list refresh is rate limited. Try again in a few seconds.' });
return;
}
const list = Array.from(rooms.entries()).map(([id, r]) => ({
id,
peerCount: r.peers.size,
@@ -584,6 +652,7 @@ io.on('connection', (socket) => {
socket.on('disconnect', () => {
eventCounts.delete(socket.id);
roomListCooldowns.delete(socket.id);
const mapping = socketToRoom.get(socket.id);
if (mapping) {
// Socket is already disconnected — no need to call socket.leave().
@@ -595,7 +664,7 @@ io.on('connection', (socket) => {
});
// Active Room & Dead Peer Cleanup (Every 2m)
setInterval(() => {
const roomCleanupInterval = setInterval(() => {
const now = Date.now();
const roomCutoff = now - (2 * 60 * 60 * 1000); // 2 hours
const peerCutoff = now - (5 * 60 * 1000); // 5 minutes
@@ -632,9 +701,29 @@ setInterval(() => {
}
}, 2 * 60 * 1000);
httpServer.listen(PORT, () => {
log('SERVER', `KoalaSync Relay running on port ${PORT}`);
});
export function startServer(port = PORT, host) {
if (httpServer.listening) return Promise.resolve(httpServer);
return new Promise((resolve, reject) => {
const onError = (err) => {
httpServer.off('listening', onListening);
reject(err);
};
const onListening = () => {
httpServer.off('error', onError);
const address = httpServer.address();
const actualPort = address && typeof address === 'object' ? address.port : port;
log('SERVER', `KoalaSync Relay running on port ${actualPort}`);
resolve(httpServer);
};
httpServer.once('error', onError);
httpServer.once('listening', onListening);
if (host) {
httpServer.listen(port, host);
} else {
httpServer.listen(port);
}
});
}
// --- M-4: Graceful Shutdown ---
function gracefulShutdown(signal) {
@@ -653,15 +742,44 @@ function gracefulShutdown(signal) {
}, 5000);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
export async function stopServerForTests() {
clearInterval(authFailureCleanupInterval);
clearInterval(rateLimitCleanupInterval);
clearInterval(roomCleanupInterval);
rooms.clear();
socketToRoom.clear();
peerToSocket.clear();
roomCreationLocks.clear();
connectionCounts.clear();
failedAuthAttempts.clear();
eventCounts.clear();
healthCounts.clear();
adminMetricsAuthCounts.clear();
roomListCooldowns.clear();
healthResponseCache.clear();
io.removeAllListeners();
io.disconnectSockets(true);
if (!httpServer.listening) return;
await new Promise((resolve, reject) => {
httpServer.close((err) => err ? reject(err) : resolve());
});
}
process.on('uncaughtException', (err) => {
log('ERROR', `Uncaught exception: ${err.message}`, err.stack);
process.exit(1);
});
const isMainModule = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
process.on('unhandledRejection', (reason) => {
log('ERROR', `Unhandled rejection: ${reason}`);
process.exit(1);
});
if (isMainModule) {
startServer(PORT);
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('uncaughtException', (err) => {
log('ERROR', `Uncaught exception: ${err.message}`, err.stack);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
log('ERROR', `Unhandled rejection: ${reason}`);
process.exit(1);
});
}
+92
View File
@@ -0,0 +1,92 @@
import crypto from 'crypto';
export function checkCooldown(cooldowns, key, windowMs, now = Date.now()) {
const lastAllowedAt = cooldowns.get(key) || 0;
if (now - lastAllowedAt < windowMs) {
return false;
}
cooldowns.set(key, now);
return true;
}
export function getCachedPayload(cache, key, ttlMs, buildPayload, now = Date.now()) {
const cached = cache.get(key);
if (cached && now - cached.createdAt < ttlMs) {
return cached.payload;
}
const payload = buildPayload();
cache.set(key, { createdAt: now, payload });
return payload;
}
export function isAdminMetricsAuthorized(authHeader, adminToken) {
if (!adminToken || typeof adminToken !== 'string') return false;
if (!authHeader || typeof authHeader !== 'string') return false;
const prefix = 'Bearer ';
if (!authHeader.startsWith(prefix)) return false;
const provided = authHeader.slice(prefix.length);
if (!provided) return false;
const expectedBuffer = Buffer.from(adminToken);
const providedBuffer = Buffer.from(provided);
if (expectedBuffer.length !== providedBuffer.length) return false;
return crypto.timingSafeEqual(expectedBuffer, providedBuffer);
}
export function isAdminMetricsTokenStrong(adminToken, minLength = 32) {
return !adminToken || (typeof adminToken === 'string' && adminToken.length >= minLength);
}
export function buildHealthPayload({
rooms,
connections,
includeMetrics = false,
now = Date.now(),
uptime = 0,
memoryUsage = () => process.memoryUsage(),
rateLimitSizes = {}
}) {
const payload = {
status: 'ok',
uptime,
rooms: rooms.size,
connections,
timestamp: now
};
if (!includeMetrics) return payload;
const roomValues = Array.from(rooms.values());
const roomSizes = roomValues.map(room => room.peers?.size || 0);
const peers = roomSizes.reduce((sum, size) => sum + size, 0);
const maxPeersInRoom = roomSizes.length > 0 ? Math.max(...roomSizes) : 0;
const avgPeersPerRoom = roomSizes.length > 0
? Math.round((peers / roomSizes.length) * 100) / 100
: 0;
const mem = memoryUsage();
return {
...payload,
peers,
roomsWithLobby: roomValues.filter(room => !!room.activeLobby).length,
avgPeersPerRoom,
maxPeersInRoom,
rateLimitEntries: {
connections: rateLimitSizes.connections || 0,
events: rateLimitSizes.events || 0,
health: rateLimitSizes.health || 0,
adminMetricsAuth: rateLimitSizes.adminMetricsAuth || 0,
authFailures: rateLimitSizes.authFailures || 0,
roomList: rateLimitSizes.roomList || 0
},
memory: {
rss: mem.rss,
heapUsed: mem.heapUsed,
heapTotal: mem.heapTotal
}
};
}
+14 -24
View File
@@ -9,7 +9,6 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.3",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"socket.io": "^4.8.3"
@@ -70,15 +69,6 @@
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
@@ -266,9 +256,9 @@
}
},
"node_modules/engine.io": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
"integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==",
"version": "6.6.8",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz",
"integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
@@ -280,7 +270,7 @@
"cors": "~2.8.5",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3"
"ws": "~8.20.1"
},
"engines": {
"node": ">=10.2.0"
@@ -749,9 +739,9 @@
}
},
"node_modules/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -951,13 +941,13 @@
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz",
"integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==",
"license": "MIT",
"dependencies": {
"debug": "~4.4.1",
"ws": "~8.18.3"
"ws": "~8.20.1"
}
},
"node_modules/socket.io-parser": {
@@ -1079,9 +1069,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
-1
View File
@@ -11,7 +11,6 @@
"license": "ISC",
"type": "module",
"dependencies": {
"bcryptjs": "^3.0.3",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"socket.io": "^4.8.3"
+1 -2
View File
@@ -67,7 +67,6 @@ export const BLACKLIST_DOMAINS = [
// Productivity & Project Management
'atlassian.net',
'jira',
'trello.com',
'notion.so',
'monday.com',
@@ -168,7 +167,7 @@ export const BLACKLIST_DOMAINS = [
// Games & Idle Sites
'milkywayidle.com',
'melvoridle.com',
'cookieclicker.',
'orteil.dashnet.org',
'clickerheroes.com',
'kongregate.com',
'armorgames.com',
+2 -2
View File
@@ -310,7 +310,7 @@ document.addEventListener('DOMContentLoaded', () => {
const updateDynamicVersion = async () => {
try {
const versionPath = document.documentElement.lang === 'de' ? '../version.json' : 'version.json';
const versionPath = document.documentElement.lang === 'en' ? 'version.json' : '../version.json';
const response = await fetch(versionPath);
if (!response.ok) return;
const data = await response.json();
@@ -555,7 +555,7 @@ document.addEventListener('DOMContentLoaded', () => {
const user = this.getAttribute('data-user');
const domain = this.getAttribute('data-domain');
if (user && domain) {
this.innerHTML = `${user}@${domain}`;
this.textContent = `${user}@${domain}`;
}
});
});
+5
View File
@@ -119,11 +119,16 @@ async function compile() {
const localesDir = path.join(websiteDir, 'locales');
const languages = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
// Read version for build-time injection (SEO: crawlers see real version)
const versionJson = JSON.parse(fs.readFileSync(path.join(websiteDir, 'version.json'), 'utf8'));
const buildVersion = versionJson.version || '?';
const englishHtml = {}; // track for join-page cross-ref
function compilePage(locale, assetPath, lang) {
let compiled = templateContent;
compiled = compiled.replace(/\{\{ASSET_PATH\}\}/g, assetPath);
compiled = compiled.replace(/\{\{VERSION\}\}/g, buildVersion);
languages.forEach(l => {
compiled = compiled.replace(new RegExp(`\\{\\{SELECTED_${l.toUpperCase()}\\}\\}`, 'g'), l === lang ? 'selected' : '');
});
+3 -1
View File
@@ -40,7 +40,6 @@
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" 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>
@@ -50,6 +49,8 @@
<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">
<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="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -62,6 +63,7 @@
</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>
+3 -1
View File
@@ -40,7 +40,6 @@
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" 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>
@@ -50,6 +49,8 @@
<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">
<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="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -62,6 +63,7 @@
</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>
+3 -1
View File
@@ -44,7 +44,6 @@
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="64" height="64">
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" 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>
@@ -54,6 +53,8 @@
<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">
<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="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -66,6 +67,7 @@
</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>
+4
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Unterstützte Webseiten und Player-Kompatibilität.",
"COMP_FEAT_5_KOALA": "Fast jedes HTML5-Video",
"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": "6 Sprachen (wachsend)",
"COMP_FEAT_6_TELE": "Nur Englisch",
"COMP_FOOTNOTE_1": "Stand des Vergleichs: Mai 2026.",
"COMP_FOOTNOTE_2": "Details zu Preisen und unterstützten Netzwerken von Teleparty Premium:",
"COMP_FOOTNOTE_3": "Datenschutzerklärung und Tracker-Erfassung von Teleparty:",
+4
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Supported websites and player compatibility.",
"COMP_FEAT_5_KOALA": "Almost any HTML5 Video",
"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": "6 Languages (and growing)",
"COMP_FEAT_6_TELE": "English only",
"COMP_FOOTNOTE_1": "Comparison state: May 2026.",
"COMP_FOOTNOTE_2": "Official Teleparty Premium pricing and supported networks details:",
"COMP_FOOTNOTE_3": "Teleparty privacy policies and tracking data collection:",
+4
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Sitios web compatibles y compatibilidad de reproductores.",
"COMP_FEAT_5_KOALA": "Casi cualquier video HTML5",
"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": "6 idiomas (y creciendo)",
"COMP_FEAT_6_TELE": "Solo inglés",
"COMP_FOOTNOTE_1": "Estado de la comparación: mayo de 2026.",
"COMP_FOOTNOTE_2": "Detalles oficiales de precios de Teleparty Premium y redes compatibles:",
"COMP_FOOTNOTE_3": "Políticas de privacidad y recopilación de datos de seguimiento de Teleparty:",
+4
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Sites web pris en charge et compatibilité des lecteurs.",
"COMP_FEAT_5_KOALA": "Presque toutes les vidéos HTML5",
"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": "6 langues (et plus à venir)",
"COMP_FEAT_6_TELE": "Anglais uniquement",
"COMP_FOOTNOTE_1": "État de la comparaison : mai 2026.",
"COMP_FOOTNOTE_2": "Détails sur les tarifs officiels de Teleparty Premium et les réseaux pris en charge :",
"COMP_FOOTNOTE_3": "Politiques de confidentialité et collecte de données de suivi de Teleparty :",
+4
View File
@@ -69,6 +69,10 @@
"COMP_FEAT_5_DESC": "Sites suportados e compatibilidade do player.",
"COMP_FEAT_5_KOALA": "Quase qualquer vídeo HTML5",
"COMP_FEAT_5_TELE": "Apenas sites suportados",
"COMP_FEAT_6_NAME": "Localização / Tradução",
"COMP_FEAT_6_DESC": "Idiomas de interface disponíveis e suporte a tradução.",
"COMP_FEAT_6_KOALA": "6 idiomas (e crescendo)",
"COMP_FEAT_6_TELE": "Apenas inglês",
"COMP_FOOTNOTE_1": "Estado da comparação: maio de 2026.",
"COMP_FOOTNOTE_2": "Preços oficiais do Teleparty Premium e detalhes das redes suportadas:",
"COMP_FOOTNOTE_3": "Políticas de privacidade do Teleparty e coleta de dados de rastreamento:",
+4
View File
@@ -69,6 +69,10 @@
"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": "6 языков (и расширяется)",
"COMP_FEAT_6_TELE": "Только английский",
"COMP_FOOTNOTE_1": "Состояние сравнения: май 2026.",
"COMP_FOOTNOTE_2": "Официальные цены Teleparty Premium и поддерживаемые сети:",
"COMP_FOOTNOTE_3": "Политика конфиденциальности Teleparty и сбор трекеров:",
+50 -2
View File
@@ -131,7 +131,6 @@ nav {
.nav-content {
display: flex;
justify-content: space-between;
align-items: center;
}
@@ -182,6 +181,7 @@ nav {
.nav-links {
display: flex;
gap: 2rem;
margin-left: auto;
}
.nav-links a {
@@ -195,6 +195,14 @@ nav {
color: var(--accent);
}
.nav-right {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: 2rem;
flex-shrink: 0;
}
/* --- Hero Section --- */
.hero {
min-height: 100vh;
@@ -314,6 +322,14 @@ nav {
border: 1px solid rgba(99, 102, 241, 0.3);
letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
}
.version-badge:hover {
background: rgba(99, 102, 241, 0.2);
box-shadow: 0 0 16px rgba(99, 102, 241, 0.3);
transform: translateY(-1px);
}
/* --- Interactive CSS Extension Mockup --- */
@@ -1889,12 +1905,44 @@ footer {
padding: 1rem 2rem;
gap: 1rem;
border-bottom: 1px solid var(--glass-border);
margin-left: 0;
}
.nav-links.open {
display: flex;
}
.nav-right {
margin-left: auto;
}
.hamburger {
display: flex;
display: none !important;
}
nav .container {
padding: 0 0.5rem;
}
.logo-area {
gap: 0.25rem;
font-size: 1.1rem;
}
.logo-area img {
height: 40px;
width: 40px;
margin: 0;
}
.lang-select-container {
padding: 4px 10px 4px 8px;
gap: 4px;
}
.lang-select-container .globe-icon {
width: 14px;
height: 14px;
}
.lang-dropdown {
font-size: 0.7rem;
padding: 0 6px 0 0;
}
.lang-select-container .chevron-icon {
width: 10px;
height: 10px;
}
.legal-card {
padding: 1.5rem;
+28 -16
View File
@@ -185,9 +185,6 @@
<span>Installed</span>
</div>
</div>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
<a href="#features"><span>{{NAV_FEATURES}}</span></a>
<a href="#how-it-works"><span>{{NAV_HOW_IT_WORKS}}</span></a>
@@ -195,6 +192,16 @@
<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>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
</div>
<div class="nav-right">
<div class="lang-select-container">
<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="globe-icon"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -207,14 +214,9 @@
</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>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
</div>
</div>
</nav>
@@ -222,10 +224,10 @@
<header class="hero" id="main-content">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal>
<a href="https://github.com/Shik3i/KoalaSync/blob/main/CHANGELOG.md" target="_blank" class="version-badge" data-reveal style="text-decoration: none; display: inline-block; transition: transform 0.2s, box-shadow 0.2s;" title="View Changelog">
<span class="version-text-en" lang="en">v1.5.3 OUT NOW</span>
<span class="version-text-de" lang="de">v1.5.3 JETZT VERFÜGBAR</span>
</div>
</a>
<h1 data-reveal>{{HERO_TITLE}}</h1>
<p class="hero-subtitle" data-reveal>{{HERO_SUBTITLE}}</p>
<div class="hero-mascot-container" data-reveal>
@@ -254,7 +256,7 @@
<div class="mock-header-title"><img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo">KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="mock-version-link" title="Visit GitHub Repository">
<svg viewBox="0 0 16 16" fill="currentColor" style="width: 12px; height: 12px; display: block;"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
<span>v1.9.3</span>
<span class="mockup-version">{{VERSION}}</span>
</a>
</div>
<div class="mock-tabs">
@@ -517,7 +519,7 @@
<div style="margin-top: 16px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; flex-shrink: 0;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v1.9.3</div>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;" class="mockup-version">{{VERSION}}</div>
</div>
</div>
</div>
@@ -762,6 +764,16 @@
<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>
</tr>
<tr>
<td class="feat-name">
<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">
<span>{{COMP_FEAT_6_TELE}}</span>
</td>
</tr>
</tbody>
</table>
<div class="table-footnotes">
@@ -875,7 +887,7 @@
<img src="{{ASSET_PATH}}assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14">
<span>KoalaSync</span>
</div>
<div class="illus-popup-version">v1.9.3</div>
<div class="illus-popup-version mockup-version">{{VERSION}}</div>
</div>
<div class="illus-popup-tabs">
<div class="illus-popup-tab active">
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "2.0.0",
"date": "2026-06-01T13:50:26Z"
"version": "2.0.5",
"date": "2026-06-03T09:32:00Z"
}
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+6 -4
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutz / Privacy Policy | KoalaSync</title>
<link rel="preload" href="style.23314305.min.css" as="style">
<link rel="stylesheet" href="style.23314305.min.css">
<link rel="preload" href="style.3ca56f45.min.css" as="style">
<link rel="stylesheet" href="style.3ca56f45.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
@@ -40,7 +40,6 @@
<picture><source srcset="assets/NewLogoIcon.avif" type="image/avif"><img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40"></picture>
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" style="width:16px;height:16px;display:block" viewBox="0 0 24 24"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg>
@@ -50,6 +49,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -62,6 +63,7 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
</div>
</div>
</nav>
@@ -216,6 +218,6 @@
</div>
</footer>
<script src="app.e6679b8d.min.js"></script>
<script src="app.deb95738.min.js"></script>
</body>
</html>
+31 -19
View File
@@ -6,9 +6,9 @@
<title>KoalaSync | Netflix, YouTube & jedes Video mit Freunden synchronisieren</title>
<meta name="description" content="Schaue Netflix, YouTube, Twitch und jedes HTML5-Video synchron mit Freunden. Kostenlose, quelloffene Browser-Erweiterung. Keine Anmeldung erforderlich.">
<link rel="preload" href="../style.23314305.min.css" as="style">
<link rel="preload" href="../style.3ca56f45.min.css" as="style">
<link rel="preload" as="image" href="../assets/LookDownKoala.webp" imagesrcset="../assets/LookDownKoala-1x.webp 90w, ../assets/LookDownKoala.webp 205w" imagesizes="90px" fetchpriority="high">
<link rel="stylesheet" href="../style.23314305.min.css">
<link rel="stylesheet" href="../style.3ca56f45.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="../assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="../assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/icon-192x192.png">
@@ -185,9 +185,6 @@
<span>Installed</span>
</div>
</div>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
<a href="#features"><span>Funktionen</span></a>
<a href="#how-it-works"><span>So funktioniert's</span></a>
@@ -195,6 +192,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -207,14 +214,9 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
</nav>
@@ -222,10 +224,10 @@
<header class="hero" id="main-content">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal>
<a href="https://github.com/Shik3i/KoalaSync/blob/main/CHANGELOG.md" target="_blank" class="version-badge" data-reveal style="text-decoration: none; display: inline-block; transition: transform 0.2s, box-shadow 0.2s;" title="View Changelog">
<span class="version-text-en" lang="en">v1.5.3 OUT NOW</span>
<span class="version-text-de" lang="de">v1.5.3 JETZT VERFÜGBAR</span>
</div>
</a>
<h1 data-reveal>Gemeinsam schauen.<br>Perfekt synchron.</h1>
<p class="hero-subtitle" data-reveal>Dein Kino-Abend auf Distanz. Keine Lags, keine Anmeldung. Einfach Link teilen und zusammen schauen.</p>
<div class="hero-mascot-container" data-reveal>
@@ -254,7 +256,7 @@
<div class="mock-header-title"><picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo"></picture>KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="mock-version-link" title="Visit GitHub Repository">
<svg fill="currentColor" style="width:12px;height:12px;display:block" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>
<span>v1.9.3</span>
<span class="mockup-version">2.0.5</span>
</a>
</div>
<div class="mock-tabs">
@@ -517,7 +519,7 @@
<div style="margin-top: 16px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; flex-shrink: 0;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v1.9.3</div>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;" class="mockup-version">2.0.5</div>
</div>
</div>
</div>
@@ -762,6 +764,16 @@
<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>
</tr>
<tr>
<td class="feat-name">
<strong><span>Lokalisierung / Übersetzung</span></strong>
<span class="feat-desc"><span>Verfügbare Oberflächensprachen und Übersetzungsunterstützung.</span></span>
</td>
<td class="check highlight"><span>6 Sprachen (wachsend)</span></td>
<td class="cross">
<span>Nur Englisch</span>
</td>
</tr>
</tbody>
</table>
<div class="table-footnotes">
@@ -875,7 +887,7 @@
<picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"></picture>
<span>KoalaSync</span>
</div>
<div class="illus-popup-version">v1.9.3</div>
<div class="illus-popup-version mockup-version">2.0.5</div>
</div>
<div class="illus-popup-tabs">
<div class="illus-popup-tab active">
@@ -1177,6 +1189,6 @@
</div>
</footer>
<script src="../app.e6679b8d.min.js"></script>
<script src="../app.deb95738.min.js"></script>
</body>
</html>
+31 -19
View File
@@ -6,9 +6,9 @@
<title>KoalaSync | Sincroniza Netflix, YouTube y cualquier video con amigos</title>
<meta name="description" content="Mira Netflix, YouTube, Twitch y cualquier video HTML5 en perfecta sincronización con amigos. Extensión de navegador gratuita y de código abierto. Sin registro.">
<link rel="preload" href="../style.23314305.min.css" as="style">
<link rel="preload" href="../style.3ca56f45.min.css" as="style">
<link rel="preload" as="image" href="../assets/LookDownKoala.webp" imagesrcset="../assets/LookDownKoala-1x.webp 90w, ../assets/LookDownKoala.webp 205w" imagesizes="90px" fetchpriority="high">
<link rel="stylesheet" href="../style.23314305.min.css">
<link rel="stylesheet" href="../style.3ca56f45.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="../assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="../assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/icon-192x192.png">
@@ -185,9 +185,6 @@
<span>Installed</span>
</div>
</div>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
<a href="#features"><span>Características</span></a>
<a href="#how-it-works"><span>Cómo funciona</span></a>
@@ -195,6 +192,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -207,14 +214,9 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
</nav>
@@ -222,10 +224,10 @@
<header class="hero" id="main-content">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal>
<a href="https://github.com/Shik3i/KoalaSync/blob/main/CHANGELOG.md" target="_blank" class="version-badge" data-reveal style="text-decoration: none; display: inline-block; transition: transform 0.2s, box-shadow 0.2s;" title="View Changelog">
<span class="version-text-en" lang="en">v1.5.3 OUT NOW</span>
<span class="version-text-de" lang="de">v1.5.3 JETZT VERFÜGBAR</span>
</div>
</a>
<h1 data-reveal>Miren juntos.<br>Sincronización perfecta.</h1>
<p class="hero-subtitle" data-reveal>Tu noche de películas a distancia sin retrasos. Sin registro ni recopilación de datos. Solo comparte un enlace y miren juntos.</p>
<div class="hero-mascot-container" data-reveal>
@@ -254,7 +256,7 @@
<div class="mock-header-title"><picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo"></picture>KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="mock-version-link" title="Visit GitHub Repository">
<svg fill="currentColor" style="width:12px;height:12px;display:block" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>
<span>v1.9.3</span>
<span class="mockup-version">2.0.5</span>
</a>
</div>
<div class="mock-tabs">
@@ -517,7 +519,7 @@
<div style="margin-top: 16px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; flex-shrink: 0;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v1.9.3</div>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;" class="mockup-version">2.0.5</div>
</div>
</div>
</div>
@@ -762,6 +764,16 @@
<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>
</tr>
<tr>
<td class="feat-name">
<strong><span>Localización / Traducción</span></strong>
<span class="feat-desc"><span>Idiomas de interfaz disponibles y soporte de traducción.</span></span>
</td>
<td class="check highlight"><span>6 idiomas (y creciendo)</span></td>
<td class="cross">
<span>Solo inglés</span>
</td>
</tr>
</tbody>
</table>
<div class="table-footnotes">
@@ -875,7 +887,7 @@
<picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"></picture>
<span>KoalaSync</span>
</div>
<div class="illus-popup-version">v1.9.3</div>
<div class="illus-popup-version mockup-version">2.0.5</div>
</div>
<div class="illus-popup-tabs">
<div class="illus-popup-tab active">
@@ -1177,6 +1189,6 @@
</div>
</footer>
<script src="../app.e6679b8d.min.js"></script>
<script src="../app.deb95738.min.js"></script>
</body>
</html>
+31 -19
View File
@@ -6,9 +6,9 @@
<title>KoalaSync | Synchronisez Netflix, YouTube et n'importe quelle vidéo avec vos amis</title>
<meta name="description" content="Regardez Netflix, YouTube, Twitch et des vidéos HTML5 en synchro avec vos amis. Extension de navigateur gratuite et open-source. Aucune inscription requise.">
<link rel="preload" href="../style.23314305.min.css" as="style">
<link rel="preload" href="../style.3ca56f45.min.css" as="style">
<link rel="preload" as="image" href="../assets/LookDownKoala.webp" imagesrcset="../assets/LookDownKoala-1x.webp 90w, ../assets/LookDownKoala.webp 205w" imagesizes="90px" fetchpriority="high">
<link rel="stylesheet" href="../style.23314305.min.css">
<link rel="stylesheet" href="../style.3ca56f45.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="../assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="../assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/icon-192x192.png">
@@ -185,9 +185,6 @@
<span>Installed</span>
</div>
</div>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
<a href="#features"><span>Fonctionnalités</span></a>
<a href="#how-it-works"><span>Comment ça marche</span></a>
@@ -195,6 +192,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -207,14 +214,9 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
</nav>
@@ -222,10 +224,10 @@
<header class="hero" id="main-content">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal>
<a href="https://github.com/Shik3i/KoalaSync/blob/main/CHANGELOG.md" target="_blank" class="version-badge" data-reveal style="text-decoration: none; display: inline-block; transition: transform 0.2s, box-shadow 0.2s;" title="View Changelog">
<span class="version-text-en" lang="en">v1.5.3 OUT NOW</span>
<span class="version-text-de" lang="de">v1.5.3 JETZT VERFÜGBAR</span>
</div>
</a>
<h1 data-reveal>Regardez ensemble.<br>Synchronisation parfaite.</h1>
<p class="hero-subtitle" data-reveal>Votre soirée cinéma à distance sans décalage. Pas d'inscription, pas de collecte de données. Partagez simplement un lien et regardez ensemble.</p>
<div class="hero-mascot-container" data-reveal>
@@ -254,7 +256,7 @@
<div class="mock-header-title"><picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo"></picture>KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="mock-version-link" title="Visit GitHub Repository">
<svg fill="currentColor" style="width:12px;height:12px;display:block" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>
<span>v1.9.3</span>
<span class="mockup-version">2.0.5</span>
</a>
</div>
<div class="mock-tabs">
@@ -517,7 +519,7 @@
<div style="margin-top: 16px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; flex-shrink: 0;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v1.9.3</div>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;" class="mockup-version">2.0.5</div>
</div>
</div>
</div>
@@ -762,6 +764,16 @@
<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>
</tr>
<tr>
<td class="feat-name">
<strong><span>Localisation / Traduction</span></strong>
<span class="feat-desc"><span>Langues d'interface disponibles et support de traduction.</span></span>
</td>
<td class="check highlight"><span>6 langues (et plus à venir)</span></td>
<td class="cross">
<span>Anglais uniquement</span>
</td>
</tr>
</tbody>
</table>
<div class="table-footnotes">
@@ -875,7 +887,7 @@
<picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"></picture>
<span>KoalaSync</span>
</div>
<div class="illus-popup-version">v1.9.3</div>
<div class="illus-popup-version mockup-version">2.0.5</div>
</div>
<div class="illus-popup-tabs">
<div class="illus-popup-tab active">
@@ -1177,6 +1189,6 @@
</div>
</footer>
<script src="../app.e6679b8d.min.js"></script>
<script src="../app.deb95738.min.js"></script>
</body>
</html>
+6 -4
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum / Legal Notice | KoalaSync</title>
<link rel="preload" href="style.23314305.min.css" as="style">
<link rel="stylesheet" href="style.23314305.min.css">
<link rel="preload" href="style.3ca56f45.min.css" as="style">
<link rel="stylesheet" href="style.3ca56f45.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
@@ -40,7 +40,6 @@
<picture><source srcset="assets/NewLogoIcon.avif" type="image/avif"><img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40"></picture>
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" style="width:16px;height:16px;display:block" viewBox="0 0 24 24"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg>
@@ -50,6 +49,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -62,6 +63,7 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
</div>
</div>
</nav>
@@ -190,6 +192,6 @@
</div>
</footer>
<script src="app.e6679b8d.min.js"></script>
<script src="app.deb95738.min.js"></script>
</body>
</html>
+31 -19
View File
@@ -6,9 +6,9 @@
<title>KoalaSync | Sync Netflix, YouTube & Any Video with Friends</title>
<meta name="description" content="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.">
<link rel="preload" href="style.23314305.min.css" as="style">
<link rel="preload" href="style.3ca56f45.min.css" as="style">
<link rel="preload" as="image" href="assets/LookDownKoala.webp" imagesrcset="assets/LookDownKoala-1x.webp 90w, assets/LookDownKoala.webp 205w" imagesizes="90px" fetchpriority="high">
<link rel="stylesheet" href="style.23314305.min.css">
<link rel="stylesheet" href="style.3ca56f45.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="assets/icon-192x192.png">
@@ -185,9 +185,6 @@
<span>Installed</span>
</div>
</div>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
<a href="#features"><span>Features</span></a>
<a href="#how-it-works"><span>How it works</span></a>
@@ -195,6 +192,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -207,14 +214,9 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
</nav>
@@ -222,10 +224,10 @@
<header class="hero" id="main-content">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal>
<a href="https://github.com/Shik3i/KoalaSync/blob/main/CHANGELOG.md" target="_blank" class="version-badge" data-reveal style="text-decoration: none; display: inline-block; transition: transform 0.2s, box-shadow 0.2s;" title="View Changelog">
<span class="version-text-en" lang="en">v1.5.3 OUT NOW</span>
<span class="version-text-de" lang="de">v1.5.3 JETZT VERFÜGBAR</span>
</div>
</a>
<h1 data-reveal>Watch Together.<br>Sync Perfectly.</h1>
<p class="hero-subtitle" data-reveal>Your remote movie night without lags. No registration, no data collection. Just share a link and watch together.</p>
<div class="hero-mascot-container" data-reveal>
@@ -254,7 +256,7 @@
<div class="mock-header-title"><picture><source srcset="assets/NewLogoIcon.avif" type="image/avif"><img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo"></picture>KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="mock-version-link" title="Visit GitHub Repository">
<svg fill="currentColor" style="width:12px;height:12px;display:block" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>
<span>v1.9.3</span>
<span class="mockup-version">2.0.5</span>
</a>
</div>
<div class="mock-tabs">
@@ -517,7 +519,7 @@
<div style="margin-top: 16px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; flex-shrink: 0;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v1.9.3</div>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;" class="mockup-version">2.0.5</div>
</div>
</div>
</div>
@@ -762,6 +764,16 @@
<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>
</tr>
<tr>
<td class="feat-name">
<strong><span>Localization / Translation</span></strong>
<span class="feat-desc"><span>Available interface languages and translation support.</span></span>
</td>
<td class="check highlight"><span>6 Languages (and growing)</span></td>
<td class="cross">
<span>English only</span>
</td>
</tr>
</tbody>
</table>
<div class="table-footnotes">
@@ -875,7 +887,7 @@
<picture><source srcset="assets/NewLogoIcon.avif" type="image/avif"><img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"></picture>
<span>KoalaSync</span>
</div>
<div class="illus-popup-version">v1.9.3</div>
<div class="illus-popup-version mockup-version">2.0.5</div>
</div>
<div class="illus-popup-tabs">
<div class="illus-popup-tab active">
@@ -1177,6 +1189,6 @@
</div>
</footer>
<script src="app.e6679b8d.min.js"></script>
<script src="app.deb95738.min.js"></script>
</body>
</html>
+6 -4
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Join Room | KoalaSync</title>
<link rel="preload" href="style.23314305.min.css" as="style">
<link rel="stylesheet" href="style.23314305.min.css">
<link rel="preload" href="style.3ca56f45.min.css" as="style">
<link rel="stylesheet" href="style.3ca56f45.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="assets/icon-192x192.png">
@@ -44,7 +44,6 @@
<picture><source srcset="assets/NewLogoIcon.avif" type="image/avif"><img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="64" height="64"></picture>
<span>KoalaSync</span>
</a>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="./" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" style="width:16px;height:16px;display:block" viewBox="0 0 24 24"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg>
@@ -54,6 +53,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -66,6 +67,7 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
</div>
</div>
</nav>
@@ -119,6 +121,6 @@
</div>
</footer>
<script src="app.e6679b8d.min.js"></script>
<script src="app.deb95738.min.js"></script>
</body>
</html>
+31 -19
View File
@@ -6,9 +6,9 @@
<title>KoalaSync | Sincronize Netflix, YouTube e qualquer vídeo com amigos</title>
<meta name="description" content="Assista Netflix, YouTube, Twitch e qualquer vídeo HTML5 em perfeita sincronia com amigos. Extensão de navegador gratuita e de código aberto. Sem registro.">
<link rel="preload" href="../style.23314305.min.css" as="style">
<link rel="preload" href="../style.3ca56f45.min.css" as="style">
<link rel="preload" as="image" href="../assets/LookDownKoala.webp" imagesrcset="../assets/LookDownKoala-1x.webp 90w, ../assets/LookDownKoala.webp 205w" imagesizes="90px" fetchpriority="high">
<link rel="stylesheet" href="../style.23314305.min.css">
<link rel="stylesheet" href="../style.3ca56f45.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="../assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="../assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/icon-192x192.png">
@@ -185,9 +185,6 @@
<span>Installed</span>
</div>
</div>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
<a href="#features"><span>Recursos</span></a>
<a href="#how-it-works"><span>Como funciona</span></a>
@@ -195,6 +192,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -207,14 +214,9 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
</nav>
@@ -222,10 +224,10 @@
<header class="hero" id="main-content">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal>
<a href="https://github.com/Shik3i/KoalaSync/blob/main/CHANGELOG.md" target="_blank" class="version-badge" data-reveal style="text-decoration: none; display: inline-block; transition: transform 0.2s, box-shadow 0.2s;" title="View Changelog">
<span class="version-text-en" lang="en">v1.5.3 OUT NOW</span>
<span class="version-text-de" lang="de">v1.5.3 JETZT VERFÜGBAR</span>
</div>
</a>
<h1 data-reveal>Assistam juntos.<br>Sincronia perfeita.</h1>
<p class="hero-subtitle" data-reveal>Sua noite de cinema à distância sem atrasos. Sem registro, sem coleta de dados. Apenas compartilhe o link e assistam juntos.</p>
<div class="hero-mascot-container" data-reveal>
@@ -254,7 +256,7 @@
<div class="mock-header-title"><picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo"></picture>KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="mock-version-link" title="Visit GitHub Repository">
<svg fill="currentColor" style="width:12px;height:12px;display:block" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>
<span>v1.9.3</span>
<span class="mockup-version">2.0.5</span>
</a>
</div>
<div class="mock-tabs">
@@ -517,7 +519,7 @@
<div style="margin-top: 16px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; flex-shrink: 0;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v1.9.3</div>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;" class="mockup-version">2.0.5</div>
</div>
</div>
</div>
@@ -762,6 +764,16 @@
<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>
</tr>
<tr>
<td class="feat-name">
<strong><span>Localização / Tradução</span></strong>
<span class="feat-desc"><span>Idiomas de interface disponíveis e suporte a tradução.</span></span>
</td>
<td class="check highlight"><span>6 idiomas (e crescendo)</span></td>
<td class="cross">
<span>Apenas inglês</span>
</td>
</tr>
</tbody>
</table>
<div class="table-footnotes">
@@ -875,7 +887,7 @@
<picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"></picture>
<span>KoalaSync</span>
</div>
<div class="illus-popup-version">v1.9.3</div>
<div class="illus-popup-version mockup-version">2.0.5</div>
</div>
<div class="illus-popup-tabs">
<div class="illus-popup-tab active">
@@ -1177,6 +1189,6 @@
</div>
</footer>
<script src="../app.e6679b8d.min.js"></script>
<script src="../app.deb95738.min.js"></script>
</body>
</html>
+31 -19
View File
@@ -6,9 +6,9 @@
<title>KoalaSync | Синхронизация Netflix, YouTube и любого видео с друзьями</title>
<meta name="description" content="Смотрите Netflix, YouTube, Twitch и любые HTML5-видео в синхронизации с друзьями. Бесплатное расширение браузера с открытым кодом. Регистрация не требуется.">
<link rel="preload" href="../style.23314305.min.css" as="style">
<link rel="preload" href="../style.3ca56f45.min.css" as="style">
<link rel="preload" as="image" href="../assets/LookDownKoala.webp" imagesrcset="../assets/LookDownKoala-1x.webp 90w, ../assets/LookDownKoala.webp 205w" imagesizes="90px" fetchpriority="high">
<link rel="stylesheet" href="../style.23314305.min.css">
<link rel="stylesheet" href="../style.3ca56f45.min.css">
<link rel="icon" type="image/png" sizes="16x16" href="../assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="../assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/icon-192x192.png">
@@ -185,9 +185,6 @@
<span>Installed</span>
</div>
</div>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div class="nav-links" id="primary-nav" role="navigation" aria-label="Primary">
<a href="#features"><span>Функции</span></a>
<a href="#how-it-works"><span>Как это работает</span></a>
@@ -195,6 +192,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M12 0C5.374 0 0 5.373 0 12c0 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.23A11.5 11.5 0 0 1 12 5.803c1.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.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
</div>
<div class="nav-right">
<div class="lang-select-container">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="globe-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10"/></svg>
<select class="lang-dropdown" aria-label="Select Language">
@@ -207,14 +214,9 @@
</select>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="chevron-icon" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</div>
<nav class="sr-only" aria-label="Language alternatives">
<a href="https://sync.koalastuff.net/" hreflang="en">English</a>
<a href="https://sync.koalastuff.net/de/" hreflang="de">Deutsch</a>
<a href="https://sync.koalastuff.net/fr/" hreflang="fr">Français</a>
<a href="https://sync.koalastuff.net/es/" hreflang="es">Español</a>
<a href="https://sync.koalastuff.net/pt-BR/" hreflang="pt-br">Português (Brasil)</a>
<a href="https://sync.koalastuff.net/ru/" hreflang="ru">Русский</a>
</nav>
<button class="hamburger" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="primary-nav">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" aria-hidden="true" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
</nav>
@@ -222,10 +224,10 @@
<header class="hero" id="main-content">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal>
<a href="https://github.com/Shik3i/KoalaSync/blob/main/CHANGELOG.md" target="_blank" class="version-badge" data-reveal style="text-decoration: none; display: inline-block; transition: transform 0.2s, box-shadow 0.2s;" title="View Changelog">
<span class="version-text-en" lang="en">v1.5.3 OUT NOW</span>
<span class="version-text-de" lang="de">v1.5.3 JETZT VERFÜGBAR</span>
</div>
</a>
<h1 data-reveal>Смотрите вместе.<br>Синхронно на 100%.</h1>
<p class="hero-subtitle" data-reveal>Ваш киновечер на расстоянии без задержек. Без регистрации и сбора данных. Просто поделитесь ссылкой и смотрите вместе.</p>
<div class="hero-mascot-container" data-reveal>
@@ -254,7 +256,7 @@
<div class="mock-header-title"><picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo"></picture>KoalaSync</div>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="mock-version-link" title="Visit GitHub Repository">
<svg fill="currentColor" style="width:12px;height:12px;display:block" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>
<span>v1.9.3</span>
<span class="mockup-version">2.0.5</span>
</a>
</div>
<div class="mock-tabs">
@@ -517,7 +519,7 @@
<div style="margin-top: 16px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; flex-shrink: 0;">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;">GitHub Repository</a>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v1.9.3</div>
<div style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;" class="mockup-version">2.0.5</div>
</div>
</div>
</div>
@@ -762,6 +764,16 @@
<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>
</tr>
<tr>
<td class="feat-name">
<strong><span>Локализация / Перевод</span></strong>
<span class="feat-desc"><span>Доступные языки интерфейса и поддержка перевода.</span></span>
</td>
<td class="check highlight"><span>6 языков (и расширяется)</span></td>
<td class="cross">
<span>Только английский</span>
</td>
</tr>
</tbody>
</table>
<div class="table-footnotes">
@@ -875,7 +887,7 @@
<picture><source srcset="../assets/NewLogoIcon.avif" type="image/avif"><img src="../assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="14" height="14"></picture>
<span>KoalaSync</span>
</div>
<div class="illus-popup-version">v1.9.3</div>
<div class="illus-popup-version mockup-version">2.0.5</div>
</div>
<div class="illus-popup-tabs">
<div class="illus-popup-tab active">
@@ -1177,6 +1189,6 @@
</div>
</footer>
<script src="../app.e6679b8d.min.js"></script>
<script src="../app.deb95738.min.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "1.9.3",
"date": "2026-05-30T00:01:24Z"
"version": "2.0.5",
"date": "2026-06-03T09:32:00Z"
}