Compare commits

...

191 Commits

Author SHA1 Message Date
Timo ebf3178e32 feat: multi-video overview in debug report
- GET_VIDEO_STATE now returns allVideos[] with summary per video element
- Copy report shows markdown table when multiple videos on page
- Table includes: resolution, muted, paused, readyState, duration, and marks selected target
- Helps diagnose Prime Video scenarios where preview vs main video both exist
2026-06-01 15:58:31 +02:00
Timo ba96cf2765 fix: prioritize largest video element, fix history action field in debug report
- findVideo() now scores all video elements by size + unmuted + duration
- Fixes Prime Video selecting 0x0 placeholder instead of actual player
- Fix history entries showing '?' by using h.action instead of h.event/h.type
- Update Prime Video status in TESTED_SERVICES.md to partial support
2026-06-01 15:57:11 +02:00
GitHub Action d026ed891a chore(release): update versions to v2.0.0 [skip ci] 2026-06-01 13:50:26 +00:00
Timo d6665fe0f5 feat: v2.0.0 - enhanced debug reports, platform detection, copy report, mockup i18n, new icons
- Copy Logs button now generates full Markdown debug report (system, tab, connection, video, history, logs)
- Enhanced Dev tab with 20+ video state fields (network, dimensions, error, shadow DOM, platform detection)
- Platform auto-detection (YouTube, Netflix, Twitch, Prime Video, Disney+, Hulu, HBO, Vimeo, Dailymotion)
- 'No video found' diagnostic mode with hints
- Convert mockup from inline <span lang> to {{MOCK_*}} template vars (all 6 languages)
- New TwoPointZero branding: icons (16/32/48/96/128), favicons (PNG 16/32 + apple-touch + 192)
- Fix logo vertical centering via display:contents on picture wrappers
- Increase popup logo to 48px, website nav logo to 64px
- AVIF quality 70->80, speed 6->4, remove min 3KB threshold
- Add 32+96 icon sizes to manifest
- Remove amazon. from tab blacklist (unblocks Prime Video)
- Update TESTED_SERVICES.md: mark Prime Video as non-functional
- Update CONTRIBUTING.md with Copy Debug Report workflow
2026-06-01 15:49:55 +02:00
Timo 79025f4d18 feat: new TwoPointZero branding - icons, favicon, mockup i18n
- Replace extension icons with new TwoPointZero design (16/32/48/96/128)
- Update manifest.base.json with 32+96 icon sizes
- Increase popup logo to 48px, website nav logo to 64px
- Generate website webp sizes (64/128/200) + AVIF variants from 600px source
- Add favicon PNGs (16x16, 32x32) + apple-touch-icon + 192x192
- Fix logo vertical centering via display:contents on picture wrappers
- Convert mockup section from inline <span lang> pairs to {{MOCK_*}} template vars
- Add MOCK translations for FR, ES, PT-BR, RU locale files
- Bump AVIF quality 70->80, speed 6->4, remove min 3KB threshold
- Regenerate www/ with build.js
2026-06-01 15:29:33 +02:00
Timo a1f921407c Smart disconnect + human-readable room IDs + expanded word lists
- Add currentServerUrl tracking: only disconnect/reconnect when server URL actually
  changes, otherwise reuse existing connection and just switch rooms
- Add resolveServerUrl() helper for consistent URL resolution across handlers
- forceDisconnect() now resets currentServerUrl to null
- CONNECT and WEB_JOIN_REQUEST handlers: smart comparison before reconnecting
- Add generateRoomId(): human-readable 'ADJECTIVE-NOUN-Number' format (e.g. HAPPY-KOALA-16)
- Replace Math.random() room IDs with generateRoomId() in joinBtn and handleCreateRoom()
- Expand USERNAME_ADJECTIVES: +16 words (Turbo, Zen, Pixel, Cyber, Solar, etc.)
- Expand USERNAME_NOUNS: +12 words (Yeti, Goblin, Pirate, Ninja, Wizard, Storm, etc.)
- Add emoji mappings for all new words
2026-06-01 13:29:02 +02:00
Timo f2669ed769 docs: add relay reachability check hint to readme 2026-06-01 13:27:18 +02:00
Timo 7a6ec8087e Fix WEB_JOIN_REQUEST and joinBtn: custom server invite + password generation
- WEB_JOIN_REQUEST handler: forceDisconnect+connect instead of reusing old socket
  (same bug as CONNECT: JOIN_ROOM was sent to official server instead of custom)
- joinBtn handler: generate random password when roomId is empty and password empty
  (previously created rooms without passwords when using the advanced join button)
- Auto-copy invite link when creating room via joinBtn (consistent with Create Room button)
2026-06-01 13:20:45 +02:00
Timo 8cc622bda0 Fix custom server reconnection and reconnect strategy
- Fix: CONNECT/RETRY_CONNECT always force-disconnect before reconnecting
  (prevents JOIN_ROOM going to official server when custom is selected)
- Add forceDisconnect() helper: clears socket, eventQueue, episodeLobby,
  expectedAcksCount, broadcast status, persists cleanup to storage
- Fix: save useCustomServer to storage on Join Room click (no race condition)
- Fix: trigger RETRY_CONNECT on server mode toggle and custom URL change
- Fix: show error when Custom selected but no URL entered (no silent fallback)
- Fix: button label changed to 'Join / Create Room' in all 6 locales
- Rewrite scheduleReconnect() for two-phase strategy:
  Phase 1 (aggressive): 500ms-5s backoff, max 20 attempts or 5 minutes
  Phase 2 (slow): retry every 5 minutes indefinitely, never give up
- Persist reconnectAttempts/reconnectStartTime/reconnectFailed to session storage
- Remove 'reconnect_failed' status; slow mode shows as 'reconnecting' with retry button
- Add scheduleReconnect() call on offline path to continue retry cycle
2026-06-01 13:06:40 +02:00
KoalaDev 2b5da0dbb7 Modify sync status for YouTube and Twitch
Updated sync status for YouTube and Twitch to indicate episode auto-sync issues.
2026-06-01 12:25:58 +02:00
Timo 1a7ff6b3a7 docs: shorten compose example link text in readme 2026-06-01 12:24:09 +02:00
Timo b1b858d771 docs: replace single docker-compose example with caddy + static ip variants 2026-06-01 12:16:25 +02:00
Timo 608f742f83 fix(i18n): resolve hardcoded strings, missing translation keys, SW notification race conditions, and toast username bugs 2026-06-01 05:47:08 +02:00
Timo b2ff24e155 SEO: add crawlable language navigation links for Google indexing
- Add sr-only CSS class for screen-reader/crawler-visible elements
- Add hidden <nav> with real <a> links to all 6 language versions in template.html
- Googlebot can now discover /de/, /fr/, /es/, /pt-BR/, /ru/ via HTML links
- No visual changes - existing <select> dropdown remains the user-facing switcher
- Rebuild all www/ output with updated CSS hash
2026-06-01 05:36:06 +02:00
Koala 9285e1041e seo: per-locale HowTo schema (8 new keys × 6 languages)
HowTo schema was hardcoded English — now uses {{HOWTO_*}}
placeholders. All 3 rich-result schemas are now locale-aware:
  - FAQPage  (expandable SERP snippets)
  - HowTo    (step-by-step SERP snippets)
  - SoftwareApplication (entity understanding)

SoftwareApplication and Organization use language-neutral
brand name — no locale keys needed.
2026-06-01 04:02:21 +02:00
Koala 3ac110f8f5 seo: update sitemap lastmod + add xhtml:link hreflang alternates
- lastmod bumped to 2026-06-01 for all 6 language roots
- xhtml:link hreflang entries added per URL (Google-recommended
  belt-and-suspenders approach alongside HTML hreflang tags)
2026-06-01 03:58:17 +02:00
Koala cc8c58fac5 seo: per-locale FAQPage schema + Q6 self-hosting mention
- FAQPage schema now uses {{FAQ_Q1}}..{{FAQ_A6}} placeholders
  instead of hardcoded English — each language gets its own
  schema for locale-matched Google Rich Snippets
- Q6 updated across all 6 languages: 'What if I don't trust
  the official server?' + self-hosting Docker mention
2026-06-01 03:56:58 +02:00
Koala 53768980d0 docs: add TESTED_SERVICES.md compatibility matrix; update FAQ Q1
- TESTED_SERVICES.md: table of tested platforms with sync/title/auto-sync columns
- FAQ Q1: explicitly mention Emby + Jellyfin, remove Disney+/Prime mention
- All 6 locale files updated (Q1+A1)
- README links to TESTED_SERVICES.md
2026-06-01 03:54:52 +02:00
Koala 1b04db009a fix: add *.avif to Caddy static cache matcher
PageSpeed Insights flagged AVIF files as uncached because the
@static matcher only included *.webp. Adding *.avif enables
1-year Cache-Control for all AVIF assets.
2026-06-01 03:51:03 +02:00
Koala a2720da5f8 seo: add visible FAQ section with FAQPage schema (6 keyword-targeted Q&As)
Targets high-volume search queries:
- netflix watch2gether / netflix watch in sync
- free teleparty alternative / teleparty alternative
- emby syncplay / jellyfin sync
- watch together extension browsers
- privacy-friendly watch party

Native <details> elements — no JS, Google-visible, Rich-Snippet eligible.
All 6 languages translated (EN/DE/FR/ES/PT-BR/RU).
2026-06-01 03:49:49 +02:00
Koala d2c380d6fc fix: revert store URLs from SoftwareApplication sameAs
Google does not support sameAs for app store links in SoftwareApplication
rich results. sameAs is for identity pages only (Wikipedia, Wikidata, GitHub).
Store pages are independently indexed by Google and need no schema linking.
2026-06-01 03:41:01 +02:00
Koala 1a36b138c0 seo: add Chrome Store and Firefox Add-on URLs to SoftwareApplication sameAs
Google treats SoftwareApplication.sameAs as app listing references
(not social profiles like Organization.sameAs). This connects
KoalaSync's entity graph to both browser stores without
duplication risk.
2026-06-01 03:37:47 +02:00
Koala 47ead27344 docs: improve CONTRIBUTING.md and SECURITY.md readability
- CONTRIBUTING: add ways to contribute table, project structure, setup guide
- CONTRIBUTING: add website testing, translation guide, bug report template
- SECURITY: add scope/out-of-scope, architecture overview, threat model
- SECURITY: structured reporting timeline, responsible disclosure policy
2026-06-01 03:21:59 +02:00
Koala 72180ba817 feat(website): SEO, a11y, and build pipeline overhaul
- SEO: per-locale og:locale, Organization+WebSite schema, FAQPage replacing ItemList
- SEO: BreadcrumbList on legal pages, site.webmanifest, expanded sitemap
- a11y: hamburger ARIA+ESC, skip-to-content, aria-hidden on step-num
- a11y: focus-visible states, prefers-reduced-motion, emoji→SVG icons
- CSS: extract .mock-section-label, replace transition:all, will-change hints
- Build: esbuild JS minifier (-46%), sharp AVIF conversion (26 files, quality 70)
- Build: content hashing (style.XXXX.min.css), SVG minification, picture injection
- Cleanup: remove lightningcss (caused CSS merging bugs)
2026-06-01 03:20:51 +02:00
Koala 9eab699e2a feat(website): output minified files as .min.* for safety and clarity 2026-06-01 02:14:31 +02:00
Koala c95e72b713 docs: document build minifier and warn against editing www/ directly 2026-06-01 02:12:05 +02:00
Koala 14be38fe11 feat(website): add build-time minification and optimize meta descriptions 2026-06-01 02:10:27 +02:00
Koala 1bb2123da2 feat(website): technical SEO and UI/UX accessibility improvements
- Add LCP image preload for hero mascot
- Add HowTo structured data schema (3-step install guide)
- Update SoftwareApplication schema with screenshot + image fields
- Add glassmorphic :focus-visible outlines for keyboard accessibility
- Add content-visibility: auto to heavy offscreen sections for INP
- Expand lang-init.js auto-redirect to all 6 supported languages
- Remove impressum/datenschutz from sitemap.xml (noindex conflict)
2026-06-01 01:57:16 +02:00
Koala e29c6666b6 feat(website): redesign bento tiles in 'Why KoalaSync' section
- Swap last two tiles: Invitation becomes 2nd-last (bento-large),
  Self-Hosting becomes last small tile
- bento-large layout: icon centered above heading, 33/67 grid split
- Fix icon consistency on small tiles (uniform 44x44 rounded squares)
- Improve responsive breakpoints for all viewports
- Delete legacy website/index.html (obsoleted by template + build.js)
2026-06-01 01:42:42 +02:00
Koala b08e8ba06b docs: update advanced caddyfile configuration for syncserver in example and website 2026-06-01 01:20:31 +02:00
Koala 30a4057c99 docs: update documentations to reflect browser extension i18n features 2026-06-01 01:07:41 +02:00
Koala fd47eb82b9 feat(extension): implement full dynamic i18n support and automated consistency checks 2026-06-01 01:05:46 +02:00
Koala 3fcafbd081 fix(eslint): resolve all 6 repository lint errors
- Removed unused html variable from localizeHomeLinks in website/app.js.
- Added website/build.js to ESLint flat config Node files match pattern to define require, process, and __dirname.
2026-06-01 01:04:47 +02:00
Koala d430501b82 feat(website): update sitemap.xml to index clean URLs
- Replaced /impressum.html and /datenschutz.html with clean URLs /impressum and /datenschutz in sitemap.xml.
- Recompiled website to copy clean sitemap to www/ folder.
2026-06-01 00:23:54 +02:00
Koala d738352c90 feat(website): implement Clean URLs and split Caddy configurations
- Removed all .html file extension suffixes from website and invite links (impressum, datenschutz, join).
- Updated app.js dynamic localizer and path switching to target Clean URLs ('./', '../', etc.).
- Refactored Caddyfile.example into simple (lightweight try_files) and advanced (hardened production) options.
- Split Caddy configs into Caddyfile (Simple) and Caddyfile (Advanced) tabs on the landing page.
2026-06-01 00:21:29 +02:00
Koala dbf1b3e81b docs(translation): refine TRANSLATION.md for better clarity and human readability
- Restructured translation guide with clean markdown layout and tables.
- Removed mermaid sequence diagram to improve text-based clarity.
- Audited steps for contributing new language keys.
2026-05-31 23:54:44 +02:00
Koala 5a3f1c7019 docs(website): update Caddyfile configuration and website README for www/ path
- Corrected static server root path to website/www/ in Caddyfile.example and website/README.md.
- Updated core roles to reflect multi-language (EN/DE/FR/ES/PT-BR/RU) support.
- Refactored local development and compilation instructions to target build.js and www/ output.
2026-05-31 23:50:49 +02:00
Koala 57b0dd1632 feat(website): implement custom i18n static compiler & full 6-language expansion
- Added pure Node.js dynamic i18n static site generator (build.js).
- Structured locales for English, German, French, Spanish, Brazilian Portuguese, and Russian.
- Replaced two-state toggle with premium glassmorphic language select dropdown.
- Integrated robust segment-based locale routing with safe dynamic fallbacks for legal and invite pages.
- Audited Core Web Vitals (LCP preloads, CLS dimensions) and SEO structures (robots, sitemap).
- Added dedicated Localization section to README and created contributor TRANSLATION guide.
2026-05-31 23:49:36 +02:00
Koala b6f7c1ccdb refactor(datenschutz): unify email obfuscation to .email-reveal (scraper-safe) 2026-05-31 00:47:39 +02:00
Koala 4bada9533a feat(impressum): add name (Timo/KoalaDev), Mastodon for private msgs, GitHub Issues for concerns 2026-05-31 00:43:58 +02:00
Koala df32385fa6 fix(website): correct Mastodon SVG icon path to official branding 2026-05-31 00:22:47 +02:00
Koala ce9778245d feat(website): add Mastodon profile link (rel=me) to all page footers 2026-05-31 00:16:18 +02:00
Koala 5d6c0cd1fb fix(seo): shorten title (<70 chars), meta description (<160 chars), fix duplicate h1 2026-05-30 21:43:32 +02:00
Koala bbba50f643 fix(website): add responsive srcset to nav logo (40w/80w/200w) 2026-05-30 21:26:36 +02:00
Koala 56293209f8 fix(website): switch from x-descriptors to w-descriptors + sizes for responsive images to satisfy PageSpeed Insights 2026-05-30 21:21:51 +02:00
Koala c55691a535 feat(website): add responsive images with srcset (1x/2x WebP) for all mascot and illustration images 2026-05-30 21:16:09 +02:00
Koala ce3b17f55f feat(website): add hreflang tags, comparison JSON-LD schema, and update sitemap with lastmod 2026-05-30 20:58:35 +02:00
Koala 5e8e9f61c2 fix(website): use Web Animations API for hero button pulse, avoids CSS transition/inline-style conflicts from mouse handlers 2026-05-30 20:50:34 +02:00
Koala 59eb1a6092 fix(website): apple-mobile-web-app-capable deprecated meta tag + download button pulse animation 2026-05-30 20:45:08 +02:00
Timo 00c9ff8cfc feat(website): Add smart browser-badge breathing animation, nav installed indicator, clickable smooth-scroll footnotes, and mockup height layout adjustments 2026-05-30 20:35:48 +02:00
Timo 748ccaa835 Refactor(website): Overhaul Getting Started section with extension-accurate workflows, premium CSS animations, stacked browser badges, and refined German translations 2026-05-30 20:25:36 +02:00
Timo 25b23d083d feat(website,extension): integrate NewLogoIcon as WebP favicon/header logo and convert to PNG extension icons 2026-05-30 20:14:59 +02:00
Timo 74042c3c78 docs(readme): replace header icon with Juggler mascot and feat(website): optimize technical SEO & add mobile theme colors 2026-05-30 19:55:25 +02:00
Timo fb1451022b feat(website): integrate KoalaDeploy mascot, enlarge Juggler mascot by 15%, and add premium interactive hover effects to all main mascots 2026-05-30 19:48:53 +02:00
Timo 54ffbfa4bd feat(website): integrate KoalaSearching, KoalaImprintl, and KoalaPrivacy mascots with custom animations and tight layouts 2026-05-30 19:05:35 +02:00
Timo 0db0b5eea2 feat(website): add KoalaThumbsUp and KoalaThumbsDown status mascots on invite page, hide pulsing ring, and support dev simulation mode 2026-05-30 18:22:05 +02:00
Timo d58041bd4e feat(website): add lookdown koala to hero, questions koala to features, pro-contra koala to comparison, translate learning tile to Gemeinsam Lernen, and refine copy 2026-05-30 17:50:03 +02:00
Timo 1f1b99a1fe feat(website): add koala holding git page next to octocat in bottom CTA and improve LDR couple alt text description 2026-05-30 16:57:31 +02:00
Timo c608d2d9e9 style(website): standardize use-case image container heights to 150px and set object-fit contain to align headings 2026-05-30 16:40:55 +02:00
Timo 06203eb5cb feat(website): replace LDR emoji with hugging koala couple and translate title to Fernbeziehungen 2026-05-30 16:39:03 +02:00
Timo ed599a96ab feat(website): replace remote learning emoji with professor koala mascot in use cases grid 2026-05-30 16:11:44 +02:00
Timo 5d1707a7d7 feat(website): replace movie night emoji with popcorn-sharing koalas mascot and optimize card padding 2026-05-30 15:59:38 +02:00
Timo f4343ca6b1 feat(website): add juggling koala mascot to platforms section and optimize spacing 2026-05-30 15:43:01 +02:00
Koala 3644a7b8ac website: refactor interactive extension mockup and align styling with popup UI
- Update mockup tabs layout, colors, inputs, cards, and spacing to perfectly mirror popup.html 1-to-1.
- Enable full interactivity for toggle switches in Settings mockup screen.
- Link the top mockup version badge directly to the GitHub repository with smooth CSS hover transitions and cursor pointer.
- Port all hover tooltips (title attributes) from popup.html to the mockup index.html.
- Add mandatory caveman communication protocol to AI_INIT.md onboarding instructions.
2026-05-30 13:14:47 +02:00
Koala c621c851af style(website): optimize SEO, UX, accessibility, and navigation icons 2026-05-30 11:49:04 +02:00
Koala 1113fb8bb5 Fix app.js brace syntax error and remove duplicate tbody in index.html 2026-05-30 11:24:13 +02:00
Koala c852826091 Enhance Join page with animated radar status indicator, resolve silent status icon bug, and clean inline script button styles to CSS 2026-05-30 11:20:40 +02:00
Koala cfef79ec1d Refactor comparison table categories to be neutral, consolidate privacy footnotes, and add comparison date stamp 2026-05-30 11:18:06 +02:00
Koala 8fbada8b03 Add clickable proof footnotes to Teleparty comparison table linking to official policy pages 2026-05-30 11:14:04 +02:00
Koala 2261a8133a Refactor landing page use-cases, browser CTA badging, official brand SVGs, and streamlined competitor comparison 2026-05-30 11:13:29 +02:00
GitHub Action d5241b19e8 chore(release): update versions to v1.9.3 [skip ci] 2026-05-30 00:01:24 +00:00
Timo f7096edd30 Refactor: Behebung Force-Sync ACK-Zaehler-Inflation, SHA-256 Hashing, Room-ID-Desinfizierung, Reduzierung MIN_SEEK_DELTA auf 2s und seekDebounce auf 300ms 2026-05-30 02:00:17 +02:00
GitHub Action e2b76b05a1 chore(release): update versions to v1.9.2 [skip ci] 2026-05-29 23:35:36 +00:00
Timo 93acd0b44c fix: resolve F-01 to F-08 audit bugs and QoL improvements 2026-05-30 01:34:48 +02:00
GitHub Action ce7b5c47f2 chore(release): update versions to v1.9.1 [skip ci] 2026-05-29 22:08:39 +00:00
Timo 92fb8e2d00 Roll back network latency compensation to maximize simplicity and clock skew safety 2026-05-30 00:06:52 +02:00
Timo 43cde9bbef Optimize network latency compensation cap to protect against system clock skew 2026-05-30 00:03:56 +02:00
Timo 56955027f9 Fix critical sync lockouts, zombie disconnects, play-seek debounces, active lobbies, SW keep-alives, branding matches and ESLint warnings 2026-05-30 00:02:19 +02:00
GitHub Action 5ef059a94f chore(release): update versions to v1.9.0 [skip ci] 2026-05-28 02:55:04 +00:00
Koala 98b4fc5fb4 release: v1.9.0 — command sequencing, episode-aware sync, echo suppression
- Add per-peer monotonically increasing seq numbers (localSeq + lastSeqBySender)
- Server relays seq for stale command detection (backward compatible)
- Replace expectedEvents (1500ms timeout) with _suppressTimers (per-type, 300ms)
- Fix FORCE_SYNC_ACK missing seq (stale-ACK guard)
- Fix episode lobby readyPeers asymmetry (initiator now included)
- Add extractEpisodeId() parsing S01E01, Season 1 Ep 1, Folge 5, Ep. 3, #42
- Add isDifferentEpisode() guard blocking cross-episode sync commands
- Add sameEpisode() for format-tolerant lobby title matching
- Guard controlled via autoSyncNextEpisode setting
- Reduce reactive lock 1000ms → 300ms
- Fix routeToContent unbounded retry (max 3 attempts)
- Fix server bcrypt.hash failure crashing join flow
- Fix sameEpisode(null, title) returning true
- Fix forceSyncTimeout leak on rapid force sync clicks
- Persist lastSeqBySender across service worker restarts
- Bump version: 1.8.10 → 1.9.0
2026-05-28 04:54:54 +02:00
Koala 762d6425be fix: audit corrections for sameEpisode, CMD_ACK, lastSeqBySender, forceSyncTimeout
- sameEpisode: return false when one title is null (prevent false lobby completions)
- Episode guard: skip CMD_ACK for FORCE_SYNC_EXECUTE on mismatch (H1)
- Persist lastSeqBySender in chrome.storage.session (M3, survives SW restart)
- Clear forceSyncTimeout before overwriting (H3, pre-existing timer leak)
2026-05-28 04:45:52 +02:00
Koala 1fba2fb69c perf: add command sequence numbers, episode-aware sync guard, fix echo suppression
- Add monotonically increasing seq per peer (localSeq + lastSeqBySender)
- Server relays seq field for stale command detection (backward compatible)
- Replace expectedEvents (1500ms timeout) with _suppressTimers (per-type, 300ms)
- Fix FORCE_SYNC_ACK missing seq (stale-ACK could trigger premature exec)
- Fix episode lobby readyPeers asymmetry (initiator never broadcasted readiness)
- Add extractEpisodeId() parsing S01E01/Season 1 Episode 1/Folge 5 patterns
- Add isDifferentEpisode() guard blocking sync commands across different episodes
- add sameEpisode() for format-tolerant lobby title matching
- Guard controlled by autoSyncNextEpisode setting with disable hint in warning
- Reduce reactive lock from 1000ms to 300ms (seq numbers handle ordering)
2026-05-28 04:30:32 +02:00
Koala eca259281a feat: expand emoji/name system with comprehensive animal mapping
- Create shared/names.js as single source of truth for emoji maps, noun
  lists, adjectives, and username generation (215 animal emoji entries)
- Remove duplicated inline lists from popup.js and background.js; both
  now import getAvatarForName / generateUsername from shared/names.js
- Add names.js to the build script sync list so it stays DRY across
  extension builds
- Fix missing cat emoji: CoolCat now correctly resolves to 🐱
- Sort emoji map keys by length at lookup time to prevent substring
  false-matches (e.g. 'caterpillar' before 'cat')
2026-05-28 03:35:17 +02:00
GitHub Action afd28be2e6 chore(release): update versions to v1.8.10 [skip ci] 2026-05-26 15:42:24 +00:00
Koala cc0265c836 v1.8.10: fix description length, solo peer detection, tab-close peer notify, lastHeartbeat guard, time interpolation stale guard 2026-05-26 17:42:11 +02:00
GitHub Action 6ebff9ab4c chore(release): update versions to v1.8.9 [skip ci] 2026-05-26 01:07:16 +00:00
Koala 35e779c1ff move version link outside h1 to avoid inherited heading styles 2026-05-26 03:00:29 +02:00
Koala 09f0e04891 add clickable version with GitHub icon to popup header 2026-05-26 02:58:36 +02:00
Koala 9eff53ba46 chore: remove CSP-blocked inline handlers from landing page 2026-05-26 02:47:45 +02:00
GitHub Action bde2f7ea55 chore(release): update versions to v1.8.8 [skip ci] 2026-05-26 00:34:04 +00:00
Koala 901861269a fix: Firefox invite — cloneInto() for CustomEvent detail (XrayWrapper) 2026-05-26 02:33:54 +02:00
GitHub Action ba91e2744c chore(release): update versions to v1.8.7 [skip ci] 2026-05-26 00:06:39 +00:00
Koala b7a44024ab fix: suppress ghost seek events on tab re-focus (Firefox tab throttling) 2026-05-26 02:06:23 +02:00
Koala 3c49bfe54c feat: SEO — add Emby/Jellyfin to descriptions, optimize meta tags, fix APP_VERSION 2026-05-26 01:51:34 +02:00
GitHub Action d2ea7c7423 chore(release): update versions to v1.8.6 [skip ci] 2026-05-25 23:07:55 +00:00
Koala db11812bd6 fix: suppress seek events when solo, add moz-extension CORS, add server logging 2026-05-26 01:06:04 +02:00
GitHub Action 6db8fdbf75 chore(release): update versions to v1.8.5 [skip ci] 2026-05-25 21:56:58 +00:00
Koala d23c37f87f chore(release): v1.8.5 2026-05-25 23:56:50 +02:00
Koala acd428d4f7 fix: audit fixes — auto-match title corruption, duplicate HTML attrs, stale ℹ️ icons, missing tooltip, remove fix_ui.js 2026-05-25 23:56:14 +02:00
Koala c0a6f0adc2 ui: clipboard emoji for invite button, movie emoji for audible tab indicator 2026-05-25 23:56:14 +02:00
GitHub Action 42b73bb97f chore(release): update versions to v1.8.3 [skip ci] 2026-05-25 21:43:51 +00:00
Koala dc36bfdded chore(release): v1.8.3 2026-05-25 23:43:41 +02:00
Koala 06db850387 fix(ui): add comprehensive tooltips to all labels, buttons and inputs 2026-05-25 23:43:09 +02:00
Koala 0de92b5b61 fix(ui): tooltips, full emoji map, and onboarding layout 2026-05-25 23:37:21 +02:00
GitHub Action 8ff8e7beb6 chore(release): update versions to v1.8.1 [skip ci] 2026-05-25 21:32:16 +00:00
Koala 8317099072 chore(release): prep for v1.8.1 2026-05-25 23:32:07 +02:00
Koala 04c63dcf68 fix(ui): add missing avatar emoji in episode lobby UI 2026-05-25 23:30:26 +02:00
Koala fd23ccc23a feat(ui): v1.8.1 visual upgrades (emojis, animations, audio indicator) 2026-05-25 23:29:06 +02:00
Koala 4d5caeda9e style(ui): redesign sync button layout and rename to SYNC 2026-05-25 23:20:37 +02:00
Koala f1f41e5cac fix(ui): apply tooltip to full label text for settings 2026-05-25 23:19:23 +02:00
GitHub Action 473eacda22 chore(release): update versions to v1.8.0 [skip ci] 2026-05-25 21:09:07 +00:00
Koala 42029f86bf chore(release): prep for v1.8.0 2026-05-25 23:08:55 +02:00
Koala 4c4a2638d7 docs: rearrange README to prioritize user quick start 2026-05-25 23:06:58 +02:00
Koala 5b57970c4c docs: use for-the-badge style for download links 2026-05-25 23:05:22 +02:00
Koala 61492f953b docs: Update README quick start for users 2026-05-25 23:04:14 +02:00
Koala da6a1cc643 Server updates 2026-05-25 22:59:54 +02:00
Koala 62fdffa5ee UX Improvements: Onboarding, UI Feedback, Auto-Copy Invite 2026-05-25 22:59:54 +02:00
GitHub Action fb13978c9d chore(release): update versions to v1.7.5 [skip ci] 2026-05-25 11:26:16 +00:00
Koala 82c09a5328 fix(popup): use hostname-aware domain matching to prevent false positives
The noise filter used naive String.includes() on the full URL, causing
blacklist entry 'x.com' to match netflix.com (since 'netflix.com'
contains 'x.com' as substring). This made Netflix tabs disappear when
the filter was enabled.

Fixed by extracting the URL hostname and matching at domain boundaries:
- Domain entries (x.com): hostname === domain || endsWith('.' + domain)
- Prefix entries (amazon.): startsWith || includes('.' + domain)
- Keyword entries (jira): substring fallback on full URL

Release v1.7.5
2026-05-25 13:26:01 +02:00
Koala 2fbeafeb3f fix(website): 'any' → 'almost any' for honesty 2026-05-25 13:01:59 +02:00
Koala 80f8c821cb docs: clarify that website and documentation changes do not need release tags 2026-05-25 13:00:38 +02:00
GitHub Action 7d3965a9fd chore(release): update versions to v1.7.4 [skip ci] 2026-05-25 10:59:16 +00:00
Koala 1aca6c37d4 fix(website): replace misleading 'local MP4s' with accurate platform list
The hero subtitle claimed support for 'local MP4s' which makes no sense
for a browser-based synchronization tool (local files can't be synced
across different computers). Changed to accurately reflect actual platform
support: YouTube, Twitch, Netflix, and any website with a video player.
2026-05-25 12:59:00 +02:00
GitHub Action aa740592dd chore(release): update versions to v1.7.3 [skip ci] 2026-05-25 10:52:55 +00:00
Koala 6f8bcf8478 fix(popup): replace innerHTML with DOM API to fix Firefox warnings
Firefox flagged unsafe innerHTML assignments:
- renderEmpty() container.innerHTML with template literal
- renderOnboardingStep() dots.innerHTML with mapped content
- updateLastActionUI() lastActionCard.innerHTML static string
- refreshRooms click handler publicRooms.innerHTML

Replaced all with createElement + textContent/replaceChildren.
2026-05-25 12:52:33 +02:00
GitHub Action 807a620fe9 chore(release): update versions to v1.7.2 [skip ci] 2026-05-25 10:46:16 +00:00
Koala 6e138f51d3 docs: enforce tag immutability and force-push policy in AI_INIT.md
- Tags are permanent once pushed; never reuse or move existing tags
- If a release is missing a fix, increment version and create new tag
- Force pushing (branches or tags) requires explicit user confirmation
- Added to Section 10 (Release Workflow) for AI agents
2026-05-25 12:43:36 +02:00
GitHub Action ed50e354ab chore(release): update versions to v1.7.0 [skip ci] 2026-05-25 10:40:34 +00:00
Koala 48bd503b5c fix(popup): seed lastKnownPeers in init() to prevent false join toasts
init() called updatePeerList() directly but never set lastKnownPeers.
On the first PEER_UPDATE message (heartbeat, play/pause, any state
change), detectPeerChanges() saw lastKnownPeers=[] and treated ALL
peers including self as newly joined — triggering 'name joined the room'
toast on every action while the popup was open.
2026-05-25 12:40:05 +02:00
GitHub Action 35351bdacd chore(release): update versions to v1.7.0 [skip ci] 2026-05-25 10:30:48 +00:00
Koala a0063d42b1 docs: consolidate duplicate auth failure retention rows in privacy table 2026-05-25 12:30:20 +02:00
Koala 8b3f9e1242 fix: sync FORCE_SYNC_TIMEOUT constant with actual usage, tokenize showError cleanup, fix EVENT_ACK indent
- FORCE_SYNC_TIMEOUT in shared/constants.js was 5000 but all code uses
  8500ms. Update constant to 8500 and reference it in background.js
  instead of hardcoded values (4 occurrences)
- Add errorToken counter in popup showError() so stale 5s timeout
  doesn't clear styling of a newer error that arrived in between
- Fix EVENT_ACK handler indentation in server/index.js to match
  surrounding socket.on handlers
2026-05-25 12:28:22 +02:00
Koala eb5515fc1b fix(extension): ensureState timeout guard, skip queued LEAVE_ROOM, persist onclose state, cleanup forceSync timer
- Add 10s timeout to ensureState() so extension doesn't hang forever if
  chrome.storage.session.get() never calls back (storage API failure)
- Skip emit(LEAVE_ROOM) in leaveOldRoomIfSwitching when socket is down;
  server already cleaned up via disconnect handler, avoid queued no-op
- Persist cleared peers to storage in socket.onclose to prevent stale
  peer list restoration on service worker restart
- Store and clean up forceSyncReset setTimeout in popup unload handler
  and when force_sync_execute completes
2026-05-25 12:26:21 +02:00
Koala c621685aae fix: use real client IP for auth rate limiting; preserve lobby across socket disconnect
- Store real client IP from x-forwarded-for on socket for use in JOIN_ROOM
  auth rate limiting (was using proxy IP, breaking brute-force protection)
- Remove clearEpisodeLobbyState() from socket.onclose to preserve lobby
  across brief disconnects; ensureState() recovers lobby + timeout on reconnect
- Lobby is still properly cleared on intentional LEAVE_ROOM and room switch
2026-05-25 12:24:14 +02:00
Koala b98cfc9ca1 fix(server): add missing room creation lock to prevent concurrent join race
Two parallel JOIN_ROOM to a non-existent room could race past each
other during bcrypt.hash, causing the second to overwrite the first
room (password hash lost). The lock was read but never written.

- Create lock promise before bcrypt.hash async boundary
- Release in finally to cover all exit paths (success, MAX_ROOMS, error)
- Concurrent waiters now correctly await existing room creation
2026-05-25 12:23:16 +02:00
Koala 6f08a9d7c4 fix: restore auth failure records retention line, update to 15 minutes 2026-05-25 11:57:37 +02:00
Koala 440ed2db47 docs: update PRIVACY.md and ARCHITECTURE.md for v1.7.0 changes 2026-05-25 11:56:46 +02:00
Koala ed24a4c263 fix: commandSenderMap race - embed senderId in SERVER_COMMAND/CMD_ACK instead of global Map 2026-05-25 11:52:05 +02:00
Koala 284b82a910 fix: v1.7.0 - critical bug fixes, race conditions, memory leaks, null guards, server hardening 2026-05-25 11:48:54 +02:00
Koala f59d30569e perf: optimize reconnect flow - aggressive backoff (500ms→5s), reconnect UI status, 30s keepAlive 2026-05-25 10:29:19 +02:00
Koala f23c7eb3c8 docs: remove unapproved features from roadmap 2026-05-25 10:14:44 +02:00
Koala 9a4dd41555 Re-add Netflix to supported platforms — works via HTML5 video tag 2026-05-25 10:12:11 +02:00
Koala 2067f76ced improve onboarding copy - add welcome step, friendlier text, 5 steps 2026-05-25 10:08:02 +02:00
Koala 3b65af1bbb feat: sprint 2 - empty states, onboarding tour, dev tab optimization, expanded usernames
- Add renderEmpty() helper with icons and hints for peers/history/logs/rooms
- Implement onboarding tour (4 steps) with overlay, dots, skip/next
- Dev tab logs only poll when tab is visible (isDevTabVisible flag)
- Expand username generation from 12/12 to 30/30 adjective-noun pairs
- Update ROADMAP.md to remove implemented features
2026-05-25 10:05:47 +02:00
Koala bb316340a7 Add remaining koalastuff.net subdomains to blacklist 2026-05-25 10:02:28 +02:00
Koala a3af397fe9 Add koalastuff.net and snippets.koalastuff.net to blacklist 2026-05-25 10:01:27 +02:00
Koala 0ac2b49d89 Add sync.koalastuff.net to domain blacklist 2026-05-25 09:57:31 +02:00
Koala c9f93dc4ba fix: add null guards to prevent runtime crashes
- popup.js: guard peers/state.acks in updateLastActionUI()
- popup.js: guard state.duration in refreshDebugInfo()
- popup.js: guard elements.lobbyPeerStatus in updateLobbyUI()
- background.js: use Array.isArray() for currentRoom.peers.length checks
2026-05-25 09:57:01 +02:00
Koala 4909a86a13 feat: implement sprint 1 quick wins - toast system, notifications, UX polish
- Add central toast notification system (popup.html, popup.js)
- Add browser notifications toggle (opt-in) with event toasts
- Fix interpolation memory leak (unload listener)
- Add /health endpoint with IP-based rate limiting (server)
- Improve tab sorting (current tab first, matches, alphabetical)
- Add copy-to-clipboard visual feedback with toast
- Show targetTime in last action card for seek/force sync
- Add explicit video cleanup when element removed (content.js)
- Update ROADMAP.md to remove implemented features
2026-05-25 09:53:25 +02:00
Koala d66c68be5d Use normal font size for italic description 2026-05-25 09:52:14 +02:00
Koala 23d4b7068e Center and shrink description text in README 2026-05-25 09:50:48 +02:00
Koala f40d4e8de4 Center heading and italicize description in README 2026-05-25 09:50:01 +02:00
Koala e996275c2a Revert "Restore centered banner design with italic tagline and honest copy"
This reverts commit dd37045afc.
2026-05-25 09:49:35 +02:00
Koala dd37045afc Restore centered banner design with italic tagline and honest copy 2026-05-25 09:46:54 +02:00
Koala 92bec29215 Remove exaggerated marketing claims and fix technical inaccuracies across docs and website 2026-05-25 09:37:13 +02:00
Koala 552afac26a docs: add comprehensive roadmap with implementation plans 2026-05-25 09:36:42 +02:00
Koala 3c671bcfab Expand blacklist with more mail, search, and social domains 2026-05-25 09:35:03 +02:00
Koala 18ed8953db Add more domains to tab blacklist 2026-05-25 09:24:49 +02:00
Koala 967fe8872e Split Chrome/Firefox badges and link to web stores 2026-05-25 08:44:31 +02:00
GitHub Action 0e93e31bd0 chore(release): update versions to v1.6.1 [skip ci] 2026-05-25 00:49:48 +00:00
Timo 19ddfd1e21 Add ESLint setup, fix 6 lint errors, update AI_INIT.md with lint checks 2026-05-25 02:46:29 +02:00
Timo 4d85362020 Fix 5 frontend audit findings: FORCE_SYNC_ACK timeout, expectedTimeouts leak, undefined var, unreachable error handler, Smart Match perf 2026-05-25 02:35:02 +02:00
Timo 53c1b8eea3 feat(website): add Chrome Web Store link to download buttons 2026-05-25 02:30:15 +02:00
Timo 2b2aeeba00 fix(extension): compact Last Activity Status card with scroll and smaller elements 2026-05-25 02:28:33 +02:00
Timo 5067b8e541 fix(extension): exclude own tab from smart match and send immediate heartbeat after injection 2026-05-25 02:24:50 +02:00
Timo 8778403449 docs: add mandatory pre-session git pull instruction to AI_INIT.md 2026-05-25 02:17:50 +02:00
GitHub Action af87e34f5d chore(release): update versions to v1.6.0 [skip ci] 2026-05-25 00:15:24 +00:00
Timo 385602c194 fix(extension,server): resolve 12 audit findings (lobby cleanup, dedup race, protocol version, rate limiting, observer scope, and more) 2026-05-25 02:14:59 +02:00
Timo bf0fa55b9d feat: add ForceSync mode selector (Jump to Others / Jump to Me) 2026-05-25 02:14:59 +02:00
GitHub Action f063fd5f3d chore(release): update versions to v1.6.0 [skip ci] 2026-05-22 22:19:12 +00:00
Koala 0a555942f8 chore: release v1.6.0 and apply fixes 2026-05-23 00:19:02 +02:00
GitHub Action d3f680e313 chore(release): update versions to v1.5.4 [skip ci] 2026-05-18 19:39:54 +00:00
Timo e8203419a3 fix(extension): eliminate potential async race condition in slow reconnect by capturing attempt flag synchronously 2026-05-18 21:37:39 +02:00
Timo e3536ad1a7 refactor(extension): implement infinite, low-frequency background reconnect checks after initial 5-minute failure 2026-05-18 21:34:46 +02:00
Timo aa1382b2ad security(website): refactor inline scripts and click handlers to external files to achieve 100% strict CSP compliance 2026-05-18 21:21:38 +02:00
Timo 44ee3fab25 docs(website): add static asset caching and strict CSP template configurations in Caddyfile examples 2026-05-18 21:13:29 +02:00
Timo e9b55c72f0 style(website): replace emoji in Step 1 illustration with official brand logo 2026-05-18 20:52:19 +02:00
Timo a83cdf4b03 feat(website): link hero github button to releases and add packages version link under docker pane 2026-05-18 20:51:24 +02:00
Timo 3fb48ee822 feat(website): expand features grid from 4 to 6 symmetrical tiles including self-hosting and invites 2026-05-18 20:49:03 +02:00
Timo 2d20af7199 fix(website): add missing navbar home bilingual translation on join page 2026-05-18 20:43:56 +02:00
Timo 579677e11c feat(website): overhaul interactive extension mockup alignment and update server docker/caddy examples 2026-05-18 20:43:02 +02:00
Timo 05e1801653 feat(website): fully redesign website and invitation page with premium responsive layouts, inline SVG icons, and a sleek vector Globe language selector 2026-05-18 20:31:18 +02:00
Timo 6774b2bfb7 feat(website): fix Chrome icon, add dynamic Firefox targeting and update versioning guidelines 2026-05-18 20:15:10 +02:00
GitHub Action 108d015e9f chore(release): update versions to v1.5.3 [skip ci] 2026-05-18 18:10:54 +00:00
Timo e0b9e9ef27 perf(background): reduce reactive update lock duration to 1.0s 2026-05-18 20:10:34 +02:00
GitHub Action 641c9bfb24 chore(release): update versions to v1.5.2 [skip ci] 2026-05-18 18:09:45 +00:00
Timo cf993b4ef0 fix(background): reset reactive locks on execute force sync for instant play update 2026-05-18 20:09:20 +02:00
Timo 5b1d6d7ba4 docs: add mandatory node -c syntax check before committing/pushing 2026-05-18 19:45:01 +02:00
GitHub Action 9ed7c98933 chore(release): update versions to v1.5.1 [skip ci] 2026-05-18 17:44:37 +00:00
Timo 68b0f3306c fix(background): resolve missing closing brace syntax error in alarms onAlarm listener 2026-05-18 19:44:12 +02:00
GitHub Action a71cdc3e53 chore(release): update versions to v1.5.0 [skip ci] 2026-05-18 17:41:13 +00:00
185 changed files with 20723 additions and 1202 deletions
+34 -5
View File
@@ -5,6 +5,9 @@ Welcome to the KoalaSync project. This file is the primary entry point for any d
> [!IMPORTANT]
> **Privacy & Data Sovereignty**: KoalaSync follows a strict **Zero-External-Requests Policy**: The extension and website must not make requests to any third-party domains (Google Fonts, CDNs, etc.). All assets (fonts, icons, scripts) must be self-hosted or use system defaults.
> - **Font Stack**: Use a modern system font stack (e.g., -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif) to maintain a premium look without external dependencies. Prohibit the use of `@import` or `<link>` for external font services.
>
> [!IMPORTANT]
> **Caveman Communication Protocol**: To conserve context window and tokens, AI agents MUST communicate with the user in **caveman language** (broken, short German/English hybrid caveman style, e.g., "Ich Antigravity. Ich machen Git pull. Code fertig.") at all times. This rule is highly strict and must NOT be broken under any circumstances unless the user explicitly requests to drop it.
---
@@ -18,6 +21,8 @@ KoalaSync is a specialized tool for **synchronized video playback** across multi
- `extension/`: Browser Extension (Chrome & Firefox, Manifest V3). Contains background service worker, content scripts, and popup UI.
- `server/`: Node.js Relay Server using Socket.IO (WebSocket-only).
- `website/`: **Landing Page** & Invitation Bridge (Marketing, Tutorials, and Downloads).
- **`build.js`**: Zero-dependency static site compiler. Translates `template.html` + `locales/*.json``www/`. Also minifies CSS/JS automatically.
- **`www/` is auto-generated**: Never edit files in `www/` directly. Always edit source files (`template.html`, `style.css`, `app.js`, `lang-init.js`, `locales/*.json`) and run `node website/build.js` to regenerate. CSS/JS are output as `.min.*` files — a built-in cleanup step removes stale artifacts on each build.
- `shared/`: **Single Source of Truth** for protocol constants and event names.
- `scripts/`: Development utilities (e.g., `build-extension.js`).
- `docker-compose.yml`: Root-level orchestration for the relay server.
@@ -68,7 +73,7 @@ The following features are critical and must not be removed or fundamentally alt
- **Two-Phase Force Sync**: The `Prepare``ACK``Execute` flow ensures all peers are buffered before playback resumes.
- **Episode Auto-Sync**: Ensures series binges stay perfectly synced. A lobby initiates during title transitions, freezing peers until everyone is ready.
- **Dual Heartbeat**:
- **Background Heartbeat (30s)**: Ensures session persistence even without a video element.
- **Background Heartbeat (1m)**: Ensures session persistence even without a video element.
- **Content Heartbeat (15s)**: Transmits current video metadata (time, title).
- **Dead Peer Pruning**: Server "Reaper" disconnects peers after 5 minutes of total silence (no heartbeats or events).
- **Deduplication**: Server kills old sockets if a user re-joins with the same `peerId` to prevent ghosts.
@@ -85,6 +90,7 @@ The following features are critical and must not be removed or fundamentally alt
- **Server Transport**: Restricted to `websocket` only. Polling is disabled.
- **Docker Context**: The Docker build must run from the **Repo Root**.
- **Manifest Settings**: `run_at` must remain `document_idle`, and `all_frames` must remain `false`.
- **Strict Backward & Forward Compatibility (Store Delay Rule)**: Browser extensions are distributed through stores (e.g., Chrome Web Store, Firefox Add-ons) which can take up to 2 weeks to approve updates. Therefore, the server MUST NOT reject older extension clients unless a critical protocol version bump is explicitly authorized, and new extension versions MUST remain fully operational when connected to older servers (e.g., by silently falling back if a new feature is not supported). This is a core architectural requirement.
## 9. Security & Deployment
- **Tokens**: Security tokens are intentionally managed via `shared/constants.js` and server `.env`.
@@ -94,20 +100,43 @@ The following features are critical and must not be removed or fundamentally alt
## 10. Common Workflows
### ⚠️ Pre-Session Git Sync (MANDATORY)
Before starting any task, committing, or pushing, you **MUST** run `git pull --rebase` to ensure your local branch is up-to-date with `origin/main`. CI pipelines and other agents may push commits concurrently. Skipping this step will cause merge conflicts and rejected pushes.
### Releasing a New Version (CRITICAL WORKFLOW FOR AI AGENTS)
> [!CAUTION]
> **AI AGENTS MUST FOLLOW THIS EXACT SEQUENCE WHEN RELEASING A NEW VERSION OR TAGGING.**
> The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json`. You do NOT need to manually bump version numbers.
1. Commit all code changes and push to `main`.
2. Create and push a new tag. **MANDATORY**: Tags MUST start with a `v` (e.g., `v1.4.0`). The GitHub Actions release workflow is strictly configured to ignore any tags without the `v` prefix.
3. The CI will extract the version from the tag (e.g., `v1.4.0``1.4.0`), inject it into all source files, build the extension artifacts, publish the Docker image, and create a GitHub Release.
4. Verify the release builds on GitHub Actions.
> - **Website Versioning**: **NEVER** manually modify the version fallback strings in `website/index.html`. The website dynamically fetches the latest version and release date from `website/version.json` at runtime using `website/app.js`. Manual bumps in the HTML file are completely redundant and should be avoided.
1. **MANDATORY SYNTAX & LINT CHECKS**: Before staging, committing, or pushing any changes, you **MUST** run both checks on every modified JavaScript file:
- **Syntax Validation**: Run `node -c` on every single modified JavaScript file (e.g., `node -c extension/background.js` and `node -c extension/content.js`). **NEVER** commit or push code that fails this check.
- **ESLint Validation**: Run `npm run lint` (or `npx eslint .`). The output must show **zero errors and zero warnings**. ESLint is configured to catch undefined variables, unused vars, unreachable code, and other semantic issues. **NEVER** commit or push code that fails this check.
2. Commit all verified code changes and push to `main`.
3. Create and push a new tag. **MANDATORY**: Tags MUST start with a `v` (e.g., `v1.4.0`). The GitHub Actions release workflow is strictly configured to ignore any tags without the `v` prefix.
- **🚫 TAG IMMUTABILITY**: Once a tag is pushed to `origin`, it is **PERMANENT**. You MUST **NEVER** reuse, move, or force-push an existing tag — not even to "fix" a mistake. If a release is missing a fix, increment the version and create a **new** tag (e.g., `v1.7.0``v1.7.1`). Tags are immutable identifiers; moving them breaks CI pipelines, corrupts the release history, and causes unreproducible builds.
- **🚫 WHEN NOT TO TAG**: Do NOT create a release tag for changes that do NOT affect the shipped extension or server artifacts. Website text changes, documentation updates (`.md` files), and landing page content do NOT require a version tag. Tags trigger the full CI pipeline (Docker build, extension packaging, GitHub Release) — running this for a typo fix wastes CI resources and creates meaningless releases. Only tag when extension code (`extension/`), server code (`server/`), or shared protocol constants (`shared/`) have changed.
4. The CI will extract the version from the tag (e.g., `v1.4.0``1.4.0`), inject it into all source files, build the extension artifacts, publish the Docker image, and create a GitHub Release.
5. Verify the release builds on GitHub Actions.
### 🚫 Force Push Policy
> [!CAUTION]
> **Force pushing (`git push --force` or `git push -f`) is FORBIDDEN without explicit user confirmation.**
> - If a push is rejected due to a non-fast-forward conflict, you **MUST** run `git pull --rebase` first.
> - If a force push is absolutely required (e.g., squashed history, amended commits), you **MUST** ask the user for explicit permission with a clear explanation of why it's necessary. Never force-push autonomously.
> - This applies to both branches (`main`) and **tags** (see Tag Immutability above). Force-pushing tags is doubly destructive. Never do it.
### Adding a Protocol Event
1. Add the event name to `shared/constants.js`.
2. Run the build script (`node scripts/build-extension.js`).
3. Implement the handler in `server/index.js` and `background.js`.
### Making Website Changes
1. Edit source files in `website/` (`template.html`, `style.css`, `app.js`, `lang-init.js`, or `locales/*.json`).
2. Run the compiler: `node website/build.js`. This generates the multilingual pages in `www/` and minifies CSS/JS.
3. Verify the output: `node --check website/www/app.js && node --check website/www/lang-init.js`.
4. Test locally: `npx serve website/www` or `python3 -m http.server 8080 -d website/www`.
5. Commit both source changes and the updated `www/` output.
### Testing Locally
1. Run the build script: `node scripts/build-extension.js`.
2. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (or `dist/firefox/` in Firefox).
+139 -43
View File
@@ -1,58 +1,154 @@
# Contributing to KoalaSync
Thank you for your interest in contributing to KoalaSync! We welcome all contributions, from bug reports to new features.
Thanks for your interest in improving KoalaSync. All contributions are welcome — from bug reports and translations to core protocol changes.
## Development Workflow
---
### 1. Prerequisites
- Node.js (v18+)
- Docker (for local server testing)
## Ways to Contribute
### 2. Setup
1. Clone the repository.
2. Run `npm install` in the root directory to install build dependencies.
3. Run the build script to synchronize protocol constants and generate browser bundles:
```bash
node scripts/build-extension.js
```
| Area | Description |
|------|-------------|
| **Bug Reports** | Found a bug? Open an issue with repro steps (see template below). |
| **Code** | Fix bugs, add features, or improve the extension / server / website. |
| **Translations** | Help localize the extension and website into more languages. See [TRANSLATION.md](website/TRANSLATION.md). |
| **Documentation** | Improve docs, fix typos, or add missing examples. |
| **Security** | Found a vulnerability? See [SECURITY.md](SECURITY.md) — do NOT open a public issue. |
### 3. Testing Locally
1. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (`chrome://extensions/` → Developer Mode → Load Unpacked).
2. For Firefox, load `dist/firefox/` via `about:debugging` → "Load Temporary Add-on".
3. Start the relay server: `docker-compose up --build`.
4. Use **two different browser profiles** (or Chrome + Firefox) to test multi-peer synchronization.
5. Use the extension's **Dev tab** to verify real-time video element metadata (`readyState`, `currentTime`, `paused`).
---
### 4. Protocol Synchronization
KoalaSync uses a "Single Source of Truth" for protocol constants in `shared/constants.js`.
- **CRITICAL**: If you modify the constants, you MUST run the build script:
```bash
node scripts/build-extension.js
```
This will automatically synchronize the changes to the extension and generate the browser-specific bundles in the `dist/` folder.
## Development Setup
### 5. Code Standards
- **Vanilla JS**: The extension must remain dependency-free. Do not add npm packages to the `extension/` directory.
- **Privacy**: Do not add external requests (CDNs, fonts, analytics, etc.).
- **Comments**: Maintain the existing documentation style, especially for complex sync logic.
- **Room IDs**: Room IDs are restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only). Ensure any UI that generates room IDs follows this constraint.
### Prerequisites
- **Node.js** v18+
- **Docker** (for local relay server testing)
### Quick Start
```bash
git clone https://github.com/Shik3i/KoalaSync.git
cd KoalaSync
npm install
node scripts/build-extension.js
```
---
## Project Structure
| Directory | Purpose |
|-----------|---------|
| `extension/` | Browser extension (Manifest V3, Chrome & Firefox) |
| `server/` | Node.js + Socket.IO relay server (Dockerized) |
| `website/` | Landing page, invitation bridge, and marketing site |
| `shared/` | Protocol constants — single source of truth |
| `scripts/` | Build and sync utilities |
| `docs/` | Architecture, sync protocol, and deep-dive guides |
---
## Testing Locally
### Extension
1. Load `dist/chrome/` as an unpacked extension in Chrome (`chrome://extensions/` → Developer Mode → **Load unpacked**).
2. For Firefox: load `dist/firefox/` via `about:debugging`**Load Temporary Add-on**.
3. Start the relay server: `docker compose up --build`.
4. Use **two different browser profiles** (or Chrome + Firefox) to test multi-peer sync.
5. Use the extension's **Dev tab** to inspect real-time video element state (`readyState`, `currentTime`, `paused`).
### Website
```bash
node website/build.js # Compile static site → www/
python3 -m http.server 8080 -d website/www # Serve locally
```
Then open `http://localhost:8080`. For multi-language testing: `http://localhost:8080/de/`.
---
## Protocol Constants
KoalaSync uses a **single source of truth** for all protocol constants in `shared/constants.js`.
### 6. Version Numbers
> [!IMPORTANT]
> **Do NOT manually bump version numbers.** The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Manually changing version numbers in a PR will cause conflicts.
> After modifying `shared/constants.js`, you **must** run the build script to sync changes to the extension:
> ```bash
> node scripts/build-extension.js
> ```
> This automatically injects constants into `content.js` and regenerates browser bundles in `dist/`.
---
## Code Standards
- **Vanilla JS**: The extension must remain dependency-free. No npm packages in `extension/`.
- **Privacy-first**: Zero external requests — no CDNs, fonts, analytics, or trackers. All assets self-hosted.
- **System font stack**: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, ...` — never `@import` external fonts.
- **Room IDs**: Restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only). Enforced server-side.
- **Comments**: Document complex sync logic. The codebase uses inline comments for protocol reasoning.
---
## Version Numbers
> [!CAUTION]
> **Never manually bump version numbers.** The CI pipeline injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Manual bumps cause conflicts.
---
## Pull Request Process
1. Create a new branch for your feature or bugfix.
2. Ensure your code is tested locally (Chrome and Firefox).
3. Update relevant documentation (e.g., `docs/ARCHITECTURE.md` if you change the protocol).
4. Submit your PR with a clear description of the changes.
## Bug Reports
When reporting a bug, please include:
- **Browser**: Chrome / Firefox / Edge + version number.
- **Extension Version**: Visible in the popup's Dev tab.
- **Dev Tab Output**: Copy the connection status, logs, and video debug info from the Dev tab.
- **Steps to Reproduce**: A clear sequence of actions that triggers the issue.
1. **Branch** from `main` for your feature or fix.
2. **Test locally** on both Chrome and Firefox.
3. **Website changes**: Run `node website/build.js` and verify the compiled output in `www/`.
4. **Lint**: Ensure `npm run lint` passes with zero errors and zero warnings.
5. **Syntax**: Run `node -c` on every modified `.js` file.
6. **Protocol changes**: Update relevant documentation in `docs/`.
7. **Submit your PR** with a clear description and linked issue (if applicable).
---
## Bug Report Template
When filing a bug, the easiest way is to use the **Copy Logs** button in the extension's **Status** tab. It copies a fully formatted Markdown report to your clipboard containing:
- System info (version, protocol, peer ID, browser)
- Connection status (server, room, peers, reconnect state)
- Video debug info (playback state, readyState, network state, dimensions, error codes, shadow DOM detection, platform)
- Action history (last 20 events)
- Log entries (last 50)
Simply paste the clipboard contents into your GitHub issue and add:
| Field | Example |
|-------|---------|
| **Steps to Reproduce** | 1. Create room → 2. Join from second browser → 3. Play video |
| **Expected Behavior** | Both peers play simultaneously |
| **Actual Behavior** | Peer B remains paused |
If you cannot access the Status tab, include as much of the following manually:
| Field | Example |
|-------|---------|
| **Browser** | Chrome 125, Firefox 128 |
| **Extension Version** | v1.9.3 (visible at bottom of Settings tab) |
| **Website/Platform** | Netflix, YouTube, Twitch, Jellyfin, etc.
---
## Translation Contributions
KoalaSync supports 6 languages: English, German, French, Spanish, Portuguese (Brazilian), and Russian.
To add or improve translations:
1. Edit the locale files in `website/locales/` (for the website).
2. For extension translations, see [TRANSLATION.md](website/TRANSLATION.md).
3. Run `node website/build.js` to regenerate the static site.
---
## Security
If you find a security vulnerability, please do not open a public issue. Instead, refer to our [SECURITY.md](SECURITY.md) for responsible disclosure instructions.
If you discover a security vulnerability, **do not open a public issue**. Report it privately as described in [SECURITY.md](SECURITY.md).
+72 -26
View File
@@ -1,37 +1,83 @@
# ==============================================================================
# KoalaSync - Production Caddy Configuration Example
# Replace domains and paths with your actual setup.
# ==============================================================================
# This file provides examples of both a lightweight "Simple" configuration
# and a production-hardened "Advanced" configuration.
# Replace domains, reverse proxy locations, and directories with your actual setup.
# ------------------------------------------------------------------------------
# OPTION A: Simple Configuration
# ------------------------------------------------------------------------------
# Minimal configuration that serves the static website, enables gzip compression,
# supports extension-less Clean URLs, and reverse proxies the relay server.
# sync.koalastuff.net {
# root * /var/www/koalasync/website/www
# encode zstd gzip
#
# # Clean URLs support (resolves /join to join.html, etc.)
# try_files {path} {path}.html {path}/
# file_server
# }
#
# syncserver.koalastuff.net {
# reverse_proxy localhost:3000
# }
# ------------------------------------------------------------------------------
# OPTION B: Advanced Configuration (Production-Hardened)
# ------------------------------------------------------------------------------
# Highly secure, optimized configuration using advanced HTTP security headers,
# aggressive static assets caching, server signature concealment, and strict
# hardware permission access policies.
(security_headers) {
header {
# Enable HTTP Strict Transport Security (HSTS)
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Prevent clickjacking attacks (Sameorigin)
X-Frame-Options "SAMEORIGIN"
# Prevent MIME-sniffing
X-Content-Type-Options "nosniff"
# Enable browser XSS protection
X-XSS-Protection "1; mode=block"
# Control referrer information
Referrer-Policy "strict-origin-when-cross-origin"
# Hide Caddy server stamp signature
-Server
}
}
# 1. Marketing Website & Invitation Bridge
sync.koalastuff.net {
root * /var/www/koalasync/website
file_server
encode zstd gzip
root * /var/www/koalasync/website/www
# Security Headers
# Clean URLs: Resolves paths without .html in the URL
try_files {path} {path}.html {path}/
file_server
# Static Caching for high-performance PageSpeed (1 year with validation)
@static {
file
path *.ico *.css *.js *.png *.svg *.webp *.avif
}
header @static Cache-Control "public, max-age=31536000, must-revalidate"
# Security Headers & Content Security Policy (CSP)
import security_headers
header {
# Prevent FLoC tracking
Permissions-Policy interest-cohort=()
# Security best practices
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy no-referrer-when-downgrade
# CSP hardened with base-uri and form-action limits
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
# Modern Permissions Policy (blocks browser hardware access for enhanced privacy)
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
}
}
# 2. Relay Server (Socket.IO / WebSocket)
syncserver.koalastuff.net {
reverse_proxy localhost:3000 {
# Ensure WebSocket support is explicitly handled if needed
# (Caddy usually handles this automatically)
header_up Host {host}
header_up X-Real-IP {remote_host}
}
# Security Headers for the relay
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy no-referrer
}
import security_headers
encode zstd gzip
reverse_proxy KoalaSync:3000
}
+1 -2
View File
@@ -15,8 +15,7 @@ KoalaSync does not use a database. All active session data exists only in the se
|:----------|:------------------|:---------------------|
| Session data (peerId, username, video metadata) | Duration of session | User leaves room or disconnects |
| Room state | 2 hours max | Last peer leaves, or inactivity timeout |
| Failed auth lockout records | 15 minutes | Automatic expiry |
| Auth failure records | 1 hour | Periodic cleanup |
| Auth failure records (lockout after 5 failed attempts) | 15 minutes | Periodic cleanup |
| Connection rate-limit counters | 60 seconds | Automatic expiry |
| Event rate-limit counters | 10 seconds | Automatic expiry + periodic cleanup |
+49 -18
View File
@@ -1,18 +1,23 @@
# <img src="extension/icons/icon128.png" width="32" valign="middle"> KoalaSync
<p align="center">
<img src="website/assets/PlatformJuggler_New.webp" width="280" alt="KoalaSync Mascot">
</p>
<h1 align="center">KoalaSync</h1>
<p align="center">
<a href="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml"><img src="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml/badge.svg" alt="Release Status"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/github/v/release/Shik3i/KoalaSync" alt="GitHub release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Shik3i/KoalaSync?color=blue" alt="License"></a>
<img src="https://img.shields.io/badge/Browser-Chrome%20|%20Firefox-blueviolet" alt="Cross Browser">
<a href="https://addons.mozilla.org/de/firefox/addon/koalasync/"><img src="https://img.shields.io/badge/Firefox-Download-orange?logo=firefoxbrowser&logoColor=white" alt="Firefox Add-on"></a>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
</p>
KoalaSync is a premium, lightweight Browser Extension and Relay Server for synchronized video playback across any website—YouTube, Twitch, Netflix, and custom HTML5 players. Built with a focus on **Data Sovereignty** and **Extreme Performance**.
<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?
* **🛡️ Security-First**: Volatile RAM-based relay with built-in brute-force protection and zero-persistence architecture.
* **📡 Direct Logic**: Custom wire protocol implementation for frame-perfect synchronization.
* **📡 Direct Logic**: Manual Socket.IO wire implementation for reliable synchronization.
* **🛠️ Clean Build**: Dependency-free extension runtime with no library overhead.
* **🌐 Universal**: Works on any website with a `<video>` tag.
@@ -24,12 +29,33 @@ KoalaSync is a premium, lightweight Browser Extension and Relay Server for synch
- **Episode Auto-Sync**: Perfectly sync series binges. All peers wait until everyone has loaded the next episode before starting together.
- **Smart Matching**: Automatically highlights tabs containing matching video titles.
- **Dual Heartbeat Architecture**: Robust session tracking that prevents ghost rooms and stale connections.
- **Zero-Latency Relay**: Custom wire protocol implementation for maximum performance.
- **Efficient Relay**: Minimal overhead WebSocket message forwarding.
- **Seamless Invitations**: Smart links that automatically configure server and room credentials for your friends.
---
### 📂 Repository Structure
### 🚀 Quick Start
#### For Users (Installation & Usage)
The easiest and safest way to install KoalaSync is directly through the official browser stores:
<p>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white&style=for-the-badge" alt="Chrome Extension"></a>
<a href="https://addons.mozilla.org/de/firefox/addon/koalasync/"><img src="https://img.shields.io/badge/Firefox-Download-orange?logo=firefoxbrowser&logoColor=white&style=for-the-badge" alt="Firefox Add-on"></a>
</p>
*(For manual offline installation: Download the latest `.zip` from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page and load it as an "Unpacked Extension" in Developer Mode).*
**How to use:**
1. **Create a Room:** Click the Koala icon in your browser and hit `+ Create New Room`.
2. **Invite Friends:** Share the auto-copied invite link. Once they click it, they automatically join.
3. **Pick a Video:** Navigate to the Sync Tab, select the tab playing your video, and grab some popcorn! 🍿
---
### 🛠️ For Developers & Self-Hosters
#### 📂 Repository Structure
- `extension/`: Browser Extension (Chrome & Firefox).
- `server/`: Node.js + Socket.IO Relay Server (Containerized).
@@ -38,16 +64,7 @@ KoalaSync is a premium, lightweight Browser Extension and Relay Server for synch
- `scripts/`: Automated build and synchronization utilities.
- `docs/`: Technical deep-dives ([Architecture](docs/ARCHITECTURE.md), [Sync Guide](docs/SYNC_GUIDE.md)).
---
### 🚀 Quick Start
#### For Users (Installation)
The easiest way to install KoalaSync is to download the pre-compiled version from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page.
1. Download the latest `koalasync-chrome.zip` or `koalasync-firefox.zip`.
2. Extract the file and load it as an "Unpacked Extension" in your browser's Developer Mode.
#### For Developers (Building)
#### Building from Source
To build the extension from source and synchronize protocol constants:
```bash
npm install
@@ -62,19 +79,33 @@ Deploy your own private relay server using our official image:
docker pull ghcr.io/shik3i/koalasync:latest
# Or use our example compose file
cp docker-compose.example.yml docker-compose.yml
cp docker-compose.caddy.example.yml docker-compose.yml
docker-compose up -d
```
The server will be available at `ws://localhost:3000`. See [docker-compose.example.yml](docker-compose.example.yml) for advanced configuration.
The server will be available at `ws://localhost:3000`. See [Docker network compose](docker-compose.caddy.example.yml) or [Static IP compose](docker-compose.ip.example.yml) for ready-to-use Docker Compose files.
To connect your extension to a self-hosted server, open the popup → **Room** tab → select **Custom Server** → enter your server's WebSocket URL (e.g., `ws://localhost:3000`).
> **⚠️ Note**: `ws://` only works for `localhost`. If you deploy to a real domain, you **must** use `wss://` (e.g., `wss://sync.yourdomain.com`). This requires a TLS-terminating reverse proxy (e.g., Caddy, Nginx, or Traefik) in front of the relay server. See [Caddyfile.example](Caddyfile.example) for a production-ready template.
To verify your relay is reachable from outside, visit `https://your-domain.com` in a browser — it should return `{"status":"online","service":"KoalaSync Relay"}`.
---
### 🌐 Localization & Translations
Both the official KoalaSync website and the **v2.0 Browser Extension** feature full dynamic localization:
- **Available Languages**: Support is included for 6 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), and Russian (`ru`).
- **Real-Time Extension Localization**: Inside the extension Settings panel, users can swap languages instantly. The entire interface, notifications, Empty States, and onboarding guides re-translate dynamically in real-time.
- **Contributing**: We welcome community translations for both the website and the extension! Please refer directly to the [TRANSLATION.md](website/TRANSLATION.md) guide for step-by-step instructions on how to audit, refine, or add new languages.
---
### 📖 Documentation & Links
- **[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.
- **[HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)**: Step-by-step walkthrough of the complete user flow.
+85 -13
View File
@@ -1,23 +1,95 @@
# Security Policy
KoalaSync is built on a **zero-persistence, privacy-first** architecture. We take security seriously and appreciate responsible disclosure of vulnerabilities.
---
## Supported Versions
We take the security of our users and their data very seriously. We actively support and patch the latest stable releases of KoalaSync.
Only the latest stable release receives security patches.
| Version | Supported |
| -------------- | ------------------ |
| Latest Release | :white_check_mark: |
| Older Versions | :x: |
| Version | Supported |
|---------|-----------|
| Latest release | :white_check_mark: Active |
| Older versions | :x: Unsupported |
Users on older versions are encouraged to update. The server enforces a minimum client version via `MIN_VERSION`.
---
## Scope
The following components are within scope for security reports:
| Component | Examples |
|-----------|----------|
| **Relay Server** (`server/`) | Authentication bypass, rate-limit evasion, room hijacking, DoS vectors |
| **Browser Extension** (`extension/`) | XSS via content scripts, privilege escalation, data exfiltration, tab snooping |
| **WebSocket Protocol** | Message injection, replay attacks, man-in-the-middle (WSS bypass) |
| **Website** (`website/`) | XSS, CSP bypass, invitation-hash leaks |
### Out of Scope
- Theoretical attacks requiring physical device access
- Social engineering or phishing
- Denial of service via resource exhaustion on self-hosted instances
- Vulnerabilities in third-party browser extensions or websites
---
## Reporting a Vulnerability
If you discover a security vulnerability within KoalaSync (e.g., related to the Node.js relay server, WebSocket wire protocol, or the Chrome/Firefox browser extension), please **DO NOT** report it by creating a public GitHub issue.
> [!CAUTION]
> **Do NOT open a public GitHub issue for security vulnerabilities.** Public disclosure before a patch is available puts users at risk.
Publicly disclosing a vulnerability before a patch is available puts our users at risk. Instead, please send an email privately to the project administrator at:
**koalasync_admin@koalamail.rocks**
Instead, email the project maintainer privately:
### What to expect
1. **Acknowledgment**: You should receive an acknowledgment of your report within 48 hours.
2. **Investigation**: We will investigate the issue, confirm its severity, and work on a patch.
3. **Resolution**: We will notify you when the patch is deployed to the Chrome Web Store, Mozilla Add-on Store, and our GitHub Docker releases.
4. **Disclosure**: Once the fix is confirmed and users have had time to update, we will publicly acknowledge your contribution in our release notes (unless you prefer to remain anonymous).
**`koalasync_admin@koalamail.rocks`**
Encrypt sensitive findings with our PGP key (available on request).
### What to Include
- **Affected component**: Server / Extension / Website / Protocol
- **Steps to reproduce**: Clear, minimal steps to trigger the vulnerability
- **Impact**: What an attacker could achieve (data access, privilege escalation, etc.)
- **Environment**: Browser version, extension version, server configuration
- **Suggested fix** (optional): If you have ideas for a patch
### What to Expect
| Timeline | Action |
|----------|--------|
| **Within 48 hours** | Acknowledgment of your report |
| **Within 7 days** | Initial assessment and severity confirmation |
| **As needed** | Collaborative discussion for clarification |
| **After patch** | Notification that the fix is deployed |
| **After rollout** | Public acknowledgment in release notes (or anonymity if preferred) |
---
## Architecture & Threat Model
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).
- **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.
If you find a way to bypass any of these protections, we want to know about it.
---
## Responsible Disclosure
We follow the principle of **coordinated vulnerability disclosure**:
1. You report privately.
2. We investigate and develop a patch.
3. We deploy to the Chrome Web Store, Firefox Add-ons, and Docker registry.
4. We credit you publicly (unless you prefer to remain anonymous).
We do not pursue legal action against researchers who act in good faith and follow this disclosure process.
+46
View File
@@ -0,0 +1,46 @@
# Tested Streaming Services
This document tracks which streaming platforms and media servers have been tested with KoalaSync.
| Service | Sync Works | Media Title | Episode Auto-Sync | Notes |
|---------|:----------:|:-----------:|:-----------------:|-------|
| **YouTube** | ✅ Full | ✅ Full | ❌ | Best-in-class support. Native player API, reliable title detection. |
| **Twitch** | ✅ Full | ✅ Full | ❌ | Live-only platform. Tested on regular streams. |
| **Netflix** | ✅ Full | ⚠️ Hidden | ⚠️ Manual | Sync works perfectly, but DRM prevents media title detection. Episode transitions may require manual lobby. |
| **Emby** | ✅ Full | ✅ Full | ✅ Full | Self-hosted. Full HTML5 player access. |
| **Jellyfin** | ✅ Full | ✅ Full | ✅ Full | Self-hosted. Full HTML5 player access. |
| **Plex** | Not tested | Not tested | Not tested | Community reports indicate compatibility via HTML5 player mode. |
| **Disney+** | Not tested | Not tested | Not tested | Widevine DRM may restrict title detection similar to Netflix. |
| **Prime Video** | ⚠️ Partial | ⚠️ Partial | ❌ | Video elements detected (2 on page, picks larger one). Playback state + time readable. However, the preview/trailer video may be selected instead of the main content. Play/Pause commands may not reach the correct player. Title detection from MediaSession API may work for some content. |
| **HBO Max / Max** | Not tested | Not tested | Not tested | — |
| **Crunchyroll** | Not tested | Not tested | Not tested | — |
| **Vimeo** | Not tested | Not tested | Not tested | — |
| **Dailymotion** | Not tested | Not tested | Not tested | — |
| **ARD / ZDF Mediathek** | Not tested | Not tested | Not tested | German public broadcasters. HTML5 players expected to work. |
## Legend
| Symbol | Meaning |
|--------|---------|
| ✅ Full | Works without limitations. |
| ⚠️ Partial | Works with caveats (see Notes). |
| ❌ N/A | Not applicable or not supported. |
## How to Contribute
Tested a service that's not listed? Found different behavior than documented?
1. Test KoalaSync on the service with two browser profiles
2. Use the extension's **Dev tab** to check `readyState`, `currentTime`, and media title
3. Open a GitHub issue or PR updating this table
## Technical Background
KoalaSync works on any website with a **standard HTML5 `<video>` element** that allows script injection.
Limited functionality on certain platforms is typically caused by:
- **DRM/Copy Protection** (e.g., Widevine on Netflix, Disney+) which restricts access to media metadata like title and playback state
- **Shadow DOM encapsulation** that hides video elements from content scripts
- **Strict Content Security Policies** (CSP) that block script injection
Websites with heavily obfuscated custom players (e.g., complex Shadow DOM, iframe isolation) may require platform-specific workarounds in `content.js`.
+17
View File
@@ -0,0 +1,17 @@
services: # Top-level key defining all containers in this Compose file
koala-sync: # Name of the KoalaSync service
image: ghcr.io/shik3i/koalasync:latest # Pulls the latest KoalaSync image from GitHub Container Registry
container_name: KoalaSync # Sets a fixed container name instead of an auto-generated one
restart: always # Always restart the container if it stops or if Docker starts
environment: # Environment variables passed into the container
- TZ=Europe/Berlin # Sets the timezone inside the container
- 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
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
networks: # Top-level networks definition
caddy_net: # Network name as referenced by the service
external: true # Marks the network as managed outside of Compose (created by Caddy)
-25
View File
@@ -1,25 +0,0 @@
services:
koala-sync:
image: ghcr.io/shik3i/koalasync:latest
container_name: KoalaSync
restart: always
ports:
- "3000:3000"
environment:
- TZ=Europe/Berlin
- PORT=3000
- MIN_VERSION=1.0.0
- MAX_ROOMS=100
- MAX_PEERS_PER_ROOM=50
# KoalaSync uses in-memory storage for the relay,
# so no persistent database volume is required.
pids_limit: 2048
# Example for custom network (e.g., Unraid/Macvlan)
# networks:
# custom_network:
# ipv4_address: 192.168.1.XXX
# networks:
# custom_network:
# external: true
# name: br0
+21
View File
@@ -0,0 +1,21 @@
services: # Top-level key defining all containers in this Compose file
koala-sync: # Name of the KoalaSync service
image: ghcr.io/shik3i/koalasync:latest # Pulls the latest KoalaSync image from GitHub Container Registry
container_name: KoalaSync # Sets a fixed container name instead of an auto-generated one
restart: always # Always restart the container if it stops or if Docker starts
ports: # Exposes the container port to the host
- "3000:3000" # Maps host port 3000 to container port 3000
environment: # Environment variables passed into the container
- TZ=Europe/Berlin # Sets the timezone inside the container
- 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
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
ipv4_address: 192.168.1.XXX # Static IPv4 address for the KoalaSync container
networks: # Top-level networks definition
bond0_network: # Network name inside Compose
external: true # Marks the network as managed outside of Compose
name: bond0 # Name of the pre-existing network on the Docker host
+14 -7
View File
@@ -11,18 +11,19 @@ This document describes the communication flows and internal logic of the KoalaS
- **Protocol Version**: Client must match the server's protocol (currently `1.0.0`).
3. Server responds with Engine.IO handshake (`0`) and the client joins the namespace (`40`).
- **Room Join**: Background emits `JOIN_ROOM` containing `roomId`, `password`, `peerId`, and `username`.
- **Deduplication**: If a user joins with a `peerId` that already has an active socket, the server kills the old socket to prevent "Ghost Peers".
- **Deduplication**: If a user joins with a `peerId` that already has an active socket, the server kills the old socket to prevent "Ghost Peers". Deduplication re-validates after acquiring the room creation lock to avoid kicking the wrong socket during concurrent joins.
## 2. Media Event Synchronization
When a user interacts with a video:
1. **Detection**: `content.js` listens to native events (`play`, `pause`, `seeked`) on the `<video>` element.
2. **Prevention of Loops**: Uses an `expectedEvents` Set to distinguish between user actions and programmatic actions. Expected events are consumed on match and expire via timeout.
1. **Detection**: `content.js` listens to native events (`play`, `pause`, `seeked`) on the `<video>` element, including videos inside Shadow DOM (YouTube, Netflix, etc.).
2. **Prevention of Loops**: Uses an `expectedEvents` Set to distinguish between user actions and programmatic actions. Expected events are consumed on match and expire via timeout. Timeout IDs are cleaned up immediately to prevent memory leaks.
3. **Reporting**: `content.js` sends a `CONTENT_EVENT` to `background.js`.
4. **Relay**: The Server forwards the event to all other peers in the room.
5. **Execution**: Remote peers receive the command and call `video.play()`, `video.pause()`, or `video.currentTime = targetTime`.
5. **Execution**: Remote peers receive the command via `SERVER_COMMAND` (which includes the original `senderId` for correct ACK routing) and call `video.play()`, `video.pause()`, or `video.currentTime = targetTime`.
6. **ACK Routing**: `content.js` echoes the `commandSenderId` back in `CMD_ACK`, ensuring the `EVENT_ACK` is routed to the correct initiating peer even when multiple commands arrive concurrently.
## 3. Two-Phase Force Sync
Ensures all peers are frame-perfect and buffered before resuming:
Ensures all peers are buffered and synchronized before resuming:
1. **Prepare**: Initiator sends `FORCE_SYNC_PREPARE` with the target timestamp.
2. **Buffer**: Peers seek and pause. Once buffered (`readyState >= 3`), they send a `FORCE_SYNC_ACK`. (Note: `content.js` limits polling to 8000ms).
3. **Execute**: Once the Initiator collects ACKs (or after an 8.5s timeout), they send `FORCE_SYNC_EXECUTE`.
@@ -44,6 +45,7 @@ To maintain a clean room state and eliminate "Ghost Peers":
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
- **Reconnect Strategy**: Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin.
> [!CAUTION]
> **Identity Rule**: Differentiate between `peerId` and `socket.id`. Use `socket.id` exclusively for ephemeral transport routing on the server. Use `peerId` exclusively for identity, state management, and room tracking across the stack.
@@ -54,8 +56,13 @@ KoalaSync uses a megaphone routing approach to minimize server logic:
- **Storm Prevention**: When dispatching state updates in response to a new user joining (e.g., an active lobby state), ensure ONLY the initiator (or a designated leader) calls `emit()` to prevent $O(N)$ broadcast storms.
## 7. Security & Stability
- **Service Worker Lifecycle**: Uses `chrome.alarms` to prevent the Manifest V3 service worker from suspending while in an active room.
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or DoS.
- **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.
- **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.
- **Process Guards**: `uncaughtException` and `unhandledRejection` handlers prevent silent server crashes.
- **Noise Filtering**: Uses a curated blacklist of domains (Search Engines, Social Media) to declutter the "Target Tab" selector in the popup.
- **Diagnostics**: A "Dev" tab provides real-time access to the underlying `<video>` state (`readyState`, `paused`, `currentTime`) for easier troubleshooting.
+4 -4
View File
@@ -10,7 +10,7 @@ This guide walks through the complete user flow of KoalaSync, from creating a ro
2. The extension adds a small icon to your browser toolbar.
3. On first install, a unique 8-character **Peer ID** is generated locally and stored in `chrome.storage.local`. This ID is never sent to any external service — it only travels to the relay server when you join a room.
> **What's stored locally**: `peerId` (8-char hex), `username` (customizable), `serverUrl`, `filterNoise` preference. All stored via `chrome.storage.sync` and `chrome.storage.local`.
> **What's stored locally**: `peerId` (8-char hex), `username` (customizable, defaults to a readable adjective-noun pair), `serverUrl`, `filterNoise` preference. All stored via `chrome.storage.sync` and `chrome.storage.local`.
---
@@ -109,7 +109,7 @@ Both users now need to select which browser tab contains the video to sync:
4. Tabs with a **matching video title** are highlighted with a ⭐ prefix for easy identification.
5. Selecting a tab causes `background.js` to set `currentTabId` and inject `content.js` into that tab via `chrome.scripting.executeScript`.
> **What `content.js` does on injection**: Finds the first `<video>` element on the page and attaches event listeners for `play`, `pause`, `seeked`, `timeupdate`, and `volumechange`. It uses an `expectedEvents` Set to distinguish between user actions and programmatic actions (loop prevention).
> **What `content.js` does on injection**: Finds the first `<video>` element on the page and attaches event listeners for `play`, `pause`, `seeked`, and `loadeddata`. (Time and volume state are tracked via a 15-second heartbeat interval, not continuous event listeners). It uses an `expectedEvents` Set to distinguish between user actions and programmatic actions (loop prevention).
---
@@ -143,7 +143,7 @@ If videos drift out of sync, either user can click **"Force Sync"**:
### Phase 2 — Execute
6. Once all ACKs are received (or after 8.5 seconds), the initiator emits `FORCE_SYNC_EXECUTE`.
7. All peers call `video.play()` simultaneously, achieving frame-perfect sync.
7. All peers call `video.play()` simultaneously, achieving synchronized playback.
> **Why two phases?** Without buffering confirmation, peers with slower connections would start playing before they've loaded the target timestamp, causing immediate desync.
@@ -155,7 +155,7 @@ While in a room, two heartbeats keep the session alive:
| Heartbeat | Interval | Source | Purpose |
|:----------|:---------|:-------|:--------|
| **Background** | 30 seconds | `background.js` | Signals "I'm still connected" even without a video |
| **Background** | 30 seconds | `background.js` | Signals "I'm still connected" and triggers aggressive reconnect (500ms base, max 5s) |
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, `mediaTitle`, `playbackState`, `volume`, `muted` |
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
+15
View File
@@ -0,0 +1,15 @@
# KoalaSync Roadmap
Dieses Dokument erfasst zukünftige technische Pläne und Optimierungen für das KoalaSync-System.
---
## Geplante Optimierungen & Technische Roadmap
### 1. Behebung des synchronen Sortierungs-Flaschenhalses bei Auth-Failure LRU-Eviction
* **Kategorie**: Performance / DoS-Prävention
* **Hintergrund**: Der Server schützt Räume vor Brute-Force-Angriffen durch die Nachverfolgung fehlerhafter Anmeldeversuche (`failedAuthAttempts` Map). Bei Erreichen des Limits von 50.000 Einträgen wird ein Bereinigungsverfahren gestartet, das die gesamte Map in ein Array konvertiert und dieses per `Array.from().sort()` sortiert.
* **Problem**: Dies blockiert den Node.js-Main-Thread für mehrere Millisekunden und stellt einen potenziellen Denial-of-Service-Vektor (DoS) dar, wenn ein Angreifer gezielt Fehlversuche spamt.
* **Geplante Lösung**:
- Umstellung auf eine echte, $O(1)$-basierte LRU-Cache-Datenstruktur (z. B. doppelt verkettete Liste in Kombination mit einer Map).
- Alternativ: Ein vereinfachtes zeitbasiertes Ablauf-Verfahren oder ein schrittweises Löschen von Segmenten (Chunk-Eviction), um Blockaden des Main-Threads vollständig auszuschließen.
+70
View File
@@ -0,0 +1,70 @@
export default [
{
ignores: ["dist/**", "node_modules/**", "scratch/**"]
},
{
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
chrome: "readonly",
window: "readonly",
document: "readonly",
navigator: "readonly",
console: "readonly",
localStorage: "readonly",
setTimeout: "readonly",
setInterval: "readonly",
clearTimeout: "readonly",
clearInterval: "readonly",
fetch: "readonly",
CustomEvent: "readonly",
MutationObserver: "readonly",
IntersectionObserver: "readonly",
Uint32Array: "readonly",
Set: "readonly",
Map: "readonly",
Promise: "readonly",
Array: "readonly",
Object: "readonly",
JSON: "readonly",
Math: "readonly",
Number: "readonly",
String: "readonly",
Date: "readonly",
Error: "readonly",
URL: "readonly",
URLSearchParams: "readonly",
WebSocket: "readonly",
self: "readonly"
}
},
rules: {
"no-undef": "error",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
"no-unreachable": "error",
"no-constant-condition": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-extra-semi": "error",
"no-prototype-builtins": "warn",
"no-unsafe-optional-chaining": "error",
"valid-typeof": "error"
}
},
{
files: ["server/**/*.js", "scripts/**/*.js", "website/build.js"],
languageOptions: {
globals: {
require: "readonly",
__dirname: "readonly",
__filename: "readonly",
process: "readonly",
module: "readonly",
exports: "readonly",
Buffer: "readonly"
}
}
}
];
+3 -2
View File
@@ -8,11 +8,12 @@ A Manifest V3 Browser Extension (Chrome & Firefox) for synchronized video playba
- **Smart Peer IDs**: Hexadecimal IDs combined with customizable Usernames for easy identification.
- **Dual Heartbeat**: Advanced session tracking (Background) and video synchronization (Content) to prevent ghost sessions.
- **Live Diagnostics**: Built-in "Dev" tab for real-time video state debugging (ReadyState, CurrentTime, etc.).
- **Dynamic i18n (Multi-Language)**: Fully localized in 6 languages (`en`, `de`, `fr`, `es`, `pt-BR`, `ru`) with auto-detected fallback and dynamic on-the-fly language selectors.
## Tab Overview
1. **Room**: Manage connections, view active peers, and share invitation links.
2. **Sync**: Control video playback (Play/Pause/Force Sync) and view recent activity.
3. **Settings**: Customize your Username and toggle domain-based Noise Filtering.
3. **Settings**: Customize your Username, toggle domain-based Noise Filtering, and switch the App Language.
4. **Dev**: Monitor connection status and view real-time video element metadata for debugging.
## Privacy & Permissions
@@ -20,7 +21,7 @@ KoalaSync requires `<all_urls>` permission to detect and interact with video ele
- **No Browsing History**: We do not track or store your browsing history.
- **State Management**: Sensitive data (Room Passwords) is stored locally using `chrome.storage`.
- **Zero Telemetry**: No analytics or external tracking scripts.
- **Zero Runtime Dependencies**: The extension is built with pure Vanilla JS and contains no external libraries or tracking scripts, ensuring maximum performance and privacy.
- **Zero Runtime Dependencies**: The extension is built with pure Vanilla JS and contains no external libraries or tracking scripts, ensuring performance and privacy.
## Installation
1. **Prepare Extension**: From the repository root, run:
+575 -170
View File
File diff suppressed because it is too large Load Diff
+11 -7
View File
@@ -1,3 +1,4 @@
/* global cloneInto */
/**
* KoalaSync Bridge Script
* Injected into sync.koalastuff.net to facilitate communication between
@@ -22,12 +23,15 @@ window.addEventListener('KOALASYNC_JOIN_REQUEST', (e) => {
// 3. Listen for Status Updates from the Extension and relay to Website
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'JOIN_STATUS') {
const event = new CustomEvent('KOALASYNC_STATUS', {
detail: {
success: msg.success,
message: msg.message
}
});
window.dispatchEvent(event);
const detail = { success: msg.success, message: msg.message };
// Firefox MV3 content scripts run in an isolated world. When dispatching
// a CustomEvent with a detail object, Firefox wraps it in an XrayWrapper
// that the page's JavaScript cannot destructure (Permission denied).
// cloneInto() exposes the object to the page's context correctly.
// Chrome doesn't have this issue — cloneInto() is undefined there.
const safeDetail = typeof cloneInto === 'function'
? cloneInto(detail, document.defaultView)
: detail;
window.dispatchEvent(new CustomEvent('KOALASYNC_STATUS', { detail: safeDetail }));
}
});
+346 -68
View File
@@ -9,7 +9,7 @@
if (window.koalaSyncInjected && chrome.runtime.id) {
return;
}
} catch (e) {
} catch (_e) {
// Context invalidated, proceed with re-injection
}
window.koalaSyncInjected = true;
@@ -29,39 +29,90 @@
};
// --- SHARED_EVENTS_INJECT_END ---
let expectedEvents = new Set();
let expectedTimeouts = {};
// Suppresses native event reporting after a programmatic action.
// Each entry is a per-type timer (key = 'playing'|'paused'|'seek').
// While a timer exists, matching native events are consumed and not relayed.
// Timers self-clean after 300ms if the native event never fires.
let _suppressTimers = {};
function _setSuppress(state) {
if (_suppressTimers[state]) clearTimeout(_suppressTimers[state]);
_suppressTimers[state] = setTimeout(() => {
delete _suppressTimers[state];
}, 300);
}
function _clearSuppress(state) {
if (_suppressTimers[state]) {
clearTimeout(_suppressTimers[state]);
delete _suppressTimers[state];
}
}
// --- Seek Relay Filtering ---
// Minimum seek delta (seconds) to report. Prevents HLS/DASH buffering micro-seeks
// from being relayed to peers as user-initiated seeks.
const MIN_SEEK_DELTA = 3.0;
const MIN_SEEK_DELTA = 2.0;
let lastReportedSeekTime = null; // last currentTime we relayed as a SEEK
let seekDebounceTimer = null; // debounce timer for rapid seek events
// --- Episode Auto-Sync State ---
let lastKnownMediaTitle = null;
let episodeTransitionDebounce = null;
let pendingLobbyTitle = null; // Title we're waiting to match (from remote lobby)
let _pendingLobbyTitle = null; // Title we're waiting to match (from remote lobby)
let lobbyPollTimer = null;
let _autoSyncEnabled = true; // Cached setting, updated via storage.onChanged
function expectEvent(state) {
expectedEvents.add(state);
if (expectedTimeouts[state]) clearTimeout(expectedTimeouts[state]);
const timeout = state === 'seek' ? 10000 : 1500;
expectedTimeouts[state] = setTimeout(() => {
expectedEvents.delete(state);
}, timeout);
}
// Cache the autoSyncNextEpisode setting
chrome.storage.sync.get(['autoSyncNextEpisode'], (data) => {
_autoSyncEnabled = data.autoSyncNextEpisode !== false; // default: enabled
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes.autoSyncNextEpisode) {
_autoSyncEnabled = changes.autoSyncNextEpisode.newValue !== false;
}
});
function reportLog(message, level = 'info') {
chrome.runtime.sendMessage({ type: 'LOG', message, level }).catch(() => {});
}
// --- Helper: find the best video element on the page ---
function findVideo() {
const videos = document.querySelectorAll('video');
return videos.length > 0 ? videos[0] : null;
// 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;
}
}
return null;
}
// Multiple videos found → pick the best one
if (allVideos.length === 1) return allVideos[0];
let best = null;
let bestScore = -1;
for (const v of allVideos) {
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);
const unmutedBonus = v.muted ? 0 : 100000;
const durationBonus = (v.duration && isFinite(v.duration) ? v.duration : 0) * 100;
const score = area + unmutedBonus + durationBonus;
if (score > bestScore) {
bestScore = score;
best = v;
}
}
return best;
}
// --- Episode Auto-Sync: Detection ---
@@ -71,6 +122,43 @@
: null;
}
// Extract a canonical episode identifier from a title string.
// Handles: S01E01, S1E1, S01 - E01, Season 1 Episode 1, "Folge 5", "Episode 5", "Ep. 5", "#5"
// Returns null if no episode pattern found.
function extractEpisodeId(title) {
if (!title || typeof title !== 'string') return null;
// S01E01 patterns (with optional spaces, dashes, dots between S and E)
const se = title.match(/S(?:eason\s*)?(\d+)[\s\-\.]*E(?:pisode\s*)?(\d+)/i);
if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`;
// "Episode X", "Folge X", "Ep. X", "#X"
const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i);
if (ep) return `EP${String(ep[1]).padStart(3, '0')}`;
return null;
}
// Returns true if two titles likely refer to the same episode.
// Strict: both must have IDs and match, OR neither has IDs and exact match.
function sameEpisode(titleA, titleB) {
if (!titleA && !titleB) return true; // Both unknown → assume same (backward compat)
if (!titleA || !titleB) return false; // One unknown, one known → different
const idA = extractEpisodeId(titleA);
const idB = extractEpisodeId(titleB);
if (idA && idB) return idA === idB; // Both have parseable IDs → compare IDs
if (idA || idB) return false; // One has ID, other doesn't → different
return titleA === titleB; // Neither has ID → exact string match
}
// Returns true only when we are CERTAIN the episodes differ.
// Permissive: only blocks if BOTH titles have parseable IDs AND they differ.
// Films, music, unparseable titles always pass through.
function isDifferentEpisode(titleA, titleB) {
if (!titleA || !titleB) return false; // Unknown → allow
const idA = extractEpisodeId(titleA);
const idB = extractEpisodeId(titleB);
if (!idA || !idB) return false; // At least one unparseable → allow
return idA !== idB; // Both parseable → only block if different
}
function checkEpisodeTransition() {
const currentTitle = getMediaTitle();
const video = findVideo();
@@ -78,7 +166,7 @@
// Only trigger if: we had a previous title, the title changed,
// a video exists, and we're near the start of new content.
if (lastKnownMediaTitle && currentTitle
&& currentTitle !== lastKnownMediaTitle
&& !sameEpisode(currentTitle, lastKnownMediaTitle)
&& video
&& video.currentTime < 5
&& video.readyState >= 1) {
@@ -111,11 +199,11 @@
const video = findVideo();
const currentTitle = getMediaTitle();
if (video && currentTitle && currentTitle === expectedTitle
if (video && currentTitle && sameEpisode(currentTitle, expectedTitle)
&& video.currentTime < 5 && video.readyState >= 1) {
// Match! Pause at start and report ready.
if (!video.paused) {
expectEvent('paused');
_setSuppress('paused');
video.pause();
}
stopLobbyPoll();
@@ -131,7 +219,7 @@
function startLobbyPoll(expectedTitle) {
stopLobbyPoll();
pendingLobbyTitle = expectedTitle;
_pendingLobbyTitle = expectedTitle;
// NOTE: Do NOT pause here. Three callers reach this function:
// 1. PAUSE_FOR_LOBBY (initiator): already paused by that handler before calling us.
@@ -151,7 +239,7 @@
function stopLobbyPoll() {
pendingLobbyTitle = null;
_pendingLobbyTitle = null;
if (lobbyPollTimer) {
clearInterval(lobbyPollTimer);
lobbyPollTimer = null;
@@ -169,7 +257,7 @@
reportLog(`Media Action Error: Invalid seek payload - ${JSON.stringify(data)}`, 'error');
return;
}
data.targetTime = target;
data = { ...data, targetTime: target };
}
try {
@@ -182,11 +270,11 @@
if (ytButton) {
const isCurrentlyPlaying = !video.paused;
if ((action === EVENTS.PLAY && !isCurrentlyPlaying) || (action === EVENTS.PAUSE && isCurrentlyPlaying)) {
expectEvent(action === EVENTS.PLAY ? 'playing' : 'paused');
_setSuppress(action === EVENTS.PLAY ? 'playing' : 'paused');
ytButton.click();
}
if (action === EVENTS.SEEK) {
expectEvent('seek');
_setSuppress('seek');
video.currentTime = data.targetTime;
}
return;
@@ -198,11 +286,11 @@
if (twitchButton) {
const isCurrentlyPlaying = !video.paused;
if ((action === EVENTS.PLAY && !isCurrentlyPlaying) || (action === EVENTS.PAUSE && isCurrentlyPlaying)) {
expectEvent(action === EVENTS.PLAY ? 'playing' : 'paused');
_setSuppress(action === EVENTS.PLAY ? 'playing' : 'paused');
twitchButton.click();
}
if (action === EVENTS.SEEK) {
expectEvent('seek');
_setSuppress('seek');
video.currentTime = data.targetTime;
}
return;
@@ -211,19 +299,19 @@
// Fallback for native HTML5
if (action === EVENTS.PLAY) {
expectEvent('playing');
_setSuppress('playing');
video.play().catch((e) => {
reportLog(`Playback prevented: ${e.message}`, 'warn');
expectedEvents.delete('playing');
_clearSuppress('playing');
});
} else if (action === EVENTS.PAUSE) {
expectEvent('paused');
_setSuppress('paused');
video.pause();
} else if (action === EVENTS.SEEK) {
expectEvent('seek');
_setSuppress('seek');
video.currentTime = data.targetTime;
}
} catch (e) {
} catch (e) {
reportLog(`Media Action Error: ${e.message}`, 'error');
}
}
@@ -259,25 +347,43 @@
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'get_current_time') {
const video = findVideo();
sendResponse({ currentTime: video ? video.currentTime : undefined });
sendResponse({ currentTime: video ? video.currentTime : null });
return true;
}
if (message.type === 'SERVER_COMMAND') {
const { action, payload } = message;
let actionCompleted = false;
// Guard: Don't execute sync commands if peers are on different episodes.
// Only active when autoSyncNextEpisode setting is enabled (default: on).
// Only blocks when BOTH sides have parseable S01E01-style IDs that differ.
// Films and unparseable titles always pass through.
const syncActions = [EVENTS.PLAY, EVENTS.PAUSE, EVENTS.SEEK,
EVENTS.FORCE_SYNC_PREPARE, EVENTS.FORCE_SYNC_EXECUTE];
if (_autoSyncEnabled && syncActions.includes(action)) {
const senderTitle = payload?.mediaTitle;
const myTitle = getMediaTitle();
if (isDifferentEpisode(senderTitle, myTitle)) {
reportLog(`Episode mismatch: sender="${senderTitle || '?'}" vs mine="${myTitle || '?'}" — skipping ${action}. Disable "Auto-Sync next Episode" in settings if this causes issues.`, 'warn');
if (action !== EVENTS.FORCE_SYNC_PREPARE && action !== EVENTS.FORCE_SYNC_EXECUTE) {
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId });
}
return;
}
}
if (action === EVENTS.PLAY) {
tryMediaAction(EVENTS.PLAY);
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp });
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId });
actionCompleted = true;
} else if (action === EVENTS.PAUSE) {
tryMediaAction(EVENTS.PAUSE);
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp });
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId });
actionCompleted = true;
} else if (action === EVENTS.SEEK) {
tryMediaAction(EVENTS.SEEK, payload);
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp });
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId });
actionCompleted = true;
} else if (action === EVENTS.FORCE_SYNC_PREPARE) {
if (!payload || payload.targetTime === undefined) return;
@@ -287,21 +393,23 @@
reportLog(`Media Action Error: Invalid force sync payload - ${JSON.stringify(payload)}`, 'error');
return;
}
expectEvent('paused');
expectEvent('seek');
_setSuppress('paused');
_setSuppress('seek');
video.pause();
video.currentTime = payload.targetTime;
pollSeekReady(payload.targetTime).then((ready) => {
chrome.runtime.sendMessage({ type: 'FORCE_SYNC_ACK' }).catch(() => {});
if (ready) {
chrome.runtime.sendMessage({ type: 'FORCE_SYNC_ACK' });
scheduleProactiveHeartbeat();
} else {
reportLog('Force Sync: Seek ready timeout, proceeding anyway', 'warn');
}
});
}).catch(() => {});
}
} else if (action === EVENTS.FORCE_SYNC_EXECUTE) {
stopLobbyPoll(); // Clear any pending lobby on force sync
stopLobbyPoll();
tryMediaAction(EVENTS.PLAY);
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp });
chrome.runtime.sendMessage({ type: 'CMD_ACK', actionTimestamp: message.actionTimestamp, commandSenderId: message.commandSenderId });
actionCompleted = true;
}
@@ -332,7 +440,7 @@
if (message.type === 'PAUSE_FOR_LOBBY') {
const video = findVideo();
if (video && !video.paused) {
expectEvent('paused');
_setSuppress('paused');
video.pause();
}
// Start lobby poll now that we know the feature is enabled
@@ -345,6 +453,59 @@
if (message.type === 'GET_VIDEO_STATE') {
const video = findVideo();
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';
return 'Generic';
})();
const networkStates = ['NETWORK_EMPTY', 'NETWORK_IDLE', 'NETWORK_LOADING', 'NETWORK_NO_SOURCE'];
const readyStates = ['HAVE_NOTHING', 'HAVE_METADATA', 'HAVE_CURRENT_DATA', 'HAVE_FUTURE_DATA', 'HAVE_ENOUGH_DATA'];
const videoCount = document.querySelectorAll('video').length;
const inShadowDom = (() => {
let el = video;
while (el) {
if (el.toString() === '[object ShadowRoot]') return true;
el = el.parentNode;
}
// Also check if any potential host has shadow root (even if no video found)
if (!video) {
const hosts = document.querySelectorAll('[id*="player" i], [class*="player" i], [id*="video" i], [class*="video" i]');
for (const host of hosts) {
if (host.shadowRoot) return true;
}
}
return false;
})();
// Build multi-video summary for debug reports
const allVideos = [];
const allVideoEls = document.querySelectorAll('video');
for (let i = 0; i < allVideoEls.length; i++) {
const v = allVideoEls[i];
allVideos.push({
index: i,
width: v.videoWidth || v.offsetWidth || 0,
height: v.videoHeight || v.offsetHeight || 0,
muted: v.muted,
paused: v.paused,
duration: (v.duration && isFinite(v.duration)) ? Math.round(v.duration) : 0,
readyState: v.readyState,
src: (v.currentSrc || v.src || '').substring(0, 80),
selected: v === video
});
}
if (video) {
const dataAttributes = {};
if (video.attributes) {
@@ -362,30 +523,78 @@
artwork: Array.from(navigator.mediaSession.metadata.artwork || []).map(a => a.src)
} : null;
const errorInfo = video.error ? {
code: video.error.code,
message: video.error.message
} : null;
sendResponse({
found: true,
paused: video.paused,
currentTime: video.currentTime,
duration: video.duration || 0,
readyState: video.readyState,
readyStateLabel: readyStates[video.readyState] || 'UNKNOWN',
networkState: video.networkState,
networkStateLabel: networkStates[video.networkState] || 'UNKNOWN',
muted: video.muted,
volume: video.volume,
playbackRate: video.playbackRate,
videoWidth: video.videoWidth,
videoHeight: video.videoHeight,
seeking: video.seeking,
ended: video.ended,
error: errorInfo,
buffered: video.buffered && video.buffered.length > 0
? Array.from({ length: video.buffered.length }, (_, i) =>
`${video.buffered.start(i).toFixed(1)}-${video.buffered.end(i).toFixed(1)}s`).join(', ')
: 'none',
loop: video.loop,
url: window.location.href,
pageTitle: document.title,
id: video.id || 'none',
className: video.className || 'none',
src: video.src || 'none',
currentSrc: video.currentSrc || 'none',
dataAttributes,
metadata
metadata,
videoCount,
inShadowDom,
platform,
allVideos
});
} else {
sendResponse({ error: 'No video found' });
sendResponse({
found: false,
videoCount,
inShadowDom,
platform,
allVideos,
url: window.location.href,
pageTitle: document.title,
metadata: (navigator.mediaSession && navigator.mediaSession.metadata) ? {
title: navigator.mediaSession.metadata.title,
artist: navigator.mediaSession.metadata.artist,
album: navigator.mediaSession.metadata.album
} : null
});
}
}
});
// Detect native events
function reportEvent(action) {
if (seekDebounceTimer && (action === EVENTS.PLAY || action === EVENTS.PAUSE)) {
clearTimeout(seekDebounceTimer);
seekDebounceTimer = null;
const v = findVideo();
if (v && Number.isFinite(v.currentTime)) {
lastReportedSeekTime = v.currentTime;
reportLog(`[Seek] Debounce flushed immediately due to ${action.toUpperCase()}`, 'info');
reportEvent(EVENTS.SEEK);
}
}
const video = findVideo();
if (!video) return;
@@ -396,10 +605,14 @@
const eventState = action === EVENTS.PLAY ? 'playing' : (action === EVENTS.PAUSE ? 'paused' : (action === EVENTS.SEEK ? 'seek' : null));
if (eventState && expectedEvents.has(eventState)) {
expectedEvents.delete(eventState); // Consume the match
return; // Ignore event caused by our programmatic action
if (_suppressTimers[eventState]) {
_clearSuppress(eventState);
return;
}
// Suppress only SEEK during visibility grace period (tab re-focus ghost jump).
// Play/Pause pass through — user may want to immediately pause after tabbing back.
if (Date.now() < visibilityGraceUntil && action === EVENTS.SEEK) return;
chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
@@ -416,6 +629,38 @@
scheduleProactiveHeartbeat();
}
// --- Tab Visibility Handling ---
// Browsers (especially Firefox) aggressively throttle background tabs.
// When the user returns to a video tab, the video element may have lost
// time-sync and fires spurious seek events as it recovers (jumping back).
// We suppress only SEEK for a short grace period after tab re-focus.
// Play/Pause are NOT suppressed — the user may legitimately want to
// pause immediately after switching back.
let pageVisible = !document.hidden;
let visibilityGraceUntil = 0;
const VISIBILITY_GRACE_MS = 300;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
pageVisible = false;
} else if (!pageVisible) {
pageVisible = true;
visibilityGraceUntil = Date.now() + VISIBILITY_GRACE_MS;
reportLog(`Tab re-focused — suppressing seeks for ${VISIBILITY_GRACE_MS / 1000}s to prevent ghost relay`, 'warn');
}
});
// Reset on page hide/show (bfcache, tab discard)
window.addEventListener('pagehide', () => { pageVisible = false; });
window.addEventListener('pageshow', (event) => {
// event.persisted is true ONLY when restored from bfcache, not on initial load
if (event.persisted && !pageVisible) {
pageVisible = true;
visibilityGraceUntil = Date.now() + VISIBILITY_GRACE_MS;
reportLog(`Page restored from cache — suppressing seeks for ${VISIBILITY_GRACE_MS / 1000}s`, 'warn');
}
});
const handlePlay = () => reportEvent(EVENTS.PLAY);
const handlePause = () => reportEvent(EVENTS.PAUSE);
@@ -427,25 +672,27 @@
const current = video.currentTime;
if (!Number.isFinite(current)) return;
// Step 1: Check expectedEvents (programmatic seek suppression)
if (expectedEvents.has('seek')) {
expectedEvents.delete('seek');
lastReportedSeekTime = current; // Update baseline so next user seek is relative to here
// No log — this is routine programmatic behavior (Force Sync, lobby, peer command)
// Step 1: Check _suppressTimers (programmatic seek from remote peer)
if (_suppressTimers['seek']) {
_clearSuppress('seek');
lastReportedSeekTime = current;
return;
}
// Step 2: Suppress during visibility grace period (tab re-focus ghost events)
if (Date.now() < visibilityGraceUntil) return;
const delta = lastReportedSeekTime !== null ? Math.abs(current - lastReportedSeekTime) : null;
const deltaStr = delta !== null ? `Δ${delta.toFixed(2)}s` : 'Δ?';
// Step 2: Delta check — skip micro-seeks (buffering, chapter markers, etc.)
// Step 3: Delta check — skip micro-seeks (buffering, chapter markers, etc.)
if (lastReportedSeekTime !== null && delta < MIN_SEEK_DELTA) {
reportLog(`[Seek] Filtered (${deltaStr} < ${MIN_SEEK_DELTA}s threshold) @ ${current.toFixed(2)}s — not relayed`, 'warn');
return;
}
// Step 3: Debounce rapid consecutive seeks (e.g. scrubbing)
// — wait 800ms for the user to settle before relaying
// Step 4: Debounce rapid consecutive seeks (e.g. scrubbing)
// — wait 300ms for the user to settle before relaying
if (seekDebounceTimer) clearTimeout(seekDebounceTimer);
seekDebounceTimer = setTimeout(() => {
seekDebounceTimer = null;
@@ -457,11 +704,11 @@
lastReportedSeekTime = settled;
reportLog(`[Seek] Relayed @ ${settled.toFixed(2)}s (${finalDeltaStr})`, 'info');
reportEvent(EVENTS.SEEK);
}, 800);
}, 300);
};
let lastVideoSrc = null;
let lastVideoSrc = undefined;
// Episode detection handler for loadeddata event
const handleLoadedData = () => {
@@ -471,19 +718,22 @@
function setupListeners() {
const video = findVideo();
if (video) {
video.removeEventListener('play', handlePlay);
video.removeEventListener('pause', handlePause);
video.removeEventListener('seeked', handleSeeked);
video.removeEventListener('loadeddata', handleLoadedData);
const existing = video._koalaHandlers;
if (existing) {
video.removeEventListener('play', existing.play);
video.removeEventListener('pause', existing.pause);
video.removeEventListener('seeked', existing.seeked);
video.removeEventListener('loadeddata', existing.loadeddata);
}
video._koalaHandlers = { play: handlePlay, pause: handlePause, seeked: handleSeeked, loadeddata: handleLoadedData };
video.addEventListener('play', handlePlay);
video.addEventListener('pause', handlePause);
video.addEventListener('seeked', handleSeeked);
video.addEventListener('loadeddata', handleLoadedData);
video.dataset.koalaAttached = 'true';
lastVideoSrc = video.currentSrc || video.src;
lastVideoSrc = video.currentSrc || video.src || null;
// Initialize episode tracking title on first attach
if (!lastKnownMediaTitle) {
lastKnownMediaTitle = getMediaTitle();
}
@@ -497,13 +747,19 @@
function checkVideo() {
lastMutate = Date.now();
const video = findVideo();
if (!video && lastVideoSrc !== undefined) {
reportLog('Video element removed from page', 'warn');
lastVideoSrc = undefined;
return;
}
if (!video) return;
const currentSrc = video.currentSrc || video.src;
const currentSrc = video.currentSrc || video.src || null;
if (!video.dataset.koalaAttached || (lastVideoSrc && currentSrc && lastVideoSrc !== currentSrc)) {
// If src changed, also check for episode transition
if (lastVideoSrc && currentSrc && lastVideoSrc !== currentSrc) {
if (!video.dataset.koalaAttached || (lastVideoSrc !== undefined && currentSrc && lastVideoSrc !== currentSrc)) {
if (lastVideoSrc !== undefined && currentSrc && lastVideoSrc !== currentSrc) {
checkEpisodeTransition();
}
setupListeners();
@@ -519,7 +775,7 @@
observerTimeout = setTimeout(checkVideo, 1000 - (now - lastMutate));
}
});
observer.observe(document.body, { childList: true, subtree: true });
observer.observe(document.documentElement, { childList: true, subtree: true });
// --- SHARED_HEARTBEAT_INJECT_START ---
const HEARTBEAT_INTERVAL_VAL = 15000;
@@ -575,10 +831,32 @@
// Initial Setup
setupListeners();
// Maintain a persistent keep-alive port connection to prevent background SW suspension
let keepAlivePort = null;
function connectKeepAlivePort() {
try {
if (chrome.runtime.id) {
keepAlivePort = chrome.runtime.connect({ name: 'keepAlive' });
keepAlivePort.onDisconnect.addListener(() => {
keepAlivePort = null;
setTimeout(connectKeepAlivePort, 1000);
});
}
} catch (_e) {
// Extension context invalidated or disabled
}
}
connectKeepAlivePort();
schedulePeriodicHeartbeat();
// Immediate heartbeat on injection — populate peer data without waiting 15s
setTimeout(() => sendHeartbeat(), 300);
// Episode Auto-Sync: Boot recovery — check if background has an active lobby
chrome.runtime.sendMessage({ type: 'CONTENT_BOOT' }, (res) => {
if (chrome.runtime.lastError) return;
if (res && res.lobbyActive && res.expectedTitle) {
reportLog(`Boot: Active lobby detected for "${res.expectedTitle}"`, 'info');
startLobbyPoll(res.expectedTitle);
+89
View File
@@ -0,0 +1,89 @@
// extension/i18n.js
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
export const DEFAULT_LANGUAGE = 'en';
let activeDictionary = {};
/**
* Resolves, loads, and merges the target language with the English baseline fallback.
* @param {string} langCode - Target language (e.g. 'de')
*/
export async function loadLocale(langCode) {
const resolvedLang = SUPPORTED_LANGUAGES.includes(langCode) ? langCode : DEFAULT_LANGUAGE;
try {
// Load Baseline English
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
const enDict = await enResponse.json();
if (resolvedLang === DEFAULT_LANGUAGE) {
activeDictionary = enDict;
return;
}
// Load Target Locale
const targetResponse = await fetch(chrome.runtime.getURL(`locales/${resolvedLang}.json`));
const targetDict = await targetResponse.json();
// Airtight Fallback Merge: target overrides en, missing elements fallback to en
activeDictionary = Object.assign({}, enDict, targetDict);
} catch (err) {
console.error('[i18n] Failed to load locale. Defaulting to English:', err);
// Fallback directly to static English if fetching fails
try {
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
activeDictionary = await enResponse.json();
} catch (_) {
activeDictionary = {};
}
}
}
/**
* Returns the translated string for a given key. Supports optional value interpolation.
* @param {string} key - Dictionary key
* @param {object} [placeholders] - Key-value map for replacements (e.g., { name: 'Alice' })
* @returns {string} Translated string or the key itself
*/
export function getMessage(key, placeholders = null) {
let msg = activeDictionary[key] || key;
if (placeholders && typeof placeholders === 'object') {
for (const [k, v] of Object.entries(placeholders)) {
msg = msg.replace(new RegExp(`{${k}}`, 'g'), v);
}
}
return msg;
}
/**
* Performs dynamic DOM replacements for elements carrying data-i18n attributes.
*/
export function translateDOM() {
// 1. Text Content
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const translated = getMessage(key);
// Special case: Preserve logo image elements inside headers (like h1 logo)
const img = el.querySelector('img');
if (img) {
el.innerHTML = '';
el.appendChild(img);
el.appendChild(document.createTextNode(' ' + translated));
} else {
el.textContent = translated;
}
});
// 2. Tooltips (titles)
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.setAttribute('title', getMessage(key));
});
// 3. Placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.setAttribute('placeholder', getMessage(key));
});
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "de",
"HTML_CLASS": "lang-de",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Raum",
"TAB_ROOM_TOOLTIP": "Raum-Einstellungen und Verbindung",
"TAB_SYNC": "Sync",
"TAB_SYNC_TOOLTIP": "Video-Synchronisations-Steuerung und Aktionen",
"TAB_SETTINGS": "Optionen",
"TAB_SETTINGS_TOOLTIP": "Erweiterungseinstellungen",
"TAB_STATUS": "Status",
"TAB_STATUS_TOOLTIP": "Erweiterte Diagnose & Protokolle",
"BTN_CREATE_ROOM": "+ Neuer Raum",
"MANUAL_CONNECT_HEADER": "Manuell verbinden / Erweitert",
"LABEL_SERVER": "Server",
"BTN_SERVER_OFFICIAL": "Offiziell",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Verwende den offiziellen, zuverlässigen Server",
"BTN_SERVER_CUSTOM": "Eigener",
"BTN_SERVER_CUSTOM_TOOLTIP": "Verbinde dich mit deinem eigenen, selbstgehosteten Server",
"PLACEHOLDER_SERVER_URL": "wss://dein-server:3000",
"LABEL_ROOM_ID": "Raum-ID",
"LABEL_ROOM_ID_TOOLTIP": "Der eindeutige Identifikator für deinen Sync-Raum",
"PLACEHOLDER_ROOM_ID": "Raum-ID eingeben",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Die eindeutige ID des Raums, dem du beitreten möchtest",
"LABEL_PASSWORD": "Passwort (Optional)",
"LABEL_PASSWORD_TOOLTIP": "Optionales Passwort, um den Raumzugang zu beschränken",
"PLACEHOLDER_PASSWORD": "Raum-Passwort (optional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Passwort für den Raum (leer lassen, wenn keines vorhanden)",
"BTN_JOIN_ROOM": "Raum beitreten / erstellen",
"BTN_JOIN_ROOM_TOOLTIP": "Mit dem Raum verbinden",
"LABEL_PUBLIC_ROOMS": "Öffentliche Räume",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste der öffentlich verfügbaren Räume auf diesem Server",
"BTN_REFRESH": "AKTUALISIEREN",
"BTN_REFRESH_TOOLTIP": "Die Liste der öffentlichen Räume aktualisieren",
"PUBLIC_ROOMS_REFRESHING": "Aktualisiere...",
"LABEL_ACTIVE_ROOM": "Aktiver Raum",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Der Raum, mit dem du gerade verbunden bist",
"ACTIVE_ROOM_NONE": "KEINER",
"ACTIVE_SERVER_OFFICIAL": "Offizieller Server",
"LABEL_INVITE_LINK": "Einladungslink",
"LABEL_INVITE_LINK_TOOLTIP": "Teile diesen Link mit Freunden, damit sie beitreten können",
"LABEL_PEERS_IN_ROOM": "Teilnehmer im Raum",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere Benutzer, die derzeit mit diesem Raum verbunden sind",
"NO_PEERS_CONNECTED": "Keine Teilnehmer verbunden",
"BTN_LEAVE_ROOM": "Raum verlassen",
"LABEL_SELECT_VIDEO": "Video auswählen",
"LABEL_SELECT_VIDEO_TOOLTIP": "Wähle den Browser-Tab aus, der das zu synchronisierende Video enthält",
"OPTION_SELECT_TAB": "-- Wähle einen Tab --",
"LABEL_REMOTE_CONTROL": "Fernsteuerung",
"BTN_COPY_INVITE": "📋 Einladungslink",
"BTN_COPY_INVITE_TOOLTIP": "Einladungslink kopieren",
"BTN_PLAY": "▶ Abspielen",
"BTN_PLAY_TOOLTIP": "Sende einen Play-Befehl an alle",
"BTN_PAUSE": "⏸ Pause",
"BTN_PAUSE_TOOLTIP": "Sende einen Pause-Befehl an alle",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Erzwinge die Synchronisation aller Teilnehmer",
"OPTION_JUMP_TO_OTHERS": "Zu anderen springen",
"OPTION_JUMP_TO_ME": "Zu mir springen",
"OPTION_JUMP_MODE_TOOLTIP": "Synchronisationsziel auswählen",
"LABEL_LAST_ACTIVITY": "Letzter Aktivitätsstatus",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Zeigt den neuesten Play-, Pause- oder Seek-Befehl",
"NO_RECENT_COMMANDS": "Keine aktuellen Befehle",
"LOBBY_HEADER": "EPISODEN-LOBBY",
"LOBBY_WAITING_FOR": "🎬 Warte auf: \"{title}\"",
"LOBBY_WAITING_PEERS": "Warte auf Teilnehmer...",
"BTN_SKIP_PLAY": "Überspringen & Abspielen",
"BTN_SKIP_PLAY_TOOLTIP": "Lobby abbrechen und trotzdem abspielen",
"LOBBY_CONNECT_FIRST": "Zuerst Raum beitreten",
"LOBBY_CONNECT_FIRST_DESC": "Du musst über einen Einladungslink beitreten oder einen neuen Raum erstellen, um Videos zu synchronisieren.",
"BTN_CREATE_ROOM_ALT": "Neuen Raum erstellen",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Einen neuen zufälligen Raum erstellen und beitreten",
"LABEL_USERNAME": "Dein Benutzername",
"LABEL_USERNAME_TOOLTIP": "Der Benutzername hilft anderen, dich zu identifizieren.",
"PLACEHOLDER_USERNAME": "Anonymer Koala",
"LABEL_HIDE_CLUTTER": "Aufgeräumte Tab-Liste",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtert Nicht-Video-Tabs und irrelevante Domains heraus, um die Liste sauber zu halten",
"LABEL_AUTO_SYNC_NEXT": "Nächste Episode auto-syncen",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausiert automatisch und wartet auf alle Teilnehmer bei Episodenwechsel, startet dann synchron.",
"LABEL_AUTO_COPY_INVITE": "Einladungslink auto-kopieren",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Kopiert den Einladungslink automatisch beim Erstellen eines Raums.",
"LABEL_NOTIFICATIONS": "Browser-Benachrichtigungen",
"LABEL_NOTIFICATIONS_TOOLTIP": "Zeigt Systembenachrichtigungen, wenn jemand beitritt/verlässt oder abspielt/pausiert.",
"LABEL_LANGUAGE": "Sprache",
"LABEL_LANGUAGE_TOOLTIP": "Wähle deine bevorzugte Sprache für die Erweiterung aus",
"LABEL_TROUBLESHOOTING": "Fehlerbehebung",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Werkzeuge zur Behebung von Verbindungsproblemen",
"BTN_REGEN_ID": "Peer-ID neu generieren",
"BTN_REGEN_ID_TOOLTIP": "Generiere deine interne ID neu und verbinde dich erneut",
"REGEN_ID_DESC": "Verwende dies, wenn du Fehler wegen doppelter Identität siehst.",
"LABEL_CONN_STATUS": "Verbindungsstatus",
"LABEL_CONN_STATUS_TOOLTIP": "Aktueller WebSocket-Verbindungsstatus",
"CONN_STATUS_DISCONNECTED": "Getrennt",
"BTN_RETRY": "WIEDERHOLEN",
"BTN_RETRY_TOOLTIP": "Versuchen, die Verbindung zum Server wiederherzustellen",
"BTN_COPY_LOGS": "Logs kopieren",
"BTN_COPY_LOGS_TOOLTIP": "Logs zum Teilen in die Zwischenablage kopieren",
"LABEL_VIDEO_DEBUG": "Video-Debug-Info",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Technische Details zum aktuell ausgewählten Video-Element",
"VIDEO_DEBUG_EMPTY": "Kein Tab ausgewählt oder kein Video erkannt.",
"LABEL_HISTORY": "Aktionsverlauf",
"LABEL_HISTORY_TOOLTIP": "Chronologisches Protokoll aller Sync-Befehle im Raum",
"HISTORY_EMPTY": "Noch keine Aktivität",
"LABEL_LOGS": "Logs (Letzte 50)",
"LABEL_LOGS_TOOLTIP": "Technische Verbindungsprotokolle zur Fehlersuche",
"BTN_CLEAR": "LEEREN",
"BTN_CLEAR_TOOLTIP": "Protokollausgabe leeren",
"LABEL_GITHUB": "GitHub-Repository",
"BTN_ONBOARDING_SKIP": "Überspringen",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Einführung überspringen",
"BTN_ONBOARDING_NEXT": "Weiter",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Zum nächsten Schritt gehen",
"ONBOARDING_1_TITLE": "Willkommen bei KoalaSync!",
"ONBOARDING_1_TEXT": "Schau Videos in perfekter Synchronität zusammen — egal wo ihr seid. Machen wir eine kurze Tour!",
"ONBOARDING_2_TITLE": "1. Raum erstellen",
"ONBOARDING_2_TEXT": "Hier geht's los. Erstelle einen Raum und teile den Einladungslink mit deinen Freunden.",
"ONBOARDING_3_TITLE": "2. Video auswählen",
"ONBOARDING_3_TEXT": "Wähle hier das Video aus, das du synchronisieren willst. Abspielen, Pause, Spulen — alle bleiben synchron.",
"ONBOARDING_4_TITLE": "3. Personalisieren",
"ONBOARDING_4_TEXT": "Wähle einen lustigen Benutzernamen, damit deine Freunde wissen, wer du bist.",
"ONBOARDING_5_TITLE": "Alles startklar!",
"ONBOARDING_5_TEXT": "Zeit, das Popcorn zu holen. Viel Spaß beim gemeinsamen Schauen!",
"ERR_CONN_TIMEOUT": "Verbindung abgelaufen. Bitte versuche es erneut.",
"ERR_INVALID_SERVER_URL": "Ungültiges Server-URL-Format.",
"ERR_IDENTITY_NOT_LOADED": "Identität noch nicht geladen. Warte kurz und versuche es erneut.",
"ERR_NO_PEERS_TIME": "Keine anderen Teilnehmer mit bekannter Zeit. Wechsle zu 'Zu mir springen'.",
"ERR_NO_VIDEO_TAB": "Verbindung zum Video-Tab fehlgeschlagen.",
"ERR_SELECT_VIDEO": "Bitte wähle zuerst ein Video aus!",
"TOAST_INVITE_COPIED": "Einladungslink kopiert!",
"TOAST_COPY_FAILED": "Kopieren in Zwischenablage fehlgeschlagen",
"TOAST_LOBBY_SKIPPED": "Episoden-Lobby übersprungen.",
"TOAST_LOBBY_SKIP_FAILED": "Überspringen der Lobby fehlgeschlagen.",
"TOAST_LOGS_COPIED": "Kopiert!",
"TOAST_PEER_JOINED": "{name} ist dem Raum beigetreten",
"TOAST_PEER_LEFT": "{name} hat den Raum verlassen",
"TOAST_PEER_ACTION": "{name} hat {action}",
"STATUS_CONNECTED": "Verbunden",
"STATUS_RECONNECTING": "Verbinde erneut...",
"STATUS_CONNECTING": "Verbinde...",
"STATUS_FAILED": "Fehlgeschlagen",
"STATUS_DISCONNECTED": "Getrennt",
"BTN_STATE_JOINING": "🚀 Trete bei...",
"BTN_STATE_RECONNECTING": "🔄 Verbinde erneut...",
"BTN_STATE_PLAYING": "▶ Spiele ab...",
"BTN_STATE_PAUSING": "⏸ Pausiere...",
"BTN_STATE_SYNCING_GROUP": "Synce zur Gruppe ({time})...",
"BTN_STATE_SYNCING": "Synchronisiere...",
"BTN_STATE_SYNCED": "✅ Synchronisiert!",
"NOTIF_PLAY": "die Wiedergabe gestartet",
"NOTIF_PAUSE": "die Wiedergabe pausiert",
"NOTIF_SEEK": "im Video gespult",
"NOTIF_FORCE_PREPARE": "eine erzwungene Synchronisation gestartet",
"NOTIF_FORCE_EXECUTE": "alle Teilnehmer synchronisiert",
"DEBUG_NO_TAB": "Kein Ziel-Tab ausgewählt.",
"DEBUG_COMM_FAIL": "Kommunikation mit dem Tab-Video fehlgeschlagen.",
"EMPTY_PEERS_TITLE": "Noch keine Teilnehmer",
"EMPTY_PEERS_HINT": "Teile deinen Einladungslink, um loszulegen",
"EMPTY_HISTORY_TITLE": "Noch keine Aktivität",
"EMPTY_HISTORY_HINT": "Abspielen, Pausieren oder Spulen, um Verlauf zu sehen",
"EMPTY_LOGS_TITLE": "Keine Logs",
"EMPTY_LOGS_HINT": "Verbindungsereignisse werden hier angezeigt",
"EMPTY_ROOMS_TITLE": "Keine aktiven Räume",
"EMPTY_ROOMS_HINT": "Raum erstellen oder aktualisieren, um öffentliche zu finden",
"LABEL_YOU": "Du",
"ONBOARDING_DONE": "Fertig!",
"LABEL_LOBBY_PEER_READY": "Bereit",
"LABEL_LOBBY_PEER_LOADING": "Lädt...",
"LABEL_PASSWORD_PROTECTED": "Passwortgeschützt",
"LABEL_PEERS_COUNT": "{count} Teilnehmer",
"LABEL_CUSTOM_SERVER": "Eigener Server",
"BTN_STATE_CREATING": "🚀 Erstelle Raum...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Episoden-Synchronisation fehlgeschlagen",
"NOTIF_LOBBY_CANCEL_MSG": "Auto-Sync abgebrochen: {reason}. Du musst eventuell manuell synchronisieren.",
"LOBBY_CANCEL_TIMEOUT": "Zeitüberschreitung",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Zeitüberschreitung (wiederhergestellt)",
"LOBBY_CANCEL_PEERS_LEFT": "Alle anderen Teilnehmer haben den Raum verlassen",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zeitüberschreitung — nicht alle Teilnehmer haben die Episode geladen",
"LOBBY_CANCEL_USER": "Vom Benutzer abgebrochen",
"NOTIF_ERROR_TITLE": "KoalaSync-Fehler"
}
+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "en",
"HTML_CLASS": "lang-en",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Room",
"TAB_ROOM_TOOLTIP": "Room settings and connection",
"TAB_SYNC": "Sync",
"TAB_SYNC_TOOLTIP": "Video sync controls and remote actions",
"TAB_SETTINGS": "Settings",
"TAB_SETTINGS_TOOLTIP": "Extension preferences",
"TAB_STATUS": "Status",
"TAB_STATUS_TOOLTIP": "Advanced Diagnostics & Logs",
"BTN_CREATE_ROOM": "+ Create New Room",
"MANUAL_CONNECT_HEADER": "Manual Connect / Advanced",
"LABEL_SERVER": "Server",
"BTN_SERVER_OFFICIAL": "Official",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Use the official reliable server",
"BTN_SERVER_CUSTOM": "Custom",
"BTN_SERVER_CUSTOM_TOOLTIP": "Connect to your own self-hosted server",
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
"LABEL_ROOM_ID": "Room ID",
"LABEL_ROOM_ID_TOOLTIP": "The unique identifier for your sync room",
"PLACEHOLDER_ROOM_ID": "Enter Room ID",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "The unique ID of the room you want to join",
"LABEL_PASSWORD": "Password (Optional)",
"LABEL_PASSWORD_TOOLTIP": "Optional password to restrict room access",
"PLACEHOLDER_PASSWORD": "Room Password (optional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Password for the room (leave empty if none)",
"BTN_JOIN_ROOM": "Join / Create Room",
"BTN_JOIN_ROOM_TOOLTIP": "Connect to the room",
"LABEL_PUBLIC_ROOMS": "Public Rooms",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "List of publicly available rooms on this server",
"BTN_REFRESH": "REFRESH",
"BTN_REFRESH_TOOLTIP": "Refresh the list of public rooms",
"PUBLIC_ROOMS_REFRESHING": "Refreshing...",
"LABEL_ACTIVE_ROOM": "Active Room",
"LABEL_ACTIVE_ROOM_TOOLTIP": "The room you are currently connected to",
"ACTIVE_ROOM_NONE": "NONE",
"ACTIVE_SERVER_OFFICIAL": "Official Server",
"LABEL_INVITE_LINK": "Invite Link",
"LABEL_INVITE_LINK_TOOLTIP": "Share this link with friends so they can join",
"LABEL_PEERS_IN_ROOM": "Peers in Room",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Other users currently connected to this room",
"NO_PEERS_CONNECTED": "No peers connected",
"BTN_LEAVE_ROOM": "Leave Room",
"LABEL_SELECT_VIDEO": "Select Video",
"LABEL_SELECT_VIDEO_TOOLTIP": "Choose the browser tab containing the video to sync",
"OPTION_SELECT_TAB": "-- Select a Tab --",
"LABEL_REMOTE_CONTROL": "Remote Control",
"BTN_COPY_INVITE": "📋 Invite Link",
"BTN_COPY_INVITE_TOOLTIP": "Copy Invite Link",
"BTN_PLAY": "▶ Play",
"BTN_PLAY_TOOLTIP": "Send a Play command to everyone",
"BTN_PAUSE": "⏸ Pause",
"BTN_PAUSE_TOOLTIP": "Send a Pause command to everyone",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Force all users to sync up",
"OPTION_JUMP_TO_OTHERS": "Jump to Others",
"OPTION_JUMP_TO_ME": "Jump to Me",
"OPTION_JUMP_MODE_TOOLTIP": "Choose sync target",
"LABEL_LAST_ACTIVITY": "Last Activity Status",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Shows the most recent play, pause, or seek command",
"NO_RECENT_COMMANDS": "No recent commands",
"LOBBY_HEADER": "EPISODE LOBBY",
"LOBBY_WAITING_FOR": "🎬 Waiting for: \"{title}\"",
"LOBBY_WAITING_PEERS": "Waiting for peers...",
"BTN_SKIP_PLAY": "Skip & Play anyway",
"BTN_SKIP_PLAY_TOOLTIP": "Cancel lobby and play anyway",
"LOBBY_CONNECT_FIRST": "Connect to a room first",
"LOBBY_CONNECT_FIRST_DESC": "You need to join a room via an invite link or create a new one to sync videos.",
"BTN_CREATE_ROOM_ALT": "Create New Room",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Create a new random room and join it",
"LABEL_USERNAME": "Your Username",
"LABEL_USERNAME_TOOLTIP": "Username helps others identify you.",
"PLACEHOLDER_USERNAME": "Anonymous Koala",
"LABEL_HIDE_CLUTTER": "Hide Clutter Tabs",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filters out non-video tabs and unrelated domains to keep the list clean",
"LABEL_AUTO_SYNC_NEXT": "Auto-Sync Next Episode",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pauses automatically and waits for all peers when an episode changes, then sync-starts together.",
"LABEL_AUTO_COPY_INVITE": "Auto-Copy Invite Link",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Automatically copies the invite link to your clipboard when creating a new room.",
"LABEL_NOTIFICATIONS": "Browser Notifications",
"LABEL_NOTIFICATIONS_TOOLTIP": "Shows native system notifications when someone joins/leaves or plays/pauses.",
"LABEL_LANGUAGE": "App Language",
"LABEL_LANGUAGE_TOOLTIP": "Choose your preferred extension language",
"LABEL_TROUBLESHOOTING": "Troubleshooting",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Tools for fixing connection issues",
"BTN_REGEN_ID": "Regenerate Peer ID",
"BTN_REGEN_ID_TOOLTIP": "Regenerate your internal ID and reconnect",
"REGEN_ID_DESC": "Use this if you see \"Duplicate Identity\" errors.",
"LABEL_CONN_STATUS": "Connection Status",
"LABEL_CONN_STATUS_TOOLTIP": "Current WebSocket connection state",
"CONN_STATUS_DISCONNECTED": "Disconnected",
"BTN_RETRY": "RETRY",
"BTN_RETRY_TOOLTIP": "Attempt to reconnect to the server",
"BTN_COPY_LOGS": "Copy Logs",
"BTN_COPY_LOGS_TOOLTIP": "Copy logs to clipboard for sharing",
"LABEL_VIDEO_DEBUG": "Video Debug Info",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Technical details about the currently selected video element",
"VIDEO_DEBUG_EMPTY": "No tab selected or video detected.",
"LABEL_HISTORY": "Full Action History",
"LABEL_HISTORY_TOOLTIP": "Chronological log of all sync commands in the room",
"HISTORY_EMPTY": "No activity yet",
"LABEL_LOGS": "Logs (Last 50)",
"LABEL_LOGS_TOOLTIP": "Technical connection logs for debugging",
"BTN_CLEAR": "CLEAR",
"BTN_CLEAR_TOOLTIP": "Clear log output",
"LABEL_GITHUB": "GitHub Repository",
"BTN_ONBOARDING_SKIP": "Skip",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Skip the tutorial",
"BTN_ONBOARDING_NEXT": "Next",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Go to next step",
"ONBOARDING_1_TITLE": "Welcome to KoalaSync!",
"ONBOARDING_1_TEXT": "Watch videos together in perfect sync — no matter where you are. Let's take a quick tour!",
"ONBOARDING_2_TITLE": "1. Create a Room",
"ONBOARDING_2_TEXT": "Start here. Create a room and share the invite link with your friends.",
"ONBOARDING_3_TITLE": "2. Select Video",
"ONBOARDING_3_TEXT": "Navigate here to select the video you want to sync. Play, pause, and seek — everyone stays in sync.",
"ONBOARDING_4_TITLE": "3. Personalize",
"ONBOARDING_4_TEXT": "Pick a fun username so your friends know who you are.",
"ONBOARDING_5_TITLE": "You're all set!",
"ONBOARDING_5_TEXT": "Time to grab some popcorn. Enjoy watching together!",
"ERR_CONN_TIMEOUT": "Connection timed out. Please try again.",
"ERR_INVALID_SERVER_URL": "Invalid Server URL format.",
"ERR_IDENTITY_NOT_LOADED": "Identity not yet loaded. Wait a moment and try again.",
"ERR_NO_PEERS_TIME": "No other peers with a known time. Switch to 'Jump to Me'.",
"ERR_NO_VIDEO_TAB": "Could not connect to video tab.",
"ERR_SELECT_VIDEO": "Please select a video first!",
"TOAST_INVITE_COPIED": "Invite link copied!",
"TOAST_COPY_FAILED": "Failed to copy to clipboard",
"TOAST_LOBBY_SKIPPED": "Episode Lobby skipped.",
"TOAST_LOBBY_SKIP_FAILED": "Failed to skip lobby.",
"TOAST_LOGS_COPIED": "Copied!",
"TOAST_PEER_JOINED": "{name} joined the room",
"TOAST_PEER_LEFT": "{name} left the room",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Connected",
"STATUS_RECONNECTING": "Reconnecting...",
"STATUS_CONNECTING": "Connecting...",
"STATUS_FAILED": "Failed",
"STATUS_DISCONNECTED": "Disconnected",
"BTN_STATE_JOINING": "🚀 Joining...",
"BTN_STATE_RECONNECTING": "🔄 Reconnecting...",
"BTN_STATE_PLAYING": "▶ Playing...",
"BTN_STATE_PAUSING": "⏸ Pausing...",
"BTN_STATE_SYNCING_GROUP": "Syncing to group ({time})...",
"BTN_STATE_SYNCING": "Syncing...",
"BTN_STATE_SYNCED": "✅ Synced!",
"NOTIF_PLAY": "started playback",
"NOTIF_PAUSE": "paused playback",
"NOTIF_SEEK": "seeked the video",
"NOTIF_FORCE_PREPARE": "started force sync",
"NOTIF_FORCE_EXECUTE": "synchronized everyone",
"DEBUG_NO_TAB": "No target tab selected.",
"DEBUG_COMM_FAIL": "Could not communicate with tab video.",
"EMPTY_PEERS_TITLE": "No peers yet",
"EMPTY_PEERS_HINT": "Share your invite link to get started",
"EMPTY_HISTORY_TITLE": "No activity yet",
"EMPTY_HISTORY_HINT": "Play, pause, or seek to see history",
"EMPTY_LOGS_TITLE": "No logs",
"EMPTY_LOGS_HINT": "Connection events will appear here",
"EMPTY_ROOMS_TITLE": "No active rooms",
"EMPTY_ROOMS_HINT": "Create a room or refresh to find public ones",
"LABEL_YOU": "You",
"ONBOARDING_DONE": "Done!",
"LABEL_LOBBY_PEER_READY": "Ready",
"LABEL_LOBBY_PEER_LOADING": "Loading...",
"LABEL_PASSWORD_PROTECTED": "Password Protected",
"LABEL_PEERS_COUNT": "{count} peers",
"LABEL_CUSTOM_SERVER": "Custom Server",
"BTN_STATE_CREATING": "🚀 Creating Room...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Episode Sync Failed",
"NOTIF_LOBBY_CANCEL_MSG": "Auto-sync cancelled: {reason}. You may need to manually sync.",
"LOBBY_CANCEL_TIMEOUT": "Timeout",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Timeout (recovered)",
"LOBBY_CANCEL_PEERS_LEFT": "All other peers left",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — not all peers loaded the episode",
"LOBBY_CANCEL_USER": "Cancelled by user",
"NOTIF_ERROR_TITLE": "KoalaSync Error"
}
+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "es",
"HTML_CLASS": "lang-es",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Sala",
"TAB_ROOM_TOOLTIP": "Configuración de sala y conexión",
"TAB_SYNC": "Sincro",
"TAB_SYNC_TOOLTIP": "Controles de sincronización de video y acciones",
"TAB_SETTINGS": "Opciones",
"TAB_SETTINGS_TOOLTIP": "Preferencias de la extensión",
"TAB_STATUS": "Estado",
"TAB_STATUS_TOOLTIP": "Diagnósticos avanzados y registros",
"BTN_CREATE_ROOM": "+ Crear nueva sala",
"MANUAL_CONNECT_HEADER": "Conexión manual / Avanzado",
"LABEL_SERVER": "Servidor",
"BTN_SERVER_OFFICIAL": "Oficial",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar el servidor oficial confiable",
"BTN_SERVER_CUSTOM": "Perso",
"BTN_SERVER_CUSTOM_TOOLTIP": "Conectarse a su propio servidor auto-alojado",
"PLACEHOLDER_SERVER_URL": "wss://tu-servidor:3000",
"LABEL_ROOM_ID": "ID de sala",
"LABEL_ROOM_ID_TOOLTIP": "El identificador único para tu sala de sincronización",
"PLACEHOLDER_ROOM_ID": "Ingresa el ID de la sala",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "El ID único de la sala a la que deseas unirte",
"LABEL_PASSWORD": "Contraseña (Opcional)",
"LABEL_PASSWORD_TOOLTIP": "Contraseña opcional para restringir el acceso a la sala",
"PLACEHOLDER_PASSWORD": "Contraseña de la sala (opcional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Contraseña para la sala (dejar vacío si no hay)",
"BTN_JOIN_ROOM": "Unirse / Crear sala",
"BTN_JOIN_ROOM_TOOLTIP": "Conectarse a la sala",
"LABEL_PUBLIC_ROOMS": "Salas públicas",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponibles en este servidor",
"BTN_REFRESH": "ACTUALIZAR",
"BTN_REFRESH_TOOLTIP": "Actualizar la lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Actualizando...",
"LABEL_ACTIVE_ROOM": "Sala activa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "La sala a la que estás conectado actualmente",
"ACTIVE_ROOM_NONE": "NINGUNA",
"ACTIVE_SERVER_OFFICIAL": "Servidor oficial",
"LABEL_INVITE_LINK": "Enlace de invitación",
"LABEL_INVITE_LINK_TOOLTIP": "Comparte este enlace con amigos para que se unan",
"LABEL_PEERS_IN_ROOM": "Participantes en la sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Otros usuarios conectados actualmente a esta sala",
"NO_PEERS_CONNECTED": "Sin participantes conectados",
"BTN_LEAVE_ROOM": "Salir de la sala",
"LABEL_SELECT_VIDEO": "Seleccionar video",
"LABEL_SELECT_VIDEO_TOOLTIP": "Elige la pestaña del navegador que contiene el video a sincronizar",
"OPTION_SELECT_TAB": "-- Selecciona una pestaña --",
"LABEL_REMOTE_CONTROL": "Control remoto",
"BTN_COPY_INVITE": "📋 Enlace de invitación",
"BTN_COPY_INVITE_TOOLTIP": "Copiar enlace de invitación",
"BTN_PLAY": "▶ Reproducir",
"BTN_PLAY_TOOLTIP": "Enviar un comando de reproducción a todos",
"BTN_PAUSE": "⏸ Pausar",
"BTN_PAUSE_TOOLTIP": "Enviar un comando de pausa a todos",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forzar la sincronización de todos los usuarios",
"OPTION_JUMP_TO_OTHERS": "Unirse a otros",
"OPTION_JUMP_TO_ME": "Traerlos a mí",
"OPTION_JUMP_MODE_TOOLTIP": "Elegir el objetivo de sincronización",
"LABEL_LAST_ACTIVITY": "Última actividad",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Muestra el comando más reciente de reproducción, pausa o búsqueda",
"NO_RECENT_COMMANDS": "Sin comandos recientes",
"LOBBY_HEADER": "SALA DE ESPERA DE EPISODIO",
"LOBBY_WAITING_FOR": "🎬 Esperando a: \"{title}\"",
"LOBBY_WAITING_PEERS": "Esperando participantes...",
"BTN_SKIP_PLAY": "Omitir y reproducir",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar la sala de espera y reproducir de todos modos",
"LOBBY_CONNECT_FIRST": "Únete a una sala primero",
"LOBBY_CONNECT_FIRST_DESC": "Necesitas unirte a una sala a través de un enlace de invitación o crear una nueva para sincronizar videos.",
"BTN_CREATE_ROOM_ALT": "Crear nueva sala",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Crear una nueva sala aleatoria y unirse",
"LABEL_USERNAME": "Tu nombre de usuario",
"LABEL_USERNAME_TOOLTIP": "El nombre de usuario ayuda a otros a identificarte.",
"PLACEHOLDER_USERNAME": "Koala anónimo",
"LABEL_HIDE_CLUTTER": "Lista de pestañas limpia",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra pestañas que no son de video y dominios no relacionados para mantener limpia la lista",
"LABEL_AUTO_SYNC_NEXT": "Sincro auto del siguiente episodio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automáticamente y espera a todos al cambiar de episodio, luego inicia de forma sincrónica.",
"LABEL_AUTO_COPY_INVITE": "Auto-copiar enlace",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automáticamente el enlace de invitación a tu portapapeles al crear una nueva sala.",
"LABEL_NOTIFICATIONS": "Notificaciones del navegador",
"LABEL_NOTIFICATIONS_TOOLTIP": "Muestra notificaciones del sistema cuando alguien se une/sale o reproduce/pausa.",
"LABEL_LANGUAGE": "Idioma de la aplicación",
"LABEL_LANGUAGE_TOOLTIP": "Elige tu idioma preferido para la extensión",
"LABEL_TROUBLESHOOTING": "Resolución de problemas",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Herramientas para solucionar problemas de conexión",
"BTN_REGEN_ID": "Regenerar ID de participante",
"BTN_REGEN_ID_TOOLTIP": "Regenerar tu ID interno y volver a conectarte",
"REGEN_ID_DESC": "Usa esto si ves errores de 'Identidad duplicada'.",
"LABEL_CONN_STATUS": "Estado de la conexión",
"LABEL_CONN_STATUS_TOOLTIP": "Estado actual de la conexión WebSocket",
"CONN_STATUS_DISCONNECTED": "Desconectado",
"BTN_RETRY": "REINTENTAR",
"BTN_RETRY_TOOLTIP": "Intentar volver a conectarse al servidor",
"BTN_COPY_LOGS": "Copiar registros",
"BTN_COPY_LOGS_TOOLTIP": "Copiar registros al portapapeles para compartir",
"LABEL_VIDEO_DEBUG": "Información de depuración de video",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalles técnicos sobre el elemento de video seleccionado actualmente",
"VIDEO_DEBUG_EMPTY": "No hay pestaña seleccionada o no se detectó ningún video.",
"LABEL_HISTORY": "Historial completo",
"LABEL_HISTORY_TOOLTIP": "Registro cronológico de todos los comandos de sincronización en la sala",
"HISTORY_EMPTY": "Sin actividad aún",
"LABEL_LOGS": "Logs (Últimos 50)",
"LABEL_LOGS_TOOLTIP": "Registros técnicos de conexión para depuración",
"BTN_CLEAR": "LIMPIAR",
"BTN_CLEAR_TOOLTIP": "Limpiar salida de registros",
"LABEL_GITHUB": "Repositorio de GitHub",
"BTN_ONBOARDING_SKIP": "Omitir",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Omitir el tutorial",
"BTN_ONBOARDING_NEXT": "Siguiente",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ir al siguiente paso",
"ONBOARDING_1_TITLE": "¡Bienvenido a KoalaSync!",
"ONBOARDING_1_TEXT": "Mira videos juntos en perfecta sincronización, sin importar dónde estén. ¡Hagamos un recorrido rápido!",
"ONBOARDING_2_TITLE": "1. Crear una sala",
"ONBOARDING_2_TEXT": "Comienza aquí. Crea una sala y comparte el enlace de invitación con tus amigos.",
"ONBOARDING_3_TITLE": "2. Seleccionar video",
"ONBOARDING_3_TEXT": "Navega aquí para seleccionar el video que deseas sincronizar. Reproduce, pausa y busca: todos permanecen sincronizados.",
"ONBOARDING_4_TITLE": "3. Personalizar",
"ONBOARDING_4_TEXT": "Elige un nombre de usuario divertido para que tus amigos sepan quién eres.",
"ONBOARDING_5_TITLE": "¡Todo listo!",
"ONBOARDING_5_TEXT": "Hora de preparar las palomitas. ¡Disfruta viendo con tus amigos!",
"ERR_CONN_TIMEOUT": "Tiempo de conexión agotado. Inténtalo de nuevo.",
"ERR_INVALID_SERVER_URL": "Formato de URL de servidor no válido.",
"ERR_IDENTITY_NOT_LOADED": "Identidad no cargada aún. Espera un momento e inténtalo de nuevo.",
"ERR_NO_PEERS_TIME": "No hay otros participantes con posición conocida. Cambia a 'Traerlos a mí'.",
"ERR_NO_VIDEO_TAB": "No se pudo conectar a la pestaña de video.",
"ERR_SELECT_VIDEO": "¡Selecciona un video primero!",
"TOAST_INVITE_COPIED": "¡Enlace de invitación copiado!",
"TOAST_COPY_FAILED": "Error al copiar al portapapeles",
"TOAST_LOBBY_SKIPPED": "Sala de espera de episodio omitida.",
"TOAST_LOBBY_SKIP_FAILED": "Error al omitir la sala de espera.",
"TOAST_LOGS_COPIED": "¡Copiado!",
"TOAST_PEER_JOINED": "{name} se unió a la sala",
"TOAST_PEER_LEFT": "{name} salió de la sala",
"TOAST_PEER_ACTION": "{name} ha {action}",
"STATUS_CONNECTED": "Conectado",
"STATUS_RECONNECTING": "Reconectando...",
"STATUS_CONNECTING": "Conectando...",
"STATUS_FAILED": "Error",
"STATUS_DISCONNECTED": "Desconectado",
"BTN_STATE_JOINING": "🚀 Uniéndose...",
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
"BTN_STATE_PLAYING": "▶ Reproduciendo...",
"BTN_STATE_PAUSING": "⏸ Pausando...",
"BTN_STATE_SYNCING_GROUP": "Sincronizando al grupo ({time})...",
"BTN_STATE_SYNCING": "Sincronizando...",
"BTN_STATE_SYNCED": "✅ ¡Sincronizado!",
"NOTIF_PLAY": "inició la reproducción",
"NOTIF_PAUSE": "pausó la reproducción",
"NOTIF_SEEK": "cambió la posición del video",
"NOTIF_FORCE_PREPARE": "inició una sincronización forzada",
"NOTIF_FORCE_EXECUTE": "sincronizó a todos",
"DEBUG_NO_TAB": "No hay pestaña objetivo seleccionada.",
"DEBUG_COMM_FAIL": "No se pudo comunicar con el video de la pestaña.",
"EMPTY_PEERS_TITLE": "Sin participantes aún",
"EMPTY_PEERS_HINT": "Comparte tu enlace de invitación para comenzar",
"EMPTY_HISTORY_TITLE": "Sin actividad aún",
"EMPTY_HISTORY_HINT": "Reproduce, pausa o busca para ver el historial",
"EMPTY_LOGS_TITLE": "Sin registros",
"EMPTY_LOGS_HINT": "Los eventos de conexión aparecerán aquí",
"EMPTY_ROOMS_TITLE": "Sin salas activas",
"EMPTY_ROOMS_HINT": "Crea una sala o actualiza para buscar salas públicas",
"LABEL_YOU": "Tú",
"ONBOARDING_DONE": "¡Hecho!",
"LABEL_LOBBY_PEER_READY": "Listo",
"LABEL_LOBBY_PEER_LOADING": "Cargando...",
"LABEL_PASSWORD_PROTECTED": "Protegido con contraseña",
"LABEL_PEERS_COUNT": "{count} participantes",
"LABEL_CUSTOM_SERVER": "Servidor personalizado",
"BTN_STATE_CREATING": "🚀 Creando sala...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Error al sincronizar episodio",
"NOTIF_LOBBY_CANCEL_MSG": "Sincronización automática cancelada: {reason}. Es posible que debas sincronizar manualmente.",
"LOBBY_CANCEL_TIMEOUT": "Tiempo de espera agotado",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tiempo de espera agotado (recuperado)",
"LOBBY_CANCEL_PEERS_LEFT": "Todos los demás participantes se han ido",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tiempo de espera agotado: no todos los participantes cargaron el episodio",
"LOBBY_CANCEL_USER": "Cancelado por el usuario",
"NOTIF_ERROR_TITLE": "Error de KoalaSync"
}
+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "fr",
"HTML_CLASS": "lang-fr",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Salon",
"TAB_ROOM_TOOLTIP": "Paramètres de salon et connexion",
"TAB_SYNC": "Synchro",
"TAB_SYNC_TOOLTIP": "Contrôles de synchronisation vidéo et actions",
"TAB_SETTINGS": "Options",
"TAB_SETTINGS_TOOLTIP": "Préférences de l'extension",
"TAB_STATUS": "Statut",
"TAB_STATUS_TOOLTIP": "Diagnostics avancés & Journaux",
"BTN_CREATE_ROOM": "+ Créer un nouveau salon",
"MANUAL_CONNECT_HEADER": "Connexion manuelle / Avancé",
"LABEL_SERVER": "Serveur",
"BTN_SERVER_OFFICIAL": "Officiel",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Utiliser le serveur officiel fiable",
"BTN_SERVER_CUSTOM": "Perso",
"BTN_SERVER_CUSTOM_TOOLTIP": "Se connecter à votre propre serveur auto-hébergé",
"PLACEHOLDER_SERVER_URL": "wss://votre-serveur:3000",
"LABEL_ROOM_ID": "ID du salon",
"LABEL_ROOM_ID_TOOLTIP": "L'identifiant unique de votre salon de synchronisation",
"PLACEHOLDER_ROOM_ID": "Entrer l'ID du salon",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "L'ID unique du salon que vous souhaitez rejoindre",
"LABEL_PASSWORD": "Mot de passe (Optionnel)",
"LABEL_PASSWORD_TOOLTIP": "Mot de passe optionnel pour restreindre l'accès au salon",
"PLACEHOLDER_PASSWORD": "Mot de passe du salon (optionnel)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Mot de passe du salon (laisser vide si aucun)",
"BTN_JOIN_ROOM": "Rejoindre / Créer le salon",
"BTN_JOIN_ROOM_TOOLTIP": "Se connecter au salon",
"LABEL_PUBLIC_ROOMS": "Salons publics",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Liste des salons publics disponibles sur ce serveur",
"BTN_REFRESH": "ACTUALISER",
"BTN_REFRESH_TOOLTIP": "Actualiser la liste des salons publics",
"PUBLIC_ROOMS_REFRESHING": "Actualisation...",
"LABEL_ACTIVE_ROOM": "Salon actif",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Le salon auquel vous êtes actuellement connecté",
"ACTIVE_ROOM_NONE": "AUCUN",
"ACTIVE_SERVER_OFFICIAL": "Serveur officiel",
"LABEL_INVITE_LINK": "Lien d'invitation",
"LABEL_INVITE_LINK_TOOLTIP": "Partagez ce lien avec vos amis pour qu'ils vous rejoignent",
"LABEL_PEERS_IN_ROOM": "Membres dans le salon",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Autres utilisateurs connectés à ce salon",
"NO_PEERS_CONNECTED": "Aucun membre connecté",
"BTN_LEAVE_ROOM": "Quitter le salon",
"LABEL_SELECT_VIDEO": "Choisir une vidéo",
"LABEL_SELECT_VIDEO_TOOLTIP": "Choisissez l'onglet du navigateur contenant la vidéo à synchroniser",
"OPTION_SELECT_TAB": "-- Choisir un onglet --",
"LABEL_REMOTE_CONTROL": "Contrôle à distance",
"BTN_COPY_INVITE": "📋 Lien d'invitation",
"BTN_COPY_INVITE_TOOLTIP": "Copier le lien d'invitation",
"BTN_PLAY": "▶ Lecture",
"BTN_PLAY_TOOLTIP": "Envoyer une commande Lecture à tout le monde",
"BTN_PAUSE": "⏸ Pause",
"BTN_PAUSE_TOOLTIP": "Envoyer une commande Pause à tout le monde",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forcer tous les utilisateurs à se synchroniser",
"OPTION_JUMP_TO_OTHERS": "Rejoindre les autres",
"OPTION_JUMP_TO_ME": "Les amener à moi",
"OPTION_JUMP_MODE_TOOLTIP": "Choisir la cible de synchronisation",
"LABEL_LAST_ACTIVITY": "Dernière activité",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Affiche la dernière commande de lecture, pause ou recherche",
"NO_RECENT_COMMANDS": "Aucune commande récente",
"LOBBY_HEADER": "LOBBY D'ÉPISODE",
"LOBBY_WAITING_FOR": "🎬 En attente de : \"{title}\"",
"LOBBY_WAITING_PEERS": "En attente des membres...",
"BTN_SKIP_PLAY": "Ignorer & Lancer",
"BTN_SKIP_PLAY_TOOLTIP": "Annuler le lobby et lancer la lecture quand même",
"LOBBY_CONNECT_FIRST": "Rejoignez d'abord un salon",
"LOBBY_CONNECT_FIRST_DESC": "Vous devez rejoindre un salon via un lien d'invitation ou en créer un nouveau pour synchroniser des vidéos.",
"BTN_CREATE_ROOM_ALT": "Créer un nouveau salon",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Créer un salon aléatoire et le rejoindre",
"LABEL_USERNAME": "Votre pseudo",
"LABEL_USERNAME_TOOLTIP": "Votre pseudo permet aux autres de vous identifier.",
"PLACEHOLDER_USERNAME": "Koala anonyme",
"LABEL_HIDE_CLUTTER": "Liste d'onglets épurée",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtre les onglets non-vidéo et les domaines non pertinents pour garder la liste propre",
"LABEL_AUTO_SYNC_NEXT": "Synchro auto l'épisode suivant",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Met en pause et attend tous les membres lors d'un changement d'épisode, puis démarre de manière synchrone.",
"LABEL_AUTO_COPY_INVITE": "Copie auto du lien",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copie automatiquement le lien d'invitation dans votre presse-papiers lors de la création d'un salon.",
"LABEL_NOTIFICATIONS": "Notifications de navigateur",
"LABEL_NOTIFICATIONS_TOOLTIP": "Affiche des notifications système lorsqu'un membre arrive/part ou lance/met en pause la lecture.",
"LABEL_LANGUAGE": "Langue de l'application",
"LABEL_LANGUAGE_TOOLTIP": "Choisissez la langue de l'application",
"LABEL_TROUBLESHOOTING": "Dépannage",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Outils pour résoudre les problèmes de connexion",
"BTN_REGEN_ID": "Régénérer l'identifiant",
"BTN_REGEN_ID_TOOLTIP": "Régénérer votre identifiant interne et vous reconnecter",
"REGEN_ID_DESC": "Utilisez cette option si vous rencontrez des erreurs de 'Double identité'.",
"LABEL_CONN_STATUS": "Statut de la connexion",
"LABEL_CONN_STATUS_TOOLTIP": "Statut actuel de la connexion WebSocket",
"CONN_STATUS_DISCONNECTED": "Déconnecté",
"BTN_RETRY": "RÉESSAYER",
"BTN_RETRY_TOOLTIP": "Tenter de se reconnecter au serveur",
"BTN_COPY_LOGS": "Copier les journaux",
"BTN_COPY_LOGS_TOOLTIP": "Copier les journaux dans le presse-papiers",
"LABEL_VIDEO_DEBUG": "Infos de débogage vidéo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Détails techniques de l'élément vidéo actuellement sélectionné",
"VIDEO_DEBUG_EMPTY": "Aucun onglet sélectionné ou aucune vidéo détectée.",
"LABEL_HISTORY": "Historique complet",
"LABEL_HISTORY_TOOLTIP": "Historique chronologique de toutes les commandes de synchronisation dans le salon",
"HISTORY_EMPTY": "Aucune activité pour le moment",
"LABEL_LOGS": "Journaux (50 derniers)",
"LABEL_LOGS_TOOLTIP": "Journaux techniques pour le débogage de la connexion",
"BTN_CLEAR": "EFFACER",
"BTN_CLEAR_TOOLTIP": "Effacer la sortie des journaux",
"LABEL_GITHUB": "Dépôt GitHub",
"BTN_ONBOARDING_SKIP": "Passer",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Passer le didacticiel",
"BTN_ONBOARDING_NEXT": "Suivant",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Étape suivante",
"ONBOARDING_1_TITLE": "Bienvenue sur KoalaSync !",
"ONBOARDING_1_TEXT": "Regardez des vidéos en parfaite synchronisation — où que vous soyez. Faisons un petit tour !",
"ONBOARDING_2_TITLE": "1. Créer un salon",
"ONBOARDING_2_TEXT": "Commencez ici. Créez un salon et partagez le lien d'invitation avec vos amis.",
"ONBOARDING_3_TITLE": "2. Choisir une vidéo",
"ONBOARDING_3_TEXT": "Naviguez ici pour sélectionner la vidéo à synchroniser. Lecture, pause, recherche — tout le monde reste synchrone.",
"ONBOARDING_4_TITLE": "3. Personnaliser",
"ONBOARDING_4_TEXT": "Choisissez un pseudo sympa pour que vos amis sachent qui vous êtes.",
"ONBOARDING_5_TITLE": "Vous êtes prêt !",
"ONBOARDING_5_TEXT": "Il est temps de préparer le pop-corn. Bon visionnage !",
"ERR_CONN_TIMEOUT": "Délai de connexion dépassé. Veuillez réessayer.",
"ERR_INVALID_SERVER_URL": "Format d'adresse de serveur non valide.",
"ERR_IDENTITY_NOT_LOADED": "Identifiant non encore chargé. Veuillez patienter et réessayer.",
"ERR_NO_PEERS_TIME": "Aucun autre membre avec une position connue. Basculez sur 'Les amener à moi'.",
"ERR_NO_VIDEO_TAB": "Impossible de se connecter à l'onglet vidéo.",
"ERR_SELECT_VIDEO": "Veuillez d'abord sélectionner une vidéo !",
"TOAST_INVITE_COPIED": "Lien d'invitation copié !",
"TOAST_COPY_FAILED": "Échec de la copie dans le presse-papiers",
"TOAST_LOBBY_SKIPPED": "Lobby d'épisode ignoré.",
"TOAST_LOBBY_SKIP_FAILED": "Échec de l'annulation du lobby.",
"TOAST_LOGS_COPIED": "Copié !",
"TOAST_PEER_JOINED": "{name} a rejoint le salon",
"TOAST_PEER_LEFT": "{name} a quitté le salon",
"TOAST_PEER_ACTION": "{name} a {action}",
"STATUS_CONNECTED": "Connecté",
"STATUS_RECONNECTING": "Reconnexion...",
"STATUS_CONNECTING": "Connexion...",
"STATUS_FAILED": "Échec",
"STATUS_DISCONNECTED": "Déconnecté",
"BTN_STATE_JOINING": "🚀 Connexion...",
"BTN_STATE_RECONNECTING": "🔄 Reconnexion...",
"BTN_STATE_PLAYING": "▶ Lecture...",
"BTN_STATE_PAUSING": "⏸ Pause...",
"BTN_STATE_SYNCING_GROUP": "Synchro au groupe ({time})...",
"BTN_STATE_SYNCING": "Synchronisation...",
"BTN_STATE_SYNCED": "✅ Synchronisé !",
"NOTIF_PLAY": "lancé la lecture",
"NOTIF_PAUSE": "mis la lecture en pause",
"NOTIF_SEEK": "déplacé la position dans la vidéo",
"NOTIF_FORCE_PREPARE": "lancé une synchronisation forcée",
"NOTIF_FORCE_EXECUTE": "synchronisé tout le monde",
"DEBUG_NO_TAB": "Aucun onglet cible sélectionné.",
"DEBUG_COMM_FAIL": "Impossible de communiquer avec l'onglet vidéo.",
"EMPTY_PEERS_TITLE": "Aucun membre pour l'instant",
"EMPTY_PEERS_HINT": "Partagez votre lien d'invitation pour commencer",
"EMPTY_HISTORY_TITLE": "Aucune activité pour l'instant",
"EMPTY_HISTORY_HINT": "Lancez, mettez en pause ou déplacez la position pour voir l'historique",
"EMPTY_LOGS_TITLE": "Aucun journal",
"EMPTY_LOGS_HINT": "Les événements de connexion s'afficheront ici",
"EMPTY_ROOMS_TITLE": "Aucun salon actif",
"EMPTY_ROOMS_HINT": "Créez un salon ou actualisez pour trouver des salons publics",
"LABEL_YOU": "Vous",
"ONBOARDING_DONE": "Terminé !",
"LABEL_LOBBY_PEER_READY": "Prêt",
"LABEL_LOBBY_PEER_LOADING": "Chargement...",
"LABEL_PASSWORD_PROTECTED": "Protégé par mot de passe",
"LABEL_PEERS_COUNT": "{count} membres",
"LABEL_CUSTOM_SERVER": "Serveur personnalisé",
"BTN_STATE_CREATING": "🚀 Création de la salle...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Échec de la synchronisation de l'épisode",
"NOTIF_LOBBY_CANCEL_MSG": "Synchronisation automatique annulée : {reason}. Vous devrez peut-être synchroniser manuellement.",
"LOBBY_CANCEL_TIMEOUT": "Délai d'attente dépassé",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Délai dépassé (récupéré)",
"LOBBY_CANCEL_PEERS_LEFT": "Tous les autres membres sont partis",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Délai dépassé — tous les membres n'ont pas chargé l'épisode",
"LOBBY_CANCEL_USER": "Annulé par l'utilisateur",
"NOTIF_ERROR_TITLE": "Erreur KoalaSync"
}
+204
View File
@@ -0,0 +1,204 @@
# KoalaSync Browser Extension v2.0 - i18n Technical Implementation Plan
Welcome, future Antigravity AI agent! This document is placed directly in the codebase at `/extension/locales/i18n_plan.md` to serve as a comprehensive architectural handbook for the next steps in adding **full internationalization (i18n) support to the browser extension itself** while maintaining 100% video-sync and background communication safety.
---
## 🔍 Context & Audited Scope
KoalaSync is a lightweight, premium browser extension (Chrome & Firefox Manifest V3) for synchronized video playback. The landing pages are already compiled dynamically in 6 languages:
* English (`en`)
* German (`de`)
* French (`fr`)
* Spanish (`es`)
* Portuguese (Brazil) (`pt-BR`)
* Russian (`ru`)
Our goal is to build an identical, premium translation engine for the extension itself.
---
## 🏛️ Architectural Choice: Custom JSON Dictionary Engine (Approach B)
We evaluated two paths:
1. **Approach A (Native `chrome.i18n` API):** Uses `_locales/` directory. Too rigid—cannot support real-time dynamic switching inside the extension Settings dropdown without closing/re-opening the popup.
2. **Approach B (Custom Unified JSON Engine):** Uses flat JSON files matching our website files (`"KEY": "Value"`). Dynamically merges the target dictionary with baseline English `en.json` at runtime, programmatically providing an **airtight English fallback** and **real-time DOM translations** without popup reload.
We chose **Approach B** for maximum compatibility, premium real-time toggling, and clean fallback safety.
---
## 🔄 Resolve, Load, & State Flow
1. **On launch:** Look for saved language in `chrome.storage.sync.get('locale')`.
2. **Fallback Autodetect:** If no saved language, read `navigator.language` or `chrome.i18n.getUILanguage()`.
* If the detected locale is supported, set as active.
* If not supported, default to English (`en`).
3. **Dictionary Resolution:**
* Asynchronously load the English baseline dictionary (`/extension/locales/en.json`).
* If the target language is different, load target JSON (e.g. `/extension/locales/de.json`) and execute `Object.assign({}, enDict, targetDict)`. This guarantees dynamic translation while cleanly falling back to English for any missing keys.
4. **DOM Replacements:** Scan for `data-i18n`, `data-i18n-title`, and `data-i18n-placeholder` attributes, and translate them on the fly.
5. **Persistence:** Save dynamic selection modifications from the dropdown into `chrome.storage.sync`. Trigger instant DOM re-translation on change.
---
## 📂 Proposed File Structure
```
KoalaPlay/
└── extension/
├── locales/ # [NEW] Contains flat translation maps
│ ├── i18n_plan.md # This roadmap file
│ ├── en.json # Flat English baseline keys
│ ├── de.json # German keys
│ ├── fr.json # French keys
│ ├── es.json # Spanish keys
│ ├── pt-BR.json # Portuguese (Brasil) keys
│ └── ru.json # Russian keys
├── i18n.js # [NEW] ESM translation engine module
├── popup.html # Modified with data-i18n attributes
├── popup.js # Modified to initialize locales and update variables
└── background.js # Modified to push localized notification alerts
```
---
## 🛠️ Draft Code Snippets
### 1. `i18n.js` (Zero-Dependency Engine Module)
```javascript
// extension/i18n.js
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
export const DEFAULT_LANGUAGE = 'en';
let activeDictionary = {};
export async function loadLocale(langCode) {
const resolvedLang = SUPPORTED_LANGUAGES.includes(langCode) ? langCode : DEFAULT_LANGUAGE;
try {
const enResponse = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
const enDict = await enResponse.json();
if (resolvedLang === DEFAULT_LANGUAGE) {
activeDictionary = enDict;
return;
}
const targetResponse = await fetch(chrome.runtime.getURL(`locales/${resolvedLang}.json`));
const targetDict = await targetResponse.json();
activeDictionary = Object.assign({}, enDict, targetDict);
} catch (err) {
console.error('[i18n] Failed to load dictionary. Falling back to English:', err);
const rescue = await fetch(chrome.runtime.getURL(`locales/${DEFAULT_LANGUAGE}.json`));
activeDictionary = await rescue.json();
}
}
export function getMessage(key) {
return activeDictionary[key] || key;
}
export function translateDOM() {
// Translate text nodes
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const translated = getMessage(key);
const img = el.querySelector('img');
if (img) {
el.innerHTML = '';
el.appendChild(img);
el.appendChild(document.createTextNode(' ' + translated));
} else {
el.textContent = translated;
}
});
// Translate tooltips
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.setAttribute('title', getMessage(key));
});
// Translate placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.setAttribute('placeholder', getMessage(key));
});
}
```
### 2. Markup Changes (`popup.html`)
* Annotate text elements: `<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP">Settings</button>`
* Add Language Dropdown in the Settings Panel:
```html
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" data-i18n="LABEL_LANGUAGE" title="Choose your preferred extension language">App Language</label>
<select id="langSelector" style="width: 150px; padding: 6px 10px; font-size: 13px; cursor: pointer;">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
</select>
</div>
```
### 3. Dynamic Script Wiring (`popup.js`)
* Import our engine: `import { loadLocale, translateDOM, getMessage } from './i18n.js';`
* Initialize during startup:
```javascript
async function init() {
const data = await chrome.storage.sync.get(['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();
// Select matching option in selector
const langSelector = document.getElementById('langSelector');
if (langSelector) langSelector.value = activeLang;
// rest of standard init...
}
```
* Listen to Settings change event:
```javascript
const langSelector = document.getElementById('langSelector');
if (langSelector) {
langSelector.addEventListener('change', async () => {
const selectedLang = langSelector.value;
await chrome.storage.sync.set({ locale: selectedLang });
await loadLocale(selectedLang);
translateDOM();
// Re-render empty elements and tab list using new dynamic strings
refreshLogs();
refreshHistory();
populateTabs();
});
}
```
### 4. Background Notifications (`background.js`)
* Notifications inside `showNotification` should fetch dynamic keys based on `chrome.storage.sync` language settings and build clean alerts dynamically using `chrome.storage.sync.get('locale')`.
* Ensure that WebSocket event transmissions and video player scripts in `content.js` remain **completely untouched and unaware** of the localization engine.
---
## 🔒 Safety Guardrail
* **WebSocket Engine Integrity:** Under no circumstances should `background.js` Socket.IO handshakes, rate limits, or binary protocol headers be adjusted. i18n is strictly an interface rendering skin layer.
* **Player Control Pipeline Integrity:** `content.js` expectedEvent suppression tables, HLS buffer calculations, and Media Session API listeners must remain untouched. Do not bind any i18n logic into the content script's active tracking loop.
---
## 🧪 Verification Tasks
1. **Autodetection Audit:** Force-load the extension in Chrome/Firefox in a non-English language container and ensure it starts in that language or English gracefully.
2. **Integrity Validation Script:** Write a sanity node checker script `/scripts/test-locales.js` to ensure all key structures are uniform across all JSON files.
3. **Real-Time Refresh Verification:** Change dynamic drop-down preferences and verify that every element and alert swaps languages cleanly with zero redraw glitches or page crashes.
+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "pt-BR",
"HTML_CLASS": "lang-pt-br",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Sala",
"TAB_ROOM_TOOLTIP": "Configurações da sala e conexão",
"TAB_SYNC": "Sincro",
"TAB_SYNC_TOOLTIP": "Controles de sincronia de vídeo e ações",
"TAB_SETTINGS": "Opções",
"TAB_SETTINGS_TOOLTIP": "Preferências da extensão",
"TAB_STATUS": "Status",
"TAB_STATUS_TOOLTIP": "Diagnósticos avançados e logs",
"BTN_CREATE_ROOM": "+ Criar nova sala",
"MANUAL_CONNECT_HEADER": "Conexão manual / Avançado",
"LABEL_SERVER": "Servidor",
"BTN_SERVER_OFFICIAL": "Oficial",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar o servidor oficial confiável",
"BTN_SERVER_CUSTOM": "Perso",
"BTN_SERVER_CUSTOM_TOOLTIP": "Conectar ao seu próprio servidor auto-hospedado",
"PLACEHOLDER_SERVER_URL": "wss://seu-servidor:3000",
"LABEL_ROOM_ID": "ID da sala",
"LABEL_ROOM_ID_TOOLTIP": "O identificador exclusivo para sua sala de sincronização",
"PLACEHOLDER_ROOM_ID": "Digite o ID da sala",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "O ID exclusivo da sala na qual você deseja entrar",
"LABEL_PASSWORD": "Senha (Opcional)",
"LABEL_PASSWORD_TOOLTIP": "Senha opcional para restringir o acesso à sala",
"PLACEHOLDER_PASSWORD": "Senha da sala (opcional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Senha para a sala (deixe em branco se não houver)",
"BTN_JOIN_ROOM": "Entrar / Criar sala",
"BTN_JOIN_ROOM_TOOLTIP": "Conectar à sala",
"LABEL_PUBLIC_ROOMS": "Salas públicas",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponíveis neste servidor",
"BTN_REFRESH": "ATUALIZAR",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Atualizando...",
"LABEL_ACTIVE_ROOM": "Sala ativa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "A sala à qual você está conectado no momento",
"ACTIVE_ROOM_NONE": "NENHUMA",
"ACTIVE_SERVER_OFFICIAL": "Servidor oficial",
"LABEL_INVITE_LINK": "Link de convite",
"LABEL_INVITE_LINK_TOOLTIP": "Compartilhe este link com amigos para que eles entrem",
"LABEL_PEERS_IN_ROOM": "Participantes na sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Outros usuários conectados atualmente a esta sala",
"NO_PEERS_CONNECTED": "Sem participantes conectados",
"BTN_LEAVE_ROOM": "Sair da sala",
"LABEL_SELECT_VIDEO": "Selecionar vídeo",
"LABEL_SELECT_VIDEO_TOOLTIP": "Escolha a aba do navegador contendo o vídeo a ser sincronizado",
"OPTION_SELECT_TAB": "-- Selecione uma aba --",
"LABEL_REMOTE_CONTROL": "Controle remoto",
"BTN_COPY_INVITE": "📋 Link de convite",
"BTN_COPY_INVITE_TOOLTIP": "Copiar link de convite",
"BTN_PLAY": "▶ Reproduzir",
"BTN_PLAY_TOOLTIP": "Enviar um comando de reproduzir para todos",
"BTN_PAUSE": "⏸ Pausar",
"BTN_PAUSE_TOOLTIP": "Enviar um comando de pausar para todos",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forçar todos os usuários a sincronizar",
"OPTION_JUMP_TO_OTHERS": "Ir para os outros",
"OPTION_JUMP_TO_ME": "Trazer para mim",
"OPTION_JUMP_MODE_TOOLTIP": "Escolher o alvo de sincronização",
"LABEL_LAST_ACTIVITY": "Última atividade",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra o comando mais recente de reproduzir, pausar ou buscar",
"NO_RECENT_COMMANDS": "Sem comandos recentes",
"LOBBY_HEADER": "LOBBY DE EPISÓDIO",
"LOBBY_WAITING_FOR": "🎬 Aguardando: \"{title}\"",
"LOBBY_WAITING_PEERS": "Aguardando participantes...",
"BTN_SKIP_PLAY": "Pular e reproduzir",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar o lobby e reproduzir assim mesmo",
"LOBBY_CONNECT_FIRST": "Entre em uma sala primeiro",
"LOBBY_CONNECT_FIRST_DESC": "Você precisa entrar em uma sala através de um link de convite ou criar uma nova para sincronizar vídeos.",
"BTN_CREATE_ROOM_ALT": "Criar nova sala",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Criar uma nova sala aleatória e entrar",
"LABEL_USERNAME": "Seu nome de usuário",
"LABEL_USERNAME_TOOLTIP": "O nome de usuário ajuda os outros a identificá-lo.",
"PLACEHOLDER_USERNAME": "Coala anônimo",
"LABEL_HIDE_CLUTTER": "Lista de abas limpa",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra abas que não são de vídeo e domínios não relacionados para manter a lista organizada",
"LABEL_AUTO_SYNC_NEXT": "Sincro auto do próximo episódio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e aguarda todos ao mudar de episódio, depois inicia em sincronia.",
"LABEL_AUTO_COPY_INVITE": "Auto-copiar link",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automaticamente o link de convite para a área de transferência ao criar uma nova sala.",
"LABEL_NOTIFICATIONS": "Notificações do navegador",
"LABEL_NOTIFICATIONS_TOOLTIP": "Mostra notificações do sistema quando alguém entra/sai ou reproduz/pausa.",
"LABEL_LANGUAGE": "Idioma do aplicativo",
"LABEL_LANGUAGE_TOOLTIP": "Escolha o idioma de sua preferência para a extensão",
"LABEL_TROUBLESHOOTING": "Resolução de problemas",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Ferramentas para solucionar problemas de conexão",
"BTN_REGEN_ID": "Regenerar ID de participante",
"BTN_REGEN_ID_TOOLTIP": "Regenerar seu ID interno e conectar novamente",
"REGEN_ID_DESC": "Use esta opção se notar erros de 'Identidade duplicada'.",
"LABEL_CONN_STATUS": "Status da conexão",
"LABEL_CONN_STATUS_TOOLTIP": "Status atual da conexão WebSocket",
"CONN_STATUS_DISCONNECTED": "Desconectado",
"BTN_RETRY": "REENTRAR",
"BTN_RETRY_TOOLTIP": "Tentar reconectar ao servidor",
"BTN_COPY_LOGS": "Copiar logs",
"BTN_COPY_LOGS_TOOLTIP": "Copiar logs para a área de transferência para compartilhar",
"LABEL_VIDEO_DEBUG": "Informações de depuração de vídeo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalhes técnicos sobre o elemento de vídeo selecionado atualmente",
"VIDEO_DEBUG_EMPTY": "Nenhuma aba selecionada ou vídeo detectado.",
"LABEL_HISTORY": "Histórico completo",
"LABEL_HISTORY_TOOLTIP": "Registro cronológico de todos os comandos de sincronização na sala",
"HISTORY_EMPTY": "Sem atividade ainda",
"LABEL_LOGS": "Logs (Últimos 50)",
"LABEL_LOGS_TOOLTIP": "Logs técnicos de conexão para depuração",
"BTN_CLEAR": "LIMPAR",
"BTN_CLEAR_TOOLTIP": "Limpar a saída dos logs",
"LABEL_GITHUB": "Repositório GitHub",
"BTN_ONBOARDING_SKIP": "Pular",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Pular o tutorial",
"BTN_ONBOARDING_NEXT": "Próximo",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ir para o próximo passo",
"ONBOARDING_1_TITLE": "Bem-vindo ao KoalaSync!",
"ONBOARDING_1_TEXT": "Assista a vídeos juntos em perfeita sincronia, não importa onde estejam. Vamos fazer um tour rápido!",
"ONBOARDING_2_TITLE": "1. Criar uma sala",
"ONBOARDING_2_TEXT": "Comece aqui. Crie uma sala e compartilhe o link de convite com seus amigos.",
"ONBOARDING_3_TITLE": "2. Selecionar vídeo",
"ONBOARDING_3_TEXT": "Navegue aqui para selecionar o vídeo a ser sincronizado. Reproduza, pause e busque: todos permanecem sincronizados.",
"ONBOARDING_4_TITLE": "3. Personalizar",
"ONBOARDING_4_TEXT": "Escolha um nome de usuário divertido para que seus amigos saibam quem você é.",
"ONBOARDING_5_TITLE": "Tudo pronto!",
"ONBOARDING_5_TEXT": "Hora de pegar a pipoca. Divirta-se assistindo com seus amigos!",
"ERR_CONN_TIMEOUT": "Tempo limite de conexão esgotado. Tente novamente.",
"ERR_INVALID_SERVER_URL": "Formato de URL do servidor inválido.",
"ERR_IDENTITY_NOT_LOADED": "Identidade não carregada ainda. Aguarde um momento e tente novamente.",
"ERR_NO_PEERS_TIME": "Nenhum outro participante com posição conhecida. Mude para 'Traerlos a mí' / 'Trazer para mim'.",
"ERR_NO_VIDEO_TAB": "Não foi possível conectar à aba do vídeo.",
"ERR_SELECT_VIDEO": "Selecione um vídeo primeiro!",
"TOAST_INVITE_COPIED": "Link de convite copiado!",
"TOAST_COPY_FAILED": "Falha ao copiar para a área de transferência",
"TOAST_LOBBY_SKIPPED": "Lobby de episódio ignorado.",
"TOAST_LOBBY_SKIP_FAILED": "Falha ao ignorar o lobby.",
"TOAST_LOGS_COPIED": "Copiado!",
"TOAST_PEER_JOINED": "{name} entrou na sala",
"TOAST_PEER_LEFT": "{name} saiu da sala",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Conectado",
"STATUS_RECONNECTING": "Reconectando...",
"STATUS_CONNECTING": "Conectando...",
"STATUS_FAILED": "Falhou",
"STATUS_DISCONNECTED": "Desconectado",
"BTN_STATE_JOINING": "🚀 Entrando...",
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
"BTN_STATE_PLAYING": "▶ Reproduzindo...",
"BTN_STATE_PAUSING": "⏸ Pausando...",
"BTN_STATE_SYNCING_GROUP": "Sincronizando com o grupo ({time})...",
"BTN_STATE_SYNCING": "Sincronizando...",
"BTN_STATE_SYNCED": "✅ Sincronizado!",
"NOTIF_PLAY": "iniciou a reprodução",
"NOTIF_PAUSE": "pausou a reprodução",
"NOTIF_SEEK": "mudou a posição do vídeo",
"NOTIF_FORCE_PREPARE": "iniciou uma sincronização forçada",
"NOTIF_FORCE_EXECUTE": "sincronizou todos",
"DEBUG_NO_TAB": "Nenhuma aba alvo selecionada.",
"DEBUG_COMM_FAIL": "Não foi possível comunicar com o vídeo da aba.",
"EMPTY_PEERS_TITLE": "Nenhum participante ainda",
"EMPTY_PEERS_HINT": "Compartilhe seu link de convite para começar",
"EMPTY_HISTORY_TITLE": "Sem atividade ainda",
"EMPTY_HISTORY_HINT": "Reproduza, pause ou busque para ver o histórico",
"EMPTY_LOGS_TITLE": "Sem logs",
"EMPTY_LOGS_HINT": "Os eventos de conexão aparecerão aqui",
"EMPTY_ROOMS_TITLE": "Sem salas ativas",
"EMPTY_ROOMS_HINT": "Crie uma sala ou atualize para buscar salas públicas",
"LABEL_YOU": "Você",
"ONBOARDING_DONE": "Concluído!",
"LABEL_LOBBY_PEER_READY": "Pronto",
"LABEL_LOBBY_PEER_LOADING": "Carregando...",
"LABEL_PASSWORD_PROTECTED": "Protegido por senha",
"LABEL_PEERS_COUNT": "{count} participantes",
"LABEL_CUSTOM_SERVER": "Servidor personalizado",
"BTN_STATE_CREATING": "🚀 Criando sala...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Falha na sincronização do episódio",
"NOTIF_LOBBY_CANCEL_MSG": "Sincronização automática cancelada: {reason}. Pode ser necessário sincronizar manualmente.",
"LOBBY_CANCEL_TIMEOUT": "Tempo limite esgotado",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tempo limite esgotado (recuperado)",
"LOBBY_CANCEL_PEERS_LEFT": "Todos os outros participantes saíram",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo limite esgotado — nem todos os participantes carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo usuário",
"NOTIF_ERROR_TITLE": "Erro do KoalaSync"
}
+180
View File
@@ -0,0 +1,180 @@
{
"LANG_CODE": "ru",
"HTML_CLASS": "lang-ru",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Комната",
"TAB_ROOM_TOOLTIP": "Настройки комнаты и подключение",
"TAB_SYNC": "Синхро",
"TAB_SYNC_TOOLTIP": "Управление синхронизацией видео и действия",
"TAB_SETTINGS": "Опции",
"TAB_SETTINGS_TOOLTIP": "Настройки расширения",
"TAB_STATUS": "Статус",
"TAB_STATUS_TOOLTIP": "Дополнительная диагностика и журналы",
"BTN_CREATE_ROOM": "+ Создать комнату",
"MANUAL_CONNECT_HEADER": "Ручное подключение / Дополнительно",
"LABEL_SERVER": "Сервер",
"BTN_SERVER_OFFICIAL": "Официальный",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Использовать официальный надежный сервер",
"BTN_SERVER_CUSTOM": "Свой",
"BTN_SERVER_CUSTOM_TOOLTIP": "Подключиться к своему выделенному серверу",
"PLACEHOLDER_SERVER_URL": "wss://ваш-сервер:3000",
"LABEL_ROOM_ID": "ID комнаты",
"LABEL_ROOM_ID_TOOLTIP": "Уникальный идентификатор вашей синхронной комнаты",
"PLACEHOLDER_ROOM_ID": "Введите ID комнаты",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Уникальный ID комнаты, к которой хотите присоединиться",
"LABEL_PASSWORD": "Пароль (Опционально)",
"LABEL_PASSWORD_TOOLTIP": "Дополнительный пароль для ограничения доступа к комнате",
"PLACEHOLDER_PASSWORD": "Пароль комнаты (опционально)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Пароль от комнаты (оставьте пустым, если его нет)",
"BTN_JOIN_ROOM": "Войти / Создать комнату",
"BTN_JOIN_ROOM_TOOLTIP": "Подключиться к комнате",
"LABEL_PUBLIC_ROOMS": "Публичные комнаты",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Список публично доступных комнат на этом сервере",
"BTN_REFRESH": "ОБНОВИТЬ",
"BTN_REFRESH_TOOLTIP": "Обновить список публичных комнат",
"PUBLIC_ROOMS_REFRESHING": "Обновление...",
"LABEL_ACTIVE_ROOM": "Активная комната",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Комната, к которой вы сейчас подключены",
"ACTIVE_ROOM_NONE": "НЕТ",
"ACTIVE_SERVER_OFFICIAL": "Официальный сервер",
"LABEL_INVITE_LINK": "Ссылка-приглашение",
"LABEL_INVITE_LINK_TOOLTIP": "Поделитесь этой ссылкой с друзьями, чтобы они вошли",
"LABEL_PEERS_IN_ROOM": "Участники в комнате",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Другие пользователи, подключенные к этой комнате",
"NO_PEERS_CONNECTED": "Нет подключенных участников",
"BTN_LEAVE_ROOM": "Выйти из комнаты",
"LABEL_SELECT_VIDEO": "Выбрать видео",
"LABEL_SELECT_VIDEO_TOOLTIP": "Выберите вкладку браузера, содержащую видео для синхронизации",
"OPTION_SELECT_TAB": "-- Выберите вкладку --",
"LABEL_REMOTE_CONTROL": "Дистанционное управление",
"BTN_COPY_INVITE": "📋 Ссылка",
"BTN_COPY_INVITE_TOOLTIP": "Копировать ссылку-приглашение",
"BTN_PLAY": "▶ Запуск",
"BTN_PLAY_TOOLTIP": "Отправить команду воспроизведения всем",
"BTN_PAUSE": "⏸ Пауза",
"BTN_PAUSE_TOOLTIP": "Отправить команду паузы всем",
"BTN_SYNC": "⚡ СИНХРО",
"BTN_SYNC_TOOLTIP": "Принудительно синхронизировать всех участников",
"OPTION_JUMP_TO_OTHERS": "Перейти к другим",
"OPTION_JUMP_TO_ME": "Синхронизировать по мне",
"OPTION_JUMP_MODE_TOOLTIP": "Выбрать цель синхронизации",
"LABEL_LAST_ACTIVITY": "Последняя активность",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Показывает самую последнюю команду воспроизведения, паузы или перемотки",
"NO_RECENT_COMMANDS": "Нет последних команд",
"LOBBY_HEADER": "ЛОББИ СЕРИИ",
"LOBBY_WAITING_FOR": "🎬 Ожидание: \"{title}\"",
"LOBBY_WAITING_PEERS": "Ожидание участников...",
"BTN_SKIP_PLAY": "Пропустить и запустить",
"BTN_SKIP_PLAY_TOOLTIP": "Закрыть лобби и воспроизвести в любом случае",
"LOBBY_CONNECT_FIRST": "Сначала войдите в комнату",
"LOBBY_CONNECT_FIRST_DESC": "Вам необходимо войти в комнату по ссылке-приглашению или создать новую, чтобы синхронизировать видео.",
"BTN_CREATE_ROOM_ALT": "Создать новую комнату",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Создать новую случайную комнату и войти",
"LABEL_USERNAME": "Ваше имя пользователя",
"LABEL_USERNAME_TOOLTIP": "Имя пользователя помогает другим узнать вас.",
"PLACEHOLDER_USERNAME": "Анонимный Коала",
"LABEL_HIDE_CLUTTER": "Чистый список вкладок",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Скрывает вкладки без видео и несвязанные домены для порядка",
"LABEL_AUTO_SYNC_NEXT": "Автосинхрон следующей серии",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Автоматически ставит на паузу и ждет всех при смене серии, затем запускает видео синхронно.",
"LABEL_AUTO_COPY_INVITE": "Автокопирование ссылки",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Автоматически копирует ссылку-приглашение в буфер обмена при создании новой комнаты.",
"LABEL_NOTIFICATIONS": "Уведомления браузера",
"LABEL_NOTIFICATIONS_TOOLTIP": "Показывает системные уведомления, когда кто-то входит/выходит или запускает/приостанавливает воспроизведение.",
"LABEL_LANGUAGE": "Язык приложения",
"LABEL_LANGUAGE_TOOLTIP": "Выберите предпочтительный язык интерфейса расширения",
"LABEL_TROUBLESHOOTING": "Устранение неполадок",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Инструменты для исправления проблем с подключением",
"BTN_REGEN_ID": "Сбросить Peer ID",
"BTN_REGEN_ID_TOOLTIP": "Сбросить ваш внутренний идентификатор и переподключиться",
"REGEN_ID_DESC": "Используйте при появлении ошибок дублирования идентификации.",
"LABEL_CONN_STATUS": "Статус подключения",
"LABEL_CONN_STATUS_TOOLTIP": "Текущий статус WebSocket-соединения с сервером",
"CONN_STATUS_DISCONNECTED": "Отключено",
"BTN_RETRY": "ПОВТОРИТЬ",
"BTN_RETRY_TOOLTIP": "Попробовать переподключиться к серверу",
"BTN_COPY_LOGS": "Копировать логи",
"BTN_COPY_LOGS_TOOLTIP": "Скопировать логи в буфер обмена для отправки",
"LABEL_VIDEO_DEBUG": "Отладочные данные видео",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Технические сведения о выбранном видеоэлементе",
"VIDEO_DEBUG_EMPTY": "Вкладка не выбрана или видео не обнаружено.",
"LABEL_HISTORY": "История действий",
"LABEL_HISTORY_TOOLTIP": "Хронологический список всех команд синхронизации в комнате",
"HISTORY_EMPTY": "Нет недавней активности",
"LABEL_LOGS": "Логи (Последние 50)",
"LABEL_LOGS_TOOLTIP": "Технические системные сообщения подключения",
"BTN_CLEAR": "ОЧИСТИТЬ",
"BTN_CLEAR_TOOLTIP": "Очистить вывод логов",
"LABEL_GITHUB": "Репозиторий GitHub",
"BTN_ONBOARDING_SKIP": "Пропустить",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Пропустить руководство",
"BTN_ONBOARDING_NEXT": "Далее",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Перейти к следующему шагу",
"ONBOARDING_1_TITLE": "Добро пожаловать в KoalaSync!",
"ONBOARDING_1_TEXT": "Смотрите видео в идеальной синхронизации вместе — где бы вы ни находились. Давайте проведем небольшую экскурсию!",
"ONBOARDING_2_TITLE": "1. Создайте комнату",
"ONBOARDING_2_TEXT": "Начните здесь. Создайте комнату и поделитесь ссылкой-приглашением со своими друзьями.",
"ONBOARDING_3_TITLE": "2. Выберите видео",
"ONBOARDING_3_TEXT": "Выберите вкладку с видео для синхронизации. Запуск, пауза, перемотка — воспроизведение синхронизируется у всех.",
"ONBOARDING_4_TITLE": "3. Настройте профиль",
"ONBOARDING_4_TEXT": "Введите имя пользователя, чтобы друзья знали, кто совершает действия.",
"ONBOARDING_5_TITLE": "Всё готово!",
"ONBOARDING_5_TEXT": "Время готовить попкорн. Приятного совместного просмотра!",
"ERR_CONN_TIMEOUT": "Время ожидания подключения истекло. Пожалуйста, попробуйте снова.",
"ERR_INVALID_SERVER_URL": "Неверный формат URL-адреса сервера.",
"ERR_IDENTITY_NOT_LOADED": "Идентификатор еще не загружен. Подождите немного и повторите попытку.",
"ERR_NO_PEERS_TIME": "Нет других участников с известной позицией плеера. Выберите 'Синхронизировать по мне'.",
"ERR_NO_VIDEO_TAB": "Не удалось подключиться к вкладке с видео.",
"ERR_SELECT_VIDEO": "Пожалуйста, сначала выберите вкладку с видео!",
"TOAST_INVITE_COPIED": "Ссылка скопирована в буфер обмена!",
"TOAST_COPY_FAILED": "Не удалось скопировать ссылку в буфер обмена",
"TOAST_LOBBY_SKIPPED": "Ожидание серии отменено.",
"TOAST_LOBBY_SKIP_FAILED": "Не удалось отменить ожидание серии.",
"TOAST_LOGS_COPIED": "Скопировано!",
"TOAST_PEER_JOINED": "{name} вошел в комнату",
"TOAST_PEER_LEFT": "{name} вышел из комнаты",
"TOAST_PEER_ACTION": "{name}: {action}",
"STATUS_CONNECTED": "Подключено",
"STATUS_RECONNECTING": "Переподключение...",
"STATUS_CONNECTING": "Подключение...",
"STATUS_FAILED": "Ошибка",
"STATUS_DISCONNECTED": "Отключено",
"BTN_STATE_JOINING": "🚀 Вход...",
"BTN_STATE_RECONNECTING": "🔄 Переподключение...",
"BTN_STATE_PLAYING": "▶ Запуск...",
"BTN_STATE_PAUSING": "⏸ Пауза...",
"BTN_STATE_SYNCING_GROUP": "Синхронизация по группе ({time})...",
"BTN_STATE_SYNCING": "Синхронизация...",
"BTN_STATE_SYNCED": "✅ Синхронно!",
"NOTIF_PLAY": "запустил воспроизведение",
"NOTIF_PAUSE": "приостановил воспроизведение",
"NOTIF_SEEK": "перемотал видео",
"NOTIF_FORCE_PREPARE": "запустил принудительный синхрон",
"NOTIF_FORCE_EXECUTE": "синхронизировал воспроизведение у всех",
"DEBUG_NO_TAB": "Целевая вкладка не выбрана.",
"DEBUG_COMM_FAIL": "Не удалось связаться с плеером на вкладке.",
"EMPTY_PEERS_TITLE": "Участников пока нет",
"EMPTY_PEERS_HINT": "Поделитесь ссылкой-приглашением, чтобы начать",
"EMPTY_HISTORY_TITLE": "История действий пуста",
"EMPTY_HISTORY_HINT": "Запускайте, останавливайте или перематывайте видео, чтобы увидеть лог",
"EMPTY_LOGS_TITLE": "Журнал пуст",
"EMPTY_LOGS_HINT": "События подключения будут отображаться здесь",
"EMPTY_ROOMS_TITLE": "Нет активных комнат",
"EMPTY_ROOMS_HINT": "Создайте комнату или обновите список, чтобы найти публичные",
"LABEL_YOU": "Вы",
"ONBOARDING_DONE": "Готово!",
"LABEL_LOBBY_PEER_READY": "Готов",
"LABEL_LOBBY_PEER_LOADING": "Загрузка...",
"LABEL_PASSWORD_PROTECTED": "Защищено паролем",
"LABEL_PEERS_COUNT": "{count} участников",
"LABEL_CUSTOM_SERVER": "Свой сервер",
"BTN_STATE_CREATING": "🚀 Создание комнаты...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Ошибка синхронизации серии",
"NOTIF_LOBBY_CANCEL_MSG": "Автосинхронизация отменена: {reason}. Возможно, вам придется выполнить синхронизацию вручную.",
"LOBBY_CANCEL_TIMEOUT": "Время ожидания истекло",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Время ожидания истекло (восстановлено)",
"LOBBY_CANCEL_PEERS_LEFT": "Все остальные участники вышли",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Время ожидания истекло — не все участники загрузили серию",
"LOBBY_CANCEL_USER": "Отменено пользователем",
"NOTIF_ERROR_TITLE": "Ошибка KoalaSync"
}
+6 -2
View File
@@ -1,8 +1,8 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "1.4.5",
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, Netflix, and HTML5 sites in real-time with friends.",
"version": "2.0.0",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
"tabs",
@@ -18,7 +18,9 @@
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"96": "icons/icon96.png",
"128": "icons/icon128.png"
}
},
@@ -35,7 +37,9 @@
],
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"96": "icons/icon96.png",
"128": "icons/icon128.png"
}
}
+255 -67
View File
@@ -27,25 +27,51 @@
font-size: 14px;
}
.header-row {
display: flex;
align-items: center;
margin-bottom: 16px;
}
h1 {
font-size: 18px;
margin: 0 0 16px 0;
margin: 0;
color: var(--accent);
letter-spacing: 1px;
text-transform: uppercase;
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
h1 img {
width: 24px;
height: 24px;
width: 48px;
height: 48px;
object-fit: contain;
border-radius: 4px;
}
.popup-version {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
text-decoration: none;
opacity: 0.5;
transition: opacity 0.2s, color 0.2s;
}
.popup-version:hover {
opacity: 1;
color: var(--accent);
}
.popup-version svg {
width: 12px;
height: 12px;
display: block;
}
/* Tabs */
.tabs {
display: flex;
@@ -199,16 +225,110 @@
}
.invite-box input { flex: 1; font-size: 11px; }
.invite-box button { width: 40px; padding: 0; }
/* Toast Notifications */
#toast-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.toast {
pointer-events: auto;
padding: 10px 16px;
margin-bottom: 6px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
max-width: 280px;
text-align: center;
animation: toastSlideIn 0.3s ease-out, toastFadeOut 0.3s ease-in 2.7s forwards;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.toast-success { background: var(--success); color: white; }
.toast-error { background: var(--error); color: white; }
.toast-info { background: var(--accent); color: white; }
.toast-warning { background: #f59e0b; color: white; }
@keyframes toastSlideIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastFadeOut {
from { opacity: 1; }
to { opacity: 0; transform: translateY(-10px); }
}
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateX(5px); }
to { opacity: 1; transform: translateX(0); }
}
.tab-active-animate {
animation: fadeSlideIn 0.15s ease-out forwards;
}
/* Toggle Switch CSS */
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #334155;
transition: .3s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: #94a3b8;
transition: .3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(16px);
background-color: white;
}
</style>
</head>
<body>
<h1><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
<div id="toast-container"></div>
<div class="header-row">
<h1 data-i18n="APP_TITLE"><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" class="popup-version">
<svg viewBox="0 0 16 16" fill="currentColor">
<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 id="popupVersion">v0.0.0</span>
</a>
</div>
<div class="tabs">
<button class="tab-btn active" data-tab="tab-room">Room</button>
<button class="tab-btn" data-tab="tab-sync">Sync</button>
<button class="tab-btn" data-tab="tab-settings">Settings</button>
<button class="tab-btn" data-tab="tab-dev">Dev</button>
<button class="tab-btn active" data-tab="tab-room" data-i18n="TAB_ROOM" data-i18n-title="TAB_ROOM_TOOLTIP" title="Room settings and connection">Room</button>
<button class="tab-btn" data-tab="tab-sync" data-i18n="TAB_SYNC" data-i18n-title="TAB_SYNC_TOOLTIP" title="Video sync controls and remote actions">Sync</button>
<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP" title="Extension preferences">Settings</button>
<button class="tab-btn" data-tab="tab-dev" data-i18n="TAB_STATUS" data-i18n-title="TAB_STATUS_TOOLTIP" title="Advanced Diagnostics & Logs">Status</button>
</div>
<!-- Room Tab -->
@@ -216,47 +336,52 @@
<!-- JOIN SECTION: Visible when not in a room -->
<div id="section-join">
<button id="createRoomBtn" class="primary" style="margin-bottom: 16px; padding: 14px; font-size: 15px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);" data-i18n="BTN_CREATE_ROOM">+ Create New Room</button>
<details style="margin-bottom: 12px; background: var(--card); border: 1px solid #334155; border-radius: 8px; padding: 8px;">
<summary style="font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; cursor: pointer; outline: none;" data-i18n="MANUAL_CONNECT_HEADER">Manual Connect / Advanced</summary>
<div style="margin-top: 12px;">
<div class="form-group">
<label>Server</label>
<label title="Select which KoalaSync server to use" data-i18n="LABEL_SERVER">Server</label>
<div style="display:flex; gap:4px; margin-bottom:8px;">
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;">Official</button>
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;">Custom</button>
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_OFFICIAL" data-i18n-title="BTN_SERVER_OFFICIAL_TOOLTIP" title="Use the official reliable server">Official</button>
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;" data-i18n="BTN_SERVER_CUSTOM" data-i18n-title="BTN_SERVER_CUSTOM_TOOLTIP" title="Connect to your own self-hosted server">Custom</button>
</div>
<input type="text" id="serverUrl" placeholder="wss://your-server:3000" style="display:none;">
<input type="text" id="serverUrl" data-i18n-placeholder="PLACEHOLDER_SERVER_URL" placeholder="wss://your-server:3000" style="display:none;">
</div>
<div class="form-group">
<label>Room ID</label>
<input type="text" id="roomId" placeholder="Leave empty to create">
<label title="The unique identifier for your sync room" data-i18n="LABEL_ROOM_ID" data-i18n-title="LABEL_ROOM_ID_TOOLTIP">Room ID</label>
<input type="text" id="roomId" data-i18n-placeholder="PLACEHOLDER_ROOM_ID" data-i18n-title="PLACEHOLDER_ROOM_ID_TOOLTIP" placeholder="Enter Room ID" title="The unique ID of the room you want to join">
</div>
<div class="form-group">
<label>Password (Optional)</label>
<input type="password" id="password" placeholder="Room password">
<label title="Optional password to restrict room access" data-i18n="LABEL_PASSWORD" data-i18n-title="LABEL_PASSWORD_TOOLTIP">Password (Optional)</label>
<input type="password" id="password" data-i18n-placeholder="PLACEHOLDER_PASSWORD" data-i18n-title="PLACEHOLDER_PASSWORD_TOOLTIP" placeholder="Room Password (optional)" title="Password for the room (leave empty if none)">
</div>
<div id="roomError" style="display:none; color:var(--error); font-size:11px; margin-bottom:8px; text-align:center;"></div>
<button id="joinBtn" class="primary">Join Room</button>
<button id="createRoomBtn" class="secondary" style="border: 1px solid var(--accent); color: var(--accent);">Create Random Room</button>
<button id="joinBtn" class="primary" data-i18n="BTN_JOIN_ROOM" data-i18n-title="BTN_JOIN_ROOM_TOOLTIP" title="Connect to the room">Join Room</button>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1.5rem; margin-bottom: 8px;">
<label style="margin:0;">Public Rooms</label>
<button id="refreshRooms" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;">REFRESH</button>
<label style="margin:0;" title="List of publicly available rooms on this server" data-i18n="LABEL_PUBLIC_ROOMS" data-i18n-title="LABEL_PUBLIC_ROOMS_TOOLTIP">Public Rooms</label>
<button id="refreshRooms" class="secondary" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" data-i18n="BTN_REFRESH" data-i18n-title="BTN_REFRESH_TOOLTIP" title="Refresh the list of public rooms">REFRESH</button>
</div>
<div id="publicRooms" class="info-card" style="max-height: 120px; overflow-y: auto; padding: 4px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;">Refreshing...</div>
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;" data-i18n="PUBLIC_ROOMS_REFRESHING">Refreshing...</div>
</div>
</div>
</details>
</div>
<!-- ACTIVE SECTION: Visible when in a room -->
<div id="section-active" style="display:none;">
<div class="info-card" style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; border-left: 4px solid var(--accent);">
<div>
<label style="margin-bottom: 0;">Active Room</label>
<div id="activeRoomId" style="font-weight: 700; color: var(--accent); font-size: 16px; letter-spacing: 1px;">NONE</div>
<label style="margin-bottom: 0;" title="The room you are currently connected to" data-i18n="LABEL_ACTIVE_ROOM" data-i18n-title="LABEL_ACTIVE_ROOM_TOOLTIP">Active Room</label>
<div id="activeRoomId" style="font-weight: 700; color: var(--accent); font-size: 16px; letter-spacing: 1px;" data-i18n="ACTIVE_ROOM_NONE">NONE</div>
</div>
<div id="activeServer" style="font-size: 10px; color: var(--text-muted); text-align: right; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Official Server</div>
<div id="activeServer" style="font-size: 10px; color: var(--text-muted); text-align: right; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" data-i18n="ACTIVE_SERVER_OFFICIAL">Official Server</div>
</div>
<div class="info-card" style="margin-bottom: 20px;">
<label>Invite Link</label>
<label title="Share this link with friends so they can join" data-i18n="LABEL_INVITE_LINK" data-i18n-title="LABEL_INVITE_LINK_TOOLTIP">Invite Link</label>
<div class="invite-box">
<input type="text" id="inviteLink" readonly>
<button id="copyInvite" class="secondary">📋</button>
@@ -264,116 +389,179 @@
</div>
<div style="margin-bottom: 20px;">
<label>Peers in Room</label>
<label title="Other users currently connected to this room" data-i18n="LABEL_PEERS_IN_ROOM" data-i18n-title="LABEL_PEERS_IN_ROOM_TOOLTIP">Peers in Room</label>
<div id="peerList" class="info-card">
<div style="text-align:center; color: var(--text-muted); font-size: 12px;">No peers connected</div>
<div style="text-align:center; color: var(--text-muted); font-size: 12px;" data-i18n="NO_PEERS_CONNECTED">No peers connected</div>
</div>
</div>
<button id="leaveBtn" class="secondary" style="color: var(--error); border-color: var(--error);">Leave Room</button>
<button id="leaveBtn" class="secondary" style="color: var(--error); border-color: var(--error);" data-i18n="BTN_LEAVE_ROOM">Leave Room</button>
</div>
</div>
<!-- Sync Tab -->
<div id="tab-sync" class="tab-content">
<!-- SYNC ACTIVE: Visible when in a room -->
<div id="sync-active">
<div class="form-group">
<label>Target Tab (Video Source)</label>
<label title="Choose the browser tab containing the video to sync" data-i18n="LABEL_SELECT_VIDEO" data-i18n-title="LABEL_SELECT_VIDEO_TOOLTIP">Select Video</label>
<select id="targetTab">
<option value="">-- Select a Tab --</option>
<option value="" data-i18n="OPTION_SELECT_TAB">-- Select a Tab --</option>
</select>
</div>
<label>Remote Control</label>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="margin: 0;" data-i18n="LABEL_REMOTE_CONTROL">Remote Control</label>
<button id="syncTabCopyInvite" title="Copy Invite Link" style="background:transparent; border: 1px solid #334155; border-radius: 6px; padding: 4px 8px; font-size: 11px; cursor:pointer; opacity:0.8; transition: opacity 0.2s; color: var(--text-muted); display: flex; align-items: center; gap: 4px; white-space: nowrap;" data-i18n="BTN_COPY_INVITE" data-i18n-title="BTN_COPY_INVITE_TOOLTIP">📋 Invite Link</button>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
<button id="playBtn" class="primary" style="flex:1; background: var(--success);">▶ Play</button>
<button id="pauseBtn" class="primary" style="flex:1; background: var(--error);">⏸ Pause</button>
<button id="playBtn" class="primary" style="flex:1; background: var(--success);" title="Send a Play command to everyone" data-i18n="BTN_PLAY" data-i18n-title="BTN_PLAY_TOOLTIP">▶ Play</button>
<button id="pauseBtn" class="primary" style="flex:1; background: var(--error);" title="Send a Pause command to everyone" data-i18n="BTN_PAUSE" data-i18n-title="BTN_PAUSE_TOOLTIP">⏸ Pause</button>
</div>
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); width: 100%; margin-bottom: 15px;">⚡ Force Sync Everyone</button>
<div style="display: flex; gap: 8px; margin-bottom: 15px; align-items: stretch;">
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); flex: 1;" title="Force all users to sync up" data-i18n="BTN_SYNC" data-i18n-title="BTN_SYNC_TOOLTIP">⚡ SYNC</button>
<select id="forceSyncMode" style="flex: 1; background: var(--card); border: 1px solid #334155; color: white; padding: 10px 8px; border-radius: 8px; font-size: 11px; font-family: inherit; cursor: pointer; align-self: stretch;" title="Choose sync target" data-i18n-title="OPTION_JUMP_MODE_TOOLTIP">
<option value="jump-to-others" data-i18n="OPTION_JUMP_TO_OTHERS">Jump to Others</option>
<option value="jump-to-me" data-i18n="OPTION_JUMP_TO_ME">Jump to Me</option>
</select>
</div>
<!-- NEW: Last Action Status Card -->
<label>Last Activity Status</label>
<div id="lastActionCard" class="info-card" style="margin-bottom: 15px; min-height: 70px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding-top: 20px;">No recent commands</div>
<label title="Shows the most recent play, pause, or seek command" data-i18n="LABEL_LAST_ACTIVITY" data-i18n-title="LABEL_LAST_ACTIVITY_TOOLTIP">Last Activity Status</label>
<div id="lastActionCard" class="info-card" style="margin-bottom: 15px; max-height: 120px; overflow-y: auto;">
<div style="text-align:center; color: var(--text-muted); font-size: 10px;" data-i18n="NO_RECENT_COMMANDS">No recent commands</div>
</div>
<!-- Episode Auto-Sync Lobby Status -->
<div id="episodeLobbyCard" class="info-card" style="display:none; margin-bottom: 15px; border-left: 4px solid var(--star); animation: fadeIn 0.3s ease-out;">
<div style="display:flex; align-items:center; gap: 6px; margin-bottom: 6px;">
<span style="font-size: 16px;"></span>
<span style="font-weight: 700; color: var(--star); font-size: 12px;">EPISODE LOBBY</span>
<span style="font-weight: 700; color: var(--star); font-size: 12px;" data-i18n="LOBBY_HEADER">EPISODE LOBBY</span>
</div>
<div id="lobbyTitle" style="font-size: 11px; color: var(--text); margin-bottom: 6px; font-weight: 600;"></div>
<div id="lobbyPeerStatus" style="font-size: 10px; color: var(--text-muted);"></div>
<div id="lobbyPeerStatus" style="font-size: 10px; color: var(--text-muted); margin-bottom: 8px;"></div>
<button id="cancelLobbyBtn" class="secondary" style="margin-top: 4px; padding: 6px 10px; font-size: 11px; width: auto; display: block;" title="Cancel lobby and play anyway" data-i18n="BTN_SKIP_PLAY" data-i18n-title="BTN_SKIP_PLAY_TOOLTIP">Skip & Play anyway</button>
</div>
<div id="peerListSync" class="info-card" style="display:none;"></div>
</div>
<!-- SYNC INACTIVE: Visible when not in a room -->
<div id="sync-inactive" style="display:none; text-align: center; padding: 20px 10px;">
<div style="font-size: 32px; margin-bottom: 12px;">🔒</div>
<h3 style="margin: 0 0 8px 0; color: var(--accent); font-size: 15px;" data-i18n="LOBBY_CONNECT_FIRST">Connect to a room first</h3>
<p style="color: var(--text-muted); font-size: 12px; margin-bottom: 20px;" data-i18n="LOBBY_CONNECT_FIRST_DESC">You need to join a room via an invite link or create a new one to sync videos.</p>
<button id="syncTabCreateRoomBtn" class="primary" style="padding: 12px; font-size: 14px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);" title="Create a new random room and join it" data-i18n="BTN_CREATE_ROOM_ALT" data-i18n-title="BTN_CREATE_ROOM_ALT_TOOLTIP">Create New Room</button>
</div>
</div>
<!-- Settings Tab -->
<div id="tab-settings" class="tab-content">
<div class="form-group">
<label>Your Username</label>
<input type="text" id="username" placeholder="Anonymous Koala" maxlength="20">
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" title="Username helps others identify you." data-i18n="LABEL_USERNAME" data-i18n-title="LABEL_USERNAME_TOOLTIP">Your Username</label>
<input type="text" id="username" data-i18n-placeholder="PLACEHOLDER_USERNAME" placeholder="Anonymous Koala" maxlength="20" style="width: 150px;">
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;">Filter Noise Tabs</label>
<input type="checkbox" id="filterNoise" style="width: auto;" checked>
<label style="margin-bottom: 0; cursor: help;" title="Filters out non-video tabs and unrelated domains to keep the list clean" data-i18n="LABEL_HIDE_CLUTTER" data-i18n-title="LABEL_HIDE_CLUTTER_TOOLTIP">Hide Clutter Tabs</label>
<label class="toggle-switch">
<input type="checkbox" id="filterNoise" checked>
<span class="slider"></span>
</label>
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;">Auto-Sync Next Episode</label>
<input type="checkbox" id="autoSyncNextEpisode" style="width: auto;">
<label style="margin-bottom: 0; cursor: help;" title="Pauses automatically and waits for all peers when an episode changes, then sync-starts together." data-i18n="LABEL_AUTO_SYNC_NEXT" data-i18n-title="LABEL_AUTO_SYNC_NEXT_TOOLTIP">Auto-Sync Next Episode</label>
<label class="toggle-switch">
<input type="checkbox" id="autoSyncNextEpisode">
<span class="slider"></span>
</label>
</div>
<div style="font-size: 11px; color: var(--text-muted); padding: 8px;">
<p>• Username helps others identify you.</p>
<p>• Noise filtering uses a blacklist to hide common non-video sites (e.g. Search, Social Media) from the Target Tab selector.</p>
<p>• Auto-Sync will pause and wait for all peers when an episode changes, then sync-start together.</p>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0; cursor: help;" title="Automatically copies the invite link to your clipboard when creating a new room." data-i18n="LABEL_AUTO_COPY_INVITE" data-i18n-title="LABEL_AUTO_COPY_INVITE_TOOLTIP">Auto-Copy Invite Link</label>
<label class="toggle-switch">
<input type="checkbox" id="autoCopyInvite" checked>
<span class="slider"></span>
</label>
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0; cursor: help;" title="Shows native system notifications when someone joins/leaves or plays/pauses." data-i18n="LABEL_NOTIFICATIONS" data-i18n-title="LABEL_NOTIFICATIONS_TOOLTIP">Browser Notifications</label>
<label class="toggle-switch">
<input type="checkbox" id="browserNotifications">
<span class="slider"></span>
</label>
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" data-i18n="LABEL_LANGUAGE" data-i18n-title="LABEL_LANGUAGE_TOOLTIP" title="Choose your preferred extension language">App Language</label>
<select id="langSelector" style="width: 150px; background: var(--bg); border: 1px solid #334155; color: white; padding: 6px 10px; border-radius: 8px; font-size: 13px; cursor: pointer; outline: none; font-family: inherit;">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
</select>
</div>
<div style="margin-top: 15px; padding: 8px; border-top: 1px solid var(--card);">
<label>Troubleshooting</label>
<button id="regenId" class="secondary" style="width: 100%; font-size: 11px;">Regenerate Peer ID</button>
<p style="font-size: 9px; color: var(--text-muted); margin-top: 5px; text-align: center;">Use this if you see "Duplicate Identity" errors.</p>
<label title="Tools for fixing connection issues" data-i18n="LABEL_TROUBLESHOOTING" data-i18n-title="LABEL_TROUBLESHOOTING_TOOLTIP">Troubleshooting</label>
<button id="regenId" class="secondary" style="width: 100%; font-size: 11px;" title="Regenerate your internal ID and reconnect" data-i18n="BTN_REGEN_ID" data-i18n-title="BTN_REGEN_ID_TOOLTIP">Regenerate Peer ID</button>
<p style="font-size: 9px; color: var(--text-muted); margin-top: 5px; text-align: center;" data-i18n="REGEN_ID_DESC">Use this if you see "Duplicate Identity" errors.</p>
</div>
</div>
<!-- Dev Tab -->
<div id="tab-dev" class="tab-content">
<label>Connection Status</label>
<label title="Current WebSocket connection state" data-i18n="LABEL_CONN_STATUS" data-i18n-title="LABEL_CONN_STATUS_TOOLTIP">Connection Status</label>
<div id="connStatus" class="info-card" style="display:flex; align-items:center; gap: 10px;">
<span id="connDot" class="status-dot status-offline"></span>
<span id="connText" style="flex:1;">Disconnected</span>
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px;">Copy Logs</button>
<span id="connText" style="flex:1;" data-i18n="CONN_STATUS_DISCONNECTED">Disconnected</span>
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;" title="Attempt to reconnect to the server" data-i18n="BTN_RETRY" data-i18n-title="BTN_RETRY_TOOLTIP">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px;" title="Copy logs to clipboard for sharing" data-i18n="BTN_COPY_LOGS" data-i18n-title="BTN_COPY_LOGS_TOOLTIP">Copy Logs</button>
</div>
<label>Video Debug Info</label>
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); max-height: 250px; overflow-y: auto; line-height: 1.4;">
<label title="Technical details about the currently selected video element" data-i18n="LABEL_VIDEO_DEBUG" data-i18n-title="LABEL_VIDEO_DEBUG_TOOLTIP">Video Debug Info</label>
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); max-height: 250px; overflow-y: auto; line-height: 1.4;" data-i18n="VIDEO_DEBUG_EMPTY">
No tab selected or video detected.
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label>Full Action History</label>
<label title="Chronological log of all sync commands in the room" data-i18n="LABEL_HISTORY" data-i18n-title="LABEL_HISTORY_TOOLTIP">Full Action History</label>
</div>
<div id="historyList" class="info-card" style="max-height: 120px; overflow-y: auto; font-size: 10px; margin-bottom: 15px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px;">No activity yet</div>
<div style="text-align:center; color: var(--text-muted); font-size: 11px;" data-i18n="HISTORY_EMPTY">No activity yet</div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label>Logs (Last 50)</label>
<button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;">CLEAR</button>
<label title="Technical connection logs for debugging" data-i18n="LABEL_LOGS" data-i18n-title="LABEL_LOGS_TOOLTIP">Logs (Last 50)</label>
<button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" title="Clear log output" data-i18n="BTN_CLEAR" data-i18n-title="BTN_CLEAR_TOOLTIP">CLEAR</button>
</div>
<div id="logList"></div>
<div style="margin-top: 20px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<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>
<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;" data-i18n="LABEL_GITHUB">GitHub Repository</a>
<div id="appVersion" style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v0.0.0</div>
</div>
</div>
<script src="popup.js" type="module"></script>
<!-- Onboarding Overlay -->
<div id="onboarding-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); backdrop-filter: blur(2px); z-index:1000; align-items:flex-end; justify-content:center; padding-bottom: 20px;">
<div id="onboarding-card" style="background:var(--card); padding:24px; border-radius:16px; max-width:280px; width:90%; text-align:center; box-shadow:0 8px 32px rgba(0,0,0,0.5); ">
<div id="onboarding-icon" style="font-size:48px; margin-bottom:12px;">👋</div>
<h2 id="onboarding-title" style="color:var(--accent); margin:0 0 8px; font-size:16px;">Welcome to KoalaSync!</h2>
<p id="onboarding-text" style="color:var(--text-muted); font-size:13px; margin:0 0 16px; line-height:1.4;">Let's get you started.</p>
<div style="display:flex; gap:8px; justify-content:center;">
<button id="onboarding-skip" class="secondary" style="width:auto; padding:8px 16px;" title="Skip the tutorial" data-i18n="BTN_ONBOARDING_SKIP" data-i18n-title="BTN_ONBOARDING_SKIP_TOOLTIP">Skip</button>
<button id="onboarding-next" class="primary" style="width:auto; padding:8px 16px;" title="Go to next step" data-i18n="BTN_ONBOARDING_NEXT" data-i18n-title="BTN_ONBOARDING_NEXT_TOOLTIP">Next</button>
</div>
<div id="onboarding-dots" style="margin-top:12px; display:flex; gap:6px; justify-content:center;"></div>
</div>
</div>
</body>
</html>
+1073 -232
View File
File diff suppressed because it is too large Load Diff
+2143 -3
View File
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -1,13 +1,19 @@
{
"name": "koalasync",
"version": "1.4.5",
"version": "2.0.0",
"description": "KoalaSync Build Scripts",
"private": true,
"scripts": {
"build:extension": "node scripts/build-extension.js"
"build:extension": "node scripts/build-extension.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"devDependencies": {
"archiver": "^7.0.1",
"fs-extra": "^11.2.0"
"esbuild": "^0.28.0",
"eslint": "^10.4.0",
"fs-extra": "^11.2.0",
"sharp": "^0.34.5",
"svgo": "^4.0.1"
}
}
+2 -2
View File
@@ -22,7 +22,7 @@ if (!fs.existsSync(extSharedDir)) {
fs.mkdirSync(extSharedDir, { recursive: true });
}
const sharedFiles = ['constants.js', 'blacklist.js', 'README.md'];
const sharedFiles = ['constants.js', 'blacklist.js', 'names.js', 'README.md'];
for (const file of sharedFiles) {
const src = path.join(masterSharedDir, file);
const dest = path.join(extSharedDir, file);
@@ -31,7 +31,7 @@ for (const file of sharedFiles) {
}
fs.copyFileSync(src, dest);
}
console.log('✓ constants.js, blacklist.js, and README.md synced to extension/shared/');
console.log('✓ constants.js, blacklist.js, names.js, and README.md synced to extension/shared/');
// Read the base manifest
const baseManifest = JSON.parse(fs.readFileSync(baseManifestPath, 'utf8'));
+55
View File
@@ -0,0 +1,55 @@
const fs = require('fs');
const path = require('path');
const localesDir = path.join(__dirname, '..', 'extension', 'locales');
const enPath = path.join(localesDir, 'en.json');
if (!fs.existsSync(enPath)) {
console.error('CRITICAL: en.json is missing!');
process.exit(1);
}
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) {
const filePath = path.join(localesDir, file);
try {
const dict = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const keys = Object.keys(dict);
const missingKeys = enKeys.filter(k => !keys.includes(k));
const extraKeys = keys.filter(k => !enKeys.includes(k));
if (missingKeys.length > 0 || extraKeys.length > 0) {
hasError = true;
console.error(`${file} has inconsistencies:`);
if (missingKeys.length > 0) {
console.error(` Missing keys (${missingKeys.length}):`, missingKeys);
}
if (extraKeys.length > 0) {
console.error(` Extra keys (${extraKeys.length}):`, extraKeys);
}
} else {
console.log(`${file} is fully consistent (matches all keys).`);
}
} catch (err) {
hasError = true;
console.error(`❌ Failed to parse ${file}:`, err.message);
}
}
console.log('');
if (hasError) {
console.error('❌ Locale consistency check failed! Please fix the errors listed above.');
process.exit(1);
} else {
console.log('🎉 All locale files are perfectly synchronized and consistent!');
process.exit(0);
}
+4 -4
View File
@@ -1,11 +1,11 @@
# KoalaSync Relay Server
A high-performance Node.js relay server for synchronized video playback.
A Node.js relay server for synchronized video playback.
## Key Features
- **Zero-Persistence**: No database. All state is held in RAM.
- **Privacy First**: No tracking, no logging of user data.
- **WebSocket Only**: High performance with minimal overhead.
- **Privacy First**: No tracking, no persistent logging of user data.
- **WebSocket Only**: Minimal overhead with efficient transport.
## Setup
@@ -27,7 +27,7 @@ docker pull ghcr.io/shik3i/koalasync:latest
# Or build from the repository root
docker build -t koala-sync-server -f server/Dockerfile .
```
See [docker-compose.example.yml](../docker-compose.example.yml) in the root directory for a ready-to-use configuration.
See [Docker network compose](../docker-compose.caddy.example.yml) or [Static IP compose](../docker-compose.ip.example.yml) in the root directory for ready-to-use Docker Compose files.
### Manual Setup
```bash
+254 -80
View File
@@ -1,12 +1,18 @@
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import dotenv from 'dotenv';
import { EVENTS, OFFICIAL_SERVER_TOKEN } from '../shared/constants.js';
import { EVENTS, OFFICIAL_SERVER_TOKEN, PROTOCOL_VERSION } from '../shared/constants.js';
dotenv.config();
function hashPassword(password) {
if (!password) return null;
const salt = process.env.SERVER_SALT || 'koalasync_salt_3i';
return crypto.createHmac('sha256', salt).update(password).digest('hex');
}
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;
@@ -15,18 +21,45 @@ const MIN_VERSION = process.env.MIN_VERSION || '1.0.0';
const app = express();
app.set('trust proxy', 1); // For real client IP through reverse proxy
// Health Check
app.get('/', (req, res) => res.json({ status: 'online', service: 'KoalaSync Relay' }));
// 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' });
});
app.get('/health', (req, res) => {
const clientIp = req.ip;
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 httpServer = createServer(app);
// Socket.IO setup with security constraints
const io = new Server(httpServer, {
cors: {
origin: ["https://sync.koalastuff.net"],
origin: (origin, callback) => {
if (!origin || origin === 'https://sync.koalastuff.net' || origin.startsWith('chrome-extension://') || origin.startsWith('moz-extension://')) {
callback(null, true);
} else {
log('CORS', `Rejected origin: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
methods: ["GET", "POST"]
},
maxHttpBufferSize: 1024, // 1KB max per message
maxHttpBufferSize: 4096, // 4KB max per message (headroom for JOIN_ROOM payloads)
transports: ['websocket'],
allowUpgrades: false
});
@@ -37,6 +70,7 @@ const io = new Server(httpServer, {
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)
function log(type, message, details = '') {
const timestamp = new Date().toISOString();
@@ -66,6 +100,26 @@ function checkAuthRate(ip, roomId) {
}
function recordAuthFailure(ip, roomId) {
if (failedAuthAttempts.size > 50000) {
const now = Date.now();
// 1. Clear expired entries (> 15 mins)
for (const [key, record] of failedAuthAttempts.entries()) {
if (now - record.lastAttempt > 15 * 60 * 1000) {
failedAuthAttempts.delete(key);
}
}
// 2. If still over 50k, perform LRU-style eviction on the oldest 10,000 entries
if (failedAuthAttempts.size > 50000) {
log('SECURITY', 'failedAuthAttempts size exceeded 50000. Performing LRU-style eviction.');
const sortedEntries = Array.from(failedAuthAttempts.entries())
.sort((a, b) => a[1].lastAttempt - b[1].lastAttempt);
for (let i = 0; i < 10000 && i < sortedEntries.length; i++) {
failedAuthAttempts.delete(sortedEntries[i][0]);
}
}
}
const key = `${ip}:${roomId}`;
const record = failedAuthAttempts.get(key) || { count: 0, lastAttempt: 0 };
record.count++;
@@ -73,17 +127,18 @@ function recordAuthFailure(ip, roomId) {
failedAuthAttempts.set(key, record);
}
// Periodically clean up old auth failure records (every hour)
// Periodically clean up old auth failure records (every 15 minutes)
setInterval(() => {
const now = Date.now();
for (const [key, record] of failedAuthAttempts.entries()) {
if (now - record.lastAttempt > 60 * 60 * 1000) {
if (now - record.lastAttempt > 15 * 60 * 1000) {
failedAuthAttempts.delete(key);
}
}
}, 60 * 60 * 1000);
}, 15 * 60 * 1000);
const eventCounts = new Map(); // socketId -> { count, resetTime }
const healthCounts = new Map(); // ip -> { count, resetTime }
// Clean up connection counts and event counts to prevent memory leak
setInterval(() => {
@@ -94,10 +149,15 @@ setInterval(() => {
}
}
for (const [socketId, entry] of eventCounts.entries()) {
if (now > entry.resetTime) {
if (now > entry.resetTime || !io.sockets.sockets.has(socketId)) {
eventCounts.delete(socketId);
}
}
for (const [ip, entry] of healthCounts.entries()) {
if (now > entry.resetTime) {
healthCounts.delete(ip);
}
}
}, 60000);
function checkConnectionRate(ip) {
@@ -118,6 +178,15 @@ function checkEventRate(socketId) {
return entry.count <= 30;
}
function checkHealthRate(ip) {
const now = Date.now();
const entry = healthCounts.get(ip) || { count: 0, resetTime: now + 60000 };
if (now > entry.resetTime) { entry.count = 0; entry.resetTime = now + 60000; }
entry.count++;
healthCounts.set(ip, entry);
return entry.count <= 60;
}
/**
* Central peer teardown. Removes a socket from all room state and notifies
* remaining peers. Call this from every disconnect/leave/reaper/dedupe path.
@@ -125,12 +194,8 @@ function checkEventRate(socketId) {
* @param {string} socketId - The socket.id being removed.
* @param {string} roomId - The room it belongs to.
* @param {string} reason - Log label ('disconnect', 'leave', 'reaper', 'dedupe', 'room-switch').
* @param {boolean} [emitLeave=true] - Set false when the socket.io room leave
* is handled by the caller (e.g. reaper calls
* socket.leave() before us, or dedupe calls
* oldSocket.leave() before disconnecting).
*/
function removePeerFromRoom(socketId, roomId, reason, emitLeave = true) {
function removePeerFromRoom(socketId, roomId, reason) {
const room = rooms.get(roomId);
if (!room) return;
@@ -146,13 +211,25 @@ function removePeerFromRoom(socketId, roomId, reason, emitLeave = true) {
// 2. Remove from global maps
socketToRoom.delete(socketId);
if (peerToSocket.get(peerId) === socketId) {
const currentSocketId = peerToSocket.get(peerId);
if (currentSocketId === socketId) {
peerToSocket.delete(peerId);
}
// 3. Notify remaining peers (use io.to so the removed socket itself
// doesn't receive it — it has already left or is disconnecting)
io.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
const isPeerStillConnected = Array.from(room.peerData.values()).some(data => data.peerId === peerId);
if (!isPeerStillConnected) {
io.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
}
// 3.5. Clean up active lobby if a peer leaves
if (room.activeLobby) {
room.activeLobby.readyPeers = room.activeLobby.readyPeers.filter(id => id !== peerId);
if (room.activeLobby.readyPeers.length <= 1 || room.activeLobby.initiatorPeerId === peerId) {
room.activeLobby = null; // Dissolve lobby
}
}
// 4. Delete empty room
if (room.peers.size === 0) {
@@ -164,7 +241,10 @@ function removePeerFromRoom(socketId, roomId, reason, emitLeave = true) {
}
io.on('connection', (socket) => {
const clientIp = socket.handshake.address;
// Get real client IP behind proxy/CDN
const forwardedFor = socket.handshake.headers['x-forwarded-for'];
const clientIp = forwardedFor ? forwardedFor.split(',')[0].trim() : socket.handshake.address;
socket._clientIp = clientIp;
// 1. Connection Rate Limit
if (!checkConnectionRate(clientIp)) {
@@ -185,7 +265,14 @@ io.on('connection', (socket) => {
}
if (clientVersion) {
const [cMaj, cMin, cPatch] = clientVersion.split('.').map(Number);
if (typeof clientVersion !== 'string') {
log('AUTH', `Invalid version type from ${clientIp}`);
socket.emit(EVENTS.ERROR, { message: 'Invalid version format' });
socket.disconnect(true);
return;
}
const parts = clientVersion.split('.').map(Number);
const cMaj = parts[0], cMin = parts[1], cPatch = parts[2] || 0;
const [mMaj, mMin, mPatch] = MIN_VERSION.split('.').map(Number);
if (isNaN(cMaj) || isNaN(cMin) || isNaN(cPatch)) {
log('AUTH', `Invalid version format (${clientVersion}) from ${clientIp}`);
@@ -225,7 +312,7 @@ io.on('connection', (socket) => {
try {
// Protocol check
if (protocolVersion !== '1.0.0') {
if (protocolVersion !== PROTOCOL_VERSION) {
log('AUTH', `Protocol mismatch from ${peerId}: ${protocolVersion}`);
socket.emit(EVENTS.ERROR, { message: 'Incompatible protocol version' });
return;
@@ -233,64 +320,102 @@ io.on('connection', (socket) => {
// Cleanup old room if re-joining
const oldMapping = socketToRoom.get(socket.id);
if (oldMapping && oldMapping.roomId === roomId) {
return; // Already in this room, ignore to prevent spam
if (oldMapping && oldMapping.roomId === roomId && oldMapping.peerId === peerId) {
return; // Already in this room with same peerId, ignore to prevent spam
}
if (oldMapping && oldMapping.roomId !== roomId) {
socket.leave(oldMapping.roomId);
removePeerFromRoom(socket.id, oldMapping.roomId, 'room-switch');
}
const ip = socket.handshake.address;
const ip = socket._clientIp || socket.handshake.address;
if (!checkAuthRate(ip, roomId)) {
log('AUTH', `Auth rate limit blocked ${ip} from room ${roomId.substring(0, 3)}***`);
socket.emit(EVENTS.ERROR, { message: "Too many failed attempts. Try again later." });
return;
}
let room = rooms.get(roomId);
let createdByMe = false;
if (!room) {
if (rooms.size >= MAX_ROOMS) {
socket.emit(EVENTS.ERROR, { message: "Server capacity reached" });
return;
// Acquire per-room creation lock to prevent race conditions
let lockPromise = roomCreationLocks.get(roomId);
if (lockPromise) {
await lockPromise;
room = rooms.get(roomId);
}
if (!room) {
// Create and store lock before async boundary
let resolveLock;
lockPromise = new Promise(resolve => { resolveLock = resolve; });
roomCreationLocks.set(roomId, lockPromise);
try {
if (rooms.size >= MAX_ROOMS) {
log('ROOM', `Server at capacity: ${rooms.size}/${MAX_ROOMS} rooms — rejecting join`);
socket.emit(EVENTS.ERROR, { message: "Server capacity reached" });
return;
}
const passwordHash = password ? await bcrypt.hash(password, 10) : null;
room = {
passwordHash,
peers: new Set(),
peerIds: new Map(),
peerData: new Map(), // socketId -> { peerId, tabTitle }
lastActivity: Date.now()
};
rooms.set(roomId, room);
log('ROOM', `Created room: ${roomId.substring(0, 3)}***`);
} else {
const passwordHash = hashPassword(password);
room = {
passwordHash,
peers: new Set(),
peerIds: new Map(),
peerData: new Map(),
lastActivity: Date.now()
};
rooms.set(roomId, room);
createdByMe = true;
log('ROOM', `Created room: ${roomId.substring(0, 3)}***`);
} finally {
roomCreationLocks.delete(roomId);
resolveLock();
}
}
}
if (!room) {
socket.emit(EVENTS.ERROR, { message: "Join error" });
return;
}
if (!createdByMe) {
if (room.passwordHash) {
if (!password || !(await bcrypt.compare(password, room.passwordHash))) {
if (!password || hashPassword(password) !== room.passwordHash) {
recordAuthFailure(ip, roomId);
log('AUTH', `Invalid password from ${ip} for room ${roomId.substring(0, 3)}***`);
socket.emit(EVENTS.ERROR, { message: "Invalid password" });
return;
}
}
if (room.peers.size >= MAX_PEERS_PER_ROOM) {
log('ROOM', `Room full (${room.peers.size}/${MAX_PEERS_PER_ROOM}): ${roomId.substring(0, 3)}***`);
socket.emit(EVENTS.ERROR, { message: "Room full" });
return;
}
// Peer Deduplication: Remove existing socket for the same peerId
const dedupeSids = [];
for (const [sid, data] of room.peerData.entries()) {
if (data.peerId === peerId && sid !== socket.id) {
const oldSocket = io.sockets.sockets.get(sid);
if (oldSocket) {
oldSocket.emit(EVENTS.ERROR, { message: 'Deduplication: Another session with this ID joined. Disconnecting...' });
oldSocket.leave(roomId);
oldSocket.disconnect(true);
log('DEDUPE', `Kicked old session for peer ${peerId}`);
}
removePeerFromRoom(sid, roomId, 'dedupe');
dedupeSids.push(sid);
}
}
for (const sid of dedupeSids) {
// Re-check: the socket might have been replaced by another concurrent join
const currentMapping = room.peerData.get(sid);
if (!currentMapping || currentMapping.peerId !== peerId) continue;
const oldSocket = io.sockets.sockets.get(sid);
if (oldSocket) {
oldSocket.emit(EVENTS.ERROR, { message: 'Deduplication: Another session with this ID joined. Disconnecting...' });
oldSocket.leave(roomId);
oldSocket.disconnect(true);
log('DEDUPE', `Kicked old session for peer ${peerId}`);
}
removePeerFromRoom(sid, roomId, 'dedupe');
}
}
socket.join(roomId);
@@ -309,12 +434,15 @@ io.on('connection', (socket) => {
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, username: username || null, tabTitle: tabTitle || null, mediaTitle: mediaTitle || null, status: 'joined' });
socket.emit(EVENTS.ROOM_DATA, {
roomId,
peers: Array.from(room.peers).map(sid => room.peerData.get(sid))
peers: Array.from(room.peers).map(sid => room.peerData.get(sid)),
activeLobby: room.activeLobby || null
});
log('ROOM', `Peer ${peerId} joined: ${roomId.substring(0, 3)}***`);
} catch (err) {
log('ERROR', `Join error for ${socket.id}`, err);
socket.emit(EVENTS.ERROR, { message: "Join error" });
if (socket.connected) {
socket.emit(EVENTS.ERROR, { message: "Join error" });
}
}
});
@@ -323,23 +451,25 @@ io.on('connection', (socket) => {
EVENTS.PLAY, EVENTS.PAUSE, EVENTS.SEEK,
EVENTS.PEER_STATUS, EVENTS.FORCE_SYNC_PREPARE,
EVENTS.FORCE_SYNC_ACK, EVENTS.FORCE_SYNC_EXECUTE,
EVENTS.EPISODE_LOBBY, EVENTS.EPISODE_READY
EVENTS.EPISODE_LOBBY, EVENTS.EPISODE_READY,
EVENTS.EPISODE_LOBBY_CANCEL
];
relayEvents.forEach(eventName => {
socket.on(eventName, (data) => {
if (!checkEventRate(socket.id)) {
log('SECURITY', `Event rate limit exceeded for socket: ${socket.id}`);
socket.disconnect(true);
return;
}
try {
if (!checkEventRate(socket.id)) {
log('SECURITY', `Event rate limit exceeded for socket: ${socket.id}`);
socket.disconnect(true);
return;
}
if (!data || typeof data !== 'object') return; // Prevent null/invalid payload crash
if (!data || typeof data !== 'object') return;
const mapping = socketToRoom.get(socket.id);
if (mapping) {
const room = rooms.get(mapping.roomId);
if (room) {
const mapping = socketToRoom.get(socket.id);
if (mapping) {
const room = rooms.get(mapping.roomId);
if (room) {
room.lastActivity = Date.now();
// --- S-2 & S-3: Sanitize ALL relay fields (strings, numbers, booleans) ---
@@ -364,6 +494,7 @@ io.on('connection', (socket) => {
// --- S-3: Construct clean relay payload — never forward raw client data ---
const relayPayload = {
senderId: mapping.peerId,
seq: clampNum(data.seq, 0, Number.MAX_SAFE_INTEGER),
currentTime: clampNum(data.currentTime, 0, 86400),
targetTime: clampNum(data.targetTime, 0, 86400),
playbackState: validState(data.playbackState),
@@ -381,12 +512,35 @@ io.on('connection', (socket) => {
// Strip undefined keys for clean wire format
Object.keys(relayPayload).forEach(k => relayPayload[k] === undefined && delete relayPayload[k]);
socket.to(mapping.roomId).emit(eventName, relayPayload);
// --- Side-effects: Server-side Episode Lobby Tracking ---
if (eventName === EVENTS.EPISODE_LOBBY && relayPayload.expectedTitle) {
room.activeLobby = {
expectedTitle: relayPayload.expectedTitle,
initiatorPeerId: mapping.peerId,
readyPeers: [mapping.peerId]
};
} else if (eventName === EVENTS.EPISODE_READY && room.activeLobby) {
if (!room.activeLobby.readyPeers.includes(mapping.peerId)) {
room.activeLobby.readyPeers.push(mapping.peerId);
}
} else if ((eventName === EVENTS.FORCE_SYNC_PREPARE || eventName === EVENTS.FORCE_SYNC_EXECUTE || eventName === EVENTS.EPISODE_LOBBY_CANCEL) && room.activeLobby) {
room.activeLobby = null;
}
}
}
} catch (err) {
log('ERROR', `Relay handler error for ${eventName}: ${err.message}`);
}
});
});
socket.on(EVENTS.GET_ROOMS, () => {
if (!checkEventRate(socket.id)) {
log('SECURITY', `Event rate limit exceeded for socket (GET_ROOMS): ${socket.id}`);
socket.disconnect(true);
return;
}
const list = Array.from(rooms.entries()).map(([id, r]) => ({
id,
peerCount: r.peers.size,
@@ -403,25 +557,30 @@ io.on('connection', (socket) => {
}
});
socket.on(EVENTS.EVENT_ACK, (data) => {
if (!data || typeof data !== 'object') return;
if (typeof data.targetId !== 'string') return;
if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return;
const senderMapping = socketToRoom.get(socket.id);
const targetSocketId = peerToSocket.get(data.targetId);
const targetMapping = targetSocketId ? socketToRoom.get(targetSocketId) : null;
socket.on(EVENTS.EVENT_ACK, (data) => {
if (!checkEventRate(socket.id)) {
log('SECURITY', `Event rate limit exceeded for socket (ACK): ${socket.id}`);
socket.disconnect(true);
return;
}
if (!data || typeof data !== 'object') return;
if (typeof data.targetId !== 'string') return;
if (data.actionTimestamp !== undefined && (typeof data.actionTimestamp !== 'number' || !Number.isFinite(data.actionTimestamp))) return;
const senderMapping = socketToRoom.get(socket.id);
const targetSocketId = peerToSocket.get(data.targetId);
const targetMapping = targetSocketId ? socketToRoom.get(targetSocketId) : null;
// Security: Only relay ACK if both peers are in the same room
if (senderMapping && targetMapping && senderMapping.roomId === targetMapping.roomId) {
io.to(targetSocketId).emit(EVENTS.EVENT_ACK, {
senderId: senderMapping.peerId,
actionTimestamp: data.actionTimestamp
});
} else {
log('SECURITY', `Blocked cross-room ACK attempt from ${socket.id} to ${data.targetId}`);
}
});
// Security: Only relay ACK if both peers are in the same room
if (senderMapping && targetMapping && senderMapping.roomId === targetMapping.roomId) {
io.to(targetSocketId).emit(EVENTS.EVENT_ACK, {
senderId: senderMapping.peerId,
actionTimestamp: data.actionTimestamp
});
} else {
log('SECURITY', `Blocked cross-room ACK attempt from ${socket.id} to ${data.targetId}`);
}
});
socket.on('disconnect', () => {
eventCounts.delete(socket.id);
@@ -441,7 +600,11 @@ setInterval(() => {
const roomCutoff = now - (2 * 60 * 60 * 1000); // 2 hours
const peerCutoff = now - (5 * 60 * 1000); // 5 minutes
for (const [roomId, room] of rooms) {
// Snapshot room keys to avoid mutation during iteration
const roomIds = Array.from(rooms.keys());
for (const roomId of roomIds) {
const room = rooms.get(roomId);
if (!room) continue; // Room may have been deleted between snapshot and now
// 1. Prune dead peers
// Snapshot keys first — we must not mutate peerData while iterating it.
const staleSids = [];
@@ -453,14 +616,15 @@ setInterval(() => {
for (const sid of staleSids) {
// Gracefully evict the socket from the Socket.IO room if it is
// still technically connected (zombie with no heartbeat).
const deadSocket = io.sockets.sockets.get(sid);
const deadSocket = io.sockets?.sockets?.get(sid);
if (deadSocket) deadSocket.leave(roomId);
log('CLEANUP', `Pruning dead peer from room ${roomId.substring(0, 3)}***`);
removePeerFromRoom(sid, roomId, 'reaper');
}
// 2. Prune empty or inactive rooms
if (room.peers.size === 0 || room.lastActivity < roomCutoff) {
const currentRoom = rooms.get(roomId);
if (currentRoom && (currentRoom.peers.size === 0 || currentRoom.lastActivity < roomCutoff)) {
io.to(roomId).emit(EVENTS.ERROR, { message: 'Room closed' });
rooms.delete(roomId);
log('CLEANUP', `Deleted room ${roomId.substring(0, 3)}*** (Empty/Inactive)`);
@@ -491,3 +655,13 @@ function gracefulShutdown(signal) {
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);
});
+81 -1
View File
@@ -17,13 +17,37 @@ export const BLACKLIST_DOMAINS = [
'msn.com',
'baidu.com',
'yandex.ru',
'ecosia.org',
'startpage.com',
'search.brave.com',
'qwant.com',
'you.com',
'perplexity.ai',
'ask.com',
'search.yahoo.com',
'swisscows.ch',
'mojeek.com',
// Mail Providers
'mail.google.com',
'gmail.com',
'outlook.live.com',
'outlook.office.com',
'mail.yahoo.com',
'gmx.net',
'gmx.de',
'gmx.com',
'web.de',
'protonmail.com',
'proton.me',
't-online.de',
'posteo.de',
'mailbox.org',
'mail.de',
'zoho.com',
'fastmail.com',
'tutanota.com',
'mail.ru',
// Cloud Storage & Documents
'docs.google.com',
@@ -61,9 +85,15 @@ export const BLACKLIST_DOMAINS = [
'instagram.com',
'reddit.com',
'quora.com',
'threads.net',
'bsky.app',
'mastodon.social',
'vk.com',
'weibo.com',
'9gag.com',
'imgur.com',
// E-Commerce
'amazon.',
'ebay.com',
'aliexpress.com',
'etsy.com',
@@ -78,6 +108,15 @@ export const BLACKLIST_DOMAINS = [
'myanimelist.net',
// Development & Utilities
'koalastuff.net',
'auth.koalastuff.net',
'blog.koalastuff.net',
'clicker.koalastuff.net',
'cookies.koalastuff.net',
'multibox.koalastuff.net',
'snippets.koalastuff.net',
'status.koalastuff.net',
'sync.koalastuff.net',
'timer.koalastuff.net',
'localhost',
'zoom.us',
@@ -85,6 +124,47 @@ export const BLACKLIST_DOMAINS = [
'meet.google.com',
'chrome.google.com',
// Music Streaming
'music.youtube.com',
'open.spotify.com',
'soundcloud.com',
'deezer.com',
'tidal.com',
// Knowledge & Blogs
'wikipedia.org',
'medium.com',
'dev.to',
'news.ycombinator.com',
// Design & Creative Tools
'figma.com',
'canva.com',
'miro.com',
// Online IDEs & Hosting
'vscode.dev',
'replit.com',
'codesandbox.io',
'vercel.com',
'netlify.com',
// Social & Image Sharing
'pinterest.com',
'tumblr.com',
// Language Learning
'duolingo.com',
'hellotalk.com',
// Google Utilities
'calendar.google.com',
'keep.google.com',
// Finance & Payments
'paypal.com',
'stripe.com',
// Games & Idle Sites
'milkywayidle.com',
'melvoridle.com',
+4 -3
View File
@@ -7,7 +7,7 @@
*/
export const PROTOCOL_VERSION = "1.0.0";
export const APP_VERSION = "1.3.1";
export const APP_VERSION = "1.9.0";
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
@@ -36,9 +36,10 @@ export const EVENTS = {
// Episode Auto-Sync
EPISODE_LOBBY: "episode_lobby", // Broadcast: waiting for everyone on this episode
EPISODE_READY: "episode_ready" // Response: loaded the episode and paused at 0:00
EPISODE_READY: "episode_ready", // Response: loaded the episode and paused at 0:00
EPISODE_LOBBY_CANCEL: "episode_lobby_cancel" // Broadcast: cancel active lobby and resume
};
export const HEARTBEAT_INTERVAL = 15000; // 15s
export const FORCE_SYNC_TIMEOUT = 5000; // 5s timeout for ACKs
export const FORCE_SYNC_TIMEOUT = 8500; // 8.5s timeout for force sync ACKs (must be > content.js poll timeout of 8s)
export const EPISODE_LOBBY_TIMEOUT = 60000; // 60s timeout for episode lobby
+298
View File
@@ -0,0 +1,298 @@
/**
* KoalaSync Shared Name Generation & Emoji Mapping
*
* WARNING: This is the SINGLE SOURCE OF TRUTH.
* If you edit this file, you MUST run: node scripts/build-extension.js
* to propagate changes to the extension.
*
* The emoji map covers every animal/creature that has a Unicode emoji.
* Entries are sorted by key length (longest first) at lookup time to
* prevent substring false-matches (e.g. "caterpillar" must be checked
* before "cat").
*
* If you add a new animal noun to USERNAME_NOUNS, ensure it has a
* corresponding entry in ANIMAL_EMOJI_MAP (or a substring that already
* maps to a suitable emoji).
*/
export const USERNAME_ADJECTIVES = [
'Happy', 'Cool', 'Fast', 'Smart', 'Brave', 'Calm', 'Sneaky', 'Lazy',
'Wild', 'Chill', 'Lucky', 'Epic', 'Swift', 'Bold', 'Mighty',
'Cosmic', 'Neon', 'Shadow', 'Crystal', 'Thunder', 'Silent', 'Golden',
'Fierce', 'Noble', 'Mystic', 'Frozen', 'Blazing', 'Sapphire', 'Iron', 'Crimson',
'Turbo', 'Zen', 'Pixel', 'Cyber', 'Solar', 'Lunar', 'Astro', 'Hyper',
'Steel', 'Rogue', 'Alpha', 'Omega', 'Royal', 'Pixel', 'Warp', 'Frost',
];
export const USERNAME_NOUNS = [
'Koala', 'Panda', 'Tiger', 'Eagle', 'Fox', 'Bear', 'Wolf', 'Lion',
'Hawk', 'Seal', 'Owl', 'Shark', 'Dragon', 'Phoenix', 'Falcon',
'Panther', 'Raven', 'Cobra', 'Lynx', 'Jaguar', 'Orca', 'Mantis',
'Viper', 'Condor', 'Badger', 'Otter', 'Rhino', 'Crane', 'Mongoose',
'Specter',
'Cat', 'Dog', 'Deer', 'Bat', 'Gorilla', 'Monkey', 'Rabbit',
'Horse', 'Unicorn', 'Zebra', 'Leopard', 'Cheetah', 'Puma',
'Ram', 'Goat', 'Bull', 'Donkey', 'Moose',
'Elephant', 'Giraffe', 'Hippo', 'Sloth', 'Kangaroo',
'Raccoon', 'Hamster', 'Hedgehog', 'Skunk', 'Beaver', 'Bison',
'Camel', 'Llama', 'Hyena', 'Coyote',
'Mouse', 'Pig', 'Boar', 'Polar', 'Orangutan', 'Mammoth',
'Crow', 'Duck', 'Swan', 'Penguin', 'Parrot', 'Peacock',
'Dove', 'Dodo', 'Turkey', 'Flamingo', 'Chicken', 'Rooster', 'Goose',
'Dolphin', 'Whale', 'Crab', 'Lobster', 'Octopus', 'Squid',
'Jellyfish', 'Turtle',
'Crocodile', 'Lizard', 'Snake', 'Frog', 'Toad', 'Gecko',
'Bee', 'Ant', 'Spider', 'Scorpion', 'Butterfly', 'Ladybug',
'Beetle', 'Snail', 'Dragonfly', 'Caterpillar',
'Alien', 'Robot', 'Mermaid', 'Ghoul', 'Sprite', 'Cyborg',
'Dinosaur', 'Reaper', 'Wraith', 'Sphinx',
'Yeti', 'Goblin', 'Pirate', 'Ninja', 'Samurai', 'Wizard', 'Knight',
'Bandit', 'Storm', 'Comet', 'Nebula', 'Quasar', 'Vortex',
];
export const ANIMAL_EMOJI_MAP = {
'hippopotamus': '🦛',
'rhinoceros': '🦏',
'caterpillar': '🐛',
'chimpanzee': '🐵',
'orangutan': '🦧',
'blackbird': '🐦‍⬛',
'bumblebee': '🐝',
'cockatoo': '🦜',
'cockroach': '🪳',
'dragonfly': '🐉',
'grasshopper': '🦗',
'hedgehog': '🦔',
'jellyfish': '🪼',
'kangaroo': '🦘',
'ladybird': '🐞',
'ladybug': '🐞',
'porcupine': '🦔',
'scorpion': '🦂',
'tarantula': '🕷️',
'alligator': '🐊',
'anaconda': '🐍',
'antelope': '🦌',
'blowfish': '🐡',
'butterfly': '🦋',
'chameleon': '🦎',
'chipmunk': '🐿️',
'crocodile': '🐊',
'dinosaur': '🦖',
'elephant': '🐘',
'flamingo': '🦩',
'giraffe': '🦒',
'hamster': '🐹',
'leopard': '🐆',
'lobster': '🦞',
'mermaid': '🧜‍♀️',
'mongoose': '🦦',
'mosquito': '🦟',
'pangolin': '🦔',
'peacock': '🦚',
'penguin': '🐧',
'phoenix': '🐦‍🔥',
'raccoon': '🦝',
'seahorse': '🐴',
'sealion': '🦭',
'unicorn': '🦄',
'vampire': '🦇',
'warthog': '🐗',
'wolverine': '🦡',
'mammoth': '🦣',
'meerkat': '🦦',
'octopus': '🐙',
'opposum': '🐭',
'ostrich': '🐦',
'panther': '🐆',
'pelican': '🦩',
'rooster': '🐓',
'serpent': '🐍',
'specter': '👻',
'spectre': '👻',
'sparrow': '🐦',
'spider': '🕷️',
'sphinx': '🦁',
'squirrel': '🐿️',
'stingray': '🦈',
'termite': '🐜',
'tortoise': '🐢',
'turkey': '🦃',
'walrus': '🦭',
'wombat': '🦡',
'woodpecker': '🐦',
'alien': '👾',
'badger': '🦡',
'beaver': '🦫',
'beetle': '🪲',
'beluga': '🐋',
'bison': '🦬',
'bobcat': '🐱',
'buffalo': '🦬',
'bunny': '🐰',
'camel': '🐪',
'cheetah': '🐆',
'chicken': '🐔',
'cobra': '🐍',
'condor': '🦅',
'cougar': '🐆',
'coyote': '🐺',
'crane': '🦩',
'cricket': '🦗',
'crow': '🐦‍⬛',
'cyborg': '🤖',
'dolphin': '🐬',
'donkey': '🫏',
'dragon': '🐉',
'drake': '🐉',
'eagle': '🦅',
'falcon': '🦅',
'ferret': '🦦',
'gazelle': '🦌',
'gecko': '🦎',
'gerbil': '🐹',
'ghost': '👻',
'ghoul': '👻',
'goose': '🪿',
'gopher': '🐹',
'gorilla': '🦍',
'grizzly': '🐻',
'heron': '🦩',
'hippo': '🦛',
'hornet': '🐝',
'hyena': '🐺',
'iguana': '🦎',
'jackal': '🐺',
'jaguar': '🐆',
'kitten': '🐱',
'koala': '🐨',
'lemur': '🐒',
'lizard': '🦎',
'llama': '🦙',
'locust': '🦗',
'lynx': '🐱',
'macaw': '🦜',
'mantis': '🦗',
'mink': '🦦',
'monkey': '🐵',
'moose': '🦌',
'mouse': '🐭',
'orca': '🐋',
'otter': '🦦',
'oyster': '🦪',
'panda': '🐼',
'parrot': '🦜',
'pigeon': '🕊️',
'polar': '🐻‍❄️',
'poodle': '🐩',
'puffin': '🐧',
'puma': '🐆',
'rabbit': '🐰',
'raptor': '🦖',
'raven': '🐦‍⬛',
'reaper': '👻',
'rhino': '🦏',
'robin': '🐦',
'robot': '🤖',
'salmon': '🐟',
'shrimp': '🦐',
'skunk': '🦨',
'sloth': '🦥',
'snail': '🐌',
'snake': '🐍',
'sprite': '🧚',
'squid': '🦑',
'swan': '🦢',
'tapir': '🐗',
'tiger': '🐯',
'toad': '🐸',
'trout': '🐟',
'tuna': '🐟',
'turtle': '🐢',
'viper': '🐍',
'vulture': '🦅',
'weasel': '🦦',
'whale': '🐋',
'wolf': '🐺',
'wraith': '👻',
'zebra': '🦓',
'ape': '🦍',
'ant': '🐜',
'bat': '🦇',
'bee': '🐝',
'bug': '🐛',
'cat': '🐱',
'cow': '🐮',
'crab': '🦀',
'dog': '🐶',
'duck': '🦆',
'elk': '🦌',
'fly': '🪰',
'fox': '🦊',
'frog': '🐸',
'goat': '🐐',
'hawk': '🦅',
'hen': '🐔',
'hog': '🐷',
'lion': '🦁',
'mole': '🐭',
'moth': '🦋',
'mule': '🫏',
'owl': '🦉',
'pig': '🐷',
'ram': '🐏',
'rat': '🐀',
'seal': '🦭',
'shark': '🦈',
'wasp': '🐝',
'yak': '🐂',
'doe': '🦌',
'ewe': '🐑',
'buck': '🦌',
'ox': '🐂',
'bull': '🐂',
'dodo': '🦤',
'boar': '🐗',
'bear': '🐻',
'deer': '🦌',
'dove': '🕊️',
'fish': '🐟',
'hare': '🐰',
'horse': '🐴',
'lamb': '🐑',
'mare': '🐴',
'pony': '🐴',
'pup': '🐶',
'croc': '🐊',
'gnat': '🦟',
'gnu': '🦬',
'yeti': '🦍',
'goblin': '👺',
'pirate': '🏴',
'ninja': '🥷',
'samurai': '🥷',
'wizard': '🧙',
'knight': '⚔️',
'bandit': '🦹',
'storm': '🌩️',
'comet': '☄️',
'nebula': '🌌',
'quasar': '🌟',
'vortex': '🌪️',
};
export function getAvatarForName(username) {
if (!username) return '\u{1F464}';
const lower = username.toLowerCase();
const sorted = Object.entries(ANIMAL_EMOJI_MAP).sort((a, b) => b[0].length - a[0].length);
for (const [key, emoji] of sorted) {
if (lower.includes(key)) return emoji;
}
return '\u{1F464}';
}
export function generateUsername() {
const adj = USERNAME_ADJECTIVES[Math.floor(Math.random() * USERNAME_ADJECTIVES.length)];
const noun = USERNAME_NOUNS[Math.floor(Math.random() * USERNAME_NOUNS.length)];
return `${adj}${noun}`;
}
+28 -7
View File
@@ -5,7 +5,7 @@ This directory contains the KoalaSync website. It serves a dual purpose: it is b
## Core Roles
### 1. Marketing & Onboarding
Provides a premium, bilingual (EN/DE) overview of features, setup instructions, and direct links to the extension stores.
Provides a premium, multi-language (EN/DE/FR/ES/PT-BR/RU) overview of features, setup instructions, and direct links to the extension stores.
### 2. The Invitation Bridge (`join.html`)
The website handles incoming invitation links. When a user clicks a link like `sync.koalastuff.net/join.html#join:roomID:pass`, the website:
@@ -16,7 +16,9 @@ The website handles incoming invitation links. When a user clicks a link like `s
## Architecture
The website is 100% **Static HTML, CSS, and JS**.
- **Zero Backend**: No Node.js, PHP, or databases are required to host the website.
- **Static i18n Compiler**: The site uses a lightweight, zero-dependency Node.js compiler (`build.js`) to parse dictionary files inside `/locales/` against a single source-of-truth template (`template.html`), outputting the fully deployable static folder to `/www/`.
- **Build-time Minification**: `build.js` automatically minifies CSS and JS during compilation using a built-in state-machine tokenizer (no npm dependencies). Source files are written unminified (`style.css`, `app.js`, `lang-init.js`) — always edit source files, never the generated `.min.*` files in `www/`.
- **Zero Backend**: No Node.js, PHP, or databases are required to host the compiled website.
- **Zero Tracking**: All assets (fonts, icons) are self-hosted to prevent third-party tracking.
- **Responsive**: Fully optimized for mobile with a native-feel hamburger menu.
@@ -30,12 +32,21 @@ For a more comprehensive configuration that includes the Relay Server reverse pr
```caddy
sync.koalastuff.net {
root * /var/www/koalasync/website
root * /var/www/koalasync/website/www
file_server
encode zstd gzip
# Security Headers
# Static Caching for high-performance PageSpeed (1 year with validation)
@static {
file
path *.ico *.css *.js *.png *.svg *.webp
}
header @static Cache-Control "public, max-age=31536000, must-revalidate"
# Security Headers & Content Security Policy (CSP)
header {
# Strict Content Security Policy (restricts scripts and connections to self, forbids frames)
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';"
# Prevent FLoC tracking
Permissions-Policy interest-cohort=()
# Security best practices
@@ -47,7 +58,17 @@ sync.koalastuff.net {
}
```
## Local Development
## Local Development & Compilation
1. Open `index.html` directly in any browser.
2. To test the invitation flow locally, use a local server (e.g., `npx serve .`) and navigate to `http://localhost:5000/join.html#join:test-room:test-pass`.
1. Run the compilation script from the repository root to generate the `/website/www` folder:
```bash
node website/build.js
```
2. Serve the compiled `/www` directory using any local development server:
```bash
npx serve website/www
```
3. To test the invitation flow locally, navigate to `http://localhost:5000/join.html#join:test-room:test-pass`.
> [!IMPORTANT]
> **Never edit files inside `website/www/` directly.** This directory is fully auto-generated by `build.js`. Always edit source files (`template.html`, `style.css`, `app.js`, `lang-init.js`, locale files in `locales/`) and re-run `node website/build.js` to apply changes. CSS and JS are output as `style.min.css`, `app.min.js`, and `lang-init.min.js` — the `.min.*` naming makes it visually obvious these are build artifacts. Editing minified files in `www/` will result in lost changes on the next build.
+180
View File
@@ -0,0 +1,180 @@
# KoalaSync Translation & Localization Guide
Welcome to the **KoalaSync** translation and internationalization framework! This document provides clear, professional instructions for developers and contributors looking to maintain, audit, or add new languages to the official KoalaSync website.
---
## 🏛️ Architecture Overview
The KoalaSync website utilizes a custom, high-performance, zero-dependency static site generator built in Node.js. Instead of using complex client-side translation runtimes or bulky frameworks, localized pages are compiled ahead-of-time (AOT) to maintain lightning-fast page speeds and strict data sovereignty.
* **Template Source:** [`website/template.html`](file:///Users/koala/Documents/KoalaPlay/website/template.html) (Single Source of Truth)
* **Locales Source:** `/website/locales/[lang].json` (Structured JSON translation dictionaries)
* **Build Pipeline:** [`website/build.js`](file:///Users/koala/Documents/KoalaPlay/website/build.js) (Pure Node.js script that compiles pages into `/website/www/`)
---
## 📊 Supported Languages Dashboard
We divide supported languages into two tiers: **Core Languages** (fully hand-crafted and audited by native speakers) and **Extended Languages** (auto-generated using translation models to expand initial coverage).
> [!TIP]
> **Help Us Improve!**
> We welcome community contributions to audit "Auto-Generated" translations and elevate them to "Verified" status.
| Language Code | Language Name | Verification Status | Rationale / Context |
| :--- | :--- | :--- | :--- |
| `en` | **English** | `100% Manually Verified` | Primary developer language and system default |
| `de` | **German** | `100% Manually Verified` | Core market and compliance baseline |
| `fr` | **French** | `Auto-Generated` | Needs manual native review and polishing |
| `es` | **Spanish** | `Auto-Generated` | Needs manual native review and polishing |
| `pt-BR` | **Portuguese (Brasil)** | `Auto-Generated` | Needs manual native review and polishing |
| `ru` | **Russian** | `Auto-Generated` | Needs manual native review and polishing |
> [!WARNING]
> **Autogeneration Quality Rule**
> Any newly contributed languages must be committed as `"Auto-Generated"` until fully reviewed and signed off by a native speaker in a pull request.
---
## ⚖️ Strict Legal Exclusion Rule
Our legal pages have strict constraints to protect user privacy and avoid regulatory liabilities.
> [!IMPORTANT]
> **DO NOT TRANSLATE LEGAL DOCUMENTS**
> The legal notice ([impressum.html](file:///Users/koala/Documents/KoalaPlay/website/impressum.html)) and privacy policy ([datenschutz.html](file:///Users/koala/Documents/KoalaPlay/website/datenschutz.html)) **MUST remain exclusively in English and German**.
>
> * **Rationale:** Legal compliance under the European Union General Data Protection Regulation (GDPR) and the German Digital Services Act (DDG). Offering automated translations of legally binding notices introduces compliance risks due to potential mistranslations of liability limits.
> * **Technical Fallback:** The dynamic initializer script (`lang-init.js`) is configured to automatically fallback to **English** for legal pages if a user visits them with a French, Spanish, or other unsupported language preference, keeping their dynamic dropdown choice intact for homepage links.
---
## 🛠️ Step-by-Step: Adding a New Language
Adding a new language (e.g., Italian - `it`) is straightforward. Follow these four structured steps:
### Step 1: Create the Translation Dictionary
Create a new JSON file inside the locales directory named `[lang].json` (e.g., `website/locales/it.json`).
1. Copy the structure of [`website/locales/en.json`](file:///Users/koala/Documents/KoalaPlay/website/locales/en.json) to use as your baseline.
2. Translate all string values while keeping the JSON keys identical.
3. Configure the language metadata keys at the top of the file:
```json
{
"LANG_CODE": "it",
"HTML_CLASS": "lang-it",
"CANONICAL_PATH": "it/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN"
}
```
### Step 2: Register in the Compiler
Open the static site generator script [`website/build.js`](file:///Users/koala/Documents/KoalaPlay/website/build.js) and append your new language code to the active `languages` array:
```javascript
// Add 'it' to the array
const languages = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it'];
```
### Step 3: Run the Build Script
Execute the compiler from the root of the repository:
```bash
node website/build.js
```
The compiler will automatically:
1. Load the new JSON translation file.
2. Create the target subdirectory `/website/www/it/`.
3. Generate the compiled `/website/www/it/index.html` landing page, injecting correct relative assets and canonical metadata.
### Step 4: Update the Dashboard
Open this `TRANSLATION.md` file and add your language to the **Supported Languages Dashboard** table, marking it as `Auto-Generated` (unless manually verified).
---
## 🔮 Future Roadmap: Dynamic Utility Pages
For pages that require fully dynamic, client-side interactions (like the room invitation bridge [`join.html`](file:///Users/koala/Documents/KoalaPlay/website/join.html)), we need to scale to unlimited languages without bloating the HTML size or polluting the URL.
### Clean Client-Side i18n Architecture
To maintain zero URL pollution (e.g. keeping invitation links clean as `/join.html#join:roomID:password`), we propose an **asynchronous JSON dictionary injection architecture**:
#### 1. Page Lifecycle Flow
1. **User Landing:** The guest enters `/join.html` with a shared hash.
2. **Language Resolution:** `lang-init.js` immediately reads their saved preference (`localStorage` or `navigator.language`) and applies the active class (e.g. `html.lang = "es"`).
3. **Async Fetching:** A client-side loader script (`i18n-client.js`) runs asynchronously, downloading the correct dictionary (`fetch("/locales/es.json")`).
4. **DOM Translation:** The script scans the page for elements carrying a `data-i18n` attribute and safely updates their text content at runtime, avoiding dual-text nodes and stylesheet recalculations.
#### 2. Declarative HTML Markup
Elements are defined with custom data attributes specifying translation keys. English text is placed as the static HTML fallback:
```html
<h1 data-i18n="JOIN_TITLE">Ready to sync?</h1>
<p id="join-desc" data-i18n="JOIN_SUBTITLE">You've been invited to join a session.</p>
```
#### 3. Zero-Dependency Engine (`i18n-client.js`)
```javascript
document.addEventListener('DOMContentLoaded', async () => {
// 1. Recover the language determined during early initialization
const activeLang = document.documentElement.lang || 'en';
if (activeLang === 'en') return; // Default markup is already in English
// 2. Fetch the corresponding locale JSON asynchronously
try {
const response = await fetch(`locales/${activeLang}.json`);
if (!response.ok) throw new Error('Locale file unavailable');
const dictionary = await response.json();
// 3. Scan and translate data-i18n attributes
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (dictionary[key]) {
if (el.tagName === 'IMG') {
el.alt = dictionary[key];
} else {
el.textContent = dictionary[key];
}
}
});
} catch (err) {
console.warn('Dynamic i18n loading failed. Defaulting to English:', err);
}
});
```
#### Core Benefits
* **Zero URL Pollution:** Keeps invitation hashes private and avoids messy query parameters (`?lang=de`), protecting user privacy.
* **Optimal Performance:** Eliminates duplicate hidden text blocks, cutting page weight in half and ensuring smooth rendering.
* **Infinite Scale:** Adding new languages to dynamic pages requires zero edits to HTML markup; the engine simply fetches new JSON dictionaries on-demand.
---
## 🔌 Extension Internationalization (i18n)
In **v2.0**, we extended full internationalization support to the **Browser Extension itself**. The architecture mirrors our web-based dynamic localization model to maintain complete parity.
* **Locales Directory:** [`extension/locales/`](file:///Users/koala/Documents/KoalaPlay/extension/locales/)
* **Active Dictionaries:**
* [`en.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/en.json) (Baseline English)
* [`de.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/de.json) (German)
* [`fr.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/fr.json) (French)
* [`es.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/es.json) (Spanish)
* [`pt-BR.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/pt-BR.json) (Portuguese (Brasil))
* [`ru.json`](file:///Users/koala/Documents/KoalaPlay/extension/locales/ru.json) (Russian)
* **Translation Engine:** [`extension/i18n.js`](file:///Users/koala/Documents/KoalaPlay/extension/i18n.js)
* **Validation Script:** [`scripts/test-locales.js`](file:///Users/koala/Documents/KoalaPlay/scripts/test-locales.js)
### ⚙️ How it Works inside the Extension
1. **System Locale Auto-Detection**: On first run, the extension detects the browser system language using `navigator.language` or `chrome.i18n.getUILanguage()`.
2. **On-the-Fly Redraws**: When the user selects a different language in the settings tab (`#langSelector`), the selection is stored in `chrome.storage.sync` and the translation engine immediately triggers `translateDOM()`. The interface, empty state cards, tooltips, dynamic onboarding tutorial guides, and status badges re-render instantly without reloading the popup.
3. **Localized System Notifications**: On play, pause, or seek commands, `background.js` retrieves the user's active locale preference from storage, loads the correct dictionary, and pushes native OS notifications fully translated.
### 🧪 Auditing & Sync Checks
To ensure that no language dictionary falls out of sync (causing missing labels or blank interfaces), developers must run the locale auditor tool before packaging releases:
```bash
node scripts/test-locales.js
```
This script asserts that all JSON dictionary files under `extension/locales/` share exactly the same set of keys as the English baseline (`en.json`).
+449 -38
View File
@@ -1,26 +1,22 @@
// KoalaSync Landing Page Logic
document.addEventListener('DOMContentLoaded', () => {
// Scroll Reveal Logic
// Scroll Reveal Logic (IntersectionObserver for performance)
const revealElements = document.querySelectorAll('[data-reveal]');
const revealOnScroll = () => {
const windowHeight = window.innerHeight;
revealElements.forEach(el => {
const elementTop = el.getBoundingClientRect().top;
const revealPoint = 150;
if (elementTop < windowHeight - revealPoint) {
el.classList.add('revealed');
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
revealObserver.unobserve(entry.target);
}
});
};
}, {
rootMargin: '0px 0px -150px 0px',
threshold: 0.1
});
// Initial check
revealOnScroll();
// Scroll listener
window.addEventListener('scroll', revealOnScroll);
revealElements.forEach(el => revealObserver.observe(el));
// Navbar scroll effect
const nav = document.querySelector('nav');
@@ -50,7 +46,53 @@ document.addEventListener('DOMContentLoaded', () => {
// Invite Detection & Bridge
const checkInvite = () => {
const isJoinPage = window.location.pathname.includes('join.html');
const isJoinPage = window.location.pathname.includes('join');
// Dev Simulation Mode via URL Search Parameter (?dev=success) or Hash (#dev=success / #devsuccess)
const urlParams = new URLSearchParams(window.location.search);
let devMode = urlParams.get('dev');
if (!devMode) {
const hashClean = window.location.hash.startsWith('#') ? window.location.hash.substring(1) : window.location.hash;
const hashParams = new URLSearchParams(hashClean);
devMode = hashParams.get('dev');
}
if (!devMode) {
if (window.location.hash.includes('devsuccess') || window.location.search.includes('devsuccess')) devMode = 'success';
if (window.location.hash.includes('devfailure') || window.location.search.includes('devfailure')) devMode = 'failure';
}
if (isJoinPage && devMode) {
setTimeout(() => {
const displayRoom = document.getElementById('display-room-id');
const actions = document.getElementById('join-actions');
if (displayRoom) displayRoom.textContent = 'DEV-ROOM';
if (actions) {
actions.innerHTML = `
<div class="joining-spinner" style="text-align:center; padding: 1rem;">
<div class="join-spinner"></div>
<div style="font-weight: 600; color: var(--accent);">
<span lang="en">Simulating connection (DEV)...</span><span lang="de">Verbindung wird simuliert (DEV)...</span>
</div>
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem;">
<span lang="en">Simulating status event in 1.5 seconds.</span><span lang="de">Status-Event wird in 1,5 Sekunden simuliert.</span>
</p>
</div>
`;
setTimeout(() => {
window.dispatchEvent(new CustomEvent('KOALASYNC_STATUS', {
detail: {
success: devMode === 'success',
message: devMode === 'failure' ? 'Simulated Connection Timeout!' : ''
}
}));
}, 1500);
}
}, 600);
return;
}
// Use a short timeout to let the bridge script initialize its dataset attribute
setTimeout(() => {
@@ -71,17 +113,52 @@ document.addEventListener('DOMContentLoaded', () => {
if (actions) {
if (!isInstalled) {
actions.innerHTML = `
<a href="#" class="primary" style="text-align:center; text-decoration:none; display:block; padding: 1.2rem; background: var(--accent); color: white; border-radius: 12px; font-weight: 700;">GET IT ON CHROME WEBSTORE</a>
<a href="https://github.com/shik3i/KoalaSync" style="text-align:center; color:var(--accent); text-decoration:underline; font-size:0.85rem; margin-top:0.8rem; display:block; font-weight: 600;">Download via GitHub</a>
<p style="text-align:center; font-size:0.8rem; opacity:0.7; margin-top: 1.2rem; color: var(--text-muted);">The extension is required to join and sync videos.</p>
`;
const isFirefox = navigator.userAgent.includes('Firefox');
if (isFirefox) {
actions.innerHTML = `
<div class="join-card-actions">
<a href="https://addons.mozilla.org/de/firefox/addon/koalasync/" class="btn btn-primary btn-firefox">
<img src="assets/firefox.svg" alt="Firefox" width="20" style="display: block;">
<span lang="en">GET IT ON MOZILLA ADD-ONS</span><span lang="de">IM FIREFOX ADD-ON STORE HERUNTERLADEN</span>
</a>
<a href="https://github.com/shik3i/KoalaSync" target="_blank" class="btn btn-secondary">
<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>
<span lang="en">Download via GitHub</span><span lang="de">Über GitHub herunterladen</span>
</a>
</div>
<p style="text-align:center; font-size:0.8rem; opacity:0.7; margin-top: 1.2rem; color: var(--text-muted);">
<span lang="en">The extension is required to join and sync videos.</span>
<span lang="de">Die Erweiterung ist erforderlich, um beizutreten und Videos zu synchronisieren.</span>
</p>
`;
} else {
actions.innerHTML = `
<div class="join-card-actions">
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc" class="btn btn-primary">
<img src="assets/chrome.svg" alt="Chrome" width="20" style="display: block;">
<span lang="en">GET IT ON CHROME WEBSTORE</span><span lang="de">IM CHROME WEB STORE HERUNTERLADEN</span>
</a>
<a href="https://github.com/shik3i/KoalaSync" target="_blank" class="btn btn-secondary">
<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>
<span lang="en">Download via GitHub</span><span lang="de">Über GitHub herunterladen</span>
</a>
</div>
<p style="text-align:center; font-size:0.8rem; opacity:0.7; margin-top: 1.2rem; color: var(--text-muted);">
<span lang="en">The extension is required to join and sync videos.</span>
<span lang="de">Die Erweiterung ist erforderlich, um beizutreten und Videos zu synchronisieren.</span>
</p>
`;
}
} else {
actions.innerHTML = `
<div class="joining-spinner" style="text-align:center; padding: 1rem;">
<div style="font-size: 1.2rem; margin-bottom: 0.5rem;">🚀</div>
<div style="font-weight: 600; color: var(--accent);">Joining room automatically...</div>
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem;">Your extension is taking care of it.</p>
<div class="join-spinner"></div>
<div style="font-weight: 600; color: var(--accent);">
<span lang="en">Joining room automatically...</span><span lang="de">Raum wird automatisch betreten...</span>
</div>
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem;">
<span lang="en">Your extension is taking care of it.</span><span lang="de">Deine Erweiterung kümmert sich darum.</span>
</p>
</div>
`;
@@ -117,7 +194,7 @@ document.addEventListener('DOMContentLoaded', () => {
inviteSpan.appendChild(document.createTextNode(' detected!'));
const joinLink = document.createElement('a');
joinLink.href = 'join.html' + window.location.hash;
joinLink.href = 'join' + window.location.hash;
joinLink.className = 'btn-banner';
joinLink.textContent = 'OPEN JOIN PAGE';
@@ -151,25 +228,32 @@ document.addEventListener('DOMContentLoaded', () => {
// Listen for status from Extension
window.addEventListener('KOALASYNC_STATUS', (e) => {
const { success, message } = e.detail;
const isJoinPage = window.location.pathname.includes('join.html');
const isJoinPage = window.location.pathname.includes('join');
if (isJoinPage) {
const icon = document.getElementById('join-status-icon');
const title = document.getElementById('join-title');
const actions = document.getElementById('join-actions');
const desc = document.getElementById('join-desc');
const ring = document.getElementById('status-ring');
if (success) {
if (icon) icon.textContent = '✅';
if (ring) {
ring.classList.remove('active-pulse');
ring.style.display = 'none';
}
if (icon) {
icon.innerHTML = '<img src="assets/KoalaThumbsUp.webp" alt="Success" class="join-status-mascot">';
icon.style.transform = 'scale(1)';
}
const isDE = document.documentElement.classList.contains('lang-de');
title.textContent = isDE ? 'Erfolgreich!' : 'Success!';
desc.innerHTML = isDE
? 'Verbunden! <br><span style="color:var(--accent); font-weight:bold;">Wähle jetzt einen Video-Tab in der Erweiterung aus.</span>'
: 'Connected! <br><span style="color:var(--accent); font-weight:bold;">Now select a video tab in the extension.</span>';
let count = 3;
const updateCountdown = () => {
const closingMsg = isDE
? `Du bist dem Raum beigetreten. <br><span style="color:var(--accent); font-weight:bold;">Dieser Tab schließt sich in ${count} Sekunden...</span>`
: `You joined the room. <br><span style="color:var(--accent); font-weight:bold;">This tab will close in ${count} seconds...</span>`;
desc.innerHTML = closingMsg;
if (count <= 0) {
window.close();
desc.textContent = isDE ? 'Beitritt erfolgreich! Du kannst diesen Tab jetzt manuell schließen.' : 'Joined successfully! You can close this tab manually.';
@@ -178,25 +262,40 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(updateCountdown, 1000);
}
};
updateCountdown();
setTimeout(updateCountdown, 1000);
const closeLabel = isDE ? 'TAB JETZT SCHLIESSEN' : 'CLOSE TAB NOW';
actions.innerHTML = `<button class="primary" onclick="window.close()" style="background:var(--success); width: 100%;">${closeLabel}</button>`;
actions.innerHTML = `
<div class="join-card-actions">
<button class="btn btn-success" onclick="window.close()">${closeLabel}</button>
</div>
`;
} else {
if (icon) icon.textContent = '❌';
if (ring) {
ring.classList.remove('active-pulse');
ring.style.display = 'none';
}
if (icon) {
icon.innerHTML = '<img src="assets/KoalaThumbsDown.webp" alt="Error" class="join-status-mascot" onerror="this.outerHTML=\'❌\'">';
icon.style.transform = 'scale(1)';
}
const isDE = document.documentElement.classList.contains('lang-de');
title.textContent = isDE ? 'Fehler' : 'Error';
desc.textContent = isDE ? `Beitritt fehlgeschlagen: ${message}` : `Join failed: ${message}`;
const retryLabel = isDE ? 'ERNEUT VERSUCHEN' : 'TRY AGAIN';
actions.innerHTML = `<button class="primary" onclick="location.reload()" style="width: 100%;">${retryLabel}</button>`;
actions.innerHTML = `
<div class="join-card-actions">
<button class="btn btn-primary" onclick="location.reload()">${retryLabel}</button>
</div>
`;
}
} else {
const banner = document.getElementById('koala-banner');
if (banner) {
if (success) {
banner.style.background = 'var(--success)';
banner.innerHTML = '<div class="container">✅ Joined! This tab will close in 3s...</div>';
setTimeout(() => window.close(), 3000);
banner.innerHTML = '<div class="container">✅ Joined! This tab will close in 2s...</div>';
setTimeout(() => window.close(), 2000);
} else {
banner.style.background = 'var(--error)';
banner.innerHTML = '';
@@ -211,7 +310,8 @@ document.addEventListener('DOMContentLoaded', () => {
const updateDynamicVersion = async () => {
try {
const response = await fetch('version.json');
const versionPath = document.documentElement.lang === 'de' ? '../version.json' : 'version.json';
const response = await fetch(versionPath);
if (!response.ok) return;
const data = await response.json();
const { version, date } = data;
@@ -250,11 +350,322 @@ document.addEventListener('DOMContentLoaded', () => {
if (badgeDe) {
badgeDe.textContent = `v${version} JETZT VERFÜGBAR • ${relativeTimeDe}`;
}
// Update Schema.org structured data dynamically
const schemaScript = document.getElementById('schema-software');
if (schemaScript) {
try {
const schema = JSON.parse(schemaScript.textContent);
schema.softwareVersion = version;
schemaScript.textContent = JSON.stringify(schema, null, 2);
} catch (err) {
console.warn('Failed to dynamically update schema version:', err);
}
}
} catch (e) {
console.warn('Failed to fetch dynamic version info:', e);
}
};
// Extension Mockup Tab Switcher
const mockTabs = document.querySelectorAll('.mock-tab');
const mockScreens = document.querySelectorAll('.mock-screen');
mockTabs.forEach(tab => {
tab.addEventListener('click', () => {
mockTabs.forEach(t => t.classList.remove('active'));
mockScreens.forEach(s => s.classList.remove('active'));
tab.classList.add('active');
const targetId = tab.getAttribute('data-target');
const targetScreen = document.getElementById(targetId);
if (targetScreen) {
targetScreen.classList.add('active');
}
});
});
// Terminal Tab Switcher
const termTabBtns = document.querySelectorAll('.terminal-tab-btn');
const termPanes = document.querySelectorAll('.terminal-pane');
termTabBtns.forEach(btn => {
btn.addEventListener('click', () => {
termTabBtns.forEach(b => b.classList.remove('active'));
termPanes.forEach(p => p.classList.remove('active'));
btn.classList.add('active');
const targetPaneId = btn.getAttribute('data-tab');
const targetPane = document.getElementById(targetPaneId);
if (targetPane) {
targetPane.classList.add('active');
}
});
});
// Terminal Clipboard Copy
const copyBtn = document.querySelector('.terminal-copy-btn');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const activePane = document.querySelector('.terminal-pane.active');
if (!activePane) return;
const codeElement = activePane.querySelector('code');
if (!codeElement) return;
const textToCopy = codeElement.innerText || codeElement.textContent;
navigator.clipboard.writeText(textToCopy).then(() => {
const isDE = document.documentElement.classList.contains('lang-de');
const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = isDE ? '✅ Kopiert!' : '✅ Copied!';
copyBtn.disabled = true;
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
copyBtn.disabled = false;
}, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
});
});
}
// Mobile Hamburger Menu Toggle
const hamburger = document.querySelector('.hamburger');
const navLinks = document.querySelector('#primary-nav');
if (hamburger && navLinks) {
hamburger.setAttribute('aria-expanded', 'false');
const open = () => {
navLinks.classList.add('open');
hamburger.setAttribute('aria-expanded', 'true');
document.addEventListener('keydown', onEsc);
};
const close = () => {
navLinks.classList.remove('open');
hamburger.setAttribute('aria-expanded', 'false');
document.removeEventListener('keydown', onEsc);
};
const toggle = () => navLinks.classList.contains('open') ? close() : open();
const onEsc = (e) => { if (e.key === 'Escape') close(); };
hamburger.addEventListener('click', toggle);
navLinks.querySelectorAll('a').forEach(a => a.addEventListener('click', close));
}
// Dynamically localize home links on root dynamic pages (impressum, datenschutz, join)
const localizeHomeLinks = () => {
const activeLang = localStorage.getItem('koala_lang') || (navigator.language.startsWith('de') ? 'de' : 'en');
const path = window.location.pathname;
const pathSegments = path.split('/');
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru'].includes(seg));
// Only need to do this dynamic rewrite if we are NOT already inside a localized subdirectory
if (!isSubdir) {
const homeLinks = document.querySelectorAll('a[href="./"], a[href="de/"], a[href="fr/"], a[href="es/"], a[href="pt-BR/"], a[href="ru/"]');
homeLinks.forEach(link => {
link.href = (activeLang === 'en') ? './' : `${activeLang}/`;
});
}
};
// Modern Language Selector Navigation and State Toggling
const handleLanguageChange = (e) => {
const select = e.currentTarget;
const newLang = select.value;
const path = window.location.pathname;
// Save the user's preference
localStorage.setItem('koala_lang', newLang);
// Determine if we are on a static landing page versus a dynamic utility page
const isLegalOrJoin = path.includes('impressum') || path.includes('datenschutz') || path.includes('join');
const isIndex = !isLegalOrJoin;
if (isIndex) {
// Static navigation: Route to correct subdirectory
const pathSegments = path.split('/');
const isSubdir = pathSegments.some(seg => ['de', 'fr', 'es', 'pt-BR', 'ru'].includes(seg));
let targetPath;
if (newLang === 'en') {
if (isSubdir) {
targetPath = '../';
} else {
targetPath = './';
}
} else {
if (isSubdir) {
// Switching from one language subdirectory to another (e.g., /de/ to /fr/)
targetPath = '../' + newLang + '/';
} else {
// Switching from root (English) to a language subdirectory (e.g., / to /fr/)
targetPath = newLang + '/';
}
}
window.location.href = targetPath;
} else {
// Dynamic page: Toggle classes and update elements dynamically without navigating away
const html = document.documentElement;
html.classList.remove('lang-en', 'lang-de', 'lang-fr', 'lang-es', 'lang-pt-br', 'lang-ru');
// Fallback dynamic pages to 'en' if 'de' is not chosen (since fr/es markup is not present)
const activeDisplayLang = (newLang === 'de') ? 'de' : 'en';
html.classList.add('lang-' + activeDisplayLang);
html.lang = activeDisplayLang;
// Sync all selects on the page to the new value
document.querySelectorAll('.lang-dropdown').forEach(sel => {
sel.value = newLang;
});
// Update titles dynamically
const isJoin = path.includes('join');
if (isJoin) {
const titles = { en: 'Join Room | KoalaSync', de: 'Raum beitreten | KoalaSync' };
document.title = titles[activeDisplayLang] || titles.en;
}
// Localize home links dynamically
localizeHomeLinks();
}
};
// Register change event listener for the dropdowns
document.querySelectorAll('.lang-dropdown').forEach(select => {
select.addEventListener('change', handleLanguageChange);
});
// Initialize language select elements to show the current preferred language
const initLanguageSelectorValue = () => {
const savedLang = localStorage.getItem('koala_lang');
const browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
const activePref = savedLang || browserLang;
document.querySelectorAll('.lang-dropdown').forEach(select => {
select.value = activePref;
});
};
// Impressum Email Obfuscation Click Reveal
document.querySelectorAll('.email-reveal').forEach(el => {
el.addEventListener('click', function() {
const user = this.getAttribute('data-user');
const domain = this.getAttribute('data-domain');
if (user && domain) {
this.innerHTML = `${user}@${domain}`;
}
});
});
// Automated Store/Local Badge Linking based on User-Agent
const detectBrowserAndElevateBadge = () => {
const isFirefox = navigator.userAgent.includes('Firefox');
const isChrome = navigator.userAgent.includes('Chrome') || navigator.userAgent.includes('Chromium');
const chromeBtns = document.querySelectorAll('.btn-primary');
const firefoxBtns = document.querySelectorAll('.btn-firefox');
if (isFirefox && chromeBtns.length > 0 && firefoxBtns.length > 0) {
// User is on Firefox: Elevate Firefox button to primary, make Chrome secondary
chromeBtns.forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-secondary');
});
firefoxBtns.forEach(btn => {
// Put Firefox first in visual order
btn.style.order = '-1';
// Add subtle focus scale effect
btn.style.transform = 'scale(1.05)';
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'scale(1)';
});
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'scale(1.05) translateY(-2px)';
});
});
} else if (isChrome && chromeBtns.length > 0 && firefoxBtns.length > 0) {
// User is on Chrome: Make Firefox secondary
firefoxBtns.forEach(btn => {
btn.classList.remove('btn-firefox');
btn.classList.add('btn-secondary');
btn.style.color = 'var(--text)';
btn.style.background = 'var(--card)';
btn.style.border = '1px solid var(--glass-border)';
btn.style.boxShadow = 'none';
});
}
// Handle Step 1 Landing Page Download Badges & Nav Badge
setTimeout(() => {
const isInstalled = document.documentElement.dataset.koalasyncInstalled === 'true';
// Nav Badge Logic
const navBadge = document.getElementById('nav-extension-status');
if (isInstalled && navBadge) {
navBadge.style.display = 'inline-flex';
}
const illusChrome = document.querySelectorAll('.illus-store-btn.chrome');
const illusFirefox = document.querySelectorAll('.illus-store-btn.firefox');
if (isFirefox && illusFirefox.length > 0) {
illusFirefox.forEach(btn => {
btn.style.order = '-1';
if (!isInstalled) {
btn.classList.add('install-breathe');
btn.style.cursor = 'pointer';
btn.onclick = () => window.open('https://addons.mozilla.org/de/firefox/addon/koalasync/', '_blank');
}
});
illusChrome.forEach(btn => {
btn.style.opacity = '0.5';
btn.style.transform = 'scale(0.95)';
});
} else if (isChrome && illusChrome.length > 0) {
illusChrome.forEach(btn => {
btn.style.order = '-1';
if (!isInstalled) {
btn.classList.add('install-breathe');
btn.style.cursor = 'pointer';
btn.onclick = () => window.open('https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc', '_blank');
}
});
illusFirefox.forEach(btn => {
btn.style.opacity = '0.5';
btn.style.transform = 'scale(0.95)';
});
}
// Pulse main hero CTA buttons via Web Animations API
// (avoids CSS transition/inline-style conflicts from mouse handlers)
if (!isInstalled) {
const heroBtns = document.querySelectorAll(isFirefox ? '.btn-firefox' : (isChrome ? '.btn-primary' : null));
if (heroBtns && heroBtns.length > 0) {
heroBtns.forEach(btn => {
const isFF = btn.classList.contains('btn-firefox');
const glowColor = isFF ? 'rgba(249, 115, 22, ' : 'rgba(99, 102, 241, ';
btn.animate([
{ transform: 'scale(1)', boxShadow: `0 0 15px ${glowColor}0.2)` },
{ transform: 'scale(1.05)', boxShadow: `0 0 25px ${glowColor}0.5)` },
{ transform: 'scale(1)', boxShadow: `0 0 15px ${glowColor}0.2)` }
], {
duration: 2500,
iterations: Infinity,
easing: 'ease-in-out'
});
});
}
}
}, 600);
};
detectBrowserAndElevateBadge();
checkInvite();
updateDynamicVersion();
localizeHomeLinks();
initLanguageSelectorValue();
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#52b54b">
<path d="M11.041 0c-.007 0-1.456 1.43-3.219 3.176L4.615 6.352l.512.513.512.512-2.819 2.791L0 12.961l1.83 1.848c1.006 1.016 2.438 2.46 3.182 3.209l1.351 1.359.508-.496c.28-.273.515-.498.524-.498.008 0 1.266 1.264 2.794 2.808L12.97 24l.187-.182c.23-.225 5.007-4.95 5.717-5.656l.52-.516-.502-.513c-.276-.282-.5-.52-.496-.53.003-.009 1.264-1.26 2.802-2.783 1.538-1.522 2.8-2.776 2.803-2.785.005-.012-3.617-3.684-6.107-6.193L17.65 4.6l-.505.505c-.279.278-.517.501-.53.497-.013-.005-1.27-1.267-2.793-2.805A449.655 449.655 0 0011.041 0zM9.223 7.367c.091.038 7.951 4.608 7.957 4.627.003.013-1.781 1.056-3.965 2.32a999.898 999.898 0 01-3.996 2.307c-.019.006-.026-1.266-.026-4.629 0-3.7.007-4.634.03-4.625z"/>
</svg>

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" fill="#ffffff" xmlns="http://www.w3.org/2000/svg"><title>Firefox Browser</title><path d="M8.824 7.287c.008 0 .004 0 0 0zm-2.8-1.4c.006 0 .003 0 0 0zm16.754 2.161c-.505-1.215-1.53-2.528-2.333-2.943.654 1.283 1.033 2.57 1.177 3.53l.002.02c-1.314-3.278-3.544-4.6-5.366-7.477-.091-.147-.184-.292-.273-.446a3.545 3.545 0 01-.13-.24 2.118 2.118 0 01-.172-.46.03.03 0 00-.027-.03.038.038 0 00-.021 0l-.006.001a.037.037 0 00-.01.005L15.624 0c-2.585 1.515-3.657 4.168-3.932 5.856a6.197 6.197 0 00-2.305.587.297.297 0 00-.147.37c.057.162.24.24.396.17a5.622 5.622 0 012.008-.523l.067-.005a5.847 5.847 0 011.957.222l.095.03a5.816 5.816 0 01.616.228c.08.036.16.073.238.112l.107.055a5.835 5.835 0 01.368.211 5.953 5.953 0 012.034 2.104c-.62-.437-1.733-.868-2.803-.681 4.183 2.09 3.06 9.292-2.737 9.02a5.164 5.164 0 01-1.513-.292 4.42 4.42 0 01-.538-.232c-1.42-.735-2.593-2.121-2.74-3.806 0 0 .537-2 3.845-2 .357 0 1.38-.998 1.398-1.287-.005-.095-2.029-.9-2.817-1.677-.422-.416-.622-.616-.8-.767a3.47 3.47 0 00-.301-.227 5.388 5.388 0 01-.032-2.842c-1.195.544-2.124 1.403-2.8 2.163h-.006c-.46-.584-.428-2.51-.402-2.913-.006-.025-.343.176-.389.206-.406.29-.787.616-1.136.974-.397.403-.76.839-1.085 1.303a9.816 9.816 0 00-1.562 3.52c-.003.013-.11.487-.19 1.073-.013.09-.026.181-.037.272a7.8 7.8 0 00-.069.667l-.002.034-.023.387-.001.06C.386 18.795 5.593 24 12.016 24c5.752 0 10.527-4.176 11.463-9.661.02-.149.035-.298.052-.448.232-1.994-.025-4.09-.753-5.844z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><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>

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#00a4dc">
<path d="M12 .002C8.826.002-1.398 18.537.16 21.666c1.56 3.129 22.14 3.094 23.682 0C25.384 18.573 15.177 0 12 0zm7.76 18.949c-1.008 2.028-14.493 2.05-15.514 0C3.224 16.9 9.92 4.755 12.003 4.755c2.081 0 8.77 12.166 7.759 14.196zM12 9.198c-1.054 0-4.446 6.15-3.93 7.189.518 1.04 7.348 1.027 7.86 0 .511-1.027-2.874-7.19-3.93-7.19z"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e50914">
<path d="M5.398 0 13.746 23.602c2.346.059 4.856.398 4.856.398L10.113 0H5.398zm8.489 0v9.172l4.715 13.33V0h-4.715zM5.398 1.5V24c1.873-.225 2.81-.312 4.715-.398V14.83L5.398 1.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 264 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#00a8e1">
<text x="12" y="7.2" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="7.5" font-weight="900" letter-spacing="-0.1px">prime</text>
<text x="12" y="14.8" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" font-size="7.5" font-weight="900" letter-spacing="-0.1px">video</text>
<path d="M.045 18.02c.072-.116.187-.124.348-.022 3.636 2.11 7.594 3.166 11.87 3.166 2.852 0 5.668-.533 8.447-1.595l.315-.14c.138-.06.234-.1.293-.13.226-.088.39-.046.525.13.12.174.09.336-.12.48-.256.19-.6.41-1.006.654-1.244.743-2.64 1.316-4.185 1.726a17.617 17.617 0 01-10.951-.577 17.88 17.88 0 01-5.43-3.35c-.1-.074-.151-.15-.151-.22 0-.047.021-.09.051-.13z"/>
</svg>

After

Width:  |  Height:  |  Size: 887 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#9146ff">
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/>
</svg>

After

Width:  |  Height:  |  Size: 280 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ff0000">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B

+241
View File
@@ -0,0 +1,241 @@
/**
* KoalaSync Static Site Generator (i18n compiler)
* Build pipeline: esbuild + AVIF + hashing + SVG min.
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const sharp = require('sharp');
const esbuild = require('esbuild');
const { optimize: svgoOptimize } = require('svgo');
// CSS minifier: simple regex-based (proven, 27% reduction, no deps)
function minifyCSS(code) {
return code
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\s*([{}:;,])\s*/g, '$1')
.replace(/\s+/g, ' ')
.replace(/;\}/g, '}')
.trim();
}
const MIN_AVIF_KB = 0;
function copyDirSync(src, dest) {
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const s = path.join(src, entry.name);
const d = path.join(dest, entry.name);
entry.isDirectory() ? copyDirSync(s, d) : fs.copyFileSync(s, d);
}
}
function sha8(buf) { return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 8); }
async function minifyJS(raw) {
const result = await esbuild.transform(raw, {
loader: 'js',
minify: true,
target: 'es2020'
});
return result.code;
}
function injectAvifPictures(html) {
return html.replace(/<img\b([^>]*)>/gi, (match, attrs) => {
const srcMatch = attrs.match(/\bsrc="([^"]*)"/i);
if (!srcMatch) return match;
if (!/\.webp"$/i.test(srcMatch[0])) return match;
const src = srcMatch[1];
const avifSrc = src.replace(/\.webp$/i, '.avif');
const srcsetMatch = attrs.match(/\bsrcset="([^"]*)"/i);
if (srcsetMatch) {
const avifSrcset = srcsetMatch[1].replace(/\.webp/gi, '.avif');
return `<picture><source srcset="${avifSrcset}" type="image/avif"><img${attrs}></picture>`;
}
return `<picture><source srcset="${avifSrc}" type="image/avif"><img${attrs}></picture>`;
});
}
function minifyInlineSvgs(html) {
const svgRegex = /<svg\b[\s\S]*?<\/svg>/gi;
return html.replace(svgRegex, (svg) => {
try {
const result = svgoOptimize(svg, { multipass: true, plugins: ['preset-default'] });
return result.data;
} catch { return svg; }
});
}
async function compile() {
console.log('Starting KoalaSync i18n compilation...');
const websiteDir = __dirname;
const wwwDir = path.join(websiteDir, 'www');
fs.mkdirSync(wwwDir, { recursive: true });
// ── 1. Minify CSS/JS (must happen first so hashes go into HTML) ──
console.log('Minifying CSS/JS...');
const styleRaw = fs.readFileSync(path.join(websiteDir, 'style.css'), 'utf8');
const styleMin = minifyCSS(styleRaw);
const styleHash = sha8(styleMin);
const styleName = `style.${styleHash}.min.css`;
const appRaw = fs.readFileSync(path.join(websiteDir, 'app.js'), 'utf8');
const appMin = await minifyJS(appRaw);
const appHash = sha8(appMin);
const appName = `app.${appHash}.min.js`;
const langRaw = fs.readFileSync(path.join(websiteDir, 'lang-init.js'), 'utf8');
const langMin = await minifyJS(langRaw);
const langHash = sha8(langMin);
const langName = `lang-init.${langHash}.min.js`;
const stylePct = ((1 - styleMin.length / styleRaw.length) * 100).toFixed(0);
const appPct = ((1 - appMin.length / appRaw.length) * 100).toFixed(0);
const langPct = ((1 - langMin.length / langRaw.length) * 100).toFixed(0);
console.log(` CSS: ${styleName} (${(styleMin.length/1024).toFixed(1)} KB, -${stylePct}%)`);
console.log(` App: ${appName} (${(appMin.length/1024).toFixed(1)} KB, -${appPct}%)`);
console.log(` Lang: ${langName} (${(langMin.length/1024).toFixed(1)} KB, -${langPct}%)`);
// ── 2. Clean stale minified output ──
for (const f of fs.readdirSync(wwwDir)) {
if (/\.min\.(css|js)$/.test(f) || /\.(css|js)$/.test(f)) {
const p = path.join(wwwDir, f);
if (fs.statSync(p).isFile()) fs.unlinkSync(p);
}
}
// Write minified files
fs.writeFileSync(path.join(wwwDir, styleName), styleMin);
fs.writeFileSync(path.join(wwwDir, appName), appMin);
fs.writeFileSync(path.join(wwwDir, langName), langMin);
// ── 3. Compile HTML templates ──
const templatePath = path.join(websiteDir, 'template.html');
if (!fs.existsSync(templatePath)) {
console.error('Error: template.html not found!');
process.exit(1);
}
const templateContent = fs.readFileSync(templatePath, 'utf8');
const localesDir = path.join(websiteDir, 'locales');
const languages = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru'];
const englishHtml = {}; // track for join-page cross-ref
function compilePage(locale, assetPath, lang) {
let compiled = templateContent;
compiled = compiled.replace(/\{\{ASSET_PATH\}\}/g, assetPath);
languages.forEach(l => {
compiled = compiled.replace(new RegExp(`\\{\\{SELECTED_${l.toUpperCase()}\\}\\}`, 'g'), l === lang ? 'selected' : '');
});
for (const [key, value] of Object.entries(locale)) {
compiled = compiled.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
}
return compiled;
}
for (const lang of languages) {
const localePath = path.join(localesDir, `${lang}.json`);
if (!fs.existsSync(localePath)) { console.warn(`Warning: Locale ${lang} not found.`); continue; }
const locale = JSON.parse(fs.readFileSync(localePath, 'utf8'));
if (lang === 'en') {
console.log('Compiling English (index.html)...');
const html = compilePage(locale, '', lang);
fs.writeFileSync(path.join(wwwDir, 'index.html'), html);
englishHtml[''] = html;
} else {
console.log(`Compiling ${lang.toUpperCase()} (${lang}/index.html)...`);
const langDir = path.join(wwwDir, lang);
fs.mkdirSync(langDir, { recursive: true });
const html = compilePage(locale, '../', lang);
fs.writeFileSync(path.join(langDir, 'index.html'), html);
englishHtml[lang] = html;
}
}
// ── 4. Copy static HTML files (join, impressum, datenschutz) ──
console.log('Copying static pages...');
const staticHtml = ['join.html', 'impressum.html', 'datenschutz.html'];
for (const file of staticHtml) {
const src = path.join(websiteDir, file);
const dest = path.join(wwwDir, file);
if (fs.existsSync(src)) { fs.copyFileSync(src, dest); console.log(` Copied: ${file}`); }
}
// ── 5. Copy generic static files ──
const genericFiles = ['robots.txt', 'sitemap.xml', 'site.webmanifest', 'version.json'];
for (const file of genericFiles) {
const src = path.join(websiteDir, file);
const dest = path.join(wwwDir, file);
if (fs.existsSync(src)) { fs.copyFileSync(src, dest); }
}
// ── 6. Copy assets ──
console.log('Copying assets...');
const srcAssets = path.join(websiteDir, 'assets');
const destAssets = path.join(wwwDir, 'assets');
if (fs.existsSync(srcAssets)) {
copyDirSync(srcAssets, destAssets);
console.log(' Assets copied.');
}
// ── 7. Convert all WebP to AVIF (quality 70) ──
console.log('Converting WebP → AVIF...');
let avifCount = 0;
const webpFiles = fs.readdirSync(destAssets).filter(f => f.endsWith('.webp'));
for (const f of webpFiles) {
const src = path.join(destAssets, f);
const stat = fs.statSync(src);
if (stat.size < MIN_AVIF_KB * 1024) continue;
const dest = path.join(destAssets, f.replace(/\.webp$/, '.avif'));
if (fs.existsSync(dest) && fs.statSync(dest).mtimeMs >= stat.mtimeMs) continue;
await sharp(src).avif({ quality: 80, speed: 4 }).toFile(dest);
avifCount++;
}
console.log(` ${avifCount} AVIF files generated.`);
// ── 8. Post-process ALL HTML files ──
console.log('Post-processing HTML...');
function walkHtml(dir, fn) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) { walkHtml(p, fn); }
else if (entry.name.endsWith('.html')) { fn(p); }
}
}
walkHtml(wwwDir, (filePath) => {
let html = fs.readFileSync(filePath, 'utf8');
// 8a. Replace hashed asset refs
html = html.replace(/href="(?:\.\.\/)?style\.min\.css"/g, (m) => {
const prefix = m.includes('../') ? '../' : '';
return `href="${prefix}${styleName}"`;
});
html = html.replace(/src="(?:\.\.\/)?app\.min\.js"/g, (m) => {
const prefix = m.includes('../') ? '../' : '';
return `src="${prefix}${appName}"`;
});
html = html.replace(/src="(?:\.\.\/)?lang-init\.min\.js"/g, (m) => {
const prefix = m.includes('../') ? '../' : '';
return `src="${prefix}${langName}"`;
});
// Also update preload directives
html = html.replace(/href="(?:\.\.\/)?style\.min\.css"/g, (m) => {
const prefix = m.includes('../') ? '../' : '';
return `href="${prefix}${styleName}"`;
});
// 8b. Inject AVIF <picture> wrappers
html = injectAvifPictures(html);
// 8c. Minify inline SVGs
html = minifyInlineSvgs(html);
fs.writeFileSync(filePath, html);
});
console.log('KoalaSync build finished successfully! Output: website/www/');
}
compile().catch(err => { console.error('Build failed:', err); process.exit(1); });
+92 -37
View File
@@ -4,29 +4,28 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutz / Privacy Policy | KoalaSync</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
<script>
(function() {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
var activeLang = savedLang || browserLang;
document.documentElement.classList.add('lang-' + activeLang);
document.documentElement.lang = activeLang;
})();
function toggleLanguage(e) {
if(e) e.preventDefault();
var html = document.documentElement;
var currentIsEnglish = html.classList.contains('lang-en');
var newLang = currentIsEnglish ? 'de' : 'en';
html.classList.remove('lang-en', 'lang-de');
html.classList.add('lang-' + newLang);
html.lang = newLang;
localStorage.setItem('koala_lang', newLang);
}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sync.koalastuff.net/" },
{ "@type": "ListItem", "position": 2, "name": "Privacy Policy", "item": "https://sync.koalastuff.net/datenschutz" }
]
}
</script>
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
@@ -37,21 +36,42 @@
<nav>
<div class="container nav-content">
<a href="index.html" class="logo-area" style="text-decoration: none;">
<img src="assets/logo.png" alt="KoalaSync Logo">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu"></button>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="index.html"><span lang="de">Startseite</span><span lang="en">Home</span></a>
<a href="https://github.com/shik3i/KoalaSync" target="_blank">GitHub</a>
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
<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>
<span lang="de">Startseite</span><span lang="en">Home</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
<div 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">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
</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>
</div>
</div>
</nav>
<main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;">
<img src="assets/KoalaPrivacy.webp" alt="Cute Koala representing privacy and data security" class="legal-mascot" lang="en" width="180" height="180">
<img src="assets/KoalaPrivacy.webp" alt="Niedlicher Koala, der den Datenschutz repräsentiert" class="legal-mascot" lang="de" width="180" height="180">
</div>
<h1 lang="de">Datenschutz</h1>
<h1 lang="en">Privacy Policy</h1>
<p lang="de" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
@@ -121,8 +141,39 @@
<section>
<h2>
<span lang="de">5. Brute-Force Schutz</span>
<span lang="en">5. Brute-Force Protection</span>
<span lang="de">5. Berechtigungen der Erweiterung</span>
<span lang="en">5. Extension Permissions</span>
</h2>
<p lang="de">
Um ihren technischen Zweck zu erfüllen, benötigt die Browser-Erweiterung bestimmte Berechtigungen. Jede Berechtigung wird ausschließlich für die Kernfunktionalität genutzt:
</p>
<ul lang="de" style="margin-left: 1.5rem; margin-top: 0.5rem; color: var(--text-muted); font-size: 0.9rem; list-style-type: disc; display: flex; flex-direction: column; gap: 0.35rem;">
<li><strong>storage</strong>: Erlaubt das lokale Speichern von Benutzernamen, Server-URLs und Raum-Zugangsdaten im Browser, damit Sie sich nicht jedes Mal neu anmelden müssen.</li>
<li><strong>tabs</strong>: Wird benötigt, um geöffnete Tabs im Dropdown der Erweiterung aufzulisten und deren Titel auszulesen, damit Sie bequem den richtigen Video-Tab auswählen können.</li>
<li><strong>scripting</strong>: Erforderlich, um das Synchronisationsskript (content.js) sicher in den von Ihnen ausgewählten Videotab zu injizieren.</li>
<li><strong>alarms</strong>: Verhindert, dass der Hintergrunddienst der Erweiterung (Service Worker) während einer aktiven Synchronisationssitzung vom Browser in den Ruhezustand versetzt wird.</li>
<li><strong>activeTab</strong>: Ermöglicht eine sichere, temporäre Interaktion mit dem momentan aktiven Tab für direkte Steuerungsbefehle.</li>
<li><strong>notifications</strong>: Ermöglicht optionale Desktop-Benachrichtigungen, wenn beispielsweise ein neuer Freund dem Raum beitritt.</li>
<li><strong>&lt;all_urls&gt; (Host-Berechtigung)</strong>: Ermöglicht der Erweiterung, auf beliebigen Webseiten nach HTML5-Videoelementen zu suchen, damit die Synchronisation plattformübergreifend (z. B. auf YouTube, Netflix, Jellyfin etc.) funktioniert.</li>
</ul>
<p lang="en" style="margin-top: 1.5rem;">
To fulfill its technical purpose, the browser extension requires certain permissions. Each permission is used exclusively for core functionality:
</p>
<ul lang="en" style="margin-left: 1.5rem; margin-top: 0.5rem; color: var(--text-muted); font-size: 0.9rem; list-style-type: disc; display: flex; flex-direction: column; gap: 0.35rem;">
<li><strong>storage</strong>: Allows local storage of your username, server URL, and room credentials in your browser so you don't have to log in every time.</li>
<li><strong>tabs</strong>: Required to list open tabs in the extension's dropdown and read their titles, making it easy for you to select the correct video tab.</li>
<li><strong>scripting</strong>: Required to securely inject the synchronization script (content.js) into your selected video tab.</li>
<li><strong>alarms</strong>: Prevents the extension's background service worker from being suspended by the browser during an active synchronization session.</li>
<li><strong>activeTab</strong>: Enables secure, temporary interaction with the currently active tab for direct playback commands.</li>
<li><strong>notifications</strong>: Enables optional desktop notifications, such as when a new friend joins the room.</li>
<li><strong>&lt;all_urls&gt; (Host permission)</strong>: Allows the extension to scan for HTML5 video elements on any website, enabling cross-platform synchronization (e.g., on YouTube, Netflix, Jellyfin etc.).</li>
</ul>
</section>
<section>
<h2>
<span lang="de">6. Brute-Force Schutz</span>
<span lang="en">6. Brute-Force Protection</span>
</h2>
<p lang="de">
Zur Sicherheit unserer Nutzer speichern wir fehlgeschlagene Login-Versuche (IP-Adresse und Raum-ID) für maximal 15 Minuten im RAM, um automatisierte Angriffe zu verhindern. Diese Daten werden danach rückstandslos gelöscht.
@@ -134,8 +185,8 @@
<section>
<h2>
<span lang="de">6. Ihre Rechte</span>
<span lang="en">6. Your Rights</span>
<span lang="de">7. Ihre Rechte</span>
<span lang="en">7. Your Rights</span>
</h2>
<p lang="de">
Sie haben das Recht auf Auskunft, Berichtigung oder Löschung Ihrer Daten. Da wir jedoch keine personenbezogenen Daten dauerhaft speichern, ist eine Zuordnung zu Ihrer Person in der Regel technisch nicht möglich.
@@ -143,8 +194,8 @@
<p lang="en">
You have the right to information, correction, or deletion of your data. However, since we do not store any personal data permanently, linking data to your person is technically impossible in most cases.
</p>
<p lang="de">Kontakt bei Fragen: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_datenschutz' + '@' + 'koalamail.rocks'">[E-Mail anzeigen]</span></p>
<p lang="en">Contact for questions: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_datenschutz' + '@' + 'koalamail.rocks'">[Show Email]</span></p>
<p lang="de">Kontakt bei Fragen: <span class="email-reveal" data-user="koalasync_datenschutz" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[E-Mail anzeigen]</span></p>
<p lang="en">Contact for questions: <span class="email-reveal" data-user="koalasync_datenschutz" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[Show Email]</span></p>
</section>
</div>
</main>
@@ -154,13 +205,17 @@
<p>&copy; 2026 KoalaSync. Open source under the MIT License.</p>
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; gap: 1.5rem;">
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="impressum" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="app.js"></script>
<script src="app.min.js"></script>
</body>
</html>
+79 -35
View File
@@ -4,29 +4,28 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum / Legal Notice | KoalaSync</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.min.css">
<link rel="icon" type="image/webp" href="assets/NewLogoIcon_64.webp">
<meta name="robots" content="noindex">
<script>
(function() {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
var activeLang = savedLang || browserLang;
document.documentElement.classList.add('lang-' + activeLang);
document.documentElement.lang = activeLang;
})();
function toggleLanguage(e) {
if(e) e.preventDefault();
var html = document.documentElement;
var currentIsEnglish = html.classList.contains('lang-en');
var newLang = currentIsEnglish ? 'de' : 'en';
html.classList.remove('lang-en', 'lang-de');
html.classList.add('lang-' + newLang);
html.lang = newLang;
localStorage.setItem('koala_lang', newLang);
}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sync.koalastuff.net/" },
{ "@type": "ListItem", "position": 2, "name": "Legal Notice", "item": "https://sync.koalastuff.net/impressum" }
]
}
</script>
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
@@ -37,21 +36,42 @@
<nav>
<div class="container nav-content">
<a href="index.html" class="logo-area" style="text-decoration: none;">
<img src="assets/logo.png" alt="KoalaSync Logo">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="40" height="40">
<span>KoalaSync</span>
</a>
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu"></button>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="index.html"><span lang="de">Startseite</span><span lang="en">Home</span></a>
<a href="https://github.com/shik3i/KoalaSync" target="_blank">GitHub</a>
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
<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>
<span lang="de">Startseite</span><span lang="en">Home</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
<div 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">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
</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>
</div>
</div>
</nav>
<main class="legal-content">
<div class="legal-card" data-reveal style="padding: 2rem;">
<div style="display: flex; justify-content: center;">
<img src="assets/KoalaImprintl.webp" alt="Cute Koala representing the legal notice page" class="legal-mascot" lang="en" width="180" height="180">
<img src="assets/KoalaImprintl.webp" alt="Niedlicher Koala, der das Impressum repräsentiert" class="legal-mascot" lang="de" width="180" height="180">
</div>
<h1 lang="de">Impressum</h1>
<h1 lang="en">Legal Notice</h1>
<p lang="de" style="text-align: center; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.8rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1.5rem; margin-bottom: 2rem;">
@@ -66,10 +86,30 @@
<span lang="de">Betreiber & Kontakt</span>
<span lang="en">Operator & Contact</span>
</h2>
<p lang="de">Administrator KoalaSync (Privatperson)</p>
<p lang="en">Administrator KoalaSync (Private Individual)</p>
<p lang="de">E-Mail: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_admin' + '@' + 'koalamail.rocks'">[E-Mail anzeigen]</span></p>
<p lang="en">E-Mail: <span style="color: var(--accent); cursor: pointer; text-decoration: underline;" onclick="this.innerHTML='koalasync_admin' + '@' + 'koalamail.rocks'">[Show Email]</span></p>
<p lang="de">Timo (KoalaDev) Privatperson</p>
<p lang="en">Timo (KoalaDev) Private Individual</p>
<p lang="de" style="margin-top: 0.5rem;">
E-Mail: <span class="email-reveal" data-user="koalasync_admin" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[E-Mail anzeigen]</span>
</p>
<p lang="en" style="margin-top: 0.5rem;">
E-Mail: <span class="email-reveal" data-user="koalasync_admin" data-domain="koalamail.rocks" style="color: var(--accent); cursor: pointer; text-decoration: underline;">[Show Email]</span>
</p>
<p lang="de" style="margin-top: 0.75rem;">
<span style="opacity: 0.6;">🔒 Private Nachricht:</span>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--accent); text-decoration: none;">@koalastuff auf Mastodon</a>
</p>
<p lang="en" style="margin-top: 0.75rem;">
<span style="opacity: 0.6;">🔒 Private message:</span>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--accent); text-decoration: none;">@koalastuff on Mastodon</a>
</p>
<p lang="de" style="margin-top: 0.5rem;">
<span style="opacity: 0.6;">🐛 Active Bedenken / Bug Reports:</span>
<a href="https://github.com/Shik3i/KoalaSync/issues" target="_blank" style="color: var(--accent); text-decoration: none;">GitHub Issues</a>
</p>
<p lang="en" style="margin-top: 0.5rem;">
<span style="opacity: 0.6;">🐛 Active concerns / Bug reports:</span>
<a href="https://github.com/Shik3i/KoalaSync/issues" target="_blank" style="color: var(--accent); text-decoration: none;">GitHub Issues</a>
</p>
</section>
<section style="opacity: 0.8;">
@@ -139,13 +179,17 @@
<p>&copy; 2026 KoalaSync. Open source under the MIT License.</p>
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; gap: 1.5rem;">
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="impressum" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Impressum</span><span lang="en">Legal Notice</span></a>
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;"><span lang="de">Datenschutz</span><span lang="en">Privacy Policy</span></a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="app.js"></script>
<script src="app.min.js"></script>
</body>
</html>
-197
View File
@@ -1,197 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KoalaSync | Real-time Video Synchronization for Friends</title>
<meta name="description" content="Watch YouTube, Twitch, and HTML5 videos perfectly synchronized with friends. KoalaSync is a privacy-first, open-source Chrome extension.">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
<meta property="og:title" content="KoalaSync | Sync your videos">
<meta property="og:description" content="Watch together, stay in sync. Privacy-first video synchronization.">
<meta property="og:image" content="https://sync.koalastuff.net/assets/logo.png">
<meta property="og:type" content="website">
<script>
(function() {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
var activeLang = savedLang || browserLang;
document.documentElement.classList.add('lang-' + activeLang);
document.documentElement.lang = activeLang;
var titles = { en: 'KoalaSync | Real-time Video Synchronization for Friends', de: 'KoalaSync | Echtzeit-Video-Synchronisation für Freunde' };
document.title = titles[activeLang] || titles.en;
})();
function toggleLanguage(e) {
if(e) e.preventDefault();
var html = document.documentElement;
var currentIsEnglish = html.classList.contains('lang-en');
var newLang = currentIsEnglish ? 'de' : 'en';
html.classList.remove('lang-en', 'lang-de');
html.classList.add('lang-' + newLang);
html.lang = newLang;
localStorage.setItem('koala_lang', newLang);
var titles = { en: 'KoalaSync | Real-time Video Synchronization for Friends', de: 'KoalaSync | Echtzeit-Video-Synchronisation für Freunde' };
document.title = titles[newLang] || titles.en;
}
</script>
</head>
<body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<nav>
<div class="container nav-content">
<div class="logo-area">
<img src="assets/logo.png" alt="KoalaSync Logo">
<span>KoalaSync</span>
</div>
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu"></button>
<div class="nav-links">
<a href="#features"><span lang="en">Features</span><span lang="de">Funktionen</span></a>
<a href="#how-it-works"><span lang="en">How it works</span><span lang="de">So funktioniert's</span></a>
<a href="https://github.com/Shik3i/KoalaSync" target="_blank">GitHub</a>
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
</div>
</div>
</nav>
<header class="hero">
<div class="container hero-grid">
<div class="hero-text">
<div class="version-badge" data-reveal><span lang="en" class="version-text-en">v1.3.1 OUT NOW</span><span lang="de" class="version-text-de">v1.3.1 JETZT VERFÜGBAR</span></div>
<h1 data-reveal>
<span lang="en">Watch Together.<br>Sync Perfectly.</span>
<span lang="de">Gemeinsam schauen.<br>Perfekt synchron.</span>
</h1>
<h2 class="hero-subtitle" data-reveal>
<span lang="en">The ultimate free watch party extension for YouTube, Twitch, and local MP4s.</span>
<span lang="de">Die ultimative, kostenlose Watch-Party-Erweiterung für YouTube, Twitch und lokale MP4s.</span>
</h2>
<div class="cta-group" data-reveal>
<a href="#" class="btn btn-primary">
<img src="assets/chrome.svg" width="20" style="filter: brightness(100)">
<span lang="en">Add to Chrome</span><span lang="de">Zu Chrome hinzufügen</span>
</a>
<a href="#" class="btn btn-primary" style="background: #e66000;">
🦊
<span lang="en">Add to Firefox</span><span lang="de">Zu Firefox hinzufügen</span>
</a>
<a href="https://github.com/Shik3i/KoalaSync" class="btn btn-secondary">
GitHub Repo
</a>
</div>
</div>
</div>
</header>
<section id="features">
<div class="container">
<h2 style="font-size: 2.5rem; text-align: center; margin-bottom: 1rem;">
<span lang="en">Why KoalaSync?</span><span lang="de">Warum KoalaSync?</span>
</h2>
<p style="text-align: center; color: var(--text-muted); margin-bottom: 4rem;">
<span lang="en">Built for performance, privacy, and simplicity.</span><span lang="de">Entwickelt für Leistung, Privatsphäre und Einfachheit.</span>
</p>
<div class="features-grid">
<div class="feature-card" data-reveal>
<div class="feature-icon"></div>
<h3><span lang="en">Real-time Sync</span><span lang="de">Echtzeit-Synchronisation</span></h3>
<p lang="en">Proprietary two-phase synchronization protocol ensures sub-millisecond precision across all peers.</p>
<p lang="de">Das proprietäre Zwei-Phasen-Synchronisationsprotokoll gewährleistet Sub-Millisekunden-Präzision für alle Teilnehmer.</p>
</div>
<div class="feature-card" data-reveal>
<div class="feature-icon">🎬</div>
<h3><span lang="en">Episode Auto-Sync</span><span lang="de">Automatische Episoden-Synchronisation</span></h3>
<p lang="en">New in v1.3.1: Perfectly sync series binges. All peers wait until everyone has loaded the next episode.</p>
<p lang="de">Neu in v1.3.1: Perfekt synchronisiertes Serien-Bingeing. Alle warten, bis jeder die nächste Episode geladen hat.</p>
</div>
<div class="feature-card" data-reveal>
<div class="feature-icon">🛡️</div>
<h3><span lang="en">Privacy First</span><span lang="de">Privatsphäre zuerst</span></h3>
<p lang="en">Zero data persistence. Our relay server runs entirely in RAM and collects no telemetry or logs.</p>
<p lang="de">Keine Datenspeicherung. Unser Relay-Server läuft komplett im RAM und sammelt weder Telemetrie noch Logs.</p>
</div>
<div class="feature-card" data-reveal>
<div class="feature-icon"></div>
<h3><span lang="en">Smart Match</span><span lang="de">Intelligente Zuordnung</span></h3>
<p lang="en">Find the right tab instantly. KoalaSync highlights and sorts matching video tabs for you.</p>
<p lang="de">Finden Sie sofort den richtigen Tab. KoalaSync markiert und sortiert passende Video-Tabs für Sie.</p>
</div>
</div>
</div>
</section>
<section id="how-it-works" style="background: rgba(255,255,255,0.02)">
<div class="container">
<h2 style="font-size: 2.5rem; text-align: center; margin-bottom: 4rem;">
<span lang="en">Getting Started</span><span lang="de">Loslegen</span>
</h2>
<div class="steps">
<div class="step">
<div class="step-text" data-reveal>
<div class="step-num">01</div>
<h3><span lang="en">Install Extension</span><span lang="de">Erweiterung installieren</span></h3>
<p lang="en">Add KoalaSync to your browser from the Chrome Web Store or download the latest developer ZIP from GitHub.</p>
<p lang="de">Fügen Sie KoalaSync aus dem Chrome Web Store zu Ihrem Browser hinzu oder laden Sie die neueste Entwickler-ZIP von GitHub herunter.</p>
</div>
<div style="background: var(--card); height: 200px; border-radius: 20px; border: 1px solid var(--glass-border);"></div>
</div>
<div class="step">
<div class="step-text" data-reveal>
<div class="step-num">02</div>
<h3><span lang="en">Create a Room</span><span lang="de">Raum erstellen</span></h3>
<p lang="en">Open the extension, enter a Room ID and Password, and hit Join. Your secure synchronization space is ready.</p>
<p lang="de">Öffnen Sie die Erweiterung, geben Sie eine Raum-ID und ein Passwort ein und klicken Sie auf Beitreten. Ihr sicherer Synchronisationsraum ist bereit.</p>
</div>
<div style="background: var(--card); height: 200px; border-radius: 20px; border: 1px solid var(--glass-border);"></div>
</div>
<div class="step">
<div class="step-text" data-reveal>
<div class="step-num">03</div>
<h3><span lang="en">Share & Sync</span><span lang="de">Teilen & Synchronisieren</span></h3>
<p lang="en">Send the invite link to your friends. Once they join, select your video tab and enjoy perfectly synced playback.</p>
<p lang="de">Senden Sie den Einladungslink an Ihre Freunde. Sobald sie beitreten, wählen Sie Ihren Video-Tab aus und genießen Sie die perfekt synchronisierte Wiedergabe.</p>
</div>
<div style="background: var(--card); height: 200px; border-radius: 20px; border: 1px solid var(--glass-border);"></div>
</div>
</div>
</div>
</section>
<section style="text-align: center;">
<div class="container">
<h2 data-reveal><span lang="en">Ready to sync?</span><span lang="de">Bereit zum Synchronisieren?</span></h2>
<p data-reveal style="margin-bottom: 2rem; color: var(--text-muted);">
<span lang="en">Join thousands of users watching together.</span><span lang="de">Schließen Sie sich Tausenden von Nutzern an, die bereits zusammen schauen.</span>
</p>
<a href="https://github.com/Shik3i/KoalaSync" class="btn btn-primary" data-reveal>
<span lang="en">View on GitHub</span><span lang="de">Auf GitHub ansehen</span>
</a>
</div>
</section>
<footer>
<div class="container">
<p>&copy; 2026 KoalaSync. Open source under the MIT License.</p>
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; gap: 1.5rem;">
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Legal Notice</span><span lang="de">Impressum</span></a>
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Privacy Policy</span><span lang="de">Datenschutz</span></a>
</div>
</div>
</footer>
<script src="app.js"></script>
</body>
</html>
+73 -39
View File
@@ -4,33 +4,32 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Join Room | KoalaSync</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
<link rel="preload" href="style.min.css" as="style">
<link rel="stylesheet" href="style.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">
<meta name="robots" content="noindex">
<script>
(function() {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.startsWith('de') ? 'de' : 'en';
var activeLang = savedLang || browserLang;
document.documentElement.classList.add('lang-' + activeLang);
document.documentElement.lang = activeLang;
var titles = { en: 'Join Room | KoalaSync', de: 'Raum beitreten | KoalaSync' };
document.title = titles[activeLang] || titles.en;
})();
function toggleLanguage(e) {
if(e) e.preventDefault();
var html = document.documentElement;
var currentIsEnglish = html.classList.contains('lang-en');
var newLang = currentIsEnglish ? 'de' : 'en';
html.classList.remove('lang-en', 'lang-de');
html.classList.add('lang-' + newLang);
html.lang = newLang;
localStorage.setItem('koala_lang', newLang);
var titles = { en: 'Join Room | KoalaSync', de: 'Raum beitreten | KoalaSync' };
document.title = titles[newLang] || titles.en;
}
</script>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://sync.koalastuff.net/join">
<meta property="og:title" content="Join Room | KoalaSync">
<meta property="og:description" content="You've been invited to watch videos together in perfect sync with your friends on KoalaSync.">
<meta property="og:image" content="https://sync.koalastuff.net/assets/PlatformJuggler_New.webp">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Join Room | KoalaSync">
<meta name="twitter:description" content="You've been invited to watch videos together in perfect sync with your friends on KoalaSync.">
<meta name="twitter:image" content="https://sync.koalastuff.net/assets/PlatformJuggler_New.webp">
<!-- Mobile Browser Theme Styling -->
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script src="lang-init.min.js"></script>
</head>
<body>
<div class="bg-blobs">
@@ -41,22 +40,49 @@
<nav>
<div class="container nav-content">
<a href="index.html" class="logo-area" style="text-decoration: none;">
<img src="assets/logo.png" alt="KoalaSync Logo">
<a href="./" class="logo-area" style="text-decoration: none;">
<img src="assets/NewLogoIcon.webp" alt="KoalaSync Logo" width="64" height="64">
<span>KoalaSync</span>
</a>
<button class="hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')" aria-label="Menu"></button>
<button class="hamburger" aria-label="Menu" aria-expanded="false"></button>
<div class="nav-links">
<a href="index.html">Home</a>
<a href="https://github.com/shik3i/KoalaSync" target="_blank">GitHub</a>
<a class="lang-toggle" onclick="toggleLanguage(event)">🌍 EN/DE</a>
<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>
<span lang="de">Startseite</span><span lang="en">Home</span>
</a>
<a href="https://github.com/shik3i/KoalaSync" target="_blank" style="display: inline-flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true" style="display: block;"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
GitHub
</a>
<div 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">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="ru">Русский</option>
</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>
</div>
</div>
</nav>
<main class="legal-content join-card">
<div class="legal-card" id="join-container" data-reveal style="padding: 2.5rem;">
<div class="legal-card" id="join-container" data-reveal style="padding: 2.5rem; text-align: center;">
<div class="room-badge"><span lang="en">INVITATION DETECTED</span><span lang="de">EINLADUNG ERKANNT</span></div>
<!-- Dynamic Status Icon & Pulsing Radar Wave -->
<div class="join-status-visual">
<div class="status-ring active-pulse" id="status-ring"></div>
<div id="join-status-icon" style="z-index: 2; transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); display: flex; align-items: center; justify-content: center;">
<img src="assets/KoalaSearching.webp" alt="A cute koala looking through a telescope searching for your extension" class="join-status-mascot searching-mascot join-status-pulse" lang="en">
<img src="assets/KoalaSearching.webp" alt="Ein niedlicher Koala schaut durch ein Fernrohr und sucht nach deiner Erweiterung" class="join-status-mascot searching-mascot join-status-pulse" lang="de">
</div>
</div>
<h1 id="join-title" style="font-size: 2rem; margin-bottom: 1rem;"><span lang="en">Ready to sync?</span><span lang="de">Bereit zum Synchronisieren?</span></h1>
<p id="join-desc" style="text-align: center; color: var(--text-muted); margin-bottom: 2rem; font-size: 0.9rem;">
<span lang="en">You've been invited to join a synchronized session.</span>
@@ -69,7 +95,10 @@
</div>
<div id="join-actions" style="display: flex; flex-direction: column; gap: 1rem;">
<div style="text-align: center; color: var(--text-muted); font-size: 0.8rem;"><span lang="en">Detecting extension...</span><span lang="de">Erweiterung wird erkannt...</span></div>
<div style="text-align: center; color: var(--text-muted); font-size: 0.8rem;">
<div class="join-spinner"></div>
<span lang="en">Detecting extension...</span><span lang="de">Erweiterung wird erkannt...</span>
</div>
</div>
</div>
</main>
@@ -77,14 +106,19 @@
<footer>
<div class="container">
<p>&copy; 2026 KoalaSync. Open source under the MIT License.</p>
<p style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; gap: 1.5rem;">
<a href="impressum.html" style="color: var(--text-muted); text-decoration: none;">Impressum</a>
<a href="datenschutz.html" style="color: var(--text-muted); text-decoration: none;">Datenschutz</a>
<p lang="en" style="font-size: 0.8rem; margin-top: 0.5rem;">No data is stored on our servers. Pure RAM-based relay.</p>
<p lang="de" style="font-size: 0.8rem; margin-top: 0.5rem;">Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.</p>
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="impressum" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Legal Notice</span><span lang="de">Impressum</span></a>
<a href="datenschutz" style="color: var(--text-muted); text-decoration: none;"><span lang="en">Privacy Policy</span><span lang="de">Datenschutz</span></a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
</a>
</div>
</div>
</footer>
<script src="app.js"></script>
<script src="app.min.js"></script>
</body>
</html>
+74
View File
@@ -0,0 +1,74 @@
(function() {
var html = document.documentElement;
var path = window.location.pathname;
// Mapping of browser language codes to KoalaSync locale directories
var langMap = {
'de': 'de',
'fr': 'fr',
'es': 'es',
'pt': 'pt-BR',
'ru': 'ru'
};
// Check if we are on the root index page (either "/" or "/index.html" at the root)
var isRootIndex = path === '/' || path === '/index.html' || path === '';
if (isRootIndex) {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.split('-')[0];
var preferredLang = savedLang || langMap[browserLang] || 'en';
if (preferredLang !== 'en') {
window.location.replace(preferredLang + '/');
return;
}
}
var htmlClasses = html.className.split(' ');
var activeLang = null;
var hasStaticLang = false;
for (var i = 0; i < htmlClasses.length; i++) {
if (htmlClasses[i].indexOf('lang-') === 0) {
hasStaticLang = true;
var langPart = htmlClasses[i].substring(5);
activeLang = langPart === 'pt-br' ? 'pt-BR' : langPart;
break;
}
}
if (hasStaticLang) {
localStorage.setItem('koala_lang', activeLang);
} else {
var savedLang = localStorage.getItem('koala_lang');
var browserLang = navigator.language.split('-')[0];
activeLang = savedLang || langMap[browserLang] || 'en';
// Dynamic utility pages currently only support English and German markup.
// Fallback to English for any other language to avoid bilingual text duplication.
if (activeLang !== 'de') {
activeLang = 'en';
}
html.classList.add('lang-' + activeLang);
html.lang = activeLang;
}
// Update titles dynamically based on page
var isIndex = path === '/' || path.endsWith('index.html') || path.split('/').pop() === '';
var isJoin = path.includes('join');
if (isIndex) {
var titles = {
en: 'KoalaSync | Real-time Video Synchronization for Friends',
de: 'KoalaSync | Echtzeit-Video-Synchronisation für Freunde'
};
document.title = titles[activeLang] || titles.en;
} else if (isJoin) {
var titles = {
en: 'Join Room | KoalaSync',
de: 'Raum beitreten | KoalaSync'
};
document.title = titles[activeLang] || titles.en;
}
})();
+161
View File
@@ -0,0 +1,161 @@
{
"LANG_CODE": "de",
"HTML_CLASS": "lang-de",
"CANONICAL_PATH": "de/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "de_DE",
"META_TITLE": "KoalaSync | Netflix, YouTube & jedes Video mit Freunden synchronisieren",
"META_DESCRIPTION": "Schaue Netflix, YouTube, Twitch und jedes HTML5-Video synchron mit Freunden. Kostenlose, quelloffene Browser-Erweiterung. Keine Anmeldung erforderlich.",
"OG_TITLE": "KoalaSync | Netflix, Emby, Jellyfin & fast jedes Video mit Freunden synchronisieren",
"OG_DESCRIPTION": "Schaue Netflix, Emby, Jellyfin, YouTube, Twitch und fast jedes HTML5-Video perfekt synchronisiert. Quelloffene, datenschutzfreundliche Browser-Erweiterung für Chrome und Firefox.",
"TWITTER_TITLE": "KoalaSync | Netflix, Emby, Jellyfin & fast jedes Video mit Freunden synchronisieren Browser-Erweiterung",
"TWITTER_DESCRIPTION": "Schaue Netflix, Emby, Jellyfin, YouTube, Twitch und fast jedes HTML5-Video perfekt synchronisiert mit Freunden. Datenschutzfreundliche Browser-Erweiterung für Chrome und Firefox.",
"NAV_FEATURES": "Funktionen",
"NAV_HOW_IT_WORKS": "So funktioniert's",
"HERO_TITLE": "Gemeinsam schauen.<br>Perfekt synchron.",
"HERO_SUBTITLE": "Dein Kino-Abend auf Distanz. Keine Lags, keine Anmeldung. Einfach Link teilen und zusammen schauen.",
"HERO_MASCOT_ALT": "Ein niedlicher Koala steht da und schaut nach unten auf die Download-Buttons",
"ADD_TO_CHROME": "Zu Chrome hinzufügen",
"ADD_TO_FIREFOX": "Zu Firefox hinzufügen",
"COMPAT_HEADING": "Funktioniert auf deinen Lieblingsplattformen",
"COMPAT_MORE": "und viele mehr",
"COMPAT_TOOLTIP": "Funktioniert auf fast jeder Seite mit einem Video-Element",
"USE_CASES_TITLE": "Perfekt für jeden Anlass",
"USE_CASES_SUBTITLE": "Ob nah oder fern, KoalaSync bringt Menschen bei ihren Lieblingsvideos zusammen.",
"USE_CASE_1_ALT": "Zwei niedliche Koalas sitzen zusammen und teilen sich einen Eimer Popcorn",
"USE_CASE_1_TITLE": "Filmabend mit Freunden",
"USE_CASE_1_DESC": "Synchronisiere eure Filme in Echtzeit und quatscht nebenbei auf Discord, Zoom oder im Tool eurer Wahl. Es fühlt sich an, als säßet ihr im selben Raum.",
"USE_CASE_2_ALT": "Ein Koala-Professor hält einen Vortrag auf einem Bildschirm vor zwei remote zugeschalteten Schüler-Koalas",
"USE_CASE_2_TITLE": "Gemeinsam Lernen",
"USE_CASE_2_DESC": "Analysiert Online-Tutorials, Vorlesungen oder Streams gemeinsam mit Mitschülern oder Kollegen. Pausiert und besprecht komplizierte Schritte in perfektem Sync.",
"USE_CASE_3_ALT": "Ein niedliches Koala-Pärchen schaut remote zusammen; einer sitzt am PC und sie liegt mit dem Laptop im Bett, während Herzen zwischen ihnen fliegen",
"USE_CASE_3_TITLE": "Fernbeziehungen",
"USE_CASE_3_DESC": "Überbrückt die Distanz und genießt gemeinsame Date-Nächte. Erlebt jeden Plot-Twist, lacht über dieselben Witze und teilt emotionale Momente zur selben Millisekunde.",
"WHY_TITLE": "Warum KoalaSync?",
"WHY_SUBTITLE": "Entwickelt für zuverlässigen Sync, Datenschutz und einfache Einrichtung.",
"FEATURE_1_TITLE": "Volle Kontrolle, in Echtzeit",
"FEATURE_1_DESC": "Einer pausiert, alle pausieren. Einer spult, alle folgen. Unser Zwei-Phasen-Synchronisationsprotokoll koordiniert die Wiedergabe aller Teilnehmer in Echtzeit.",
"FEATURE_2_TITLE": "Grenzenloses Bingen",
"FEATURE_2_DESC": "Nächste Episode startet für jeden zeitgleich. KoalaSync erkennt den Episodenwechsel und pausiert, bis jeder Teilnehmer das neue Video fertig geladen hat.",
"FEATURE_3_TITLE": "Keine Accounts / Datenschutz",
"FEATURE_3_DESC": "Kein Login. Keine Daten. Einfach Link teilen und schauen. Der Server läuft flüchtig im RAM und löscht deinen Raum komplett nach dem Verlassen.",
"FEATURE_4_TITLE": "Universeller HTML5-Support",
"FEATURE_4_DESC": "Unterstützt YouTube, Twitch, Netflix, Jellyfin, Emby und fast jede beliebige Webseite mit einem HTML5-Video-Tag. Ideal auch für eigene Medienbibliotheken.",
"FEATURE_5_TITLE": "Self-Hostable & Docker-Ready",
"FEATURE_5_DESC": "Unsere Server sind kostenlos und schnell, aber du kannst auch die volle Kontrolle übernehmen, wenn du willst. Starte dein eigenes privates Relay in Sekunden via Docker.",
"FEATURE_6_TITLE": "Direkte Einladungen & 1-Klick Beitritt",
"FEATURE_6_DESC": "Keine lästigen IPs oder Passwörter austauschen. Teile einfach einen Einladungslink mit deinen Freunden, um sie mit einem Klick in den Raum zu holen.",
"COMP_TITLE": "KoalaSync vs. Teleparty",
"COMP_SUBTITLE": "Erfahre, warum Open Source, Werbefreiheit und echter Datenschutz die bessere Wahl für gemeinsames Schauen sind.",
"COMP_COL_FEATURE": "Funktion",
"COMP_FEAT_1_NAME": "Kosten / Paywalls",
"COMP_FEAT_1_DESC": "Premium-Abos, versteckte Kosten oder gesperrte Funktionen.",
"COMP_FEAT_1_KOALA": "100% Kostenlos",
"COMP_FEAT_1_TELE": "Premium-Abo / Paywalls",
"COMP_FEAT_2_NAME": "Lizenz / Quellcode",
"COMP_FEAT_2_DESC": "Ob der Quellcode quelloffen und frei einsehbar ist.",
"COMP_FEAT_2_KOALA": "Open Source (MIT)",
"COMP_FEAT_2_TELE": "Proprietär",
"COMP_FEAT_3_NAME": "Self-Hosting",
"COMP_FEAT_3_DESC": "Möglichkeit, einen eigenen privaten Relay-Server zu betreiben.",
"COMP_FEAT_3_KOALA": "Ja (Docker-bereit)",
"COMP_FEAT_3_TELE": "Nein",
"COMP_FEAT_4_NAME": "Datenschutz & Tracking",
"COMP_FEAT_4_DESC": "Registrierungszwang, Tracking-Cookies und Analysen.",
"COMP_FEAT_4_KOALA": "Keine Speicherung (RAM-only)",
"COMP_FEAT_4_TELE": "Google Analytics & Cookies",
"COMP_FEAT_5_NAME": "Kompatibilität",
"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_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:",
"COMP_FOOTNOTE_4": "Funktioniert auf allen Seiten, die Skript-Injektionen in Standard-HTML5-Videoplayer erlauben. Seiten mit extrem strengen Content Security Policies (CSP), DRM-Kopierschutz oder stark verschachtelten Player-Wrappern (z. B. komplexe Shadow-DOMs) können die automatische Steuerung blockieren.",
"STEPS_TITLE": "Erste Schritte",
"STEP_1_TITLE": "Erweiterung installieren",
"STEP_1_DESC": "Füge KoalaSync aus dem Chrome Web Store, den Firefox Add-ons oder über die Entwickler-ZIP von GitHub zu deinem Browser hinzu.",
"STEP_1_ILLUS_DESC": "Privatsphäre-fokussierte Video-Synchronisation",
"STEP_1_ILLUS_DL_CHROME": "Download für Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Download für Firefox",
"STEP_1_ILLUS_ACTIVE": "Erweiterung aktiv",
"STEP_1_ILLUS_READY": "Bereit zum Mitschauen!",
"STEP_2_TITLE": "Raum erstellen",
"STEP_2_DESC": "Öffne die Erweiterung und klicke auf „+ Neuer Raum“. KoalaSync generiert automatisch eine sichere Raum-ID samt Passwort, tritt dem Raum bei und kopiert den Einladungslink in deine Zwischenablage.",
"STEP_2_ILLUS_ROOM": "Raum",
"STEP_2_ILLUS_SYNC": "Sync",
"STEP_2_ILLUS_SETTINGS": "Optionen",
"STEP_2_ILLUS_CREATE": "+ Neuer Raum",
"STEP_2_ILLUS_MANUAL": "Manuell verbinden / Erweitert",
"STEP_2_ILLUS_COPIED": "Einladungslink kopiert!",
"STEP_3_TITLE": "Teilen & Synchronisieren",
"STEP_3_DESC": "Sende den Einladungslink an deine Freunde. Sobald sie beitreten, wähle deinen Video-Tab aus und genieße die synchronisierte Wiedergabe.",
"STEP_3_ILLUS_IN_SYNC": "SYNCHRON",
"SELF_TITLE": "Für Self-Hoster",
"SELF_SUBTITLE": "Du traust dem Server nicht? Behalte die volle Datenhoheit. Richte deinen eigenen privaten Relay-Server in wenigen Minuten ein.",
"SELF_MASCOT_ALT": "Ein niedlicher Koala sitzt am Laptop und deployt einen Docker-Container für das Self-Hosting",
"SELF_COPY_CODE": "Code kopieren",
"SELF_GITHUB_PACKAGES": "Alle Image-Tags auf GitHub Packages ansehen",
"BOTTOM_TITLE": "Noch nicht überzeugt? Überzeug dich selbst.",
"BOTTOM_SUBTITLE": "KoalaSync ist zu 100 % Open Source, werbefrei und trackingsicher. Überprüfe unseren Code auf GitHub oder installiere direkt die Browser-Erweiterung.",
"BOTTOM_MASCOT_ALT": "Ein niedlicher Koala hält eine GitHub-Projektseite zusammen mit dem GitHub Maskottchen Octocat",
"FAQ_TITLE": "Häufig gestellte Fragen",
"FAQ_SUBTITLE": "Alles, was du über synchrones Videoschauen mit Freunden wissen musst.",
"FAQ_Q1": "Kann ich Netflix, Emby oder Jellyfin synchron mit Freunden schauen?",
"FAQ_A1": "Ja! KoalaSync funktioniert mit Netflix, YouTube, Twitch und jeder Website mit einem HTML5-Videoplayer — einschließlich Emby, Jellyfin und eigenen Medienservern. Installiere die Erweiterung, erstelle einen Raum, teile den Einladungslink und alle schauen perfekt synchron.",
"FAQ_Q2": "Ist KoalaSync eine kostenlose Alternative zu Teleparty?",
"FAQ_A2": "100 % kostenlos und Open Source unter der MIT-Lizenz. Keine Premium-Stufen, keine gesperrten Funktionen, keine Werbung. Anders als Teleparty läuft KoalaSyncs Relay-Server ausschließlich im RAM — ohne Datensammlung und ohne Benutzerkonten.",
"FAQ_Q3": "Funktioniert KoalaSync mit Emby, Jellyfin und eigenen Medienservern?",
"FAQ_A3": "Ja. KoalaSync erkennt jedes HTML5-Videoelement und funktioniert perfekt mit Emby, Jellyfin, Plex und beliebigen eigenen Streaming-Setups. Du kannst den KoalaSync Relay-Server auch via Docker selbst hosten.",
"FAQ_Q4": "Welche Browser und Plattformen unterstützen KoalaSync?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave und alle Chromium-basierten Browser. Direkt aus dem Chrome Web Store oder den Firefox Add-ons installieren — keine manuelle Konfiguration nötig.",
"FAQ_Q5": "Müssen alle meine Freunde die Erweiterung installieren?",
"FAQ_A5": "Ja, jeder Teilnehmer braucht KoalaSync — aber ohne Anmeldung oder Kontoerstellung. Einfach auf den Einladungslink klicken, die Erweiterung installieren (falls nicht vorhanden) und du bist automatisch im Raum.",
"FAQ_Q6": "Sind meine Daten sicher? Was, wenn ich dem offiziellen Server nicht traue?",
"FAQ_A6": "Absolut sicher. Der KoalaSync Relay-Server läuft ausschließlich im RAM — alle Sitzungsdaten werden sofort gelöscht, sobald du den Raum verlässt. Keine Datenbank, keine Protokolle, keine Analysen, keine Benutzerkonten. Wenn du volle Kontrolle möchtest: hoste deinen eigenen privaten Relay-Server via Docker in wenigen Minuten. Der gesamte Quellcode ist Open Source und auf GitHub einsehbar.",
"HOWTO_TITLE": "So synchronisierst du Videos mit Freunden mit KoalaSync",
"HOWTO_DESC": "Schaue Videos perfekt synchron mit Freunden auf YouTube, Netflix, Twitch und mehr. Keine Anmeldung, kein Tracking.",
"HOWTO_STEP_1_NAME": "Browser-Erweiterung installieren",
"HOWTO_STEP_1_TEXT": "Füge KoalaSync aus dem Chrome Web Store oder den Firefox Add-ons zu deinem Browser hinzu. Funktioniert auf jeder Website mit einem Videoelement.",
"HOWTO_STEP_2_NAME": "Raum erstellen",
"HOWTO_STEP_2_TEXT": "Öffne das Erweiterungs-Popup und klicke auf Neuen Raum erstellen. Der Einladungslink wird automatisch in deine Zwischenablage kopiert.",
"HOWTO_STEP_3_NAME": "Teilen und synchronisieren",
"HOWTO_STEP_3_TEXT": "Sende den Einladungslink an deine Freunde. Wähle einen Video-Tab aus und genieße synchronisierte Wiedergabe in Echtzeit.",
"FOOTER_MIT": "Open Source unter der MIT-Lizenz.",
"FOOTER_RAM": "Keine Daten werden auf unseren Servern gespeichert. Reines RAM-basiertes Relay.",
"FOOTER_LEGAL": "Impressum",
"FOOTER_PRIVACY": "Datenschutz",
"MOCK_01": "Aktiver Raum",
"MOCK_02": "Offizieller Server",
"MOCK_03": "Einladungs-Link",
"MOCK_04": "Teilnehmer im Raum",
"MOCK_05": "ICH",
"MOCK_06": "Wiedergabe",
"MOCK_07": "Partner",
"MOCK_08": "Raum verlassen",
"MOCK_09": "Video auswählen",
"MOCK_10": "Fernsteuerung",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Start",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pause",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
"MOCK_14": "Zu anderen springen",
"MOCK_15": "Letzte Aktivität",
"MOCK_16": "Warten auf 1 Partner (KoalaPC)...",
"MOCK_17": "Überspringen & abspielen",
"MOCK_18": "Dein Benutzername",
"MOCK_19": "Noise-Filterung",
"MOCK_20": "Nächste Folge auto-syncen",
"MOCK_21": "Link auto-kopieren",
"MOCK_22": "Browser-Meldungen",
"MOCK_23": "Fehlerbehebung",
"MOCK_24": "Peer ID regenerieren",
"MOCK_25": "Nutze dies bei \"Duplicate Identity\" Fehlern.",
"MOCK_26": "Verbindungsstatus",
"MOCK_27": "Verbunden",
"MOCK_28": "Logs kopieren",
"MOCK_29": "Video-Debug-Info",
"MOCK_30": "Aktivitätsverlauf",
"MOCK_31": "Protokoll (Letzte 50)",
"MOCK_32": "LÖSCHEN"
}
+161
View File
@@ -0,0 +1,161 @@
{
"LANG_CODE": "en",
"HTML_CLASS": "lang-en",
"CANONICAL_PATH": "",
"LANG_TOGGLE_URL": "de/",
"LANG_TOGGLE_TEXT": "DE",
"OG_LOCALE": "en_US",
"META_TITLE": "KoalaSync | Sync Netflix, YouTube & Any Video with Friends",
"META_DESCRIPTION": "Watch Netflix, YouTube, Twitch & any HTML5 video in perfect sync with friends. Free, open-source browser extension for Chrome and Firefox. No sign-up needed.",
"OG_TITLE": "KoalaSync | Sync Netflix, Emby, Jellyfin & Almost Any Video with Friends",
"OG_DESCRIPTION": "Watch Netflix, Emby, Jellyfin, YouTube, Twitch and almost any HTML5 video in perfect sync. Privacy-first, open-source browser extension for Chrome & Firefox.",
"TWITTER_TITLE": "KoalaSync | Sync Netflix, Emby, Jellyfin & Almost Any Video with Friends Browser Extension",
"TWITTER_DESCRIPTION": "Watch Netflix, Emby, Jellyfin, YouTube, Twitch and almost any HTML5 video in perfect sync with friends. Privacy-first, open-source browser extension for Chrome and Firefox.",
"NAV_FEATURES": "Features",
"NAV_HOW_IT_WORKS": "How it works",
"HERO_TITLE": "Watch Together.<br>Sync Perfectly.",
"HERO_SUBTITLE": "Your remote movie night without lags. No registration, no data collection. Just share a link and watch together.",
"HERO_MASCOT_ALT": "A cute koala standing and looking down at the download buttons",
"ADD_TO_CHROME": "Add to Chrome",
"ADD_TO_FIREFOX": "Add to Firefox",
"COMPAT_HEADING": "Works on your favorite platforms",
"COMPAT_MORE": "and many more",
"COMPAT_TOOLTIP": "Works on almost any site with a video element",
"USE_CASES_TITLE": "Perfect for Any Occasion",
"USE_CASES_SUBTITLE": "Whether near or far, KoalaSync brings people together around their favorite videos.",
"USE_CASE_1_ALT": "Two cute koalas sitting together and sharing a bucket of popcorn",
"USE_CASE_1_TITLE": "Movie Night with Friends",
"USE_CASE_1_DESC": "Sync your movies in real time and talk via Discord, Zoom, or your favorite voice call application. It feels just like sitting in the same room.",
"USE_CASE_2_ALT": "A koala professor presenting on a screen to two remote student koalas",
"USE_CASE_2_TITLE": "Remote Learning",
"USE_CASE_2_DESC": "Analyze online tutorials, lectures, or developer training streams together with classmates or colleagues. Pause and discuss complex steps in perfect harmony.",
"USE_CASE_3_ALT": "A cute koala couple watching together remotely; one is sitting at a PC and the other is in bed with a laptop, with flying hearts between them",
"USE_CASE_3_TITLE": "Long-Distance Relationships",
"USE_CASE_3_DESC": "Bridge the distance and enjoy date nights. Experience every plot twist, laugh at the same jokes, and share emotional moments at the exact same millisecond.",
"WHY_TITLE": "Why KoalaSync?",
"WHY_SUBTITLE": "Built for reliable sync, privacy-first design, and easy setup.",
"FEATURE_1_TITLE": "Full Control / Instant Sync",
"FEATURE_1_DESC": "One pauses, everyone pauses. One seeks, everyone follows. Our two-phase sync protocol coordinates playback in real time across all participants.",
"FEATURE_2_TITLE": "Endless Binge-Watching",
"FEATURE_2_DESC": "Autoplay in sync. KoalaSync automatically detects episode transitions and holds playback until all peers have successfully loaded the next video.",
"FEATURE_3_TITLE": "Zero Accounts / Pure Privacy",
"FEATURE_3_DESC": "No registration, no tracking, no persistency footprints. The server runs entirely in ephemeral RAM and purges your room when you leave.",
"FEATURE_4_TITLE": "Universal HTML5 Support",
"FEATURE_4_DESC": "Works on YouTube, Twitch, Netflix, Jellyfin, Emby, and almost any standard webpage containing a HTML5 video element. Also compatible with custom self-hosted setups.",
"FEATURE_5_TITLE": "Self-Hostable & Docker-Ready",
"FEATURE_5_DESC": "Our official servers are free and fast, but you can take full ownership if you want to. Launch your own private relay server in seconds via Docker.",
"FEATURE_6_TITLE": "Instant Invites / 1-Click Join",
"FEATURE_6_DESC": "No IP addresses or passwords to exchange. Share a generated invite link with your friends to let them join your room automatically with a single click.",
"COMP_TITLE": "KoalaSync vs. Teleparty",
"COMP_SUBTITLE": "See why open source, ad-free and privacy-first is the superior way to watch together.",
"COMP_COL_FEATURE": "Feature",
"COMP_FEAT_1_NAME": "Cost / Paywalls",
"COMP_FEAT_1_DESC": "Premium subscriptions, hidden fees or locked features.",
"COMP_FEAT_1_KOALA": "100% Free",
"COMP_FEAT_1_TELE": "Paid Tiers / Premium Locks",
"COMP_FEAT_2_NAME": "License / Code Auditable",
"COMP_FEAT_2_DESC": "Whether the source code is open and freely auditable.",
"COMP_FEAT_2_KOALA": "Open Source (MIT)",
"COMP_FEAT_2_TELE": "Proprietary",
"COMP_FEAT_3_NAME": "Self-Hosting",
"COMP_FEAT_3_DESC": "Ability to deploy your own private relay server.",
"COMP_FEAT_3_KOALA": "Yes (Docker-ready)",
"COMP_FEAT_3_TELE": "No",
"COMP_FEAT_4_NAME": "Privacy & Tracking",
"COMP_FEAT_4_DESC": "Registration requirements, tracking cookies and analytics.",
"COMP_FEAT_4_KOALA": "Zero-Persistence (RAM-only)",
"COMP_FEAT_4_TELE": "Google Analytics & Cookies",
"COMP_FEAT_5_NAME": "Site Compatibility",
"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_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:",
"COMP_FOOTNOTE_4": "Works on websites that allow script injections into standard HTML5 video tags. Websites with highly strict Content Security Policies (CSP), DRM copy protection, or heavily obfuscated player wrappers (like complex shadow DOMs) might restrict automated control or injection.",
"STEPS_TITLE": "Getting Started",
"STEP_1_TITLE": "Install Extension",
"STEP_1_DESC": "Add KoalaSync to your browser from the Chrome Web Store, Firefox Add-ons, or download the latest developer ZIP from GitHub.",
"STEP_1_ILLUS_DESC": "Privacy-first video synchronizer",
"STEP_1_ILLUS_DL_CHROME": "Download for Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Download for Firefox",
"STEP_1_ILLUS_ACTIVE": "Extension Active",
"STEP_1_ILLUS_READY": "Ready to watch together!",
"STEP_2_TITLE": "Create a Room",
"STEP_2_DESC": "Open the extension popup and click '+ Create New Room'. KoalaSync automatically generates a secure Room ID and Password, joins it, and copies the invite link to your clipboard.",
"STEP_2_ILLUS_ROOM": "Room",
"STEP_2_ILLUS_SYNC": "Sync",
"STEP_2_ILLUS_SETTINGS": "Settings",
"STEP_2_ILLUS_CREATE": "+ Create New Room",
"STEP_2_ILLUS_MANUAL": "Manual Connect / Advanced",
"STEP_2_ILLUS_COPIED": "Invite link copied!",
"STEP_3_TITLE": "Share & Sync",
"STEP_3_DESC": "Send the invite link to your friends. Once they join, select your video tab and enjoy synchronized playback.",
"STEP_3_ILLUS_IN_SYNC": "IN SYNC",
"SELF_TITLE": "For Self-Hosters",
"SELF_SUBTITLE": "Don't trust our server? Maintain full data sovereignty. Deploy your own private relay server in minutes.",
"SELF_MASCOT_ALT": "A cute koala sitting at a laptop deploying a Docker container for self-hosting",
"SELF_COPY_CODE": "Copy Code",
"SELF_GITHUB_PACKAGES": "View all image tags on GitHub Packages",
"BOTTOM_TITLE": "Not convinced? See for yourself.",
"BOTTOM_SUBTITLE": "KoalaSync is 100% open source, ad-free and tracking-free. Audit our codebase on GitHub or install the browser extension directly.",
"BOTTOM_MASCOT_ALT": "A cute koala holding a GitHub repository page next to the GitHub mascot Octocat",
"FAQ_TITLE": "Frequently Asked Questions",
"FAQ_SUBTITLE": "Everything you need to know about watching videos together in perfect sync.",
"FAQ_Q1": "Can I watch Netflix, Emby or Jellyfin together in sync with friends?",
"FAQ_A1": "Yes! KoalaSync works on Netflix, YouTube, Twitch and any website with an HTML5 video player — including Emby, Jellyfin and self-hosted media servers. Install the extension, create a room, share the invite link, and everyone watches in perfect sync.",
"FAQ_Q2": "Is KoalaSync a free alternative to Teleparty?",
"FAQ_A2": "100% free and open source under the MIT License. No premium tiers, no locked features, no ads — everything is included. Unlike Teleparty, KoalaSync's relay server is RAM-only with zero data collection or user accounts required.",
"FAQ_Q3": "Does KoalaSync work with Emby, Jellyfin and self-hosted media servers?",
"FAQ_A3": "Yes. KoalaSync detects any standard HTML5 video element, which means it works perfectly with Emby, Jellyfin, Plex, and any custom self-hosted streaming setup. You can also self-host the KoalaSync relay server via Docker for complete data sovereignty.",
"FAQ_Q4": "Which browsers and platforms support KoalaSync?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave, and all Chromium-based browsers. Install it directly from the Chrome Web Store or Firefox Add-ons — no manual configuration needed.",
"FAQ_Q5": "Do all my friends need the extension installed?",
"FAQ_A5": "Yes, each participant needs KoalaSync installed — but there's no sign-up or account creation. Simply click the invitation link, install the extension if you haven't already, and you'll join the room automatically in seconds.",
"FAQ_Q6": "Is my data safe? What if I don't trust the official server?",
"FAQ_A6": "Completely safe. KoalaSync's relay server is RAM-only — all session data is permanently deleted the moment you leave the room. No database, no logging, no analytics, no accounts. And if you prefer full control: you can self-host your own private relay server via Docker in minutes. The entire codebase is open source and auditable on GitHub.",
"HOWTO_TITLE": "How to synchronize videos with friends using KoalaSync",
"HOWTO_DESC": "Watch any video in perfect sync with friends across YouTube, Netflix, Twitch and more. No sign-up, no tracking.",
"HOWTO_STEP_1_NAME": "Install the browser extension",
"HOWTO_STEP_1_TEXT": "Add KoalaSync to your browser from Chrome Web Store or Firefox Add-ons. It works on any website with a video element.",
"HOWTO_STEP_2_NAME": "Create a room",
"HOWTO_STEP_2_TEXT": "Open the extension popup and click Create New Room. The invite link is automatically copied to your clipboard.",
"HOWTO_STEP_3_NAME": "Share and sync",
"HOWTO_STEP_3_TEXT": "Send the invite link to your friends. Pick a video tab and enjoy synchronized playback in real time.",
"FOOTER_MIT": "Open source under the MIT License.",
"FOOTER_RAM": "No data is stored on our servers. Pure RAM-based relay.",
"FOOTER_LEGAL": "Legal Notice",
"FOOTER_PRIVACY": "Privacy Policy",
"MOCK_01": "Active Room",
"MOCK_02": "Official Server",
"MOCK_03": "Invite Link",
"MOCK_04": "Peers in Room",
"MOCK_05": "YOU",
"MOCK_06": "Playing",
"MOCK_07": "Peer",
"MOCK_08": "Leave Room",
"MOCK_09": "Select Video",
"MOCK_10": "Remote Control",
"MOCK_11": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"5 3 19 12 5 21 5 3\"/></svg> Play",
"MOCK_12": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"14\" height=\"14\" aria-hidden=\"true\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"/></svg> Pause",
"MOCK_13": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" width=\"14\" height=\"14\" aria-hidden=\"true\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg> SYNC",
"MOCK_14": "Jump to Others",
"MOCK_15": "Last Activity Status",
"MOCK_16": "Waiting for 1 peer (KoalaPC)...",
"MOCK_17": "Skip & Play anyway",
"MOCK_18": "Your Username",
"MOCK_19": "Hide Clutter Tabs",
"MOCK_20": "Auto-Sync Next Episode",
"MOCK_21": "Auto-Copy Invite Link",
"MOCK_22": "Browser Notifications",
"MOCK_23": "Troubleshooting",
"MOCK_24": "Regenerate Peer ID",
"MOCK_25": "Use this if you see \"Duplicate Identity\" errors.",
"MOCK_26": "Connection Status",
"MOCK_27": "Connected",
"MOCK_28": "Copy Logs",
"MOCK_29": "Video Debug Info",
"MOCK_30": "Full Action History",
"MOCK_31": "Logs (Last 50)",
"MOCK_32": "CLEAR"
}
+161
View File
@@ -0,0 +1,161 @@
{
"LANG_CODE": "es",
"HTML_CLASS": "lang-es",
"CANONICAL_PATH": "es/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "es_ES",
"META_TITLE": "KoalaSync | Sincroniza Netflix, YouTube y cualquier video con amigos",
"META_DESCRIPTION": "Mira Netflix, YouTube, Twitch y cualquier video HTML5 en perfecta sincronización con amigos. Extensión de navegador gratuita y de código abierto. Sin registro.",
"OG_TITLE": "KoalaSync | Sincroniza Netflix, Emby, Jellyfin y casi cualquier video con amigos",
"OG_DESCRIPTION": "Mira Netflix, Emby, Jellyfin, YouTube, Twitch y casi cualquier video HTML5 en perfecta sincronización. Extensión de navegador de código abierto y respetuosa con la privacidad para Chrome y Firefox.",
"TWITTER_TITLE": "KoalaSync | Sincroniza Netflix, Emby, Jellyfin y casi cualquier video con amigos Extensión de navegador",
"TWITTER_DESCRIPTION": "Mira Netflix, Emby, Jellyfin, YouTube, Twitch y casi cualquier video HTML5 en perfecta sincronización con amigos. Extensión de navegador de código abierto y respetuosa con la privacidad para Chrome y Firefox.",
"NAV_FEATURES": "Características",
"NAV_HOW_IT_WORKS": "Cómo funciona",
"HERO_TITLE": "Miren juntos.<br>Sincronización perfecta.",
"HERO_SUBTITLE": "Tu noche de películas a distancia sin retrasos. Sin registro ni recopilación de datos. Solo comparte un enlace y miren juntos.",
"HERO_MASCOT_ALT": "Un lindo koala de pie mirando hacia abajo a los botones de descarga",
"ADD_TO_CHROME": "Añadir a Chrome",
"ADD_TO_FIREFOX": "Añadir a Firefox",
"COMPAT_HEADING": "Funciona en tus plataformas favoritas",
"COMPAT_MORE": "y muchas más",
"COMPAT_TOOLTIP": "Funciona en casi cualquier sitio con un elemento de video",
"USE_CASES_TITLE": "Perfecto para cualquier ocasión",
"USE_CASES_SUBTITLE": "Ya sea cerca o lejos, KoalaSync une a las personas en torno a sus videos favoritos.",
"USE_CASE_1_ALT": "Dos lindos koalas sentados juntos compartiendo un cubo de palomitas de maíz",
"USE_CASE_1_TITLE": "Noche de películas con amigos",
"USE_CASE_1_DESC": "Sincroniza tus películas en tiempo real y habla por Discord, Zoom o tu aplicación de llamada de voz favorita. Es como estar en la misma habitación.",
"USE_CASE_2_ALT": "Un profesor koala presentando en una pantalla a dos estudiantes koala remotos",
"USE_CASE_2_TITLE": "Aprendizaje a distancia",
"USE_CASE_2_DESC": "Analiza tutoriales en línea, conferencias o transmisiones de capacitación de desarrolladores con compañeros o colegas. Pausa y discute pasos complejos en perfecta armonía.",
"USE_CASE_3_ALT": "Una linda pareja de koalas viendo juntos de forma remota; uno está sentado frente a una PC y el otro en la cama con una computadora portátil, con corazones volando entre ellos",
"USE_CASE_3_TITLE": "Relaciones a larga distancia",
"USE_CASE_3_DESC": "Reduce la distancia y disfruta de noches de citas. Experimenta cada giro de la trama, ríete de los mismos chistes y comparte momentos emotivos en el mismo milisegundo exacto.",
"WHY_TITLE": "¿Por qué KoalaSync?",
"WHY_SUBTITLE": "Diseñado para una sincronización confiable, privacidad y una configuración sencilla.",
"FEATURE_1_TITLE": "Control total / Sincronización instantánea",
"FEATURE_1_DESC": "Uno pausa, todos pausan. Uno avanza, todos siguen. Nuestro protocolo de sincronización en dos fases coordina la reproducción en tiempo real para todos los participantes.",
"FEATURE_2_TITLE": "Maratones sin fin",
"FEATURE_2_DESC": "Reproducción automática sincronizada. KoalaSync detecta automáticamente transiciones de episodios y suspende la reproducción hasta que todos los participantes hayan cargado el siguiente video.",
"FEATURE_3_TITLE": "Sin cuentas / Privacidad absoluta",
"FEATURE_3_DESC": "Sin registros, sin seguimiento, sin almacenamiento de datos. El servidor funciona completamente en RAM efímera y elimina tu sala tan pronto como sales.",
"FEATURE_4_TITLE": "Soporte universal HTML5",
"FEATURE_4_DESC": "Funciona en YouTube, Twitch, Netflix, Jellyfin, Emby y casi cualquier página web estándar que contenga un elemento de video HTML5. También es compatible con configuraciones personalizadas.",
"FEATURE_5_TITLE": "Auto-alojable y listo para Docker",
"FEATURE_5_DESC": "Nuestros servidores oficiales son gratuitos y rápidos, pero puedes tomar el control total si lo deseas. Inicia tu propio servidor de retransmisión privado en segundos a través de Docker.",
"FEATURE_6_TITLE": "Invitaciones al instante / Unirse en 1 clic",
"FEATURE_6_DESC": "Sin direcciones IP o contraseñas que intercambiar. Comparte un enlace de invitación generado para permitir que tus amigos se unan automáticamente a tu sala con un solo clic.",
"COMP_TITLE": "KoalaSync vs Teleparty",
"COMP_SUBTITLE": "Descubre por qué una herramienta de código abierto, sin anuncios y respetuosa con la privacidad es la mejor opción para ver juntos.",
"COMP_COL_FEATURE": "Característica",
"COMP_FEAT_1_NAME": "Costo / Funciones de pago",
"COMP_FEAT_1_DESC": "Suscripciones premium, tarifas ocultas o funciones bloqueadas.",
"COMP_FEAT_1_KOALA": "100% Gratis",
"COMP_FEAT_1_TELE": "Suscripciones de pago / Bloqueos Premium",
"COMP_FEAT_2_NAME": "Licence / Code auditable",
"COMP_FEAT_2_DESC": "Si el código fuente es abierto y libremente auditable.",
"COMP_FEAT_2_KOALA": "Código Abierto (MIT)",
"COMP_FEAT_2_TELE": "Propietario",
"COMP_FEAT_3_NAME": "Auto-alojamiento",
"COMP_FEAT_3_DESC": "Capacidad para desplegar tu propio servidor de retransmisión privado.",
"COMP_FEAT_3_KOALA": "Sí (listo para Docker)",
"COMP_FEAT_3_TELE": "No",
"COMP_FEAT_4_NAME": "Privacidad y Seguimiento",
"COMP_FEAT_4_DESC": "Requisitos de registro, cookies de seguimiento y análisis.",
"COMP_FEAT_4_KOALA": "Sin persistencia (solo RAM)",
"COMP_FEAT_4_TELE": "Google Analytics y Cookies",
"COMP_FEAT_5_NAME": "Compatibilidad del sitio",
"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_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:",
"COMP_FOOTNOTE_4": "Funciona en sitios web que permiten inyecciones de scripts en etiquetas de video HTML5 estándar. Los sitios con políticas de seguridad de contenido (CSP) muy estrictas, protección de copia DRM o contenedores de reproductores fuertemente ocultos (como DOMs de sombra complejos) pueden restringir el control automatizado o la inyección.",
"STEPS_TITLE": "Cómo empezar",
"STEP_1_TITLE": "Instalar la extensión",
"STEP_1_DESC": "Añade KoalaSync a tu navegador desde Chrome Web Store, complementos de Firefox o descarga el último archivo ZIP de desarrollador desde GitHub.",
"STEP_1_ILLUS_DESC": "Sincronizador de video respetuoso con la privacidad",
"STEP_1_ILLUS_DL_CHROME": "Descargar para Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Descargar para Firefox",
"STEP_1_ILLUS_ACTIVE": "Extensión activa",
"STEP_1_ILLUS_READY": "¡Listo para ver juntos!",
"STEP_2_TITLE": "Crear una sala",
"STEP_2_DESC": "Abre la ventana de la extensión y haz clic en '+ Crear nueva sala'. KoalaSync genera automáticamente un identificador de sala y una contraseña seguros, se conecta y copia el enlace de invitación a tu portapapeles.",
"STEP_2_ILLUS_ROOM": "Sala",
"STEP_2_ILLUS_SYNC": "Sincro",
"STEP_2_ILLUS_SETTINGS": "Opciones",
"STEP_2_ILLUS_CREATE": "+ Crear nueva sala",
"STEP_2_ILLUS_MANUAL": "Conexión manual / Avanzado",
"STEP_2_ILLUS_COPIED": "¡Enlace de invitación copiado!",
"STEP_3_TITLE": "Compartir y Sincronizar",
"STEP_3_DESC": "Envía el enlace de invitación a tus amigos. Tan pronto como se unan, selecciona tu pestaña de video y disfruta de la reproducción sincronizada.",
"STEP_3_ILLUS_IN_SYNC": "SINCRONIZADO",
"SELF_TITLE": "Para auto-alojadores",
"SELF_SUBTITLE": "¿No confías en nuestro servidor? Mantén la soberanía total de los datos. Despliega tu propio servidor de retransmisión en minutos.",
"SELF_MASCOT_ALT": "Un lindo koala sentado frente a una computadora portátil desplegando un contenedor Docker para auto-alojamiento",
"SELF_COPY_CODE": "Copiar código",
"SELF_GITHUB_PACKAGES": "Ver todas las etiquetas de imágenes en GitHub Packages",
"BOTTOM_TITLE": "¿Aún no te convence? Compruébalo tú mismo.",
"BOTTOM_SUBTITLE": "KoalaSync es 100% de código abierto, sin anuncios y sin seguimiento. Audita nuestra base de código en GitHub o instala la extensión del navegador directamente.",
"BOTTOM_MASCOT_ALT": "Un lindo koala sosteniendo una página de repositorio de GitHub junto a Octocat, la mascota de GitHub",
"FAQ_TITLE": "Preguntas frecuentes",
"FAQ_SUBTITLE": "Todo lo que necesitas saber para ver videos juntos en perfecta sincronía.",
"FAQ_Q1": "¿Puedo ver Netflix, Emby o Jellyfin sincronizado con amigos?",
"FAQ_A1": "¡Sí! KoalaSync funciona con Netflix, YouTube, Twitch y cualquier sitio con reproductor de video HTML5 — incluyendo Emby, Jellyfin y servidores multimedia propios. Instala la extensión, crea una sala, comparte el enlace y todos miran en perfecta sincronía.",
"FAQ_Q2": "¿Es KoalaSync una alternativa gratuita a Teleparty?",
"FAQ_A2": "100% gratuito y de código abierto bajo licencia MIT. Sin niveles premium, sin funciones bloqueadas, sin anuncios. A diferencia de Teleparty, el servidor relay de KoalaSync solo usa RAM — sin recolección de datos ni cuentas de usuario.",
"FAQ_Q3": "¿Funciona KoalaSync con Emby, Jellyfin y servidores multimedia propios?",
"FAQ_A3": "Sí. KoalaSync detecta cualquier elemento de video HTML5, por lo que funciona con Emby, Jellyfin, Plex y cualquier configuración de streaming personalizada. También puedes alojar tu propio servidor relay con Docker.",
"FAQ_Q4": "¿Qué navegadores y plataformas son compatibles con KoalaSync?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave y todos los navegadores basados en Chromium. Instálalo desde Chrome Web Store o Firefox Add-ons.",
"FAQ_Q5": "¿Todos mis amigos necesitan instalar la extensión?",
"FAQ_A5": "Sí, cada participante necesita KoalaSync — pero sin registro ni creación de cuenta. Solo haz clic en el enlace de invitación, instala la extensión si es necesario y te unes automáticamente a la sala.",
"FAQ_Q6": "¿Están seguros mis datos? ¿Y si no confío en el servidor oficial?",
"FAQ_A6": "Totalmente seguro. El servidor relay de KoalaSync usa solo RAM — todos los datos de sesión se eliminan al salir de la sala. Sin base de datos, sin registros, sin análisis, sin cuentas. Si prefieres control total: aloja tu propio servidor relay privado con Docker en minutos. El código fuente es completamente abierto y auditable en GitHub.",
"HOWTO_TITLE": "Cómo sincronizar videos con amigos usando KoalaSync",
"HOWTO_DESC": "Mira videos en perfecta sincronía con amigos en YouTube, Netflix, Twitch y más. Sin registro, sin seguimiento.",
"HOWTO_STEP_1_NAME": "Instalar la extensión",
"HOWTO_STEP_1_TEXT": "Añade KoalaSync a tu navegador desde Chrome Web Store o Firefox Add-ons. Funciona en cualquier sitio con un elemento de video.",
"HOWTO_STEP_2_NAME": "Crear una sala",
"HOWTO_STEP_2_TEXT": "Abre la extensión y haz clic en Crear nueva sala. El enlace de invitación se copia automáticamente al portapapeles.",
"HOWTO_STEP_3_NAME": "Compartir y sincronizar",
"HOWTO_STEP_3_TEXT": "Envía el enlace de invitación a tus amigos. Selecciona una pestaña de video y disfruta de reproducción sincronizada en tiempo real.",
"FOOTER_MIT": "Código abierto bajo la Licencia MIT.",
"FOOTER_RAM": "No se almacenan datos en nuestros servidores. Retransmisión solo en RAM.",
"FOOTER_LEGAL": "Legal Notice",
"FOOTER_PRIVACY": "Privacy Policy",
"MOCK_01": "Sala activa",
"MOCK_02": "Servidor oficial",
"MOCK_03": "Enlace de invitación",
"MOCK_04": "Participantes",
"MOCK_05": "TÚ",
"MOCK_06": "Reproduciendo",
"MOCK_07": "Compañero",
"MOCK_08": "Salir",
"MOCK_09": "Seleccionar video",
"MOCK_10": "Control remoto",
"MOCK_11": "Reproducir",
"MOCK_12": "Pausa",
"MOCK_13": "Saltar a otros",
"MOCK_14": "Última actividad",
"MOCK_15": "Esperando a 1 compañero (KoalaPC)...",
"MOCK_16": "Saltar y reproducir",
"MOCK_17": "Tu nombre",
"MOCK_18": "Ocultar pestañas",
"MOCK_19": "Auto-sync siguiente episodio",
"MOCK_20": "Auto-copiar enlace",
"MOCK_21": "Notificaciones",
"MOCK_22": "Solución de problemas",
"MOCK_23": "Regenerar ID",
"MOCK_24": "Usa esto si ves error de \"Identidad duplicada\".",
"MOCK_25": "Estado de conexión",
"MOCK_26": "Conectado",
"MOCK_27": "Copiar logs",
"MOCK_28": "Info de depuración de video",
"MOCK_29": "Historial completo",
"MOCK_30": "Registros (últimos 50)",
"MOCK_31": "BORRAR",
"MOCK_32": "CLEAR"
}
+161
View File
@@ -0,0 +1,161 @@
{
"LANG_CODE": "fr",
"HTML_CLASS": "lang-fr",
"CANONICAL_PATH": "fr/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "fr_FR",
"META_TITLE": "KoalaSync | Synchronisez Netflix, YouTube et n'importe quelle vidéo avec vos amis",
"META_DESCRIPTION": "Regardez Netflix, YouTube, Twitch et des vidéos HTML5 en synchro avec vos amis. Extension de navigateur gratuite et open-source. Aucune inscription requise.",
"OG_TITLE": "KoalaSync | Synchronisez Netflix, Emby, Jellyfin et presque toutes les vidéos avec vos amis",
"OG_DESCRIPTION": "Regardez Netflix, Emby, Jellyfin, YouTube, Twitch et presque n'importe quelle vidéo HTML5 en parfaite synchronisation. Extension de navigateur open-source et respectueuse de la vie privée pour Chrome et Firefox.",
"TWITTER_TITLE": "KoalaSync | Synchronisez Netflix, Emby, Jellyfin et presque toutes les vidéos avec vos amis Extension de navigateur",
"TWITTER_DESCRIPTION": "Regardez Netflix, Emby, Jellyfin, YouTube, Twitch et presque n'importe quelle vidéo HTML5 en parfaite synchronisation avec vos amis. Extension de navigateur open-source et respectueuse de la vie privée pour Chrome et Firefox.",
"NAV_FEATURES": "Fonctionnalités",
"NAV_HOW_IT_WORKS": "Comment ça marche",
"HERO_TITLE": "Regardez ensemble.<br>Synchronisation parfaite.",
"HERO_SUBTITLE": "Votre soirée cinéma à distance sans décalage. Pas d'inscription, pas de collecte de données. Partagez simplement un lien et regardez ensemble.",
"HERO_MASCOT_ALT": "Un koala mignon debout et regardant vers le bas les boutons de téléchargement",
"ADD_TO_CHROME": "Ajouter à Chrome",
"ADD_TO_FIREFOX": "Ajouter à Firefox",
"COMPAT_HEADING": "Fonctionne sur vos plateformes préférées",
"COMPAT_MORE": "et beaucoup d'autres",
"COMPAT_TOOLTIP": "Fonctionne sur presque tous les sites avec un élément vidéo",
"USE_CASES_TITLE": "Parfait pour toutes les occasions",
"USE_CASES_SUBTITLE": "Que ce soit de près ou de loin, KoalaSync rassemble les gens autour de leurs vidéos préférées.",
"USE_CASE_1_ALT": "Deux koalas mignons assis ensemble et partageant un seau de pop-corn",
"USE_CASE_1_TITLE": "Soirée cinéma entre amis",
"USE_CASE_1_DESC": "Synchronisez vos films en temps réel et parlez via Discord, Zoom ou votre application d'appel vocal préférée. C'est comme si vous étiez dans la même pièce.",
"USE_CASE_2_ALT": "Un professeur koala faisant une présentation sur un écran à deux élèves koalas à distance",
"USE_CASE_2_TITLE": "Apprentissage à distance",
"USE_CASE_2_DESC": "Analysez des tutoriels en ligne, des conférences ou des flux de formation de développeurs avec des camarades ou des collègues. Faites une pause et discutez des étapes complexes en parfaite harmonie.",
"USE_CASE_3_ALT": "Un couple de koalas mignons regardant ensemble à distance ; l'un est assis devant un PC et l'autre est au lit avec un ordinateur portable, avec des cœurs volants entre eux",
"USE_CASE_3_TITLE": "Relations à distance",
"USE_CASE_3_DESC": "Comblez la distance et profitez de soirées en tête-à-tête. Vivez chaque rebondissement, riez des mêmes blagues et partagez des moments émotionnels à la milliseconde près.",
"WHY_TITLE": "Pourquoi KoalaSync ?",
"WHY_SUBTITLE": "Conçu pour une synchronisation fiable, le respect de la vie privée et une configuration facile.",
"FEATURE_1_TITLE": "Contrôle total / Synchro instantanée",
"FEATURE_1_DESC": "Un joueur met en pause, tout le monde met en pause. Un joueur avance, tout le monde suit. Notre protocole de synchronisation en deux phases coordonne la lecture en temps réel pour tous les participants.",
"FEATURE_2_TITLE": "Binge-watching sans fin",
"FEATURE_2_DESC": "Lecture automatique synchronisée. KoalaSync détecte automatiquement les transitions d'épisodes et suspend la lecture jusqu'à ce que tous les participants aient chargé la vidéo suivante.",
"FEATURE_3_TITLE": "Aucun compte / Vie privée garantie",
"FEATURE_3_DESC": "Pas d'inscription, pas de suivi, pas de stockage de données. Le serveur fonctionne entièrement dans une RAM éphémère et supprime votre salon dès votre départ.",
"FEATURE_4_TITLE": "Support HTML5 universel",
"FEATURE_4_DESC": "Fonctionne sur YouTube, Twitch, Netflix, Jellyfin, Emby et presque n'importe quelle page web standard contenant un élément vidéo HTML5. Également compatible avec les installations personnalisées.",
"FEATURE_5_TITLE": "Hébergeable soi-même & prêt pour Docker",
"FEATURE_5_DESC": "Nos serveurs officiels sont gratuits et rapides, mais vous pouvez prendre le contrôle total si vous le souhaitez. Lancez votre propre serveur relais privé en quelques secondes via Docker.",
"FEATURE_6_TITLE": "Invitations instantanées / Rejoindre en 1 clic",
"FEATURE_6_DESC": "Pas d'adresses IP ou de mots de passe à échanger. Partagez un lien d'invitation généré pour permettre à vos amis de rejoindre automatiquement votre salon en un seul clic.",
"COMP_TITLE": "KoalaSync vs Teleparty",
"COMP_SUBTITLE": "Découvrez pourquoi un outil open-source, sans publicité et respectueux de la vie privée est le meilleur choix pour regarder ensemble.",
"COMP_COL_FEATURE": "Fonctionnalité",
"COMP_FEAT_1_NAME": "Coût / Limites payantes",
"COMP_FEAT_1_DESC": "Abonnements premium, frais cachés ou fonctionnalités verrouillées.",
"COMP_FEAT_1_KOALA": "100% Gratuit",
"COMP_FEAT_1_TELE": "Abonnements payants / Verrous Premium",
"COMP_FEAT_2_NAME": "Licence / Code auditable",
"COMP_FEAT_2_DESC": "Si le code source est ouvert et librement auditable.",
"COMP_FEAT_2_KOALA": "Open Source (MIT)",
"COMP_FEAT_2_TELE": "Propriétaire",
"COMP_FEAT_3_NAME": "Auto-hébergement",
"COMP_FEAT_3_DESC": "Possibilité de déployer votre propre serveur relais privé.",
"COMP_FEAT_3_KOALA": "Oui (prêt pour Docker)",
"COMP_FEAT_3_TELE": "Non",
"COMP_FEAT_4_NAME": "Vie privée & Suivi",
"COMP_FEAT_4_DESC": "Obligation d'inscription, cookies de suivi et analyses.",
"COMP_FEAT_4_KOALA": "Zéro persistance (RAM uniquement)",
"COMP_FEAT_4_TELE": "Google Analytics & Cookies",
"COMP_FEAT_5_NAME": "Compatibilité des sites",
"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_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 :",
"COMP_FOOTNOTE_4": "Fonctionne sur les sites web qui autorisent les injections de scripts dans les balises vidéo HTML5 standard. Les sites avec des politiques de sécurité du contenu (CSP) très strictes, une protection contre la copie DRM ou des conteneurs de lecteurs fortement obscurcis (comme des DOM fantômes complexes) peuvent restreindre le contrôle automatisé ou l'injection.",
"STEPS_TITLE": "Pour commencer",
"STEP_1_TITLE": "Installer l'extension",
"STEP_1_DESC": "Ajoutez KoalaSync à votre navigateur depuis le Chrome Web Store, les modules complémentaires de Firefox, ou téléchargez le dernier fichier ZIP développeur depuis GitHub.",
"STEP_1_ILLUS_DESC": "Synchronisateur vidéo respectueux de la vie privée",
"STEP_1_ILLUS_DL_CHROME": "Télécharger pour Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Télécharger pour Firefox",
"STEP_1_ILLUS_ACTIVE": "Extension active",
"STEP_1_ILLUS_READY": "Prêt à regarder ensemble !",
"STEP_2_TITLE": "Créer un salon",
"STEP_2_DESC": "Ouvrez la fenêtre de l'extension et cliquez sur '+ Créer un nouveau salon'. KoalaSync génère automatiquement un identifiant de salon et un mot de passe sécurisés, s'y connecte et copie le lien d'invitation dans votre presse-papiers.",
"STEP_2_ILLUS_ROOM": "Salon",
"STEP_2_ILLUS_SYNC": "Synchro",
"STEP_2_ILLUS_SETTINGS": "Options",
"STEP_2_ILLUS_CREATE": "+ Créer un nouveau salon",
"STEP_2_ILLUS_MANUAL": "Connexion manuelle / Avancé",
"STEP_2_ILLUS_COPIED": "Lien d'invitation copié !",
"STEP_3_TITLE": "Partager & Synchroniser",
"STEP_3_DESC": "Envoyez le lien d'invitation à vos amis. Dès qu'ils rejoignent, sélectionnez votre onglet vidéo et profitez de la lecture synchronisée.",
"STEP_3_ILLUS_IN_SYNC": "SYNCHRONISÉ",
"SELF_TITLE": "Pour l'auto-hébergement",
"SELF_SUBTITLE": "Vous ne faites pas confiance à notre serveur ? Conservez votre souveraineté totale sur les données. Déployez votre propre serveur relais en quelques minutes.",
"SELF_MASCOT_ALT": "Un koala mignon assis devant un ordinateur portable déployant un conteneur Docker pour l'auto-hébergement",
"SELF_COPY_CODE": "Copier le code",
"SELF_GITHUB_PACKAGES": "Voir tous les tags d'images sur GitHub Packages",
"BOTTOM_TITLE": "Pas encore convaincu ? Voyez par vous-même.",
"BOTTOM_SUBTITLE": "KoalaSync est 100% open source, sans publicité et sans suivi. Auditez notre base de code sur GitHub ou installez directement l'extension de navigateur.",
"BOTTOM_MASCOT_ALT": "Un koala mignon tenant une page de dépôt GitHub à côté d'Octocat, la mascotte de GitHub",
"FAQ_TITLE": "Foire aux questions",
"FAQ_SUBTITLE": "Tout ce que vous devez savoir pour regarder des vidéos en parfaite synchronisation.",
"FAQ_Q1": "Puis-je regarder Netflix, Emby ou Jellyfin en synchronisation avec des amis ?",
"FAQ_A1": "Oui ! KoalaSync fonctionne avec Netflix, YouTube, Twitch et tout site avec un lecteur vidéo HTML5 — y compris Emby, Jellyfin et les serveurs multimédia auto-hébergés. Installez l'extension, créez un salon, partagez le lien et regardez en parfaite synchronisation.",
"FAQ_Q2": "KoalaSync est-il une alternative gratuite à Teleparty ?",
"FAQ_A2": "100 % gratuit et open source sous licence MIT. Aucun niveau premium, aucune fonctionnalité verrouillée, aucune publicité. Contrairement à Teleparty, le serveur relay de KoalaSync fonctionne uniquement en RAM — sans collecte de données ni comptes utilisateur.",
"FAQ_Q3": "KoalaSync fonctionne-t-il avec Emby, Jellyfin et les serveurs multimédia auto-hébergés ?",
"FAQ_A3": "Oui. KoalaSync détecte tout élément vidéo HTML5 standard et fonctionne parfaitement avec Emby, Jellyfin, Plex et toute configuration de streaming personnalisée. Vous pouvez également auto-héberger le serveur relay via Docker.",
"FAQ_Q4": "Quels navigateurs et plateformes prennent en charge KoalaSync ?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave et tous les navigateurs basés sur Chromium. Installez-le depuis le Chrome Web Store ou les modules complémentaires Firefox.",
"FAQ_Q5": "Tous mes amis doivent-ils installer l'extension ?",
"FAQ_A5": "Oui, chaque participant a besoin de KoalaSync — mais sans inscription ni création de compte. Cliquez simplement sur le lien d'invitation, installez l'extension si nécessaire et vous rejoignez automatiquement le salon.",
"FAQ_Q6": "Mes données sont-elles en sécurité ? Et si je ne fais pas confiance au serveur officiel ?",
"FAQ_A6": "Totalement sûr. Le serveur relay de KoalaSync fonctionne uniquement en RAM — toutes les données de session sont supprimées dès que vous quittez le salon. Aucune base de données, aucun journal, aucune analyse, aucun compte. Si vous préférez un contrôle total : hébergez votre propre serveur relay privé via Docker en quelques minutes. Le code source est entièrement open source et vérifiable sur GitHub.",
"HOWTO_TITLE": "Comment synchroniser des vidéos avec des amis avec KoalaSync",
"HOWTO_DESC": "Regardez des vidéos en parfaite synchronisation avec vos amis sur YouTube, Netflix, Twitch et plus. Sans inscription, sans pistage.",
"HOWTO_STEP_1_NAME": "Installer l'extension",
"HOWTO_STEP_1_TEXT": "Ajoutez KoalaSync à votre navigateur depuis le Chrome Web Store ou les modules complémentaires Firefox. Fonctionne sur tout site avec un élément vidéo.",
"HOWTO_STEP_2_NAME": "Créer un salon",
"HOWTO_STEP_2_TEXT": "Ouvrez l'extension et cliquez sur Créer un nouveau salon. Le lien d'invitation est automatiquement copié dans votre presse-papiers.",
"HOWTO_STEP_3_NAME": "Partager et synchroniser",
"HOWTO_STEP_3_TEXT": "Envoyez le lien d'invitation à vos amis. Sélectionnez un onglet vidéo et profitez de la lecture synchronisée en temps réel.",
"FOOTER_MIT": "Open source sous licence MIT.",
"FOOTER_RAM": "Aucune donnée n'est stockée sur nos serveurs. Relais uniquement en RAM.",
"FOOTER_LEGAL": "Legal Notice",
"FOOTER_PRIVACY": "Privacy Policy",
"MOCK_01": "Salle active",
"MOCK_02": "Serveur officiel",
"MOCK_03": "Lien d'invitation",
"MOCK_04": "Participants",
"MOCK_05": "VOUS",
"MOCK_06": "Lecture",
"MOCK_07": "Partenaire",
"MOCK_08": "Quitter",
"MOCK_09": "Choisir la vidéo",
"MOCK_10": "Télécommande",
"MOCK_11": "Lire",
"MOCK_12": "Pause",
"MOCK_13": "Sauter vers les autres",
"MOCK_14": "Dernière activité",
"MOCK_15": "En attente de 1 participant (KoalaPC)...",
"MOCK_16": "Passer & lire",
"MOCK_17": "Votre pseudo",
"MOCK_18": "Masquer les onglets inutiles",
"MOCK_19": "Auto-sync épisode suivant",
"MOCK_20": "Copie automatique du lien d'invitation",
"MOCK_21": "Notifications navigateur",
"MOCK_22": "Dépannage",
"MOCK_23": "Régénérer l'ID pair",
"MOCK_24": "Utilisez ceci en cas d'erreur \"Identité dupliquée\".",
"MOCK_25": "État de la connexion",
"MOCK_26": "Connecté",
"MOCK_27": "Copier les logs",
"MOCK_28": "Infos de débogage vidéo",
"MOCK_29": "Historique complet d'actions",
"MOCK_30": "Logs (50 derniers)",
"MOCK_31": "EFFACER",
"MOCK_32": "CLEAR"
}
+161
View File
@@ -0,0 +1,161 @@
{
"LANG_CODE": "pt-BR",
"HTML_CLASS": "lang-pt-br",
"CANONICAL_PATH": "pt-BR/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "pt_BR",
"META_TITLE": "KoalaSync | Sincronize Netflix, YouTube e qualquer vídeo com amigos",
"META_DESCRIPTION": "Assista Netflix, YouTube, Twitch e qualquer vídeo HTML5 em perfeita sincronia com amigos. Extensão de navegador gratuita e de código aberto. Sem registro.",
"OG_TITLE": "KoalaSync | Sincronize Netflix, Emby, Jellyfin e quase qualquer vídeo com amigos",
"OG_DESCRIPTION": "Assista à Netflix, Emby, Jellyfin, YouTube, Twitch e quase qualquer vídeo HTML5 em perfeita sincronia. Extensão de navegador de código aberto e focada em privacidade para Chrome e Firefox.",
"TWITTER_TITLE": "KoalaSync | Sincronize Netflix, Emby, Jellyfin e quase qualquer vídeo com amigos Extensão de navegador",
"TWITTER_DESCRIPTION": "Assista à Netflix, Emby, Jellyfin, YouTube, Twitch e quase qualquer vídeo HTML5 em perfeita sincronia com amigos. Extensão de navegador de código aberto e focada em privacidade para Chrome e Firefox.",
"NAV_FEATURES": "Recursos",
"NAV_HOW_IT_WORKS": "Como funciona",
"HERO_TITLE": "Assistam juntos.<br>Sincronia perfeita.",
"HERO_SUBTITLE": "Sua noite de cinema à distância sem atrasos. Sem registro, sem coleta de dados. Apenas compartilhe o link e assistam juntos.",
"HERO_MASCOT_ALT": "Um lindo coala em pé olhando para baixo em direção aos botões de download",
"ADD_TO_CHROME": "Adicionar ao Chrome",
"ADD_TO_FIREFOX": "Adicionar ao Firefox",
"COMPAT_HEADING": "Funciona nas suas plataformas favoritas",
"COMPAT_MORE": "e muitas mais",
"COMPAT_TOOLTIP": "Funciona em quase qualquer site com um elemento de vídeo",
"USE_CASES_TITLE": "Perfeito para qualquer ocasião",
"USE_CASES_SUBTITLE": "Seja perto ou longe, o KoalaSync une as pessoas em torno de seus vídeos favoritos.",
"USE_CASE_1_ALT": "Dois coalas fofos sentados juntos e compartilhando um balde de pipoca",
"USE_CASE_1_TITLE": "Noite de cinema com amigos",
"USE_CASE_1_DESC": "Sincronize seus filmes em tempo real e converse pelo Discord, Zoom ou seu aplicativo de chamada de voz favorito. É como se estivessem na mesma sala.",
"USE_CASE_2_ALT": "Um professor coala fazendo uma apresentação em uma tela para dois alunos coala remotos",
"USE_CASE_2_TITLE": "Aprendizado remoto",
"USE_CASE_2_DESC": "Analise tutoriais online, palestras ou transmissões de treinamento de desenvolvedores junto com colegas de classe ou de trabalho. Pause e discuta etapas complexas em perfeita harmonia.",
"USE_CASE_3_ALT": "Um casal de coalas fofos assistindo juntos remotamente; um está no PC e o outro deitado com um laptop, com corações voando entre eles",
"USE_CASE_3_TITLE": "Relacionamentos à distância",
"USE_CASE_3_DESC": "Aproxime a distância e aproveite encontros virtuais. Experimente cada reviravolta, ria das mesmas piadas e compartilhe momentos emocionante no mesmo milissegundo.",
"WHY_TITLE": "Por que o KoalaSync?",
"WHY_SUBTITLE": "Desenvolvido para uma sincronização confiável, privacidade em primeiro lugar e configuração simples.",
"FEATURE_1_TITLE": "Controle total / Sincronia instantânea",
"FEATURE_1_DESC": "Um pausa, todos pausam. Um avança, todos seguem. Nosso protocolo de sincronização em duas fases coordena a reprodução em tempo real entre todos os participantes.",
"FEATURE_2_TITLE": "Maratones sem fim",
"FEATURE_2_DESC": "Reprodução automática sincronizada. O KoalaSync detecta automaticamente transições de episódios e pausa a reprodução até que todos os participantes tenham carregado o próximo vídeo.",
"FEATURE_3_TITLE": "Sem contas / Privacidade absoluta",
"FEATURE_3_DESC": "Sem registro, sem rastreamento, sem armazenamento de dados. O servidor roda inteiramente em memória RAM efêmera e limpa sua sala assim que você sai.",
"FEATURE_4_TITLE": "Suporte universal a HTML5",
"FEATURE_4_DESC": "Funciona no YouTube, Twitch, Netflix, Jellyfin, Emby e em quase qualquer página web padrão com um elemento de vídeo HTML5. Também é compatível com servidores próprios.",
"FEATURE_5_TITLE": "Auto-hospedável e pronto para Docker",
"FEATURE_5_DESC": "Nossos servidores oficiais são rápidos e gratuitos, mas você pode ter controle total se desejar. Inicie seu próprio servidor de retransmissão privado em segundos via Docker.",
"FEATURE_6_TITLE": "Convites instantâneos / Entrada em 1 clique",
"FEATURE_6_DESC": "Sem troca de endereços IP ou senhas. Compartilhe um link de convite gerado para que seus amigos entrem automaticamente na sua sala com um único clique.",
"COMP_TITLE": "KoalaSync vs Teleparty",
"COMP_SUBTITLE": "Veja por que o código aberto, sem anúncios e com privacidade em primeiro lugar é a melhor escolha para assistirem juntos.",
"COMP_COL_FEATURE": "Recurso",
"COMP_FEAT_1_NAME": "Custo / Bloqueios de recursos",
"COMP_FEAT_1_DESC": "Assinaturas premium, taxas ocultas ou recursos bloqueados.",
"COMP_FEAT_1_KOALA": "100% Gratuito",
"COMP_FEAT_1_TELE": "Assinaturas pagas / Bloqueios Premium",
"COMP_FEAT_2_NAME": "Licença / Código auditável",
"COMP_FEAT_2_DESC": "Se o código-fonte é aberto e de livre auditoria.",
"COMP_FEAT_2_KOALA": "Código Aberto (MIT)",
"COMP_FEAT_2_TELE": "Proprietário",
"COMP_FEAT_3_NAME": "Auto-hospedagem",
"COMP_FEAT_3_DESC": "Capacidade de implantar seu próprio servidor de retransmissão privado.",
"COMP_FEAT_3_KOALA": "Sim (pronto para Docker)",
"COMP_FEAT_3_TELE": "Não",
"COMP_FEAT_4_NAME": "Privacidade e Rastreamento",
"COMP_FEAT_4_DESC": "Exigências de registro, cookies de rastreamento e análises.",
"COMP_FEAT_4_KOALA": "Sem persistência (apenas RAM)",
"COMP_FEAT_4_TELE": "Google Analytics e Cookies",
"COMP_FEAT_5_NAME": "Compatibilidade de sites",
"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_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:",
"COMP_FOOTNOTE_4": "Funciona em sites que permitem injeções de scripts em tags de vídeo HTML5 padrão. Sites com políticas de segurança de conteúdo (CSP) muito rígidas, proteção contra cópia DRM ou contêineres de player fortemente ocultos (como DOMs de sombra complexos) podem limitar o controle automatizado ou a injeção.",
"STEPS_TITLE": "Como começar",
"STEP_1_TITLE": "Instalar a extensão",
"STEP_1_DESC": "Adicione o KoalaSync ao seu navegador a partir da Chrome Web Store, Firefox Add-ons ou baixe o arquivo ZIP de desenvolvedor mais recente do GitHub.",
"STEP_1_ILLUS_DESC": "Sincronizador de vídeo focado em privacidade",
"STEP_1_ILLUS_DL_CHROME": "Baixar para o Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Baixar para o Firefox",
"STEP_1_ILLUS_ACTIVE": "Extensão ativa",
"STEP_1_ILLUS_READY": "Prontos para assistir juntos!",
"STEP_2_TITLE": "Criar uma sala",
"STEP_2_DESC": "Abra a janela da extensão e clique em '+ Criar nova sala'. O KoalaSync gera automaticamente um ID de sala e senha seguros, entra nela e copia o link de convite para a sua área de transferência.",
"STEP_2_ILLUS_ROOM": "Sala",
"STEP_2_ILLUS_SYNC": "Sincro",
"STEP_2_ILLUS_SETTINGS": "Opções",
"STEP_2_ILLUS_CREATE": "+ Criar nova sala",
"STEP_2_ILLUS_MANUAL": "Conexão manual / Avançado",
"STEP_2_ILLUS_COPIED": "Link de convite copiado!",
"STEP_3_TITLE": "Compartilhar e Sincronizar",
"STEP_3_DESC": "Envie o link de convite para seus amigos. Assim que eles entrarem, selecione a aba do seu vídeo e aproveite a reprodução sincronizada.",
"STEP_3_ILLUS_IN_SYNC": "SINCRONIZADO",
"SELF_TITLE": "Para quem hospeda próprio",
"SELF_SUBTITLE": "Não confia no nosso servidor? Mantenha a soberania total dos seus dados. Implante seu próprio servidor de retransmissão em minutos.",
"SELF_MASCOT_ALT": "Um coala fofo sentado à frente de um laptop implantando um contêiner Docker para hospedagem própria",
"SELF_COPY_CODE": "Copiar código",
"SELF_GITHUB_PACKAGES": "Ver todas as tags de imagens no GitHub Packages",
"BOTTOM_TITLE": "Ainda não se convenceu? Veja por si mesmo.",
"BOTTOM_SUBTITLE": "O KoalaSync é 100% de código aberto, livre de anúncios e sem rastreamento. Audite nosso código no GitHub ou instale a extensão do navegador diretamente.",
"BOTTOM_MASCOT_ALT": "Um coala fofo segurando uma página de repositório do GitHub ao lado de Octocat, a mascotte do GitHub",
"FAQ_TITLE": "Perguntas frequentes",
"FAQ_SUBTITLE": "Tudo o que você precisa saber para assistir vídeos juntos em perfeita sincronia.",
"FAQ_Q1": "Posso assistir Netflix, Emby ou Jellyfin sincronizado com amigos?",
"FAQ_A1": "Sim! O KoalaSync funciona com Netflix, YouTube, Twitch e qualquer site com player de vídeo HTML5 — incluindo Emby, Jellyfin e servidores de mídia próprios. Instale a extensão, crie uma sala, compartilhe o link e todos assistem em sincronia perfeita.",
"FAQ_Q2": "O KoalaSync é uma alternativa gratuita ao Teleparty?",
"FAQ_A2": "100% gratuito e open source sob a licença MIT. Sem níveis premium, sem recursos bloqueados, sem anúncios. Diferente do Teleparty, o servidor relay do KoalaSync opera apenas em RAM — sem coleta de dados ou contas de usuário.",
"FAQ_Q3": "O KoalaSync funciona com Emby, Jellyfin e servidores de mídia próprios?",
"FAQ_A3": "Sim. O KoalaSync detecta qualquer elemento de vídeo HTML5, funcionando perfeitamente com Emby, Jellyfin, Plex e qualquer configuração de streaming personalizada. Você também pode hospedar seu próprio servidor relay via Docker.",
"FAQ_Q4": "Quais navegadores e plataformas suportam o KoalaSync?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave e todos os navegadores baseados em Chromium. Instale diretamente da Chrome Web Store ou das extensões do Firefox.",
"FAQ_Q5": "Todos os meus amigos precisam instalar a extensão?",
"FAQ_A5": "Sim, cada participante precisa do KoalaSync — mas sem cadastro ou criação de conta. Basta clicar no link de convite, instalar a extensão se necessário e você entra na sala automaticamente.",
"FAQ_Q6": "Meus dados estão seguros? E se eu não confiar no servidor oficial?",
"FAQ_A6": "Totalmente seguro. O servidor relay do KoalaSync opera apenas em RAM — todos os dados da sessão são excluídos ao sair da sala. Sem banco de dados, sem registros, sem análises, sem contas. Se preferir controle total: hospede seu próprio servidor relay privado via Docker em minutos. O código fonte é totalmente aberto e auditável no GitHub.",
"HOWTO_TITLE": "Como sincronizar vídeos com amigos usando o KoalaSync",
"HOWTO_DESC": "Assista vídeos em perfeita sincronia com amigos no YouTube, Netflix, Twitch e mais. Sem cadastro, sem rastreamento.",
"HOWTO_STEP_1_NAME": "Instalar a extensão",
"HOWTO_STEP_1_TEXT": "Adicione o KoalaSync ao seu navegador pela Chrome Web Store ou extensões do Firefox. Funciona em qualquer site com elemento de vídeo.",
"HOWTO_STEP_2_NAME": "Criar uma sala",
"HOWTO_STEP_2_TEXT": "Abra a extensão e clique em Criar nova sala. O link de convite é copiado automaticamente para sua área de transferência.",
"HOWTO_STEP_3_NAME": "Compartilhar e sincronizar",
"HOWTO_STEP_3_TEXT": "Envie o link de convite para seus amigos. Selecione uma aba de vídeo e aproveite a reprodução sincronizada em tempo real.",
"FOOTER_MIT": "Código aberto sob a Licença MIT.",
"FOOTER_RAM": "Nenhum dado é armazenado em nossos servidores. Retransmissão apenas em RAM.",
"FOOTER_LEGAL": "Legal Notice",
"FOOTER_PRIVACY": "Privacy Policy",
"MOCK_01": "Sala ativa",
"MOCK_02": "Servidor oficial",
"MOCK_03": "Link de convite",
"MOCK_04": "Participantes",
"MOCK_05": "VOCÊ",
"MOCK_06": "Reproduzindo",
"MOCK_07": "Parceiro",
"MOCK_08": "Sair da sala",
"MOCK_09": "Selecionar vídeo",
"MOCK_10": "Controle remoto",
"MOCK_11": "Reproduzir",
"MOCK_12": "Pausar",
"MOCK_13": "Pular para outros",
"MOCK_14": "Última atividade",
"MOCK_15": "Aguardando 1 parceiro (KoalaPC)...",
"MOCK_16": "Pular e reproduzir",
"MOCK_17": "Seu nome",
"MOCK_18": "Ocultar abas confusas",
"MOCK_19": "Auto-sync próximo episódio",
"MOCK_20": "Auto-copiar convite",
"MOCK_21": "Notificações",
"MOCK_22": "Solução de problemas",
"MOCK_23": "Regenerar Peer ID",
"MOCK_24": "Use se aparecer erro \"Identidade duplicada\".",
"MOCK_25": "Status da conexão",
"MOCK_26": "Conectado",
"MOCK_27": "Copiar logs",
"MOCK_28": "Info de depuração de vídeo",
"MOCK_29": "Histórico completo",
"MOCK_30": "Logs (últimos 50)",
"MOCK_31": "LIMPAR",
"MOCK_32": "CLEAR"
}
+161
View File
@@ -0,0 +1,161 @@
{
"LANG_CODE": "ru",
"HTML_CLASS": "lang-ru",
"CANONICAL_PATH": "ru/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN",
"OG_LOCALE": "ru_RU",
"META_TITLE": "KoalaSync | Синхронизация Netflix, YouTube и любого видео с друзьями",
"META_DESCRIPTION": "Смотрите Netflix, YouTube, Twitch и любые HTML5-видео в синхронизации с друзьями. Бесплатное расширение браузера с открытым кодом. Регистрация не требуется.",
"OG_TITLE": "KoalaSync | Синхронизация Netflix, Emby, Jellyfin и почти любого видео с друзьями",
"OG_DESCRIPTION": "Смотрите Netflix, Emby, Jellyfin, YouTube, Twitch и почти любое HTML5-видео в идеальной синхронизации. Открытое и ориентированное на конфиденциальность расширение для Chrome и Firefox.",
"TWITTER_TITLE": "KoalaSync | Синхронизация Netflix, Emby, Jellyfin и почти любого видео с друзьями – Расширение",
"TWITTER_DESCRIPTION": "Смотрите Netflix, Emby, Jellyfin, YouTube, Twitch и почти любое HTML5-видео в идеальной синхронизации с друзьями. Открытое и конфиденциальное расширение для Chrome и Firefox.",
"NAV_FEATURES": "Функции",
"NAV_HOW_IT_WORKS": "Как это работает",
"HERO_TITLE": "Смотрите вместе.<br>Синхронно на 100%.",
"HERO_SUBTITLE": "Ваш киновечер на расстоянии без задержек. Без регистрации и сбора данных. Просто поделитесь ссылкой и смотрите вместе.",
"HERO_MASCOT_ALT": "Милый коала стоит и смотрит вниз на кнопки загрузки",
"ADD_TO_CHROME": "Установить в Chrome",
"ADD_TO_FIREFOX": "Установить в Firefox",
"COMPAT_HEADING": "Работает на ваших любимых платформах",
"COMPAT_MORE": "и многих других",
"COMPAT_TOOLTIP": "Работает почти на любом сайте с видеоэлементом",
"USE_CASES_TITLE": "Идеально для любого случая",
"USE_CASES_SUBTITLE": "Близко или далеко — KoalaSync объединяет людей за просмотром любимых видео.",
"USE_CASE_1_ALT": "Два милых коалы сидят вместе и делят ведро попкорна",
"USE_CASE_1_TITLE": "Киноночь с друзьями",
"USE_CASE_1_DESC": "Синхронизируйте фильмы в реальном времени и общайтесь в Discord, Zoom или вашей любимой программе. Это ощущается так, будто вы сидите в одной комнате.",
"USE_CASE_2_ALT": "Профессор коала проводит презентацию на экране для двух удаленных студентов коал",
"USE_CASE_2_TITLE": "Удаленное обучение",
"USE_CASE_2_DESC": "Разбирайте обучающие видео, лекции или стримы для разработчиков вместе с однокурсниками или коллегами. Ставьте на паузу и обсуждайте сложные шаги в идеальном согласии.",
"USE_CASE_3_ALT": "Милая пара коал смотрит видео удаленно; один сидит за ПК, другая в постели с ноутбуком, между ними летают сердечки",
"USE_CASE_3_TITLE": "Отношения на расстоянии",
"USE_CASE_3_DESC": "Сократите расстояние и наслаждайтесь свиданиями. Переживайте каждый поворот сюжета, смейтесь над шутками и делитесь эмоциями в одну и ту же миллисекунду.",
"WHY_TITLE": "Почему KoalaSync?",
"WHY_SUBTITLE": "Создан для надежной синхронизации, конфиденциальности и простой настройки.",
"FEATURE_1_TITLE": "Полный контроль и мгновенный синхро",
"FEATURE_1_DESC": "Один ставит на паузу — пауза у всех. Один перематывает — все перематывают. Наш двухфазный протокол координирует воспроизведение в реальном времени у всех участников.",
"FEATURE_2_TITLE": "Бесконечные марафоны",
"FEATURE_2_DESC": "Синхронное автовоспроизведение. KoalaSync автоматически определяет переход на следующую серию и ждет, пока она загрузится у всех участников.",
"FEATURE_3_TITLE": "Без аккаунтов и хранения данных",
"FEATURE_3_DESC": "Без регистрации, без отслеживания, без следов на диске. Сервер работает исключительно во временной RAM и полностью стирает вашу комнату при выходе.",
"FEATURE_4_TITLE": "Универсальная поддержка HTML5",
"FEATURE_4_DESC": "Работает на YouTube, Twitch, Netflix, Jellyfin, Emby и почти на любой веб-странице со стандартным HTML5-видеотегом. Подходит для локальных медиатек.",
"FEATURE_5_TITLE": "Self-Hosted и готов к Docker",
"FEATURE_5_DESC": "Наши официальные серверы бесплатны и быстры, но вы можете получить полный контроль. Запустите свой собственный приватный ретранслятор в Docker за секунды.",
"FEATURE_6_TITLE": "Быстрые приглашения в 1 клик",
"FEATURE_6_DESC": "Не нужно обмениваться IP-адресами или паролями. Отправьте сгенерированную ссылку друзьям, чтобы они автоматически вошли в комнату в один клик.",
"COMP_TITLE": "KoalaSync против Teleparty",
"COMP_SUBTITLE": "Узнайте, почему открытый исходный код, отсутствие рекламы и конфиденциальность делают нас лучшим выбором для совместного просмотра.",
"COMP_COL_FEATURE": "Функция",
"COMP_FEAT_1_NAME": "Стоимость и ограничения",
"COMP_FEAT_1_DESC": "Платные подписки, скрытые платежи или заблокированные функции.",
"COMP_FEAT_1_KOALA": "100% Бесплатно",
"COMP_FEAT_1_TELE": "Платные тарифы и ограничения",
"COMP_FEAT_2_NAME": "Лицензия и аудит кода",
"COMP_FEAT_2_DESC": "Открыт ли исходный код для свободного изучения и аудита безопасности.",
"COMP_FEAT_2_KOALA": "Open Source (MIT)",
"COMP_FEAT_2_TELE": "Проприетарный",
"COMP_FEAT_3_NAME": "Self-Hosting",
"COMP_FEAT_3_DESC": "Возможность развернуть свой собственный приватный сервер.",
"COMP_FEAT_3_KOALA": "Да (готов к Docker)",
"COMP_FEAT_3_TELE": "Нет",
"COMP_FEAT_4_NAME": "Конфиденциальность и трекеры",
"COMP_FEAT_4_DESC": "Обязательная регистрация, отслеживающие файлы cookie и аналитика.",
"COMP_FEAT_4_KOALA": "Без сохранения (только RAM)",
"COMP_FEAT_4_TELE": "Google Analytics и Cookies",
"COMP_FEAT_5_NAME": "Совместимость с сайтами",
"COMP_FEAT_5_DESC": "Поддерживаемые веб-ресурсы и совместимость плееров.",
"COMP_FEAT_5_KOALA": "Почти любое HTML5 видео",
"COMP_FEAT_5_TELE": "Только поддерживаемые сайты",
"COMP_FOOTNOTE_1": "Состояние сравнения: май 2026.",
"COMP_FOOTNOTE_2": "Официальные цены Teleparty Premium и поддерживаемые сети:",
"COMP_FOOTNOTE_3": "Политика конфиденциальности Teleparty и сбор трекеров:",
"COMP_FOOTNOTE_4": "Работает на сайтах, разрешающих инъекцию скриптов в стандартные видеотеги HTML5. Сайты с очень строгой политикой безопасности контента (CSP), защитой от копирования DRM или сильно обфусцированными оболочками плееров (например, сложные теневые DOM) могут блокировать автоматическое управление.",
"STEPS_TITLE": "С чего начать",
"STEP_1_TITLE": "Установить расширение",
"STEP_1_DESC": "Добавьте KoalaSync в браузер из Chrome Web Store, Firefox Add-ons or скачайте последний ZIP-архив разработчика с GitHub.",
"STEP_1_ILLUS_DESC": "Ориентированный на конфиденциальность синхронизатор видео",
"STEP_1_ILLUS_DL_CHROME": "Скачать для Chrome",
"STEP_1_ILLUS_DL_FIREFOX": "Скачать для Firefox",
"STEP_1_ILLUS_ACTIVE": "Расширение активно",
"STEP_1_ILLUS_READY": "Готовы смотреть вместе!",
"STEP_2_TITLE": "Создать комнату",
"STEP_2_DESC": "Откройте меню расширения и нажмите «+ Создать новую комнату». KoalaSync автоматически сгенерирует ID и пароль, войдет в нее и скопирует ссылку-приглашение.",
"STEP_2_ILLUS_ROOM": "Комната",
"STEP_2_ILLUS_SYNC": "Синхро",
"STEP_2_ILLUS_SETTINGS": "Опции",
"STEP_2_ILLUS_CREATE": "+ Создать комнату",
"STEP_2_ILLUS_MANUAL": "Ручное подключение / Дополнительно",
"STEP_2_ILLUS_COPIED": "Ссылка скопирована!",
"STEP_3_TITLE": "Поделиться и смотреть",
"STEP_3_DESC": "Отправьте ссылку друзьям. Как только они присоединятся, выберите вкладку с видео и наслаждайтесь синхронным просмотром.",
"STEP_3_ILLUS_IN_SYNC": "В СИНХРОНЕ",
"SELF_TITLE": "Для селф-хостеров",
"SELF_SUBTITLE": "Не доверяете нашему серверу? Сохраняйте полный контроль над данными. Разверните собственный приватный сервер за пару минут.",
"SELF_MASCOT_ALT": "Милый коала сидит за ноутбуком и разворачивает контейнер Docker для селф-хостинга",
"SELF_COPY_CODE": "Копировать код",
"SELF_GITHUB_PACKAGES": "Посмотреть все теги образов на GitHub Packages",
"BOTTOM_TITLE": "Все еще сомневаетесь? Убедитесь сами.",
"BOTTOM_SUBTITLE": "KoalaSync на 100% бесплатен, открыт и безопасен. Изучите наш код на GitHub или установите расширение в браузер напрямую.",
"BOTTOM_MASCOT_ALT": "Милый коала держит страницу репозитория GitHub рядом с маскотом GitHub Octocat",
"FAQ_TITLE": "Часто задаваемые вопросы",
"FAQ_SUBTITLE": "Всё, что нужно знать о синхронном просмотре видео с друзьями.",
"FAQ_Q1": "Могу ли я смотреть Netflix, Emby или Jellyfin синхронно с друзьями?",
"FAQ_A1": "Да! KoalaSync работает с Netflix, YouTube, Twitch и любым сайтом с HTML5-видеоплеером — включая Emby, Jellyfin и собственные медиасерверы. Установите расширение, создайте комнату, поделитесь ссылкой и смотрите в идеальной синхронизации.",
"FAQ_Q2": "KoalaSync — бесплатная альтернатива Teleparty?",
"FAQ_A2": "100% бесплатно и с открытым исходным кодом под лицензией MIT. Без премиум-уровней, без заблокированных функций, без рекламы. В отличие от Teleparty, сервер KoalaSync работает только в ОЗУ — без сбора данных и учётных записей.",
"FAQ_Q3": "Работает ли KoalaSync с Emby, Jellyfin и собственными медиасерверами?",
"FAQ_A3": "Да. KoalaSync обнаруживает любой HTML5-видеоэлемент и отлично работает с Emby, Jellyfin, Plex и любыми собственными стриминговыми решениями. Вы также можете развернуть свой relay-сервер через Docker.",
"FAQ_Q4": "Какие браузеры и платформы поддерживают KoalaSync?",
"FAQ_A4": "Google Chrome, Mozilla Firefox, Microsoft Edge, Opera, Brave и все браузеры на базе Chromium. Установите из Chrome Web Store или Firefox Add-ons.",
"FAQ_Q5": "Всем ли моим друзьям нужно устанавливать расширение?",
"FAQ_A5": "Да, каждому участнику нужен KoalaSync — но без регистрации и создания учётной записи. Просто перейдите по ссылке-приглашению, установите расширение (если ещё нет) и вы автоматически в комнате.",
"FAQ_Q6": "Мои данные в безопасности? Что, если я не доверяю официальному серверу?",
"FAQ_A6": "Полностью безопасно. Relay-сервер KoalaSync работает только в ОЗУ — все данные сессии удаляются при выходе из комнаты. Никаких баз данных, логов, аналитики и учётных записей. Если хотите полный контроль: разверните свой приватный relay-сервер через Docker за пару минут. Исходный код полностью открыт и доступен для аудита на GitHub.",
"HOWTO_TITLE": "Как синхронизировать видео с друзьями с помощью KoalaSync",
"HOWTO_DESC": "Смотрите видео в идеальной синхронизации с друзьями на YouTube, Netflix, Twitch и других платформах. Без регистрации, без отслеживания.",
"HOWTO_STEP_1_NAME": "Установите расширение",
"HOWTO_STEP_1_TEXT": "Добавьте KoalaSync в браузер из Chrome Web Store или Firefox Add-ons. Работает на любом сайте с видеоэлементом.",
"HOWTO_STEP_2_NAME": "Создайте комнату",
"HOWTO_STEP_2_TEXT": "Откройте расширение и нажмите Создать новую комнату. Ссылка-приглашение автоматически копируется в буфер обмена.",
"HOWTO_STEP_3_NAME": "Поделитесь и синхронизируйте",
"HOWTO_STEP_3_TEXT": "Отправьте ссылку-приглашение друзьям. Выберите вкладку с видео и наслаждайтесь синхронным воспроизведением в реальном времени.",
"FOOTER_MIT": "Открытый исходный код под лицензией MIT.",
"FOOTER_RAM": "Никакие данные не сохраняются на наших серверах. Только RAM-трансляция.",
"FOOTER_LEGAL": "Legal Notice",
"FOOTER_PRIVACY": "Privacy Policy",
"MOCK_01": "Активная комната",
"MOCK_02": "Официальный сервер",
"MOCK_03": "Ссылка-приглашение",
"MOCK_04": "Участники",
"MOCK_05": "ВЫ",
"MOCK_06": "Воспроизведение",
"MOCK_07": "Собеседник",
"MOCK_08": "Покинуть комнату",
"MOCK_09": "Выбрать видео",
"MOCK_10": "Пульт управления",
"MOCK_11": "Воспроизвести",
"MOCK_12": "Пауза",
"MOCK_13": "Перейти к другим",
"MOCK_14": "Последняя активность",
"MOCK_15": "Ожидание 1 участника (KoalaPC)...",
"MOCK_16": "Пропустить и играть",
"MOCK_17": "Ваше имя",
"MOCK_18": "Скрыть лишние вкладки",
"MOCK_19": "Автосинхронизация серий",
"MOCK_20": "Автокопирование ссылки",
"MOCK_21": "Уведомления браузера",
"MOCK_22": "Устранение неполадок",
"MOCK_23": "Обновить Peer ID",
"MOCK_24": "Используйте при ошибке \"Дублирование идентификатора\".",
"MOCK_25": "Статус соединения",
"MOCK_26": "Подключено",
"MOCK_27": "Копировать логи",
"MOCK_28": "Отладочная информация",
"MOCK_29": "Полная история действий",
"MOCK_30": "Логи (последние 50)",
"MOCK_31": "ОЧИСТИТЬ",
"MOCK_32": "CLEAR"
}
+2
View File
@@ -1,4 +1,6 @@
# KoalaSync Website — Allow all crawlers, full indexing
User-agent: *
Allow: /
# Sitemap for search engines
Sitemap: https://sync.koalastuff.net/sitemap.xml
+29
View File
@@ -0,0 +1,29 @@
{
"name": "KoalaSync",
"short_name": "KoalaSync",
"description": "Real-time video synchronization for friends. Privacy-first, open source.",
"start_url": "/",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#0f172a",
"icons": [
{
"src": "/assets/NewLogoIcon_64.webp",
"sizes": "64x64",
"type": "image/webp",
"purpose": "any"
},
{
"src": "/assets/NewLogoIcon_128.webp",
"sizes": "128x128",
"type": "image/webp",
"purpose": "any maskable"
},
{
"src": "/assets/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
}
]
}
+72 -8
View File
@@ -1,18 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://sync.koalastuff.net/</loc>
<changefreq>monthly</changefreq>
<lastmod>2026-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/"/>
<xhtml:link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/"/>
<xhtml:link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/"/>
</url>
<url>
<loc>https://sync.koalastuff.net/impressum.html</loc>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
<loc>https://sync.koalastuff.net/de/</loc>
<lastmod>2026-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/"/>
<xhtml:link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/"/>
<xhtml:link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/"/>
</url>
<url>
<loc>https://sync.koalastuff.net/datenschutz.html</loc>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
<loc>https://sync.koalastuff.net/fr/</loc>
<lastmod>2026-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/"/>
<xhtml:link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/"/>
<xhtml:link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/"/>
</url>
<url>
<loc>https://sync.koalastuff.net/es/</loc>
<lastmod>2026-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/"/>
<xhtml:link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/"/>
<xhtml:link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/"/>
</url>
<url>
<loc>https://sync.koalastuff.net/pt-BR/</loc>
<lastmod>2026-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/"/>
<xhtml:link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/"/>
<xhtml:link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/"/>
</url>
<url>
<loc>https://sync.koalastuff.net/ru/</loc>
<lastmod>2026-06-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
<xhtml:link rel="alternate" hreflang="de" href="https://sync.koalastuff.net/de/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://sync.koalastuff.net/fr/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://sync.koalastuff.net/es/"/>
<xhtml:link rel="alternate" hreflang="pt-br" href="https://sync.koalastuff.net/pt-BR/"/>
<xhtml:link rel="alternate" hreflang="ru" href="https://sync.koalastuff.net/ru/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://sync.koalastuff.net/"/>
</url>
</urlset>
+2328 -52
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More