Compare commits

...

415 Commits

Author SHA1 Message Date
GitHub Action b3f9693d34 chore(release): update versions to v3.0.1 [skip ci] 2026-07-26 03:56:23 +00:00
Timo 53befe8bad merge: Google Drive and chat fixes for v3.0.1 2026-07-26 05:54:57 +02:00
Timo 750b47d32a fix(i18n): correct website and extension copy 2026-07-26 05:53:57 +02:00
Timo 03710cec33 docs(comparisons): reflect encrypted text chat 2026-07-26 05:06:03 +02:00
Timo 1b97151e69 docs(privacy): document encrypted room chat 2026-07-26 04:32:06 +02:00
Timo 63db02211b fix(chat): harden overlay interactions and notifications 2026-07-26 04:14:25 +02:00
GitHub Action 90a58eb507 chore(release): update versions to v3.0.0 [skip ci] 2026-07-26 00:25:06 +00:00
KoalaDev 72113b6504 Merge pull request #32 from Shik3i/agent/update-tooling-node24
Prepare v3.0.0: encrypted text chat and runtime updates
2026-07-26 02:24:12 +02:00
Timo a0954079a1 harden chat settings and server image 2026-07-26 02:22:07 +02:00
Timo 9251ae6aff add dedicated chat settings 2026-07-26 02:12:28 +02:00
Timo 511e042b0b update tooling and server runtime 2026-07-26 01:58:23 +02:00
KoalaDev 372c384a19 Merge pull request #31 from Shik3i/agent/dependabot-security-updates
Fix Dependabot build dependency alerts
2026-07-26 01:32:41 +02:00
Timo 79715046a8 fix npm 11 lockfile portability 2026-07-26 01:23:07 +02:00
Timo 2ae79386a0 fix vulnerable build dependencies 2026-07-26 01:18:24 +02:00
Timo 883e49433e Merge branch 'feature/textchat' 2026-07-26 01:04:38 +02:00
Timo 59135b319e Merge remote-tracking branch 'origin/main' into feature/textchat
# Conflicts:
#	website/build.cjs
2026-07-26 01:03:27 +02:00
Timo ae2238e3bc docs: plan modern chat for v3.0 2026-07-26 01:02:33 +02:00
Timo 77790a279c fix(chat): preserve mixed-version compatibility 2026-07-26 01:02:20 +02:00
KoalaDev 2df048621b Centralize KoalaStuff legal notice 2026-07-23 03:04:20 +02:00
KoalaDev 17c3ea295a Centralize KoalaStuff legal notice 2026-07-23 02:59:58 +02:00
Timo 5d3cee4271 fix: update sitemap dates for dirty sources 2026-07-19 21:41:45 +02:00
Timo 1149de7ab7 fix: derive sitemap dates from page history 2026-07-19 21:33:53 +02:00
Timo 4882e058f1 fix: keep support SEO and sitemap metadata accurate 2026-07-19 21:21:49 +02:00
Timo 8d7085c97e fix: hide access banner on smaller screens 2026-07-19 21:10:50 +02:00
Timo 630869260b ci: publish text chat server beta 2026-07-19 20:43:11 +02:00
Timo e9d735cd39 Merge remote-tracking branch 'origin/main' into feature/textchat
# Conflicts:
#	extension/background.js
#	extension/popup.js
2026-07-19 20:32:24 +02:00
Timo aee3588f43 Document v2.6.4 release 2026-07-16 12:01:21 +02:00
GitHub Action 7f898e6eb4 chore(release): update versions to v2.6.4 [skip ci] 2026-07-16 09:51:25 +00:00
Timo d2d4f5f3ea Localize extension store metadata 2026-07-16 11:50:21 +02:00
GitHub Action 6e625e3a38 chore(release): update versions to v2.6.3 [skip ci] 2026-07-15 11:37:32 +00:00
Timo 6aa2f53e6b Fix access guide spacing and user help link 2026-07-15 13:34:35 +02:00
Timo 2b485fed53 chore(release): update versions to v2.6.3 [skip ci] 2026-07-15 13:34:24 +02:00
Timo fe13275b5d Harden host access recovery races 2026-07-15 13:31:56 +02:00
Timo 61d0d64c7d Remove developer link from user access guide 2026-07-15 13:28:20 +02:00
Timo abcb10a1c3 Scope access help to Chromium rollout 2026-07-15 13:23:54 +02:00
Timo 03121477a8 Add dismissible localized access banner 2026-07-15 13:20:00 +02:00
Timo 285c04e00d Align browser help cards 2026-07-15 13:09:50 +02:00
Timo 1989eb639b Clarify sudden Chrome site access rollout 2026-07-15 13:07:48 +02:00
Timo fd48ce8481 Fix site access banner overlay and immediate guidance 2026-07-15 13:05:24 +02:00
GitHub Action 649c8796a9 chore(release): update versions to v2.6.2 [skip ci] 2026-07-15 10:47:24 +00:00
Timo 142153a131 Harden host access recovery for v2.6.2 2026-07-15 12:46:57 +02:00
Timo a334605dd5 Remove unrelated relay troubleshooting from help page 2026-07-15 12:27:59 +02:00
Timo 7e7e60f267 Fix blocked website access and add troubleshooting page 2026-07-15 12:24:59 +02:00
KoalaDev 354500873c fix: keep llms release version in sync 2026-07-15 10:05:26 +02:00
KoalaDev 9e0d294758 fix(chat): harden client state and messaging 2026-07-15 08:40:27 +02:00
KoalaDev 36c4c492d3 fix(chat): preserve detached overlay geometry 2026-07-15 07:31:27 +02:00
KoalaDev e9dc299fc9 feat(chat): add in-page overlay 2026-07-15 07:23:02 +02:00
KoalaDev ee0d68d8d3 feat(chat): add client encryption and key lifecycle 2026-07-15 07:10:32 +02:00
KoalaDev f2eee6dc41 feat(chat): add stateless ciphertext relay 2026-07-15 07:02:08 +02:00
KoalaDev 8fee98834c feat(chat): add encrypted invite contract 2026-07-15 06:58:22 +02:00
KoalaDev 771da1cf82 docs: add E2E chat feature brief
Design brief for the in-page E2E encrypted chat: threat model, key
derivation via URL fragment, #j2 link format, backward compatibility
with old extensions, and the constraints the discarded feature/soonTMChat
branch got wrong.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 06:25:52 +02:00
KoalaDev ab2d7747fd Merge branch 'main' of github.com:Shik3i/KoalaSync 2026-07-15 06:16:17 +02:00
KoalaDev 8f9676cc5a fix(popup): make Status tab match the other tabs
The tab is still internally named data-tab="tab-dev". It used to be the
dev/diagnostics tab and was deliberately de-emphasized with font-size:
11px and a dimmer color. It was later relabelled to the user-facing
"Status" tab, but the rule stayed, leaving it 3px smaller and paler than
Room/Sync/Settings.

Drop tab-dev from the selector. #devToolsTabBtn, the opt-in Dev tab that
shares the rule, stays de-emphasized on purpose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 06:15:40 +02:00
GitHub Action 477a814a79 chore(release): update versions to v2.6.1 [skip ci] 2026-07-15 03:57:56 +00:00
KoalaDev cb84709358 fix(popup): render lobby peer names as text, not markup
Peer usernames are remote-controlled and were interpolated into an
innerHTML string in updateLobbyUI. The server only truncates them to 30
chars, so a peer could inject markup into everyone else's popup: enough
to load a remote image (leaking viewer IPs) or spoof readiness badges.
Inline handlers were already blocked by the MV3 default CSP.

Build the peer items with the DOM API, matching the pattern the sibling
peer list already uses.

Add the two checks that would have caught this before upload:
- eslint no-unsanitized, which reproduces the AMO warning at lint time
- addons-linter on the built XPI in verify-release, with
  --warnings-as-errors since it exits 0 on warnings and AMO rejects them

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 05:57:38 +02:00
KoalaDev 43a7bb0d57 docs: scope changelog header to extension and relay server
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 05:16:58 +02:00
KoalaDev e1e86307b1 docs: drop redesign-era items from v2.6.0 Fixed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 05:16:45 +02:00
KoalaDev e934fcf484 docs: limit v2.6.0 changelog to extension and server
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 05:16:16 +02:00
GitHub Action d4181b2eaf chore(release): update versions to v2.6.0 [skip ci] 2026-07-15 03:14:35 +00:00
KoalaDev 66d650f49c docs: date v2.6.0 changelog release
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 05:13:43 +02:00
KoalaDev 2cde14b260 Merge pull request #28 from Shik3i/redesign/koala-nature-theme
Redesign the website and extension with the Koala nature theme
2026-07-15 05:03:48 +02:00
KoalaDev db8bf8f647 docs: remove em dashes from store copy 2026-07-15 04:58:51 +02:00
KoalaDev 81001f2890 docs: correct relay and self-hosting copy 2026-07-15 04:55:53 +02:00
KoalaDev 861ffc306a test(website): validate canonical relay URL 2026-07-15 04:53:29 +02:00
KoalaDev 6f6aac9c18 docs: refresh project documentation 2026-07-15 04:45:45 +02:00
KoalaDev e4984b2d03 fix(website): complete self-hosting example 2026-07-15 04:37:51 +02:00
KoalaDev 18205f6573 docs(website): expand AI project profile 2026-07-15 04:29:20 +02:00
KoalaDev a4bbddf3a4 fix(website): address accessibility audit findings 2026-07-15 04:23:14 +02:00
KoalaDev b577f06988 fix(website): restore light mode step numbers 2026-07-15 03:48:22 +02:00
KoalaDev 61514b9b6f fix(website): generate sitemap only in build output 2026-07-15 03:30:22 +02:00
Timo 62046acb81 fix(ci): preserve optional lockfile entries 2026-07-14 16:38:47 +02:00
Timo 8b6a1412cc fix(build): replace regex HTML minifier 2026-07-14 16:30:13 +02:00
Timo 6e5a790707 fix(website): restore GitHub hero button outline 2026-07-14 16:11:50 +02:00
Timo 0f4b1d7217 docs(release): prepare v2.6.0 changelog and sitemap 2026-07-14 13:58:24 +02:00
Timo af0da75d38 style(website): landing motion, interaction & layout polish
Feedback round (calmer, less "template/AI"):
- Dial back animated background motion globally (sway/leaf-drift halved,
  fewer falling leaves, gentler godrays/fireflies/light-sweep).
- Fire leaf confetti on click of real controls only (was pointerdown
  everywhere, which showered mobile users on every scroll).
- Remove hover lift/scale from non-interactive cards, screenshots and
  mascots so they no longer read as clickable.
- Fix mobile nav menu colours in the light theme (was a hard-coded dark
  panel with low-contrast links).
- Calm how-it-works steps 1-2 (no button pulse/shine/cursor bob); step 3
  keeps its lively sync animation. Remove the now-static "copied" toast
  from step 1 since it only made sense mid-animation.

UX audit follow-ups:
- Unify vertical rhythm with one section-padding scale; vertically centre
  the hero so space is balanced instead of pooling under the CTAs.
- Split the mixed "Why KoalaSync?" grid into use-case scenarios and a
  labelled "Features" subsection, and move the #features nav anchor there.
- Demote the hero GitHub CTA to a quiet text link (Chrome > Firefox >
  GitHub hierarchy); scale down oversized section mascots and give them a
  shared soft shadow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:43:05 +02:00
Timo 63414bfcdd fix: strip all parenthesized/bracketed leading numbers in tab title normalization 2026-07-12 17:54:54 +02:00
Timo af1b66c6b7 fix: support multi-digit plus notification badges while preserving large numeric titles 2026-07-12 17:43:20 +02:00
Timo cc96fc1771 fix: support (99+) style notification badges in tab title normalization 2026-07-12 17:29:33 +02:00
Timo 9e8a446d97 fix: resolve Episode Lobby stuck loading state and prevent triggers on non-episodic media 2026-07-12 16:57:06 +02:00
Timo b987d13612 fix(extension): localize theme and appearance tooltips and selector options 2026-07-12 14:54:00 +02:00
Timo c175364f9a chore(website): refresh generated assets 2026-07-12 13:13:47 +02:00
Timo 0168484382 style(extension): unify control system and accessibility 2026-07-12 13:04:23 +02:00
Timo 99ecf9c9a8 fix(extension): normalize primary action labels 2026-07-12 12:17:32 +02:00
Timo f58ab54f45 fix(extension): remove redundant status accent bar 2026-07-12 12:15:21 +02:00
Timo 104602edec fix(extension): unify status UX and theme design 2026-07-12 11:10:15 +02:00
Timo 9d6a3650c4 fix(extension): polish settings selects and dark palette 2026-07-12 11:02:14 +02:00
Timo 986b2a6e56 style(extension): redesign settings and color system 2026-07-12 10:59:19 +02:00
Timo fdf20b6bf6 style(website): declutter background and polish stacked hero on small screens
- thin out bamboo/leaf/firefly decor in tiers (<=1440 / <=1024 / <=768px)
  so nothing moves behind the text column and copy stays readable
- center the single-column hero below 1024px (text, koala, CTAs, trust
  line) and let the odd CTA button span the full grid row
- remove the periodic wind gusts; keep the constant gentle bamboo sway
- reword the German demo CTA to "Live-Demo ansehen"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 10:59:00 +02:00
KoalaDev 3ce64b8c41 style(website): place footer-ram at top of footer and adjust vertical margins 2026-07-12 06:55:32 +02:00
KoalaDev 6f8bc40023 style(website): swap footer-ram above copyright and apply unified small disclaimer style 2026-07-12 06:53:21 +02:00
KoalaDev 6f203405f9 fix(website): restore missing footer-disclaimer class styling to landing-primary.css 2026-07-12 06:52:34 +02:00
KoalaDev 949075b267 style(website): optimize header scroll-shrink and clean up footer styles/hover effects 2026-07-12 06:50:37 +02:00
KoalaDev 4fb8e0f53a style(extension): apply UI/UX audit enhancements, consolidate footers, and style capsule badges 2026-07-12 06:38:26 +02:00
KoalaDev bd33ea4804 feat(extension): implement accordion mutual exclusion behavior for settings details categories 2026-07-12 06:31:24 +02:00
KoalaDev 9cd3fc1ee2 feat(extension): redesign settings tab into 4 collapsible accordion groups with chevron indicator and hover effects 2026-07-12 06:28:34 +02:00
KoalaDev 44f9a78bb0 perf(website): migrate forest parallax to native CSS scroll-driven animations, fix step contrast, and resolve forced reflows 2026-07-12 06:18:58 +02:00
KoalaDev e27319908d refactor(website): apply day/night sky palette variables to sync theater illustration and video canvas 2026-07-12 05:48:27 +02:00
KoalaDev af4ac943a9 Landing performance: flag font subset, responsive mascots, AVIF cache, HTML minify
- Flag font: ship a committed fontTools subset (43 KB, -45%) of
  TwemojiCountryFlags.woff2 with only the 15 flags the site uses, under a
  content-hashed name with a preload on every flag-bearing page. The build
  validates the subset against a manifest and fails when a new flag
  appears without regenerating (npm run subset-flags). JS subsetters
  (subset-font/harfbuzzjs) were rejected: their wasm builds silently drop
  the COLR/CPAL color tables, rendering all flags invisible.
- Responsive mascots: legal/join/404 pages served 434-500px sources for
  175-180px slots; the build now emits 180w/360w variants (AVIF 1x: ~7 KB
  vs ~22 KB) and the pages use srcset. Small logo spots (14-42px) now use
  the existing 64/128px variants instead of the 256px file.
- injectAvifPictures copies the img sizes attribute onto the AVIF
  <source>; without it Chrome mis-selects w-descriptor candidates and can
  drop the image entirely.
- AVIF conversion is cached by content hash (.avif-cache.json); the old
  mtime check never hit because copyDirSync refreshes mtimes, so every
  verify re-encoded all 26 files.
- HTML output is minified (comments + indentation, <pre> preserved,
  newlines kept for inline-script safety): index.html 121 KB -> 93 KB.
- unicode-range trimmed to U+1F1E6-1F1FF; tag-sequence flags fall through
  to the system emoji font.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 04:34:53 +02:00
KoalaDev 9fd4e870f3 Update theme test after film bird removal
The test still required 4 foreground film birds, but commit 16cb138
deliberately removed them all; now it guards against remnants instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 04:34:36 +02:00
KoalaDev 16cb138eeb Remove film birds; fix language pill highlight
Strip every bird from the demo mockups: both hero video tabs and both
step-3 player scenes lose the background flock (.film-birds) and the
foreground flapping bird (.film-hero-bird), plus all related CSS in
styles/demo.css and the dead style.legacy.css.

Language selector: the <select> was 30px narrower than its pill, so in
appearance:base-select the click/focus highlight covered only the inner
~110px ("half the box"). Move the padding from the container onto the
control so it fills the pill edge-to-edge; mirror the fix in the mobile
legal.css override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:57:18 +02:00
KoalaDev 17c9da292c Fix Getting Started light theme 2026-07-12 03:37:07 +02:00
KoalaDev c7c124dac7 Improve animated film bird 2026-07-12 03:28:05 +02:00
KoalaDev ea0970df72 Guard landing CSS cascade stability 2026-07-11 18:10:59 +02:00
KoalaDev 7614534818 Restore landing CSS cascade stability 2026-07-11 16:11:31 +02:00
KoalaDev 72d4f04526 Split landing CSS by render priority 2026-07-11 15:48:09 +02:00
KoalaDev 7fea11de0a Polish extension and landing page UI 2026-07-11 11:21:47 +02:00
KoalaDev fbef3d180c Give light theme proper green bamboo and a frosted language pill
- Bamboo layers get their own light-mode recipes (fresh greens, visible
  segment rings, sheen) instead of dark-theme gradients faded to 45%
- Language selector pill: frosted white glass, light dropdown/picker
  (was hard-coded dark glass in both themes)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:24:18 +02:00
KoalaDev fecaa8b54d Fix bamboo parallax direction: layers follow the scroll, not oppose it
Scrolling down now moves all bamboo layers up-screen with the content
(near fastest, far slowest) instead of pushing them down against it.
Vertical overscan flipped from top (-70%) to bottom (-40%) so the
upward drift never exposes a gap at the viewport bottom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:20:38 +02:00
KoalaDev 8175f82f0b Polish demo film, bamboo forest, popup a11y, and step-1 toast
- Demo mockup video: drifting clouds, shooting star, moon craters, bird
  flock, valley mist band, floating lantern instead of bouncing ball,
  stronger cinematic vignette
- Player controls: play/pause glyph, duration, buffered range, volume,
  HD badge, fullscreen icon
- Bamboo background: two faint centre trunks, segment rings on far
  stalks, wider near stalks with extra leaf, smaller blurred layers
  (240% -> 200%) and reduced blur radii for cheaper compositing
- Step-1 illustration: invite toast floats fully below the popup card
- Extension popup: label/for associations for all settings controls
- Demo video select: drop invalid inline SVG inside <option>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:10:37 +02:00
Timo 04df35bbe6 Enhance forest scene with mid bamboo layer, butterflies, click confetti, and demo scene polish 2026-07-11 06:08:12 +02:00
KoalaDev 21383b8b9c Polish animated forest background 2026-07-10 20:42:54 +02:00
KoalaDev 9cddac47ef Make forest parallax continuous and add scroll-depth journey
The drift clamp saturated after ~2600px, so nothing moved on long
pages. The vertical drift now maps scroll progress (0..1 over the full
document) onto the stalks' overscan, so the parallax runs end to end.

Turn the background into a descent through the forest: god-ray light
shafts and the canopy fade out while a dusk tint, thicker mist, the
terracotta horizon glow and the fireflies fade in as you scroll down.
Fireflies additionally wander organically (independent translate
property composing with their scale pulse), and on hover-capable
devices the bamboo layers tilt subtly toward the cursor. All layers
animate transform/opacity only, rAF-throttled, reduced-motion safe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:42:23 +02:00
KoalaDev 1f1844e99b Fix bamboo stalk taper and align stray colors to palette
The stalks used border-radius:50%, which renders a tall ellipse whose
ends taper to points, and unbounded parallax drift pushed them off the
bottom of the viewport on long pages. Rounded caps + 20% overscan +
a viewport-relative drift clamp keep them full-height while scrolling.

Sweep the remaining off-palette colors into the nature palette:
content-script overlays (slate grays), amber warnings/badges/star
accents (now terracotta) in popup, background badge and website
illustrations, and the support-heart red (now --danger).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:28:50 +02:00
KoalaDev aa52d319e1 Redesign website and extension with koala nature theme
v3.0.0 "The Greens Update" palette: forest-green oklch color system
(accent green + terracotta secondary) replaces the indigo/slate scheme
across the landing page and the extension popup/options. The blob
background becomes an animated bamboo forest overlay (parallax stalks,
falling leaves, canopy, ground mist, fireflies, light sweep, grain)
with prefers-reduced-motion support. Layout, structure and copy are
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:18:40 +02:00
KoalaDev 05246ac485 Fix light mode step text contrast 2026-07-09 17:25:20 +02:00
KoalaDev 6412a9e6ff Unify website section canvas 2026-07-09 17:07:23 +02:00
KoalaDev ee1c2947ce Fix join mobile navigation toggle 2026-07-09 14:47:29 +02:00
KoalaDev df8133fb5b Avoid persisting passive language visits 2026-07-09 14:40:44 +02:00
KoalaDev e25ae25bbb Respect system theme on website 2026-07-09 14:38:09 +02:00
KoalaDev 1d049e1745 perf(website): fix startup forced reflow and adjust AVIF compression quality to 65 2026-07-09 05:12:46 +02:00
KoalaDev 6aa42668e9 feat(website): add SRI to scripts & styles and optimize hero image preload 2026-07-09 05:06:35 +02:00
KoalaDev 71fdea78f3 feat(website): optimize rendering performance and improve structured metadata 2026-07-09 04:53:07 +02:00
Timo 5b7ee654e4 fix(website): keep hero demo hint readable 2026-07-08 20:22:20 +02:00
KoalaDev 7718885511 seo(website): add comprehensive AI agent rules to .gitignore 2026-07-08 17:31:02 +02:00
KoalaDev 12bca85356 build(website): move locales translation utility to scripts/ and add validate-brand-names script 2026-07-08 17:28:11 +02:00
KoalaDev b4bf297a5b i18n(website): translate new alternatives pages in all languages and save translation script as workspace example 2026-07-08 17:26:41 +02:00
KoalaDev 1e417e3379 fix(website): wrap locale replacement values in function to prevent RegExp dollar replacement bugs 2026-07-08 16:59:04 +02:00
KoalaDev d7d795f076 build(website): integrate dynamic sitemap generation into main build script 2026-07-08 16:53:57 +02:00
KoalaDev a45cfb063e seo(website): regenerate sitemap.xml to index all alternatives and update llms.txt 2026-07-08 16:52:46 +02:00
KoalaDev ee28f9d275 fix(website): grammar in TwoSeven German locale 2026-07-08 16:51:08 +02:00
KoalaDev 21936e7f71 feat(website): add KoalaSync vs TwoSeven comparison page 2026-07-08 16:50:05 +02:00
KoalaDev a45e52c6ee feat(website): add KoalaSync vs Kosmi comparison page 2026-07-08 16:47:29 +02:00
KoalaDev a0b4f1a2bb feat(website): update Scener comparison with pricing and technical differences 2026-07-08 16:38:40 +02:00
KoalaDev 4a8fdfbe70 chore: remove obsolete LANDINGPAGE_UI_OVERHAUL_PLAN.md 2026-07-08 16:32:31 +02:00
KoalaDev 9127b1f8dd feat(website): add KoalaSync vs Scener comparison page 2026-07-08 16:27:04 +02:00
KoalaDev e3a27db611 docs: add v2.5.4 changelog entry 2026-07-08 16:26:17 +02:00
GitHub Action 861bb05ec2 chore(release): update versions to v2.5.4 [skip ci] 2026-07-08 14:25:32 +00:00
KoalaDev 9f02a47d98 feat(website): add KoalaSync vs Watch2Gether comparison page 2026-07-08 16:20:44 +02:00
KoalaDev c3827fa4b7 refactor: remove cloud storage providers from domains blacklist 2026-07-08 16:04:00 +02:00
KoalaDev 61ba32addf refactor: remove localhost from domains blacklist 2026-07-08 16:03:53 +02:00
KoalaDev 140e2c0d00 chore: clarify privacy and legal notice wording 2026-07-07 12:57:58 +02:00
KoalaDev cc53494cc0 Merge pull request #25 from Shik3i/landingpage-redesign
Landing page redesign and polish
2026-07-07 07:10:24 +02:00
KoalaDev fd3c3e6cdb fix: satisfy landing page resize observer lint 2026-07-07 06:28:34 +02:00
KoalaDev 7d6ca9fd68 fix: finish responsive landing page polish 2026-07-07 06:21:36 +02:00
KoalaDev 84526cf8e9 fix: complete website seo localization 2026-07-07 02:38:28 +02:00
KoalaDev c16bf30ad5 fix: persist theme and lighten demo mockup 2026-07-07 02:28:45 +02:00
KoalaDev e559b41c93 fix: polish light mode landing page 2026-07-07 02:12:40 +02:00
KoalaDev a9687219e0 fix: normalize mobile platform logos 2026-07-07 02:07:15 +02:00
KoalaDev dfe0b42252 fix: hide hero github cta on mobile 2026-07-07 02:05:31 +02:00
KoalaDev d29c842e96 fix: compact getting started section 2026-07-07 02:02:44 +02:00
KoalaDev 07839a907b fix: refine landing page responsive layout 2026-07-07 02:00:37 +02:00
KoalaDev 78efb04b6a feat: refine landing page demo experience 2026-07-07 01:49:25 +02:00
KoalaDev e093a8afae docs: refresh onboarding and examples 2026-07-06 17:31:24 +02:00
KoalaDev b8484f77d6 content: swap privacy tile for the unique Volume Leveling feature
The combined section's third feature tile was Privacy, which is already
covered by the hero trust line and the comparison table. Replace it with
Volume Leveling / Audio Compressor, a genuinely differentiating feature not
shown anywhere else on the page. Shorten its description to match the other
tiles (all 15 locales). The three tiles are now: 1-click invites, volume
leveling, universal HTML5 support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:59:09 +02:00
KoalaDev 8d7b7f68f3 copy: shorten the Universal HTML5 Support feature description
Trim the long three-sentence blurb to a tight two-liner (platform list plus
the "only sync signals, never the video" reassurance) so it matches the other
tiles in the combined section. Applied to all 15 locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:51:14 +02:00
KoalaDev 2848a4450c fix: guarantee the three hero CTAs stay on one line
The headline column in the old narrow two-column range (769-1023px) was too
tight to fit the full "Add to X" label plus two more buttons, so GitHub wrapped
to a second row. The hero is now single-column below 1024px (headline gets the
full width, demo stacks under it, matching the demo's own breakpoint) and
two-column at >=1024px where the column is wide enough. GitHub button label
shortened to "GitHub" for extra margin. Verified one row at 769/900/1024/1280
and the mobile stack at 375px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:47:37 +02:00
KoalaDev 4c519a81e0 a11y: hero demo passes WCAG contrast (Lighthouse 4x100 desktop)
- Remove the confusing, space-hungry "Episode Lobby" card from the mockup
- Give the decorative demo mockup a contrast-safe palette: accent shown as
  text uses a lighter indigo, and buttons with white text on a colour get
  darker shades, so every walkthrough state meets WCAG AA
- Drop the scroll-reveal fade from the above-the-fold hero demo; the fade
  briefly rendered its text at partial opacity, which axe caught as failing
  contrast on ~1 in 3 audits
- Mark the demo as decorative for assistive tech (aria-hidden + removed from
  tab order); mouse interaction is unchanged
- Darken the footer support pill so white text meets contrast

Desktop Lighthouse: 100/100/100/100 (stable across runs). Mobile: 100
a11y / best-practices / SEO; performance ~87 (throttled LCP, separate work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:30:37 +02:00
KoalaDev db70828c82 feat: browser-aware CTAs, tidier hero demo, comparison guides link
- Hero CTAs: the install buttons are compact (just the browser name) so all
  three fit one line; JS detects the visitor's browser and expands that one
  to the full "Add to X" call to action. Unknown browsers stay compact.
- Hero demo: caption now sits directly under the two windows (absolutely
  positioned to the card cascade) instead of far below the tall scene. The
  scene keeps a fixed height that already fits the open popup, so the
  vertically centered hero never jumps when the popup toggles.
- The background window now also shows a (non-interactive) extension icon,
  making it clear both browsers have KoalaSync installed.
- Escape now also closes the demo popup.
- Added a link from the comparison table to the fuller alternatives/guides
  hub, localized in all 15 languages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:03:48 +02:00
KoalaDev fd0ad310ce fix: hero demo popup closes on any outside click, add close button
Outside-click handling was scoped to the demo scene, so clicks elsewhere
on the page left the popup open and it felt unclosable. A document-level
listener now collapses it from anywhere, like a real extension popup,
and the popup header gained an explicit close button next to the version
link (hidden in the static mobile mockup).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 03:23:15 +02:00
KoalaDev 917afd795e fix: bottom-anchor hero demo popup instead of oversizing the scene
Reserving the full 600px open-popup height pushed the H1 down on short
viewports. The scene now stays at 500px (below the left column height,
so it never drives the grid row) and the extension popup is anchored to
the scene bottom, growing upwards on open. Layout stays pixel-stable
when the walkthrough toggles the popup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 03:10:18 +02:00
KoalaDev da681e9e3f fix: reserve open-popup height for hero demo scene
The scene animated its own height when the walkthrough toggled the
extension popup; with the vertically centered hero grid this re-centered
the left column and shifted everything below on mid-size screens. The
scene now always reserves the 600px open state, so toggling the popup
never reflows the page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:56:37 +02:00
KoalaDev 37df4b267c feat: landing page redesign
- New hero headline "Turn any video into a watch party" with animated
  gradient highlight, sharper subtitle and quiet trust line under the CTAs
- Changelog badge redesigned as the koala's speech bubble (version and
  release date baked in at build time, no version.json fetch anymore)
- Light/dark theme toggle in the header (dark default, pre-paint via
  lang-init, full light theme across sections)
- Use cases and "Why KoalaSync" merged into one section with the three
  strongest feature tiles; question-mark koala moved to the FAQ
- FAQ and self-hosting setup collapsed by default via shared
  section-collapse component (hash deep links auto-open them)
- Compat/juggler section more compact
- og:image switched to the 1280x640 repository OpenGraph card (landing
  and join page), generated during build
- "watch party" keyword added to meta title/description in all 15 locales
- All em-dashes removed from user-facing copy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:50:45 +02:00
KoalaDev 793248053c Merge pull request #24 from Shik3i/chore/email-koalastuff
chore: migrate contact emails to koalastuff.net
2026-07-03 23:50:05 +02:00
Timo (KoalaDev) 1aa95079cf chore: migrate all contact emails from koalamail.rocks to koalastuff.net 2026-07-03 23:44:04 +02:00
KoalaDev 0976e765c3 Merge pull request #23 from Kaia-Alenia/fix-cors-env
docs: Update Vix test status to full in TESTED_SERVICES.md
2026-07-03 18:51:25 +02:00
Kaia-Alenia ee2c284cb4 Merge remote-tracking branch 'origin/main' into fix-cors-env
# Conflicts:
#	docs/TESTED_SERVICES.md
2026-07-03 10:23:54 -06:00
Kaia-Alenia 883807afd7 docs: Update Vix test status to full in TESTED_SERVICES.md 2026-07-03 09:38:38 -06:00
KoalaDev 46b2004f78 Merge pull request #22 from Shik3i/landingpage-ui-overhaul
Landing page UI overhaul: new extension mockup, interactive demo, 3-step guide, hero fixes
2026-07-03 14:36:00 +02:00
Timo 3fd02246ab feat: Add llms.txt for AI crawler support
- Create website/llms.txt following llmstxt.org spec (H1, blockquote,
  H2 sections with link lists, Optional section)
- Include llms.txt in website build pipeline (generic files copy)
- Sections: Features, Download, Technical, Optional (alternatives,
  sitemap, privacy, imprint, join)
2026-07-03 14:26:15 +02:00
Timo 14c9ab1d5e fix: Ensure hero heading has minimum distance from fixed nav bar across all languages
- Change .hero from align-items:center to flex-start so content starts from top
- Increase padding-top in short-viewport media query (<=920px height) to
  clamp(7.5rem, 10vh, 9rem) from clamp(3.75rem, 6vh, 5.5rem) so it always
  clears the ~96px nav bar
- Fix pre-existing lint errors: remove unused variables demoFinishedByStory,
  firstTake; add website/www to eslint ignores
2026-07-03 14:02:47 +02:00
Timo 0127d62a49 Tighten hero demo layout 2026-07-03 13:09:11 +02:00
KoalaDev 2d94cc10e4 website: Fix popup not collapsing after demo walkthrough completes
Remove firstTake && !demoFinishedByStory guard from takeOver handler
so clicks outside the extension mockup always collapse the popup,
regardless of whether the automated walkthrough finished.
2026-07-03 12:43:59 +02:00
KoalaDev c5c140d31c website: Link label elements to their select elements via for/id attributes in mockups for better accessibility 2026-07-03 12:14:35 +02:00
KoalaDev 5a53abc636 website: Adjust extension-mockup wrapper height to 600px to display the leave room button without scrolling 2026-07-03 12:13:34 +02:00
KoalaDev f0328fb865 website: Fall back to static extension mockup on medium screens (768px to 1024px) to prevent overlapping in narrower columns 2026-07-03 12:11:30 +02:00
KoalaDev e1e18aa26c website: Completely hide hero extension mockup wrapper on all stacked viewport sizes (768px and below) 2026-07-03 12:09:16 +02:00
KoalaDev cece816e00 website: Completely hide hero extension demo mockup wrapper on mobile screens (480px and below) 2026-07-03 11:57:33 +02:00
KoalaDev 9690868e33 website: Add smooth FAQ accordion spring animation with rotation icon transition 2026-07-03 11:35:00 +02:00
KoalaDev cb6d7eb98c website: Reformat Step 2 illustration as a realistic vertical extension popup mockup in the Sync tab 2026-07-03 11:32:51 +02:00
KoalaDev ab6356c5ad website: Replace long series name with shorter alternatives and randomize season 1 episode numbers dynamically 2026-07-03 11:30:04 +02:00
KoalaDev 5460ac3a01 website: Add micro-animations including scroll progress bar, nav logo entrance spin, and button shine sweeps 2026-07-03 11:26:45 +02:00
KoalaDev 60c80ad7fb website: Synchronize and randomize mockup video titles, and fix Step 2 illustration to use realistic select video dropdown 2026-07-03 11:25:12 +02:00
KoalaDev c8df8b6d08 website: Redesign Step 2 browser mockup as video player + select dropdown, and redesign Step 3 mockups to display matching forest movie 2026-07-03 11:22:10 +02:00
KoalaDev 49712a7151 website: Bring lower browser mockup to foreground on hover 2026-07-03 11:20:28 +02:00
KoalaDev 23aa0c8a15 website: Fix Host Control mockup to use the actual 'Only I can control playback' toggle switch 2026-07-03 11:15:32 +02:00
KoalaDev ae3b837bb4 website: Add Host Control Mode interactive mockup to Bento Card 1 and update translations in all 15 locales 2026-07-03 11:12:23 +02:00
KoalaDev 14ab6b2d50 website: Restructure 3-step guide flowchart, swap illustrations, and rewrite translations in all 15 locales 2026-07-03 11:09:25 +02:00
KoalaDev 82b90d86a9 website: Rename mockup usernames to ChillCat and HappyDog across HTML, JS, and all 15 locales 2026-07-03 11:07:03 +02:00
KoalaDev 537c8f3c60 website: Visual polish of 3-step guide and description updates in 13 locales 2026-07-03 11:05:45 +02:00
KoalaDev 6b81668e16 website: Fix mockup animation sync/pausing and improve 3-step guide copy 2026-07-03 11:01:57 +02:00
KoalaDev d6a23f2eb5 website: Hero layout and mock walkthrough visual optimizations 2026-07-03 10:57:23 +02:00
KoalaDev 573847486b fix: hero CTAs invisible until scroll, seek animation skip, layout overhaul
- Remove data-reveal from hero h1/subtitle/mascot/cta-group/version-badge
  (critical above-fold content must never start hidden)
- Change IntersectionObserver rootMargin -150px to -30px, threshold 0.1 to 0.05
  (CTAs at ~530px were past the observer zone on laptops)
- Hero layout: align-items flex-start + padding-top 7.5rem for content at top
- Cards 62cqw to 70cqw, -3% offsets for dramatic overlap cascade
- Film animation: replace stick-figure walker with cinematic night scene
  (7 twinkling stars, glowing moon with drift, parallax mountains, firefly)
- Seek now restarts all film child animations via .demo-reset class toggle
  (was incorrectly resetting parent only — no visible jump)
- Add .demo-seek-sweep overlay + brightness flash during seek
- Video select: placeholder option ('Choose a video tab...') until story picks
- RATE 8 to 1 for realtime playback
- Version badge moved below download buttons
- All 15 locale files: add DEMO_SELECT_PLACEHOLDER key
2026-07-03 10:42:09 +02:00
KoalaDev e5a4032710 work in progress: UI overhaul updates, new logo, and localization translations 2026-07-03 10:01:15 +02:00
KoalaDev 44d452a82f docs: update and categorize all entries in docs/README.md 2026-07-02 22:58:19 +02:00
KoalaDev 82d0bd4eb8 docs: correct Netflix compatibility status from N/A to X 2026-07-02 22:54:32 +02:00
KoalaDev d3ca2a2378 docs: update tested services compatibility table and contribution guide 2026-07-02 22:51:08 +02:00
KoalaDev b7622a8347 Update feature_request.md 2026-07-02 18:30:37 +02:00
Timo 3a733934c5 fix(lint): add website/**/*.cjs files to Node globals configuration 2026-07-02 18:01:11 +02:00
Timo 25ad3e8d59 feat(website): add cross-platform indexnow submission script 2026-07-02 17:39:17 +02:00
Timo 223c3949d2 feat(website): auto-copy google and indexnow verification files during build 2026-07-02 17:38:15 +02:00
Timo 3392f0b3a7 docs: tighten v2.5.3 changelog wording 2026-07-02 17:16:15 +02:00
GitHub Action 72ea5d1ee9 chore(release): update versions to v2.5.3 [skip ci] 2026-07-02 14:59:12 +00:00
Timo 869c64171b Fix Disney+ force sync, seek, and HCM regressions for v2.5.3
The v2.5.2 Disney+ page-API integration leaked blob-relative <video>
time into force sync, seeks, and heartbeats when the page-API bridge
had no fresh data, so force sync on Disney+ appeared broken.

- getSyncCurrentTime/getSyncDuration now refuse native values on Disney+
  (return null/0) so stale bridge data degrades to a clean no-op instead
  of broadcasting garbage to peers. The get_current_time handler and
  episode/lobby/hcmIsLive paths are routed through the same accessor.
- Validate FORCE_SYNC_PREPARE/SEEK payloads as finite before relaying;
  the internal coercion no longer treats '' as 0.
- Stop double-routing FORCE_SYNC_PREPARE from the popup path (the
  generic popup route now covers only play/pause/seek).
- popup force-sync: exclude null/empty peer times from the jump-to-others
  median (Number(null)===0 was dragging the target to 0), guard against
  NaN end-to-end, clear the dangling reset timer on failure, and retry
  without re-injecting when the content script responds but the Disney+
  bridge has not yet delivered a finite time.
- hcmIsLive skips the native-duration live signal on Disney+ only,
  preserving YouTube/Twitch Infinity-duration live detection.

Disney-specific logic remains strictly gated to disneyplus.com; no
Netflix/YouTube/Twitch/generic path is affected.
2026-07-02 16:57:48 +02:00
GitHub Action 6ccaf45f8e chore(release): update versions to v2.5.2 [skip ci] 2026-07-02 13:34:11 +00:00
Timo 1b7be23b29 Document v2.5.2 release 2026-07-02 15:33:47 +02:00
KoalaDev 9ec0e9dd4e Merge pull request #19 from Shik3i/codex/netflix-seek-fix
[codex] Fix Netflix and Disney+ seek handling
2026-07-02 15:29:55 +02:00
Timo 9ac8c28442 Remove Disney+ DOM scraping fallback 2026-07-02 15:08:54 +02:00
Timo 076157fef1 Sync Disney+ via the page media player API (precise time + seek)
Disney+'s <video> is blob-relative (unusable as an absolute clock) and its
scrubber aria-value freezes during playback, so DOM scraping lagged and the
+/-10s button seek could neither reach far targets nor land precisely. The
real player hangs off the <disney-web-player> custom element as
`.mediaPlayer`, exposing seek(ms) and timeline.info (playhead/duration ms).

Since the isolated content world can't read that page object, route it
through the existing MAIN-world page-API bridge (as Netflix already does):

- page-api-seek-overrides.js: register a 'disney' provider.
- background.js installPageApiSeekBridge: seek Disney via mediaPlayer.seek(),
  and post the exact playhead/duration (seconds) to the content world every
  250ms. Both are gated on provider === 'disney'; Netflix path unchanged.
- content.js: cache the pushed playhead, prefer it in getDisneyPlusTimeline
  (DOM scraping stays as fallback), and check the page-API seek first in
  seekVideo. Outcome is identical for Netflix and generic sites.

Verified live on Disney+: reported time matches the player exactly and
seek lands within ~1s of the target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:56:02 +02:00
Timo 236da46f5d Lift the 120s cap on Disney+ relative-seek so large seeks reach target
Disney+ has no absolute seek on its blob <video>, so absolute seeks are
translated into repeated clicks of the player's relative skip button. The
click count was capped at 12 (=120s at 10s/step), so any seek further than
two minutes away silently landed short — breaking absolute-position sync
(e.g. syncing to 5:00 from far away never arrived).

Derive the step size from the matched button's own label (10s default,
some UIs use 5/15/30) and issue enough clicks to cover the full delta,
bounded at a sane maximum.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:45:12 +02:00
Timo 2fbcbc4f83 Harden Disney+ time fallback against <video> element recreation
When the control overlay (and its live time readout) is briefly absent,
the old fallback recomputed the timeline from the raw blob-relative
video.currentTime via a cached scale/start. Disney recreates the <video>
element on some play/pause transitions, which resets currentTime, so that
math produced a wildly wrong time ("loses the time" after a pause).

The native clock advances 1:1 with real playback, so instead anchor the
last live UI position to video.currentTime and extrapolate by the elapsed
native delta. If the delta is negative or implausibly large (element
recreated / seek), freeze at the last known position rather than emitting
a garbage time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:38:26 +02:00
Timo 8fe1c6dfbc Fix laggy Disney+ time by preferring the live "time remaining" indicator
Disney+ throttles the scrubber slider's aria-valuenow, so it can lag real
playback by several seconds (and its value snaps back when the control
overlay reappears on pause). The player's "time remaining" indicator,
however, updates live.

getDisneyPlusUiTimeline() now captures any element whose class/testid
mentions "remain", parses its clock text, and — when a reliable duration
is known — derives current = duration - remaining and prefers it over the
laggy slider value. Falls back to the previous behavior when no remaining
indicator is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:26:53 +02:00
Timo 51da169d69 Remove temporary Media Session interceptor and Video Event Log diagnostics
Clean up the exploratory Disney+ diagnostics now that the Shadow DOM
timeline/seek integration is in place:

- Remove Proposal 1 (Media Session interceptor): the background.js
  interceptor injection and the content.js message listener capturing
  __koalaMediaSessionCapture, plus the now-unused mediaSessionPosition
  reporting in content.js and popup.js.
- Remove Proposal 3 (Video Event Log): videoEventsLog, logVideoEvent(),
  the per-video event logging listeners, and the popup.html/popup.js UI.
- Keep Proposal 2 (DOM timestamp scraper) for ongoing diagnostics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:18:50 +02:00
Timo a9f5c0b9bf TEMPORARY DIAGNOSTICS: Integrate querySelectorAllShadow in Disney+ specific functions 2026-07-02 13:45:36 +02:00
Timo 705fbc146b TEMPORARY DIAGNOSTICS: Upgrade DOM scraper to support Shadow DOM recursion 2026-07-02 13:42:25 +02:00
Timo 09d35f3e48 TEMPORARY DIAGNOSTICS: Implement Proposal 2 and 3 logs in Dev tools tab 2026-07-02 13:35:08 +02:00
Timo 32994aaaa5 Support dynamic Disney+ timeline offset/scale caching for accurate seek sync 2026-07-02 13:30:20 +02:00
Timo bea0f6925c Implement cached duration fallback and simulate keyboard events fallback for Disney+ seeks when controls are hidden 2026-07-02 13:27:53 +02:00
Timo 71fa6e7315 Support absolute seeks on Disney+ by automatically translating them to relative jumps 2026-07-02 13:25:11 +02:00
Timo 54b806d270 Fix Media Session listener scoping by moving it to root IIFE level in content.js 2026-07-02 13:23:14 +02:00
Timo 066d2c9407 Implement temporary Media Session interceptor diagnostics for Disney+ and restore Netflix sessionId lookup guard 2026-07-02 13:19:47 +02:00
Timo ce522e2ab7 Safeguard Netflix player session ID lookup in background.js 2026-07-02 13:01:14 +02:00
Timo 9cbeb661d6 Save baseline state with Disney+ seek adjustments and build identifier 2026-07-02 13:00:41 +02:00
Timo beda924b65 Fix DEV_SIMULATE_REMOTE_SEEK targetTime routing in background.js and add build identifier 2026-07-02 12:57:58 +02:00
KoalaDev 01762b0c2f Normalize shared tab titles 2026-07-02 10:20:25 +02:00
KoalaDev 0af22998c8 Handle target tab navigation reinjection 2026-07-02 10:02:31 +02:00
KoalaDev 52265e84eb Generalize page API seek overrides 2026-07-02 09:57:57 +02:00
KoalaDev 57cd071d58 Add hidden remote seek test controls 2026-07-02 09:50:39 +02:00
KoalaDev 8a21fbe07f Fix Netflix seek handling 2026-07-02 09:43:11 +02:00
KoalaDev 7781d418ba Merge pull request #17 from Shik3i/feat/protocol-rate-limit-vitest
feat: Protocol spec, rate limiting, vitest, and documentation
2026-07-02 00:17:01 +02:00
Timo c022f6f490 fix: harden leave room rate limiting 2026-07-02 00:14:55 +02:00
Timo 9fe83f89b2 fix: make protocol docs match implementation 2026-07-02 00:04:49 +02:00
Timo e51e8ae436 fix: move relay changelog notes to v2.5.1 2026-07-01 23:55:42 +02:00
Timo e74abd00f7 fix: keep linux optional deps in lockfile 2026-07-01 23:51:52 +02:00
Timo dab7368537 fix: repair PR verification and docs cleanup 2026-07-01 23:46:23 +02:00
Skrockle 463f871958 feat: Add protocol spec, rate limiting, vitest framework, and documentation
## Added
- **WebSocket Protocol Specification** (docs/PROTOCOL.md) - Complete reference
  for all 20+ events with payload schemas, rate limits, and edge cases
- **LEAVE_ROOM Rate Limiting** - 10 requests/socket/minute to prevent abuse
- **Vitest Testing Framework** - Modern test setup with coverage reporting
- **Host Control Mode Documentation** - Consolidated EN documentation
  (docs/host-control-mode.md) with references to internal implementation

## Changed
- **Graceful Shutdown** - Socket.IO clients properly disconnected during
  server shutdown (server/index.js)
- **CI Workflow** - Added test step with continue-on-error for gradual rollout
- **README** - Added protocol documentation link
- **CHANGELOG** - Updated with all new features and improvements

## Moved
- Internal documentation moved to docs/internal/ for better organization
- Host Control Mode docs consolidated from 4 files to 1 comprehensive guide

## Technical Details
- Protocol spec: 495 lines, covers all events from shared/constants.js
- Rate limiter: Follows existing pattern (checkAuthRate, checkEventRate)
- Vitest: 6 tests for rate limiter, all passing
- Documentation: Host Control Mode doc includes edge cases, testing checklist,
  architecture decisions
2026-07-01 23:27:30 +02:00
GitHub Action 1f5196e1e6 chore(release): update versions to v2.5.1 [skip ci] 2026-07-01 17:48:39 +00:00
Timo 003741a63f docs: add devops.md detailing release workflow 2026-07-01 19:47:31 +02:00
KoalaDev e644ab33cd Merge pull request #15 from Shik3i/codex/issue-13-title-privacy
feat(extension): add title privacy controls and settings
2026-07-01 19:43:46 +02:00
Timo 9006ebbc67 docs: remove temporary branch planning document 2026-07-01 19:40:57 +02:00
Timo e40017f053 Move privacy settings under username 2026-07-01 19:26:25 +02:00
Timo fa7eea54d8 Split tab and media title privacy controls 2026-07-01 19:21:38 +02:00
KoalaDev 04d2360814 Remove plan for splitting large JavaScript files
Removed the plan to split large JavaScript files into smaller modules due to prioritization changes.
2026-07-01 19:16:42 +02:00
Timo 29aba936ce Clarify tab and media title privacy setting 2026-07-01 18:58:39 +02:00
Timo 4bae88e107 Fix title privacy popup import 2026-07-01 18:50:09 +02:00
Skrockle 3e4b97b11a Fix: Remove redundant TITLE_PRIVACY_MODES array in popup.js
- Remove duplicate local TITLE_PRIVACY_MODES array definition
- Use Object.values(TITLE_PRIVACY_MODES).includes() for validation instead
- Keeps single source of truth in title-privacy.js

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 18:37:25 +02:00
Timo e555906553 fix(extension): harden title privacy edge cases 2026-06-29 13:43:10 +02:00
Timo 4f1335242c feat(extension): add shared title privacy modes 2026-06-29 13:35:34 +02:00
GitHub Action e683466bf0 chore(release): update versions to v2.5.0 [skip ci] 2026-06-29 10:58:06 +00:00
KoalaDev 788a3331d3 Merge pull request #14 from Shik3i/feature/host-control-mode
Prepare v2.5.0 host control release
2026-06-29 12:55:57 +02:00
Timo 2d9b7bc099 Ignore Claude local files 2026-06-29 12:52:22 +02:00
Timo 717d37c693 Prepare v2.5.0 website release notes 2026-06-29 12:50:49 +02:00
KoalaDev 1f48e9df47 fix(popup): polish custom-server, host-control card & co-host UX
- Manual Connect: split the server picker into its own inset panel,
  visually separated from the Room ID / Password fields (they were
  welded together in one flat list).
- Custom server URL: stop auto-prepending ws:// — default to wss://
  for real hosts, ws:// only for localhost/127.0.0.1 (matches what
  the background already enforces; the displayed URL no longer lies).
- Host Control card: replace the raw native checkbox with the proper
  .toggle-switch slider used in Options; move the toggle row from a
  <label> to a <div> to avoid an illegally nested label.
- Co-host discoverability: owner-only hint in the Host Control card
  pointing to the participant list (new NOTICE_COHOST_HINT across all
  15 locales). Shown only when actionable: owner + co-host-capable
  relay + host-only active.
- Peer row: group right-side badges/actions into one container so they
  no longer scatter via space-between or collide with long names;
  restyle the give/revoke button (padding, weight, hover fill, tooltip).
- Gate promote/demote (and role badges) on host-only mode being ON,
  in addition to the existing owner-privilege check — roles are moot
  in 'everyone' mode, so no badge/button noise there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:43:28 +02:00
KoalaDev adab684ed0 perf+fix: debounce seq persistence + role changes, null-clear stale peer state
- server: SET_PEER_ROLE now uses the same per-room 500ms debounce as
  SET_CONTROL_MODE (M-4), preventing a buggy/malicious host from
  thrashing the room's UI with rapid promote/demote bursts.
- server: PEER_STATUS relay treats explicit null as 'clear' for
  tabTitle/mediaTitle/currentTime, so the tab-close heartbeat actually
  zeroes out stale state on other peers instead of being silently
  preserved by the clamp fallback.
- background: _persistLastSeq is now trailing-debounced (500ms),
  cutting storage.session IPC writes from one-per-relayed-event to
  one-per-quiet-window in active rooms.
- locales: add missing TOAST_ID_REGENERATED to all 14 non-English
  locales (was blocking locale coverage test on main).
- test: H-1 force-sync demote test now waits out the debounce window
  to reflect real-world UI timing (a host cannot promote, run a
  force-sync, and demote inside 500ms).
2026-06-29 01:22:41 +02:00
KoalaDev e23f9ab226 fix: wire up Regenerate Peer ID button + remove arbitrary MAX_CONTROLLERS cap
Two pre-existing issues surfaced during audit follow-up:

1. popup.js bound elements.regenId but never attached a click handler —
   the 'Regenerate Peer ID' button has been dead UI since it was added
   (verified across full git history). Wired it up to send REGENERATE_ID;
   the background handler already did the right thing (rotate peerId,
   persist, force reconnect). The button is functional (regen ID on
   duplicate-identity errors), not privacy-theater — username is already
   user-changeable in settings, so it stays out of scope.

2. MAX_CONTROLLERS=10 was arbitrary. Payload math (25 controllers ×
   16-char IDs ≈ 500 bytes) is well under the 4KB maxHttpBufferSize,
   controllers.has() is O(1) on a Set, and 'just use everyone mode' is
   not a real answer when a host wants 12 controllers + 3 guests in a
   15-person room. Removed the cap entirely; the only real upper bound
   is MAX_PEERS_PER_ROOM (25).

Added TOAST_ID_REGENERATED to en.json — the i18n fallback merge
(Object.assign({}, enDict, targetDict)) covers the other 14 locales
automatically with English until they're translated.
2026-06-28 14:03:59 +02:00
KoalaDev 77203fe8fd fix(extension): drop unused roomPassword field from GET_STATUS response
Defense-in-depth: the field was always undefined in practice (no code
path ever sets currentRoom.password — verified across the full git
history), and no GET_STATUS caller in the codebase reads it. But if a
future change ever populated currentRoom.password, any caller of
GET_STATUS would receive it in cleartext.

Threat surface today is narrow (externally_connectable is not set, no
content script sends GET_STATUS, only the popup uses it), but the field
has no reason to exist — removing it eliminates the future-leak vector
for free.

Backward compatible: no caller reads this field, so removing it is a no-op
for every existing code path.
2026-06-28 08:35:05 +02:00
KoalaDev 5379778a20 fix(extension): widen peerId generation to 16 hex chars (64-bit)
8 hex chars (32-bit) gives ~7% collision probability at the relay's
25k-peer capacity and ~1% at 10k peers. A same-room collision silently
triggers the dedup path and kicks the older session with a confusing
'Another session with this ID joined' error.

16 hex chars (64-bit) drops collision probability to ~1e-10 even at one
million peers. The server's JOIN_ROOM sanitizer already clamps peerId to
16 chars, so no server change is needed. Fully backward compatible:

- Existing clients keep their persisted 8-char peerId (only newly
  generated IDs change).
- Mixed 8-char + 16-char ID rooms work — both fit the server's 16-char
  clamp.
- No protocol change on the wire.

Birthday-paradox reference (computed, not estimated):
  32-bit @ 10k peers:  1.16%
  32-bit @ 25k peers:  7.02%
  64-bit @ 25k peers:  ~0.017e-9%
2026-06-28 08:29:59 +02:00
KoalaDev 9fc58dca4b docs(assets): add ready-to-paste marketing copy kit (1-sentence, 3-sentence, full)
Three drop-in lengths for product listings, launch pages, and directory
submissions where the same pitch gets re-typed repeatedly. Bonus taglines
for hero headlines and social bios. All claims verified against the
codebase and docs/TESTED_SERVICES.md.
2026-06-28 06:02:55 +02:00
KoalaDev 17ac5c0a6f fix(host-control-mode): adversarial audit — force-sync stall, desync/lobby, gate parity, BC tests
Audit fixes (each verified against the actual code path):

H-1 (server): track force-sync initiator on PREPARE; let the demoted
initiator's EXECUTE through the host-only gate so mid-sync demotion no
longer strands the whole room paused. Clear on EXECUTE/peer-leave.

M-1 (background): episode-lobby gate now uses !amController() for parity
with CONTENT_EVENT and server gates — co-hosts can drive the room and
initiate lobbies, not just the owner.

M-2/M-3 (popup/content/background): forceSyncReset respects hcmGuestLocked;
desynced guest skips EPISODE_LOBBY so they don't get frozen in pause after
lobby completion, and checkEpisodeLobbyCompletion excludes desynced peers
from the required count so they don't block the lobby.

M-4 (background): getHostSyncTarget clamps extrapolation to 2x heartbeat
interval so a stale host heartbeat can't snap the guest tens of seconds
past the host's real position.

L-1..L-4 (server/content/background/popup): clarify dedup comment re:
network-blip window, enforce desync invariant on SW-restore, add
forceSyncBtn guest-locked backstop, refresh badge text in place.

Backward compatibility (verified by BC-1..BC-4 regression tests):
- Old client ↔ new server: server adds fields only, never requires; old
  heartbeats stripped of desynced; host-only enforced server-side even
  when the client has no awareness.
- New client ↔ old server: empty capabilities → host-control UI hidden,
  all gates default to everyone, behavior byte-identical to pre-HCM.
- Mixed rooms: every pre-HCM event type relays cleanly in both directions.
2026-06-28 05:54:50 +02:00
KoalaDev 1ec2396c41 feat(co-host): popup UI — promote/demote, controller badges, controller-aware lock
- updateHostControlUI takes a state object now; role badge shows Host / Controller
  / Guest; remote-control lock uses amController (a co-host can drive, so their
  buttons aren't locked).
- Peer list: Host/Controller badges and, for the owner, a per-peer Give control /
  Revoke button (SET_PEER_ROLE). Shown only when the feature is active and the room
  is host-only or the viewer is the owner — no role noise for guests in normal rooms.
  Hidden entirely against a relay without the co-host capability.
- i18n: BADGE_CONTROLLER, BTN_GIVE_CONTROL, BTN_REVOKE_CONTROL across all 15 locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 03:45:00 +02:00
KoalaDev 7c65fe80cb feat(co-host): client gates use controller-set membership (background + content)
- background: track controllers[] from ROOM_DATA/CONTROL_MODE (restore + reset on
  teardown); add amController() (owner or co-host). Both gates now key on controller
  membership (sender: !amController(); receiver: senderId not in controllers); desync
  invariant uses amController. Thread controllers/amController/coHostSupported through
  GET_STATUS/GET_CONTROL_MODE/CONTROL_MODE. New SET_PEER_ROLE message (owner→server).
- content: gate on amController instead of amHost (a controller is not a gated guest);
  reconcile/HOST_BLOCKED updated accordingly. Drop now-unused hcmAmHost.

Single-host behavior unchanged (owner is always the sole controller until they promote).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 03:41:23 +02:00
KoalaDev a326087ca5 feat(co-host): server-side multi-controller (owner promotes peers to drive)
Generalizes host-only from a single host to a controller SET. The owner (creator)
can promote/demote peers; in host-only mode the relay gate allows any controller,
not just the owner. Single-host is now the special case controllers={owner}, so
existing behavior/tests are unchanged.

- shared: CAPABILITIES.CO_HOST enabled; new SET_PEER_ROLE event; CONTROL_MODE
  payload carries controllers.
- server: room.controllers Set (always incl. owner), advertised in ROOM_DATA;
  gate checks controllers.has(peerId); SET_PEER_ROLE handler (owner-only, target
  must be present, MAX_CONTROLLERS=10 cap, re-sync on reject); controlModePayload
  helper for consistent emits; owner-leave resets controllers + falls back to
  everyone, co-host-leave broadcasts the updated list (dedup/reconnect guarded).
- WS test: promote→drive, plain guest still gated, non-owner can't promote,
  demote→re-gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 03:36:22 +02:00
KoalaDev de518ac205 docs+test: move KNOWN_LIMITATIONS to docs/, add everyone-mode regression test, co-host plan
- Move KNOWN_LIMITATIONS.md → docs/KNOWN_LIMITATIONS.md (+ fix SECURITY.md link).
- WS test: in default 'everyone' mode a non-host guest can still drive
  play/pause/seek/force-sync/episode-lobby — proves host-control OFF == unchanged
  behavior.
- docs/host-control-mode-COHOST-PLAN.md: plan for multi-controller (owner grants
  drive rights to several co-hosts), built on the capabilities hook; covers the
  gate generalization, roles, backwards-compat, edge cases, and a separate
  large-room (510-peer) scaling track.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 03:16:28 +02:00
KoalaDev ce3cf0ef89 docs: add KNOWN_LIMITATIONS.md with threat model + NOFIX entries
Records the accepted, out-of-threat-model decisions that reviewers/agents keep
re-reporting as bugs: unauthenticated peerId (host impersonation), non-constant-
time room-password compare, public server token, enumerable room IDs, and
pause-revert-not-prevent. Spells out the threat model (ephemeral, account-less
rooms of invited participants watching a video) so the "NOFIX" rationale is clear,
and distinguishes these from genuinely-deferred items (EC-10 host grace, classifier
tuning). Cross-referenced from SECURITY.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 02:53:11 +02:00
KoalaDev 9626948f29 security(server): warn at startup when SERVER_SALT is unset
Room-password hashing falls back to a built-in salt that is public in the repo
when SERVER_SALT is not configured. Low risk (the hash never leaves the server),
but operators should set a unique salt — warn at startup, mirroring the existing
ADMIN_METRICS_TOKEN warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 02:37:37 +02:00
KoalaDev dfa0a3d03e fix(host-control-mode): audit findings — host dedup demotion, episode-lobby freeze, +cleanups
Findings recovered from the multi-agent audit (verification phase was cut off by a
usage limit; verified inline against the code):

HIGH:
- Host was demoted on peerId dedup (fast reconnect / second tab): the dedup path
  removes the old socket and the kicked socket's 'disconnect' both ran the
  host-leave fallback before the same peerId re-joined → room silently unlocked on
  every host network blip. Now skip the fallback while a join for that peerId is in
  flight (peerJoinLocks). Regression test added. (A long real disconnect still
  falls back — that's the deferred host-grace EC-10.)
- Episode auto-advance froze a gated guest: in host-only the guest's EPISODE_LOBBY
  is dropped server-side, but the guest still self-paused (PAUSE_FOR_LOBBY) waiting
  for readies that never came → 60s freeze. A host-only guest now skips creating a
  lobby (the host drives episode sync).

LOW / cleanup:
- GET_HCM_STRINGS could return a raw key name ("HCM_DIALOG_TITLE") if the locale
  dictionary failed to load (getMessage returns the key on miss) → omit it so
  content keeps its English fallback.
- hcmReset now also clears the snap-back cooldown + buffering grace, so a stale
  cooldown can't swallow the first snap-back after a room/host change.
- Popup play/pause labels reset on lock too (not just unlock), so a button can't
  freeze on "Playing…" when host-only activates mid-click.
- Fix a stale comment (heartbeat now carries 'desynced').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 02:25:11 +02:00
KoalaDev 02c41776b8 fix(host-control-mode): re-query host pos when captured target is unusable
Audit follow-up on the snap-back paths. The immediate involuntary snap used the
captured target directly; if it was null/invalid (host position not known yet,
e.g. before the first host heartbeat) hcmSnapBackToHost no-ops and the guest is
left silently paused with no dialog and no retry. The deferred and "Stay in sync"
paths already re-query with retry — make the immediate path fall back to the same
when the captured target isn't usable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:21:27 +02:00
KoalaDev b32e2fa8cc feat(host-control-mode): buffer-aware deferred snap-back (#3)
Implements the snap-back robustly instead of deferring it to device testing —
since the set of video sites is unbounded, "measure which players misbehave, then
fix" doesn't scale; the snap-back must be safe on any player.

On an involuntary pause/seek, if the player isn't ready to play (readyState<3 or
seeking) we no longer snap immediately (which fights the buffer: seek → re-buffer
→ pause → … = stutter). Instead hcmDeferredSnapBack polls until the player can
play (8s cap) and then snaps ONCE to the host's re-queried current position. A
player can't play while buffering anyway, so waiting is also more correct, not
just safer. Ready players still snap immediately (no regression).

Guards: aborts if the user goes solo or is no longer a gated guest; single
pending poll (no stacking); cleared in hcmReset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:18:22 +02:00
KoalaDev 9963da2ebc feat(host-control-mode): explicit server capabilities for feature detection
Replace the implicit "hostPeerId present ⇒ feature supported" heuristic with an
explicit capabilities list the relay advertises in ROOM_DATA. Cleaner, self-
documenting, and the extensible hook for the planned co-host feature (owner
promotes guests to extra controllers) — a future 'co-host' capability + events
slot in without a protocol bump.

- shared: CAPABILITIES { HOST_CONTROL }.
- server: SERVER_CAPABILITIES advertised in ROOM_DATA.
- background: track serverCapabilities (empty against older relay), serverSupports(),
  thread hostControlSupported through GET_STATUS / GET_CONTROL_MODE / CONTROL_MODE.
- popup: gate the host-control card on the explicit capability instead of hostPeerId.

Backwards-compatible both ways: old relay omits the field → feature stays hidden
(no errors); old client ignores the field. WS test asserts ROOM_DATA advertises
the capability. Adds docs/host-control-mode-TESTING.md (beta-server setup, wss
caveat, two-new-clients note, verification checklist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 04:48:45 +02:00
KoalaDev 9feafab617 ci: publish beta relay Docker image for branch testing
Adds a separate workflow that builds the server image and pushes it to GHCR under
non-production tags (beta, branch slug, sha-<commit>, and an optional custom tag
via manual dispatch) so the host-control branch can be deployed to a staging /
backup server and used as a custom server — without ever touching ':latest' that
the official relay tracks (flavor: latest=false).

Runs on push to feature/host-control-mode and on workflow_dispatch. Remove once
the feature is merged and released normally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 01:42:56 +02:00
KoalaDev d26b95985d fix(host-control-mode): only show host-control card when meaningful
The card was rendered in every room with a "Guest" badge by default — confusing
in normal rooms, and outright misleading against a relay that doesn't yet support
host control (older server omits hostPeerId, so amHost is always false and nobody
is ever recognized as host).

Now the card shows only when it's meaningful: the host always sees it (to toggle
host-only), a guest sees it only while host-only is active, and it's hidden in a
normal "everyone" room and whenever the server doesn't advertise hostPeerId
(feature unavailable). Thread hostPeerId through updateHostControlUI to detect
server support.

Note: the feature still requires the relay to run the host-control server code;
this just makes the client degrade cleanly instead of showing a stray "Guest".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 01:29:00 +02:00
KoalaDev bb030a9737 feat(host-control-mode): localize the in-page dialog & badge
The host-control desync dialog and the "watching on your own" badge were
hardcoded English — content.js has no i18n loader. Background does, so it now
resolves the strings (GET_HCM_STRINGS: loadLocale + getMessage) and content
fetches them on init with English fallbacks, re-rendering the badge if it was
already shown. The dialog body is now generic (dropped the paused/jumped verb)
to keep translation clean across languages.

Adds 6 keys (HCM_DIALOG_TITLE/BODY/STAY/SOLO, HCM_BADGE_SOLO/RESYNC) translated
across all 15 popup locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:26:31 +02:00
KoalaDev 95ff460856 fix(host-control-mode): hold desync invariant + stop Solo-badge flicker
Two issues surfaced by a holistic audit of the combined desync-persist +
reconcile-on-init changes:

1. Stale desync outside gated-guest state. hcmDesynced is persisted, but neither
   ROOM_DATA nor CONTROL_MODE cleared it when the local peer became host or the
   room switched to 'everyone'. Combined with reconcile-on-init, a desynced guest
   promoted to host (host-leave fallback) while content was reloading could
   re-adopt desynced=true — self-labelling "Solo" to all peers and ignoring host
   commands in 'everyone' mode. Enforce the invariant centrally
   (hcmEnforceDesyncInvariant: desynced ⟹ host-only && !host) on every role/mode
   change, and guard the content reconcile to only adopt when actually a gated guest.

2. Solo-badge flicker. The host-side PEER_STATUS handler set peer.desynced
   unconditionally, but the background-driven keepAlive heartbeat omits the field —
   so every ~30s it clobbered desynced to false, flickering the badge. Only update
   when present (matching the sibling fields), and include desynced in the
   background heartbeat so both heartbeat sources are consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:06:04 +02:00
KoalaDev 96e2207cf3 fix(host-control-mode): retry host-sync on dialog "Stay in sync" too
The Resync badge already retried REQUEST_HOST_SYNC when the host's state wasn't
known yet, but the dialog's "Stay in sync" path did a single no-retry request —
so a stay tap right after the host paused (before a heartbeat propagated) could
silently leave the guest paused/out of sync. Extracted the retry loop into a
shared hcmRequestHostSyncWithRetry() and used it in both paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:52:49 +02:00
KoalaDev 2928393b4c fix(host-control-mode): reconcile desync state on content reinjection
Fixes a split-brain where a guest who chose "watch on my own" and then reloaded
the video page would appear stuck as "Solo" to the host while actually being
back in sync.

Root cause: background persists hcmDesynced (survives SW restart) and attaches
it to content-driven heartbeats, but a freshly injected content script started
with hcmDesynced=false and GET_CONTROL_MODE never returned the desync state — so
content re-applied host commands (synced) while background kept relaying
desynced=true to the host.

Fix: GET_CONTROL_MODE now also returns desynced; content adopts it on init
(re-shows the badge) and also adopts hostPeerId so later host-change resets work.
Desync now survives a page reload consistently on both sides, as intended.

Verified the full path: background.js heartbeat (1555 omits / 2018 attaches),
reset paths, CONTENT_BOOT and keepAlive-disconnect (neither reset it), content
sendHeartbeat on inject.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:45:33 +02:00
KoalaDev 5863991bfb fix(host-control-mode): remove redundant CONTENT_EVENT guard, persist desync in storage, block stale tab HCM_DESYNC_STATE
Original code at L1957 already validates sender.tab tabId against
currentTabId before calling processEvent() — added guard was
duplicate. HCM_DESYNC_STATE had no such guard; added with same
sender.tab && currentTabId pattern (popup-safe via undefined
short-circuit).

hcmDesynced now persisted in session storage:
- Restored in ensureState()
- Set on HCM_DESYNC_STATE change
- Reset to false in all 3 teardown storage.sets
Survives SW restarts; cleared on leave/switch/idle-room
2026-06-26 13:23:24 +02:00
KoalaDev 1eca3fa5bb fix(host-control-mode): guard against stale tab events, persist desync across SW restart
- CONTENT_EVENT now rejected from non-selected tabs (popup exempted via sender.tab check)
- HCM_DESYNC_STATE same guard
- hcmDesynced persisted in session storage: restored on SW restart, cleared on teardown
2026-06-26 13:18:09 +02:00
KoalaDev 58af258fd3 fix(host-control-mode): harden gates, close UX races, debounce toggles, surface desync state
Bis auf peerId-Auth (akzeptiert) alle Audit-Findings behoben:
- Snap-back spielt nicht mehr blind ab, wenn Host-Status unbekannt
- Teardown-Pfade broadcasten jetzt CONTROL_MODE → kein verwaistes Badge/Dialog
- Dialog-Timer bei Ersetzung sauber gecleart
- Server re-synchronisiert Sender bei nicht-Host-Reject
- Popup-Toggle revertiert bei Server-Ablehnung
- Desync-Zweig sendet keine Phantom-ACKs mehr an Host
- Toggle-Debouncing serverseitig (500ms pro Raum)
- Button-Text auf Unlock zurückgesetzt
- Live-Erkennung defensiver, Resync-Retry bei fehlendem Target, Badge-Retry

Neu: Host sieht 'Solo'-Badge für desyncte Gäste via PEER_STATUS-Heartbeat
(16 Sprachen, neuer i18n-Key BADGE_DESYNCED/TOOLTIP_PEER_DESYNCED)

Tests: H-5-Reject-Unicast, M-4-Debounce, Desync-Heartbeat-Relay
2026-06-26 13:09:34 +02:00
KoalaDev 69f8f4cfd7 fix(extension): trust HOST_BLOCKED as authoritative (join race, EC-5)
hcmHandleBlocked no longer re-checks local control mode before acting. Background
only sends HOST_BLOCKED to a gated guest, so the message itself is authoritative —
adopt host-only/guest role from it. Fixes the join race where a HOST_BLOCKED
arriving before the CONTROL_MODE broadcast was silently ignored.

Also corrects the EC-4 note: "let catch-up re-sync" is invalid (sync is
event-driven, no continuous catch-up); the right fix is a buffer-aware deferred
snap-back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:01:04 +02:00
KoalaDev af9cf34f0b fix(extension): pre-test self-audit fixes for host control mode
- popup: lock remote-control buttons (Play/Pause/SYNC) for guests in host-only
  mode so clicks don't silently get gated and leave the button stuck; backstop
  guards in the handlers and the 2.5s safety reset respects the lock.
- content: rebuild the desync dialog + resync badge with the DOM API inside a
  Shadow DOM. innerHTML inline style="" attributes are stripped by strict
  style-src CSP (Netflix/YouTube/Disney+); CSSOM .style is CSP-safe and the
  shadow root isolates from page CSS.
- content: detect live-DVR in hcmIsLive() via a sliding seekable window
  (seekable.start(0) > 1), not just duration === Infinity (EC-15).

Records remaining audit items (snap-back thrash, join race, dialog i18n) in the
edge-case log for the device-testing pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:54:45 +02:00
KoalaDev b1ea280f0b docs: mark host control mode implementation status
All five layers implemented; automated checks green. Records what still needs
real-device testing (EC matrix) and what was deferred by decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:15:25 +02:00
KoalaDev 9006570bd1 feat(extension): host control toggle + role badge in popup (step 5)
Final wiring of host control mode — the user-facing UI:

- Active-room section gains a Host Control card: a Host/Guest role badge, a
  host-only toggle ("Only I can control playback") shown to the host, and a
  guest notice when the room is host-only.
- Toggle sends SET_CONTROL_MODE; UI refreshes from the server's CONTROL_MODE
  broadcast (with optimistic revert on failure). Card updates on GET_STATUS,
  CONTROL_MODE, and reconnect.
- i18n: 6 new keys (LABEL_HOST_CONTROL[_TOOLTIP], LABEL_HOST_ONLY_TOGGLE,
  NOTICE_HOST_CONTROLS, BADGE_HOST, BADGE_GUEST) translated across all 15
  popup locales.

Completes the feature end to end (server + background + content + popup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:14:55 +02:00
KoalaDev f9dc9d4017 feat(extension): host control mode guest UX in content script (step 4)
Guest-side handling when a room is in host-only mode:

- Intent classifier (EC-9): tells a deliberate guest pause/seek from an
  involuntary one (buffering/seeking/tab-refocus/no recent gesture) via
  readyState, video.seeking, a 'waiting' buffering-grace window, the existing
  visibility grace, and own-tracked keydown/pointerdown gestures.
- HOST_BLOCKED handler: live streams are left alone (EC-15); involuntary events
  snap back silently; deliberate events show a Teleparty-style choice dialog
  (Stay in sync / Watch on my own), defaulting to stay after 8s (EC-18).
- Snap-back reuses tryMediaAction + suppress, with a cooldown to avoid
  pause/play fight loops (EC-4).
- Desync ("watch on my own") mode: persistent resync badge, ignores host
  commands while solo (still ACKs so force-sync doesn't stall), and auto-clears
  on episode change (EC-12). Resync pulls the host's extrapolated position.
- background: GET_CONTROL_MODE (init fetch) + REQUEST_HOST_SYNC (resync target).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:10:15 +02:00
KoalaDev 7b37d783b5 feat(extension): host control mode wiring in background (step 3)
Background service worker now understands room control mode (no UI yet):

- Track controlMode + hostPeerId + amHost(); adopt from ROOM_DATA, update on
  CONTROL_MODE, restore from session storage, reset on leave/room-switch.
- Receiver-side gate: ignore room-moving events from non-host peers in host-only
  mode (backstop for old/buggy/modified clients past the server gate).
- Sender-side gate: a guest's play/pause/seek/force-sync is not broadcast;
  instead a HOST_BLOCKED message is sent to the content tab with an extrapolated
  host sync target (hook for step 4's snap-back + desync dialog).
- broadcastControlMode() notifies popup + content tab; GET_STATUS exposes
  controlMode/hostPeerId/amHost; new SET_CONTROL_MODE message lets the host toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:04:54 +02:00
KoalaDev 9f4be58172 feat(server): host control mode — room state, gate, toggle & host fallback
Step 2 of host control mode (server-side, fully testable without UI):

- Room now tracks hostPeerId (= first joiner) and controlMode ('everyone' default).
- ROOM_DATA carries hostPeerId + controlMode so clients know their role on join.
- SET_CONTROL_MODE handler: host-only authority check, broadcasts CONTROL_MODE.
- Relay gate: in host-only mode, drop room-moving events (play/pause/seek/
  force-sync/episode-lobby) from non-host guests — robust chokepoint that kills
  spam regardless of client. Heartbeats/ACKs/episode-ready still pass.
- removePeerFromRoom: if the host leaves, fall back to 'everyone' and reassign
  host to the earliest remaining peer (v1: immediate, no grace period).

Extends the WS integration test suite to cover all of the above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:30:57 +02:00
KoalaDev a968f708b3 feat(protocol): add host control mode events & constants
Step 1 of host control mode. Adds the protocol surface only — no behavior yet:
- EVENTS.SET_CONTROL_MODE (client->server: host sets mode)
- EVENTS.CONTROL_MODE   (server->client: mode changed)
- CONTROL_MODES { EVERYONE, HOST_ONLY }

Purely additive; old clients ignore the new events. Propagated to
extension/shared via build-extension. Also records the audited design
decisions in the edge-case log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:27:19 +02:00
KoalaDev 2995afe970 docs: add host control mode branch overview, plan & edge-case log
Planning docs for the optional per-room Host Control Mode (Teleparty-style
host-only playback control) requested via GitHub issue.

- host-control-mode-EDGECASES.md: canonical branch entry-point — goals, scope,
  trust model, architecture summary, 21 edge cases, test matrix, open decisions.
- host-control-mode-plan.md: layered implementation plan with concrete code hooks
  (server room state, three-layer gate, snap-back, guest desync flow, UI/i18n).

Temporary working docs for this branch; to be folded in or removed before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:22:26 +02:00
KoalaDev 68027a21d5 fix: use absolute /version.json path to work on all page depths
The old logic only handled en='version.json' and non-en='../version.json',
which broke on /alternatives/ and /de/alternatives/ pages.
Absolute path works for root, /de/, /alternatives/, /de/alternatives/ etc.
2026-06-25 04:29:24 +02:00
KoalaDev 4aa6f6d7a6 fix: replace deprecated apple-mobile-web-app-capable with mobile-web-app-capable
Chrome 79+ shows deprecation warning for the apple- prefix.
Main template already uses the correct name; alternatives templates
were missed.
2026-06-25 04:28:41 +02:00
KoalaDev 7602bfdb21 fix: preserve language in cross-links, add og:locale + Organization schema to alternatives
- Fix cross-links on teleparty↔screen-sharing pages: use
  {{LANG_PREFIX}} so German page links stay German, etc.
- Add og:locale meta tag with correct locale code per language
  (e.g. en_US, de_DE, fr_FR, ja_JP, zh_CN, uk_UA)
- Add Organization schema.org JSON-LD block alongside
  existing BreadcrumbList on all 3 alternatives templates
2026-06-25 04:21:02 +02:00
KoalaDev 3ccb967a45 fix(l10n): add missing zh/uk language dropdown options to alternatives pages
- Add 中文 (Chinese) and Українська (Ukrainian) to the language
  selector in index, teleparty, and screen-sharing templates
- Previously only 13 of 15 languages were listed in the dropdown
2026-06-25 04:08:06 +02:00
KoalaDev fe970de39a fix(seo): add missing sitemap locale entries, swap KoalaSearching→RemoteProf on alternatives index
- Add 39 missing language-specific sitemap entries for fr/es/it/.../uk
  alternatives pages (index, teleparty, screen-sharing)
- Replace KoalaSearching image with RemoteProf (professor koala)
  on the Guides & Comparisons overview page
- Update og:image, twitter:image, and img src accordingly
2026-06-25 03:50:00 +02:00
Timo e795339924 Add ALT_* translations for ja, ko, zh + rebuild assets
Replaced English fallback with proper Japanese, Korean, and Chinese
translations for all 27 ALT_* website locale keys. All 15 locale
files now fully translated; npm run verify and build pass cleanly.
2026-06-25 03:00:02 +02:00
KoalaDev 06b5ffbd0b feat(alternatives): add Teleparty & Screen Sharing comparison pages with i18n
- Add article-style comparison pages for Teleparty and Screen Sharing
- Replace comparison tables with honest prose sections
- Add 15-language hreflang alternates and sitemap entries
- Add English locale fallback for untranslated languages
- Replace placeholder Draft copy with real English and German text
- Remove old Draft locale keys from all 13 non-EN/DE locales
- Rebuild outputs with zero TODO/Draft/noindex issues
2026-06-25 00:12:04 +02:00
GitHub Action 24cac52674 chore(release): update versions to v2.4.6 [skip ci] 2026-06-23 16:36:23 +00:00
KoalaDev 6276ee134c fix(ui): Correct false idle state on connection errors
Bump version to 2.4.6
2026-06-23 18:36:05 +02:00
GitHub Action d847389eef chore(release): update versions to v2.4.5 [skip ci] 2026-06-23 15:40:16 +00:00
KoalaDev aa84b63c77 docs: add v2.4.5 entry to CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:38:36 +02:00
KoalaDev bd60a14754 feat(extension): send nothing to the server while alone in a room
When no other peer is present, heartbeats, force-sync, and episode auto-sync
are now fully suppressed (previously the keepAlive heartbeat, force-sync, and
episode lobby were still broadcast to an empty room):
- keepAlive heartbeat gated on a live peer count
- force-sync is a no-op when solo (no pause-then-timeout freeze, no traffic)
- episode auto-sync skips the lobby entirely and just plays the next episode

The solo state is recomputed live from the current peer list on every event
(never cached), so the instant another peer joins syncing resumes. On that
solo->not-solo transition the background also requests an immediate heartbeat
from the content script so the newcomer sees the current position without
waiting for the next interval.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:38:36 +02:00
KoalaDev a1bdcf4325 fix(extension): stop storing room/settings in chrome.storage.sync
Room ID, password, and username were resurrected from synced storage on a
fresh install (sync survives an uninstall in the user's Google account), which
made the extension silently auto-connect to a dead room and appear permanently
connected. getSettings() and all settings reads (popup, content, audio-options,
applyAudioSettingsToTab) are now local-only, and legacy keys are actively
purged from sync on install/update/startup. Only onboardingComplete and
dismissedHints remain in sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:38:36 +02:00
KoalaDev b846803062 Potential fix for code scanning alert no. 14: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-23 00:56:28 +02:00
KoalaDev 4bc7ad365d docs: add v2.4.4 entry to CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:49:42 +02:00
GitHub Action 68b2205b0d chore(release): update versions to v2.4.4 [skip ci] 2026-06-22 22:46:03 +00:00
KoalaDev b450845522 fix(extension): harden reconnect, ping, and offline-queue flush
Address client-side rate-limit edge cases that could trip the server's
per-socket event budget or per-IP connection limit:

- Pace the offline event-queue flush after (re)connect in batches
  (10 per 3s) instead of one synchronous burst, so a reconnect after a
  long outage no longer dumps the whole backlog and gets kicked.
- Wrap socket.send() in try/catch and re-queue on failure (race with a
  server-side disconnect) instead of losing the event.
- Ping liveness: tolerate one missed PONG; only force a reconnect after
  2 consecutive misses (~20s) instead of a single hard 5s timeout.
- Reconnect backoff: tune to ~8 attempts/60s (under the connection limit)
  and add +/-20% jitter to de-synchronize reconnect herds.

All magic numbers extracted into named constants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:29 +02:00
KoalaDev 27023ea58e feat(server): raise event rate limit to 50 and name rate-limit constants
Raise the per-socket event budget from 30 to 50 per 10s to give legitimate
bursts (episode joins, force-sync, ACK flurries) more headroom. Extract the
previously inline connection/event/health/admin windows and limits into named
constants (CONNECTION_RATE_LIMIT, EVENT_RATE_LIMIT, *_WINDOW_MS). Tests now
derive their thresholds from the exported constants instead of hardcoding them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:19 +02:00
KoalaDev 1a7d23ce93 ci: add PR/push verification workflow and harden release
- Add .github/workflows/ci.yml running `npm run verify` (lint, tests,
  audits, builds) on every push to main and pull request, so regressions
  can't reach main or a release tag unchecked.
- release.yml: use `npm ci` instead of `npm install` for reproducible builds.
- package.json: add `test` script aliasing the verify suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:10 +02:00
KoalaDev d2c8ca1c1e Merge PR #11: extract tab management logic into independent module
3-way merge of Kaia-Alenia's tab-manager refactor (PR #11) onto current
main (v2.4.3). Only the tab-related service-worker listeners move out of
background.js into extension/modules/tab-manager.js via initTabManager({...})
dependency injection; all other recent main changes are preserved untouched.
Behavior-preserving; listeners still register synchronously at top level.

The unrelated website/assets/NewLogoIcon_64.webp change from the PR was
excluded to keep main clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:06:38 +02:00
Kaia-Alenia 1c404a7f11 refactor(extension): extract tab management logic into independent module 2026-06-19 14:11:47 -06:00
Timo e2bba04efd docs: update README language count to 15 (add zh, uk)
The localization section still listed 13 languages; Ukrainian (uk) and
Chinese Simplified (zh) were added in v2.4.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:49:34 +02:00
GitHub Action c7b29c277c chore(release): update versions to v2.4.3 [skip ci] 2026-06-19 14:39:06 +00:00
Timo e06965b95c docs: add v2.4.3 entry to CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:38:49 +02:00
Timo 23d43741ce chore: replace Ko-Fi with Support links, drop uninstall token
Switch the Ko-Fi support links to https://support.koalastuff.net across the
README badge, the extension footer (popup.html/template.html, label
"Support KoalaSync") and remove the now-unused KOFI_URL constant. Stop
generating/sending the anonymous uninstall token: initUninstallURL() now
registers the feedback URL without the `t` parameter or storage write.
CHANGELOG updated to match.

Also guard the client CMD_ACK path: only ACK a command sender that is still
a known peer in the room, so we don't emit ACKs to peers that already left
(which the server now drops as absent-peer ACKs anyway).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:54 +02:00
Timo 027d2dbde5 fix(sync): coalesce play/pause bursts and quiet absent-peer ACK logs
Media players fire bursts of native play/pause events (source swaps, ABR,
ads, teardown). Each was relayed as a distinct command, spamming peers and
tripping the relay's event rate limit. Add leading+trailing coalescing in
the content script: emit the first event instantly (zero added latency) and,
on a 150ms window, collapse a burst to its final state. Echo-suppression and
seek-flush stay synchronous on event arrival; the trailing send is never
deduped against the leading one (a remote command may change shared state
mid-window — re-sending is the safe, idempotent choice).

Server: split the EVENT_ACK handler's logging so only a genuine different-room
ACK is logged as [SECURITY]; an ACK to a peer that already left is logged
quietly as [ACKDROP] (verbose-only), so real signals are no longer drowned out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:31 +02:00
Timo a0c16df664 feat(i18n): add Ukrainian & Chinese, fix zh/uk translation quality
Add two new languages (uk, zh / zh_CN) across the extension and website:
register them in SUPPORTED_LANGUAGES, the Chrome _locales map, the website
build/lang-init/app routing, hreflang/og/schema tags and language selectors.

Fix systematic machine-translation word-sense errors in the new zh/uk
locales (e.g. zh "Status"=地位→状态, "Leave Room"=留出空间→离开房间,
"Play"=玩→播放, "Seek"=寻找→跳转, audio Attack/Release/Knee; uk "Play"=Грати
→Відтворити, "Clear"=ЯСНО→Очистити, "Open"=ВІДЧИНЕНО→Відкрити, peers
"Однолітки"→"Учасники"), translate remaining English leftovers (Room ID,
Custom) and normalise terminology. All 15 locales pass test-locales.cjs.

Note: popup.html and website/template.html also carry the language-selector
additions here; their Ko-Fi→Support label text is part of the branding change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:20 +02:00
KoalaDev 8fd866f20a feat(website): add canonicals, hreflangs, meta descriptions, and localized schema descriptions 2026-06-19 12:10:08 +02:00
KoalaDev 8f72c238a2 docs: add v2.4.2 entry to CHANGELOG.md 2026-06-19 11:58:06 +02:00
GitHub Action 24ad7c3732 chore(release): update versions to v2.4.2 [skip ci] 2026-06-19 09:54:42 +00:00
KoalaDev 32fd677a31 feat: optimize extension uninstall URL registration with anonymous token and startup hardening 2026-06-19 11:54:07 +02:00
KoalaDev fb22700268 docs: revert completed milestones in ROADMAP.md 2026-06-19 11:39:50 +02:00
KoalaDev 59b675ce17 docs: audit and update script references, supported languages, and completed milestones 2026-06-19 11:35:49 +02:00
KoalaDev 8a9293280b chore: harden version substitution in release workflow and fix supply chain security version in README 2026-06-19 11:12:48 +02:00
KoalaDev 450f715874 ci(release): update actions to Node 24 versions & optimize workflow
- Bump all actions off the deprecated Node 20 runtime: checkout v4->v7,
  upload-artifact v4->v7, setup-node added@v6, action-gh-release v1->v3,
  docker build-push v5->v7, login v3->v4, metadata v5->v6, buildx v3->v4
  (verified latest majors via GitHub API; no breaking input/output changes)
- Add npm dependency caching via setup-node (cache: npm)
- Add GHA layer caching for the multi-arch Docker build (cache-from/to gha)
- Add concurrency group so a release run is never interrupted, only dedupes
  re-pushes of the same tag
- Tighten release-server permissions to contents: read (it never writes to the repo)

Version-injection and commit-back logic unchanged. First exercised on the next v* tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:59:30 +02:00
GitHub Action b4c00f36a0 chore(release): update versions to v2.4.1 [skip ci] 2026-06-19 08:50:27 +00:00
KoalaDev 36f494fa55 feat(extension): one-click invite button in empty peer list + v2.4.1 changelog
- "No peers yet" state now shows a localized "Copy invite link" button (reuses BTN_COPY_INVITE, present in all 13 locales) so users can share without hunting for the field
- Add v2.4.1 changelog entry covering the onboarding tour improvements, empty-state action, and the mobile landing-page fixes from this session

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:27:07 +02:00
KoalaDev 3ebe80aab2 refactor: replace dynamic GitHub API badges with static shields.io badges 2026-06-17 06:39:42 +02:00
Timo e7b59d61ac docs: update all docs for lazy-connect, refactor, and .cjs rename
- ARCHITECTURE.md: startup -> lazy connect, reconnect when in room
- HOW_IT_WORKS.md: on-demand connection, heartbeat while connected
- PRIVACY.md: clarify alarms only during active sessions
- SYNC_GUIDE.md: build-extension.cjs + episode-utils injection
- StoreDescription.md: on-demand relay, no persistent connection
- README.md + extension/README.md: npm run build:extension
- extension/README.md: replace Dual Heartbeat with On-Demand Connection
- website/privacy.html: add lazy-connect privacy note
2026-06-16 14:17:19 +02:00
GitHub Action 71f7ac425a chore(release): update versions to v2.4.0 [skip ci] 2026-06-16 12:00:03 +00:00
Timo 394b9ba474 Merge branch 'refactor/split-monoliths' 2026-06-16 13:58:32 +02:00
Timo 526736b045 fix(ci): update website build path to .cjs after rename 2026-06-16 13:54:57 +02:00
Timo 86112057b4 test: add names generator unit tests (getAvatarForName + generateUsername)
Tests: 20 deterministic avatar assertions (exact match, case insensitive,
longest-match-wins, ZWJ sequences, fallback), 30 randomized username format
checks, and validation that every noun in the list has an emoji mapping.
2026-06-16 13:53:19 +02:00
Timo c56fcfd281 test(server): add 13 WebSocket integration tests covering all server features
Tests: connect, room create/join, password auth, play/pause/seek relay,
force_sync, event_ack, episode_lobby, peer leave, ping/pong, room list,
protocol version check, deduplication, health HTTP endpoint.
All 13 tests pass as part of npm run verify.
2026-06-16 13:48:21 +02:00
Timo 5c43fc6236 docs: add v2.4.0 changelog entries for lazy-connect and refactor 2026-06-16 13:23:53 +02:00
Timo 101761e984 chore: add type:module, fix npm audit, rename CJS files, document module structure
- package.json: add type: module to silence ESM warnings
- server: npm audit fix (0 vulnerabilities, ws vuln resolved)
- Rename 4 CJS files to .cjs: build-extension, test-content-video-finder,
  test-locales, website/build
- Update eslint.config.mjs for .cjs glob patterns
- extension/README.md + server/README.md: document module structure
2026-06-16 13:22:45 +02:00
Timo 3bc68a5713 test: add unit tests for rate-limiter and episode-utils
- test-rate-limiter.mjs: 12 test groups covering all rate-limit
  functions, Maps, cleanup, and double-start guard
- test-episode-utils.mjs: 30+ assertions covering SxxExx patterns,
  6 separator types (dash/dot/slash/colon/comma/space), German/English,
  edge cases (null/undefined/empty), and sameEpisode matching logic
- verify-release.mjs: added 2 test suites + 1 syntax check to pipeline
2026-06-16 13:15:25 +02:00
Timo 6c96dd6344 chore(refactor): remove orphaned comment, add double-start guard to rate-limiter 2026-06-16 13:10:37 +02:00
Timo d7bb8dc97c refactor(extension): extract episode-utils from background.js + content.js
Move extractEpisodeId() and sameEpisode() to shared episode-utils.js.
background.js imports via ES module. content.js (IIFE) receives injected
copy via build-extension.js marker replacement, eliminating duplication.
2026-06-16 12:48:11 +02:00
Timo d38a840114 refactor(server): extract rate-limiter module from index.js
Move 6 rate-limit functions, all rate-limit Maps, cleanup intervals,
and denial counter to server/rate-limiter.js. 149 lines extracted.
Index.js re-exports what tests and ops.js need.
npm run verify passes.
2026-06-16 12:44:43 +02:00
Timo e7d2d76ba3 fix(eslint): add missing browser globals history and location 2026-06-16 10:50:40 +02:00
Timo 5277cd0f21 fix(eslint): add missing browser globals history and location 2026-06-16 10:50:32 +02:00
Timo 4d7cbfe347 Merge branch 'feature/lazy-connect' 2026-06-16 10:47:46 +02:00
Timo 91aa76e842 fix: revert premature isConnecting reset + remove popup disconnected flicker
- background.js: remove isConnecting=false at line 608 (was set before
  WebSocket handshake completed, defeating the guard for the entire
  CONNECTING phase). isConnecting is already properly reset in onclose,
  onopen/40 handler, catch block, and socket guard path.
- background.js: restore isConnecting=false in forceDisconnect() so
  subsequent connect() calls can proceed after intentional disconnect.
- popup.js: remove hardcoded applyConnectionStatus('disconnected') that
  caused a guaranteed red flicker on every popup open. Status is now
  set only by the async GET_STATUS response.
2026-06-16 10:44:57 +02:00
Timo 5f0a189451 fix(popup): trigger CONNECT when popup opens with saved roomId
If user has a room configured but the background is not connected
(e.g. service worker was killed, startup race, or stale state), the
popup now sends CONNECT on open. Previously it only called GET_STATUS
and showed 'disconnected' with 0 logs — the user had no way to know
whether the extension was working.
2026-06-16 10:35:13 +02:00
KoalaDev 312d4f2cf7 chore: update sitemap lastmod dates, automate in release CI
- Update all sitemap <lastmod> entries from 2026-06-09 to 2026-06-16.
- Add sed step in release workflow to auto-update sitemap dates on every
  tagged release, so the sitemap never goes stale.
2026-06-16 06:11:13 +02:00
KoalaDev 884feb982d fix(website): smooth anchor scrolling, language-switch scroll position, lang select UX
- Replace JS scrollIntoView handler with CSS scroll-behavior:smooth for
  native anchor navigation. Fixes #hash-based scroll position surviving
  language switches since the hash stays in the URL.
- Auto-update URL hash via IntersectionObserver as user scrolls through
  sections, so manual scrolling also sets the anchor for language switch.
- Preserve window.location.hash across all language-switch navigations.
- Fix Chrome double-arrow on language select by limiting appearance:base-select
  to ::picker(select) only, keeping the custom SVG chevron.
- Make chevron absolutely positioned over the select with pointer-events:none
  so clicks anywhere on the container open the dropdown.
- Remove max-width constraint on language select so longer names fit.
- Add scroll-margin-top to section[id] and header[id] so anchored sections
  clear the fixed navbar (100px offset).
- Add header[id] to scroll-margin-top for keyboard skip-link accessibility.

Thanks to Taradal from Reddit
2026-06-16 06:05:49 +02:00
KoalaDev b2da17ab62 fix(lazy-connect): harden connection lifecycle against race conditions and edge cases
- Prevent concurrent connect() by removing isConnecting reset from
  forceDisconnect(); the guard in connect() now reliably blocks
  re-entry since no external caller can defeat it.
- Reset isConnecting at end of connect() so subsequent calls can
  proceed without waiting for the async '40' handler.
- Clear connectIntent and cancel reconnect timer on server ERROR
  when no currentRoom exists, preventing infinite reconnect loops
  after failed join attempts (e.g. wrong password via invite link).
- Move connectIntent assignment after !roomId guard in
  WEB_JOIN_REQUEST to avoid clobbering existing intent on invalid
  input.
- Broadcast JOIN_STATUS failure to popup and bridge tabs when
  WEB_JOIN_REQUEST receives an invalid room ID, so the website
  join page receives feedback instead of hanging forever.
- Clear joinBtnTimeout on CONNECTION_STATUS connected/disconnected
  to avoid stale timeout error messages in the popup.
2026-06-16 05:39:17 +02:00
KoalaDev b518685e2c merge: bring main (v2.3.2) changes into lazy-connect 2026-06-16 04:59:13 +02:00
GitHub Action b7a7a14a35 chore(release): update versions to v2.3.2 [skip ci] 2026-06-16 02:29:41 +00:00
KoalaDev 9ee22d985f docs: remove transient _locales fix from changelog (never released) 2026-06-16 04:28:30 +02:00
KoalaDev 2070e701de docs: add v2.3.2 changelog entry (translations, locale fixes, timing leak) 2026-06-16 04:26:07 +02:00
KoalaDev dd8eefe3f9 fix(security): actually prevent timing leak — eagerly evaluate timingSafeEqual
The previous fix had a subtle bug: JavaScript && short-circuits,
so crypto.timingSafeEqual() was skipped when buffer lengths differed
(sameLength was false). The dummy buffer was allocated but never
compared, leaving the original length-based timing leak intact.

Now timingSafeEqual is eagerly assigned to a const before the &&
guard, guaranteeing it runs in constant time on every auth attempt
regardless of whether the length guess was correct.
2026-06-16 04:23:20 +02:00
KoalaDev cc97e0d371 fix(security): prevent admin token length leak via timing side-channel
isAdminMetricsAuthorized() returned early when buffer lengths differed,
allowing an attacker to discover the token length by measuring response
time. Now always calls crypto.timingSafeEqual (using a zeroed dummy
buffer on length mismatch) so every auth attempt takes constant time
regardless of whether the length guess was correct.

Reported-by: Kaia-Alenia
2026-06-16 04:21:15 +02:00
KoalaDev 7571f1986d fix: restore _locales key names, remove orphan pt/ dir, fix locale typos
- _locales/es, pt_BR: renamed appDescription → appDesc (Chrome matches by key name)
- _locales/es, pt_BR: removed extra key contextMenu_syncVideo not in baseline
- _locales/pt_PT: merged Kaia's improved translation, deleted orphan pt/ duplicate
- locales/ko: fixed BTN_REFRESH (Refreschi → 새로고침) and STATUS_CONNECTING corrupted char
- locales/ja: fixed Korean character (의 → の) in LABEL_PUBLIC_ROOMS_TOOLTIP
- locales/nl: fixed typo cmmuniceren → communiceren
2026-06-16 04:19:53 +02:00
KoalaDev bb1a88c085 Merge pull request #8 from Kaia-Alenia/feat/complete-spanish-translation
i18n: complete and refined Spanish translation for extension and website
2026-06-16 03:43:54 +02:00
Kaia-Alenia 07863bdad6 docs: credit Alenia Studios for PT and PT-BR translations 2026-06-15 13:10:55 -06:00
Kaia-Alenia 5e6306432f i18n: complete and refined Portuguese (PT and BR) translations 2026-06-15 13:10:35 -06:00
Kaia-Alenia fa5dfc1cf1 docs: credit Alenia Studios for ES and IT translations 2026-06-15 11:19:52 -06:00
Kaia-Alenia b88b8bbe47 i18n: complete and refined Italian translation 2026-06-15 11:18:46 -06:00
Kaia-Alenia 8aab8ce2f5 docs: shorten Spanish translation context 2026-06-15 11:11:23 -06:00
Kaia-Alenia 95b4608236 docs: update Spanish translation status to 100% manually verified 2026-06-15 11:07:35 -06:00
Kaia-Alenia 6d42ee7d4c i18n: complete and refined Spanish translation for extension and website 2026-06-15 11:03:57 -06:00
Timo 3f8cf33dc9 fix: WEB_JOIN_REQUEST sendResponse leak + popup validation state cleanup
- WEB_JOIN_REQUEST: add missing sendResponse when already in target room
- popup join: reset isProcessingConnection + clear timeout on validation failures
2026-06-15 14:40:15 +02:00
Timo f12bb0a532 fix(extension): reset reconnectAttempts on leave to prevent stale reconnecting status
After LEAVE_ROOM or idle-leave, reconnectAttempts was not reset.
GET_STATUS would return 'reconnecting' instead of 'disconnected',
showing wrong status in popup and leaving Retry button visible.
2026-06-15 14:22:17 +02:00
Timo 1685b6a327 fix(popup): join timeout checks connection status instead of blind 15s timer
Lazy-connect introduces a new state where the user clicks 'Raum erstellen'
and the WebSocket takes 1-2s to establish (previously always pre-connected).
The old 15s blind timeout would re-enable the button while background was
still connecting, causing silent no-op clicks. Now polls GET_STATUS and
extends the window if still connecting.
2026-06-15 14:19:18 +02:00
Timo ec8f56a85d feat(extension): lazy-connect — only maintain WebSocket when actively in room
- Add connectIntent flag to gate all reconnect attempts
- Only auto-connect on startup if roomId exists in storage
- Close socket + stop reconnect after leaveRoom or idleLeave
- Preserve existing behavior 1:1 when actively in a room
- Guard force-sync state and peer list clearing during transient disconnects
- Guard WEB_JOIN_REQUEST against empty sanitized roomId
2026-06-15 14:08:29 +02:00
Timo 38dc923a7c Merge main (v2.3.1) into feature/lazy-connect 2026-06-15 13:30:57 +02:00
KoalaDev 4ca1ef22d2 Revise CHANGELOG for v2.3.1 updates
Updated changelog for version 2.3.1 with fixes and changes.
2026-06-15 13:17:10 +02:00
Timo 27e57862c0 docs: shorten v2.3.1 changelog entry 2026-06-15 13:15:01 +02:00
GitHub Action c391068706 chore(release): update versions to v2.3.1 [skip ci] 2026-06-15 11:14:23 +00:00
Timo d76e9195c4 fix(server): race condition on concurrent peer joins, crash-safe teardown, smart unhandled rejection handling
- Add per-peerId serialization lock (peerJoinLocks) to prevent concurrent dedupe races

- Wrap removePeerFromRoom calls in disconnect/leave/reaper with try/catch

- Replace immediate process.exit on unhandledRejection with rate-limited smart exit

- Optimize buildHealthPayload from 3-pass array ops to single for-of loop

- Reset rateLimitDenied counters in stopServerForTests

Release v2.3.1
2026-06-15 13:14:00 +02:00
KoalaDev 6652a06840 docs: correct rationale for english and german translations 2026-06-15 02:56:37 +02:00
KoalaDev b1a89cff41 docs: restructure TRANSLATION.md to prioritize extension and remove outdated details 2026-06-15 02:55:00 +02:00
KoalaDev e4a77a3ef4 docs: improve contribution and translation guidelines 2026-06-15 02:47:17 +02:00
KoalaDev 5adcce2074 Release v2.3.0 with new onboarding tour and sync tab
Updated CHANGELOG for v2.3.0 release with new features.
2026-06-14 05:31:27 +02:00
GitHub Action a1398ed0e4 chore(release): update versions to v2.3.0 [skip ci] 2026-06-14 03:17:46 +00:00
Timo ed80856803 fix: restore missing Create Room onboarding step and target h1 for welcome step 2026-06-14 05:10:57 +02:00
Timo 102031e0d2 style: swap restart onboarding and regenerate peer ID buttons in settings layout 2026-06-14 05:03:27 +02:00
Timo 503c7d6dc4 chore: update extension uninstall URL and fix missing onboarding restart translations 2026-06-14 05:02:31 +02:00
KoalaDev 77793c8c6e feat(extension): setup cross-browser uninstall url architecture 2026-06-13 21:01:31 +02:00
KoalaDev 4fbf309e5a fix(extension): resolve critical race conditions and infinite seek loop
- Fix infinite seek loop by transitioning to exact expectedSeekTime tracking
- Fix zombie connections by calling forceDisconnect on ping timeouts
- Fix popup Join/Leave race conditions using isProcessingConnection lock
- Fix cross-room join bugs by properly disconnecting old rooms and bypassing same-room joins
- Update onboarding UI to switch to Sync tab automatically
- Prepare CHANGELOG.md for v2.3.0 release
2026-06-13 10:50:46 +02:00
KoalaDev aa61a24351 Add ROADMAP.md with feature tracking and link in README 2026-06-13 06:35:56 +02:00
Timo 2ee5c83ee6 docs: add v2.2.5 unreleased changelog entries 2026-06-13 04:54:57 +02:00
KoalaDev ca1cfdb382 Merge pull request #7 from Shik3i/dependabot/npm_and_yarn/npm_and_yarn-53cbaf2a5b
chore(deps-dev): bump esbuild from 0.28.0 to 0.28.1 in the npm_and_yarn group across 1 directory
2026-06-13 04:53:43 +02:00
Timo ef7b1f2e5f fix: suppress video heartbeat PEER_STATUS when alone in room 2026-06-13 04:52:14 +02:00
Timo 10fdaa23fc fix: propagate audio settings from local storage and add compressor feedback 2026-06-13 04:48:25 +02:00
Timo 6ba5e1b10b Increase failedAuthAttempts eviction limit from 50k to 200k 2026-06-13 04:38:22 +02:00
dependabot[bot] 6e234fb8fd chore(deps-dev): bump esbuild
Bumps the npm_and_yarn group with 1 update in the / directory: [esbuild](https://github.com/evanw/esbuild).


Updates `esbuild` from 0.28.0 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-13 02:07:21 +00:00
GitHub Action 9f553b4f8f chore(release): update versions to v2.2.4 [skip ci] 2026-06-09 23:42:27 +00:00
Timo 131afadc1d v2.2.4: Fix notification setting bypass and misleading reconnect message
- EVENTS.ERROR handler now respects browserNotifications setting
- Graceful shutdown message no longer implies manual reconnect
2026-06-10 01:33:45 +02:00
GitHub Action c62da66b06 chore(release): update versions to v2.2.3 [skip ci] 2026-06-09 22:56:52 +00:00
Timo 34f0c2b265 chore(release): v2.2.3 — artifact attestations, bugfixes, rate limit metrics 2026-06-10 00:56:29 +02:00
Timo af59b4c64c ci(release): add website build + artifact upload on tag 2026-06-09 22:03:38 +02:00
Timo af8184420c chore: stop tracking website/www/ (auto-generated) 2026-06-09 22:02:39 +02:00
Timo 45e1e3defe chore: add website/www/ to gitignore 2026-06-09 22:01:48 +02:00
Timo e67b0c564d fix(website): copy apple-touch-icon files to www root for iOS/Safari 2026-06-09 22:00:31 +02:00
GitHub Action f54a38b656 chore(release): update versions to v2.2.2 [skip ci] 2026-06-09 19:16:15 +00:00
Timo 5216fde641 fix: prevent hero title descender clipping, replace <br> with styled <span> for second line across all locales 2026-06-09 21:14:55 +02:00
Timo 39fe2cbd11 fix: prevent hero title descender clipping by adding padding-bottom to h1 2026-06-09 20:55:18 +02:00
Timo 03d5dbda66 website: add Open Source (MIT) feature card, bump Volume Leveling to bento-large, add footer disclaimers, update translations for all 13 locales, bump version badge to v2.2.1 2026-06-09 20:53:18 +02:00
KoalaDev 0c0cbbc090 feat: add audio compressor to comparison table, fix schema version + languages, update sitemap, add version injection to workflow 2026-06-09 19:26:50 +02:00
KoalaDev 0f4ad39fb4 feat: add Chrome _locales for store i18n, fix copy logs alignment, extend locale test 2026-06-09 19:14:30 +02:00
KoalaDev cfbb070c57 feat: add audio compressor feature to website (Card 7, FAQ Q7/A7) across all 13 locales 2026-06-09 19:09:10 +02:00
KoalaDev 0caf19cf18 docs: fix CHANGELOG v2.2.0 — remove nonexistent makeup gain slider, correct preset count 2026-06-09 18:31:49 +02:00
KoalaDev ff31f56911 Revise KoalaSync description and core features
Updated the description and features of KoalaSync to emphasize privacy, ease of use, and support for various platforms.
2026-06-09 17:37:34 +02:00
KoalaDev 54d03df0af fix: add ping to copy debug report 2026-06-09 07:15:02 +02:00
GitHub Action 4f1016fa87 chore(release): update versions to v2.2.1 [skip ci] 2026-06-09 05:08:32 +00:00
KoalaDev 3e7afd7592 feat: add server ping display with peer ping foundation
Adds application-level ping/pong between extension and relay server.
Extension sends PING every 15s, server echoes PONG. Round-trip time
displayed in Status tab (green/yellow/red color-coded).

Server also forwards PING with target peerId and routes PONG back,
enabling future peer-to-peer ping without server restart.
Extension already responds to incoming peer PINGs.

See CHANGELOG.md for details.
2026-06-09 07:08:14 +02:00
KoalaDev 6c6d4dc2a2 fix status: Netflix no title, Disney+ partial title 2026-06-09 06:52:07 +02:00
KoalaDev 498d5bd91b clean up TESTED_SERVICES notes and fix contradictions 2026-06-09 06:39:53 +02:00
KoalaDev bfd1177a14 Update Netflix, Disney+, and Prime Video sync status 2026-06-09 06:31:33 +02:00
KoalaDev 15f4f5a12e Add rel="noopener" to all target="_blank" links (tabnabbing fix)
- template.html: 10 links fixed (propagates to 13 language variants)
- join.html, privacy.html, imprint.html, impressum-de.html, datenschutz-de.html
- app.js: 2 window.open() calls updated with 'noopener'
- Rebuilt site with node website/build.js

Preserves HTTP Referer — noreferrer intentionally omitted.
2026-06-09 02:40:03 +02:00
KoalaDev fa39be7d40 Remove noindex from legal pages, add them to sitemap 2026-06-09 01:53:22 +02:00
KoalaDev 5fd00f0bda Restore original attribution line in README.md 2026-06-08 23:13:54 +02:00
KoalaDev dca9c6ed07 Revise audio compressor description in README
Updated audio compressor feature description for clarity.
2026-06-08 21:45:23 +02:00
GitHub Action d8634744ea chore(release): update versions to v2.2.0 [skip ci] 2026-06-08 19:31:38 +00:00
298 changed files with 43392 additions and 26374 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
name: 🚀 Feature Request
about: Suggest a new feature or enhancement for KoalaSync
title: ''
labels: enhancement
labels: feature
assignees: ''
---
+1 -1
View File
@@ -37,7 +37,7 @@ Closes #
- [ ] I have performed a self-review of my code
- [ ] I have added/updated tests if needed
- [ ] I have updated documentation if needed (`docs/`, README, etc.)
- [ ] Protocol changes: I ran `node scripts/build-extension.js` and updated relevant docs
- [ ] Protocol changes: I ran `node scripts/build-extension.cjs` and updated relevant docs
- [ ] No new warnings, secrets, or hardcoded credentials introduced
### Additional Context
+96
View File
@@ -0,0 +1,96 @@
name: Beta Server Image
# Publishes the relay server as a Docker image under NON-production tags so a
# feature branch can be deployed to a staging/backup server and used as a custom
# server, without ever touching the ':latest' tag the official relay tracks.
#
# Tags produced (on ghcr.io/<owner>/<repo>):
# - beta moving channel pointer to the newest build
# - <branch-slug> e.g. feature-textchat
# - sha-<short-commit> immutable, pin to an exact build
# - <custom> only on manual run, e.g. chatbeta01
# Never ':latest' (flavor: latest=false).
#
# Remove this workflow once the feature is merged & released the normal way.
on:
push:
branches:
- feature/textchat
workflow_dispatch:
inputs:
tag:
description: 'Extra immutable tag for this build (e.g. chatbeta01). Optional.'
required: false
default: ''
concurrency:
group: beta-image-${{ github.ref }}
cancel-in-progress: true
jobs:
build-beta-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: |
package-lock.json
server/package-lock.json
- name: Install root dependencies
run: npm ci
- name: Install server dependencies
run: npm ci
working-directory: server
- name: Run verification suite
run: npm run verify
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
# Never publish ':latest' from a beta build.
flavor: |
latest=false
tags: |
type=raw,value=beta
type=ref,event=branch
type=sha,prefix=sha-
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: .
file: server/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+48
View File
@@ -0,0 +1,48 @@
name: CI
# Runs the full verification suite (lint, unit/integration tests, production
# audits, extension + website build) on every push to main and every PR, so a
# regression can never reach main or a release tag unchecked.
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
# Cancel superseded runs on the same ref to save CI minutes. Unlike the release
# workflow, an interrupted CI run has no side effects.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: |
package-lock.json
server/package-lock.json
- name: Install root dependencies
run: npm ci
# The server test suite (test-server-ws/routes/ops) imports express,
# socket.io, and dotenv from server/node_modules, so install them too.
- name: Install server dependencies
run: npm ci
working-directory: server
- name: Run verification suite
run: npm run verify
+68 -11
View File
@@ -5,21 +5,29 @@ on:
tags:
- 'v*'
# A release run must never be interrupted (it commits back to main and publishes
# artifacts). Only dedupe accidental re-pushes of the same tag.
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
release-server:
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
packages: write
id-token: write
attestations: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -27,7 +35,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
@@ -35,7 +43,8 @@ jobs:
type=ref,event=tag
- name: Build and push Docker image
uses: docker/build-push-action@v5
id: build
uses: docker/build-push-action@v7
with:
context: .
file: server/Dockerfile
@@ -43,14 +52,34 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Reuse layers across releases to speed up the multi-arch build.
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate artifact attestation
uses: actions/attest@v4
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
release-extension:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Extract version from tag
id: version
@@ -67,7 +96,7 @@ jobs:
echo " ✓ manifest.base.json -> $VERSION"
# 2. shared/constants.js — APP_VERSION
sed -i "s/export const APP_VERSION = '.*'/export const APP_VERSION = '$VERSION'/" shared/constants.js
sed -i "s/export const APP_VERSION = [\"'].*[\"']/export const APP_VERSION = \"$VERSION\"/" shared/constants.js
echo " ✓ shared/constants.js -> $VERSION"
# 3. package.json
@@ -78,13 +107,26 @@ jobs:
jq -n --arg v "$VERSION" --arg d "$DATE" '{version: $v, date: $d}' > website/version.json
echo " ✓ website/version.json -> version $VERSION, date $DATE"
# 5. website/template.html — SoftwareApplication schema
sed -i "s/\"softwareVersion\": \".*\"/\"softwareVersion\": \"$VERSION\"/" website/template.html
echo " ✓ website/template.html -> softwareVersion $VERSION"
# 6. website/llms.txt — machine-readable release metadata
sed -i "s/Current website release: .*/Current website release: $VERSION/" website/llms.txt
echo " ✓ website/llms.txt -> $VERSION"
# 7. README.md — version badge & banner
sed -i "s|Release-v[0-9]\+\.[0-9]\+\.[0-9]\+-blue|Release-v$VERSION-blue|g" README.md
sed -i "s/New v[0-9]\+\.[0-9]\+\.[0-9]\+ Release/New v$VERSION Release/g" README.md
echo " ✓ README.md -> v$VERSION"
echo "Version injection complete."
- name: Commit and push version updates back to main
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add extension/manifest.base.json shared/constants.js package.json website/version.json
git add extension/manifest.base.json shared/constants.js package.json website/version.json website/template.html website/llms.txt README.md
git commit -m "chore(release): update versions to $GITHUB_REF_NAME [skip ci]" || echo "No changes to commit"
git push origin HEAD:main
env:
@@ -92,11 +134,26 @@ jobs:
- name: Build Extensions
run: |
npm install
npm ci
npm run build:extension
- name: Generate artifact attestation for extensions
uses: actions/attest@v4
with:
subject-path: dist/koalasync-*.zip
- name: Build Website
run: node website/build.cjs
- name: Upload Website Artifacts
uses: actions/upload-artifact@v7
with:
name: website-www
path: website/www/
if-no-files-found: error
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
with:
files: |
dist/koalasync-chrome.zip
+20
View File
@@ -27,6 +27,7 @@ Thumbs.db
# IDEs
.vscode/
.idea/
.claude/
*.swp
*.swo
@@ -40,5 +41,24 @@ coverage/
# the root 'shared/' remains the Single Source of Truth.
extension/shared/
# Auto-generated website build output
website/www/
website/.avif-cache.json
# Temporary scratch files
scratch/
# AI Assistants and Agents
.agents/
.antigravity/
.gemini/
.claude/
.codex/
.opencode/
.cursor/
.cursorrules
.copilot/
.cline/
.clinerules
.windsurfrules
.codeium/
+1 -1
View File
@@ -59,7 +59,7 @@ representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the project maintainer at **koaladev@koalamail.rocks**.
reported to the project maintainer at **koalasync@koalastuff.net**.
All complaints will be reviewed and investigated promptly and fairly.
All project maintainers are obligated to respect the privacy and security of the
+31 -19
View File
@@ -12,7 +12,7 @@ Please note that by participating in this project, you agree to abide by our [Co
|------|-------------|
| **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). |
| **Translations** | Help localize the extension and website into more languages. See [TRANSLATION.md](docs/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. |
@@ -22,7 +22,7 @@ Please note that by participating in this project, you agree to abide by our [Co
### Prerequisites
- **Node.js** v18+
- **Node.js** v20.9+
- **Docker** (for local relay server testing)
### Quick Start
@@ -31,7 +31,7 @@ Please note that by participating in this project, you agree to abide by our [Co
git clone https://github.com/Shik3i/KoalaSync.git
cd KoalaSync
npm install
node scripts/build-extension.js
node scripts/build-extension.cjs
```
---
@@ -62,7 +62,7 @@ node scripts/build-extension.js
### Website
```bash
node website/build.js # Compile static site → www/
node website/build.cjs # Compile static site → www/
python3 -m http.server 8080 -d website/www # Serve locally
```
@@ -77,7 +77,7 @@ KoalaSync uses a **single source of truth** for all protocol constants in `share
> [!IMPORTANT]
> After modifying `shared/constants.js`, you **must** run the build script to sync changes to the extension:
> ```bash
> node scripts/build-extension.js
> node scripts/build-extension.cjs
> ```
> This automatically injects constants into `content.js` and regenerates browser bundles in `dist/`.
@@ -100,15 +100,23 @@ KoalaSync uses a **single source of truth** for all protocol constants in `share
---
## Pull Request Process
## Open Source Workflow & Pull Requests
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).
If you are new to open-source contributions, follow these steps to propose your changes:
1. **Fork the Repository**: Click the "Fork" button at the top right of this repository to create your own copy of KoalaSync.
2. **Clone your Fork**: `git clone https://github.com/YOUR-USERNAME/KoalaSync.git`
3. **Create a Branch**: `git checkout -b my-new-feature` (e.g. `feature/dark-mode` or `fix/translation-de`)
4. **Make your Changes**: Edit the files, then verify them locally.
- *Extension/Server changes*: Test on Chrome/Firefox and check `npm run lint`.
- *Website/Translation changes*: Run `node website/build.cjs` and check the output in `www/`.
5. **Commit and Push**: `git commit -m "Add my feature"` and `git push origin my-new-feature`
6. **Open a Pull Request (PR)**: Go to the original KoalaSync repository on GitHub and click "New Pull Request".
### PR Code Requirements
- **Lint**: Ensure `npm run lint` passes with zero errors and warnings.
- **Syntax**: Run `node -c` on every modified `.js` file.
- **Protocol changes**: Update relevant documentation in `docs/`.
---
@@ -140,14 +148,18 @@ If you cannot access the Status tab, include as much of the following manually:
---
## Translation Contributions
## Translation Contributions (Translators Welcome!)
KoalaSync supports 6 languages: English, German, French, Spanish, Portuguese (Brazilian), and Russian.
We welcome native speakers to help translate KoalaSync! You **do not** need deep programming knowledge to contribute translations.
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.
KoalaSync supports multiple languages. To add or improve translations:
1. Read the **[Translation Guide](docs/TRANSLATION.md)** first. It explains how our localization system works.
2. Edit the `.json` files in `website/locales/` (for the website) and `extension/locales/` (for the extension).
3. Test your translations locally by running:
- `node scripts/test-locales.cjs` (for extension)
- `node scripts/test-website-locales.mjs` (for website)
- `node website/build.cjs` (to build the site)
4. Follow the **Open Source Workflow** above (Fork -> Branch -> Edit -> PR) to submit your translations.
---
+47 -22
View File
@@ -6,15 +6,15 @@
<p align="center">
<a href="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml"><img src="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml/badge.svg" alt="Release Status"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/github/v/release/Shik3i/KoalaSync" alt="GitHub release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Shik3i/KoalaSync?color=blue" alt="License"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v3.0.1-blue?logo=github" alt="GitHub release"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue" alt="License"></a>
<a href="https://addons.mozilla.org/de/firefox/addon/koalasync/"><img src="https://img.shields.io/badge/Firefox-Download-orange?logo=firefoxbrowser&logoColor=white" alt="Firefox Add-on"></a>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
</p>
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.2.0 Release!</b> — See what's changed</a></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v3.0.1 Release!</b> — See what's changed</a></p>
### 🌟 Why KoalaSync?
@@ -29,11 +29,12 @@
- **Global Synchronization**: Synchronize Play, Pause, and Seeking on any website with a `<video>` tag.
- **Episode Auto-Sync**: Perfectly sync series binges. All peers wait until everyone has loaded the next episode before starting together.
- **Host Control & Co-Hosts**: Room hosts can lock playback control to trusted controllers while guests keep watching in sync.
- **Smart Matching**: Automatically highlights tabs containing matching video titles.
- **Dual Heartbeat Architecture**: Robust session tracking that prevents ghost rooms and stale connections.
- **Efficient Relay**: Minimal overhead WebSocket message forwarding.
- **Seamless Invitations**: Smart links that automatically configure server and room credentials for your friends.
- **Audio Compressor**: Built-in Web Audio API dynamic range compression with presets (Recommended, Dynamic Range, Vocal Enhancement, Smooth) and fully customizable controls.
- **Smart Audio Compressor**: Tired of constantly riding the volume? Automatically balance out whispering dialogue and deafening explosions with simple presets, or fully customize the audio to your liking.
---
@@ -54,6 +55,16 @@ The easiest and safest way to install KoalaSync is directly through the official
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! 🍿
---
### 🌐 Localization & Translations
Both the official KoalaSync website and the browser extension feature dynamic localization:
- **Available Languages**: Support is included for 15 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), Chinese (Simplified) (`zh`), Ukrainian (`uk`), and European Portuguese (`pt`).
- **Real-Time Extension Localization**: Inside the extension Settings panel, users can swap languages instantly. The entire interface, notifications, Empty States, and onboarding guides re-translate dynamically in real-time.
- **Contributing**: We welcome community translations for both the website and the extension! Please refer directly to the [TRANSLATION.md](docs/TRANSLATION.md) guide for step-by-step instructions on how to audit, refine, or add new languages.
---
### 🛠️ For Developers & Self-Hosters
@@ -71,21 +82,25 @@ The easiest and safest way to install KoalaSync is directly through the official
To build the extension from source and synchronize protocol constants:
```bash
npm install
node scripts/build-extension.js
npm run build:extension
```
The compiled artifacts will be available in the `dist/` directory.
#### For Self-Hosting (Docker)
Deploy your own private relay server using our official image:
For local development or a simple private relay, use the root Compose file:
```bash
# Pull the latest image
docker pull ghcr.io/shik3i/koalasync:latest
# Or use our example compose file
cp examples/docker-compose.caddy.example.yml docker-compose.yml
docker-compose up -d
cp server/.env.example server/.env
# Edit server/.env and set SERVER_SALT to a unique random value:
# openssl rand -base64 32
docker compose up -d --build
```
The server will be available at `ws://localhost:3000`. See [Docker network compose](examples/docker-compose.caddy.example.yml) or [Static IP compose](examples/docker-compose.ip.example.yml) for ready-to-use Docker Compose files.
The local relay will be available at `ws://localhost:3000`.
For production, use the official image and one of the ready-to-edit examples:
- [Docker network compose](examples/docker-compose.caddy.example.yml): for a Caddy or reverse-proxy network. This file does not publish `localhost:3000`; Caddy routes traffic to the container.
- [Static IP compose](examples/docker-compose.ip.example.yml): for a pre-existing Docker network with a fixed container IP.
In every real deployment, set a unique `SERVER_SALT`. Without it the relay still starts, but it warns that room-password hashes are using the public fallback salt from the repository.
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`).
@@ -93,15 +108,21 @@ To connect your extension to a self-hosted server, open the popup → **Room** t
To verify your relay is reachable from outside, visit `https://your-domain.com` in a browser — it should return `{"status":"online","service":"KoalaSync Relay"}`.
---
#### Supply Chain Security (v2.2.2+)
### 🌐 Localization & Translations
All official release artifacts (Docker images and extension binaries) are published with signed [artifact attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations) to prove they were built from this repository's source code.
Both the official KoalaSync website and the **v2.0 Browser Extension** feature full dynamic localization:
- **Available Languages**: Support is included for 13 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), and European Portuguese (`pt`).
- **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.
**Verify a Docker image:**
```bash
gh attestation verify oci://ghcr.io/shik3i/koalasync:latest \
-R Shik3i/KoalaSync
```
**Verify an extension binary:**
```bash
gh attestation verify dist/koalasync-chrome.zip \
-R Shik3i/KoalaSync
```
---
@@ -109,19 +130,23 @@ Both the official KoalaSync website and the **v2.0 Browser Extension** feature f
- **[CHANGELOG.md](docs/CHANGELOG.md)**: Full version history for the extension and relay server.
- **[TESTED_SERVICES.md](docs/TESTED_SERVICES.md)**: Detailed compatibility matrix of tested streaming platforms and known limitations.
- **[TRANSLATION.md](website/TRANSLATION.md)**: Translation and localization guide for contributors.
- **[TRANSLATION.md](docs/TRANSLATION.md)**: Translation and localization guide for contributors.
- **[PRIVACY.md](docs/PRIVACY.md)**: Data Handling and Privacy Policy.
- **[CONTRIBUTING.md](CONTRIBUTING.md)**: How to help make KoalaSync better.
- **[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)**: Our community standards and expectations.
- **[HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)**: Step-by-step walkthrough of the complete user flow.
- **[ARCHITECTURE.md](docs/ARCHITECTURE.md)**: Deep-dive into the two-phase sync and heartbeat logic.
- **[PROTOCOL.md](docs/PROTOCOL.md)**: WebSocket protocol specification and event reference.
- **[ROADMAP.md](docs/ROADMAP.md)**: Planned features, backlog, and rejected ideas.
- **[SECURITY.md](SECURITY.md)**: Disclosure policy and security practices.
- **[AI_INIT.md](docs/AI_INIT.md)**: Maintainer and agent onboarding notes for safe code changes.
- **[Caddyfile.example](examples/Caddyfile.example)**: Production Caddy configuration for website and relay.
---
<div align="center">
<sub>Built with ❤️ by <a href="https://github.com/Shik3i">Shik3i</a>. KoalaSync is Open Source under the <a href="LICENSE">MIT License</a>.</sub>
<sub><a href="https://ko-fi.com/koaladev"><img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi"></a></sub>
<sub><a href="https://support.koalastuff.net"><img src="https://img.shields.io/badge/Support-KoalaSync-FF5E5B" alt="Support KoalaSync"></a></sub>
<sub><a href="https://gitgem.org/github/Shik3i/KoalaSync"><img src="https://gitgem.org/api/badge/github/Shik3i/KoalaSync.svg" alt="GitGem Badge" /></a></sub>
<sub>Built with ❤️ by <a href="https://github.com/Shik3i">Shik3i</a>. KoalaSync is Open Source under the <a href="LICENSE">MIT License</a>.</sub>
</div>
+7 -1
View File
@@ -44,7 +44,7 @@ The following components are within scope for security reports:
Instead, email the project maintainer privately:
**`koalasync_admin@koalamail.rocks`**
**`koalasync@koalastuff.net`**
Encrypt sensitive findings with our PGP key (available on request).
@@ -82,6 +82,12 @@ KoalaSync's security is grounded in its architecture:
If you find a way to bypass any of these protections, we want to know about it.
> [!NOTE]
> Some frequently-reported "issues" are **intentional and out of scope** for our
> threat model (ephemeral, account-less rooms of invited participants) — e.g. an
> unauthenticated `peerId` or non-constant-time room-password compare. Before
> reporting, please read **[`docs/KNOWN_LIMITATIONS.md`](docs/KNOWN_LIMITATIONS.md)**.
---
## Responsible Disclosure
+66
View File
@@ -0,0 +1,66 @@
# KoalaSync — Marketing Copy Kit
Ready-to-paste copy for product listings, launch pages, directory submissions, and anywhere else you keep re-typing the same pitch. Three lengths, one consistent message. Pick the one that fits the field limit.
---
## 1. One-Sentence Pitch
> KoalaSync is a privacy-first browser extension that synchronizes video playback across almost any website so you can watch with friends in real time — no accounts, no tracking, and your video never passes through anyone's server but the original site's.
**Shorter alternative** (for tight tagline fields):
> Private, universal watch parties on any website — no accounts, no tracking, no media proxying.
---
## 2. Three-Sentence Overview
> KoalaSync is a lightweight browser extension that keeps you and your friends perfectly in sync on YouTube, Netflix, Twitch, Prime Video, Jellyfin, Emby, and almost any other site with an HTML5 video player — press play once and everyone stays together. It's built privacy-first: no accounts, no telemetry, and the official relay server runs entirely in volatile RAM with zero persistence, so nothing about your sessions is ever stored. Open source under the MIT license and fully self-hostable with a single Docker Compose command, KoalaSync is a transparent watch-party tool that works everywhere and respects your data sovereignty.
---
## 3. Full Description
### Watch together — on any site, on your terms.
Counting down "3, 2, 1, play" over voice chat doesn't scale past two people. KoalaSync fixes that with a tiny browser extension that synchronizes play, pause, and seeking across everyone in the room, on almost any website with a `<video>` element. Create a room, share a link, press play — that's it.
### What makes KoalaSync different
Most watch-party tools fall into one of two traps: they only work on a short allowlist of sites (site-specific extensions that need a separate build for every platform), or they route your video through their own player and servers. KoalaSync was built around three principles that break that mold.
**Universal by design.** If the site has an HTML5 `<video>` element, KoalaSync can usually sync it. YouTube, Netflix, Twitch, Prime Video, Disney+, Jellyfin, Emby, and countless niche sites work out of the box — no per-site integration to wait for, no extension swap when your friends want to switch services.
**Your video never touches our servers.** KoalaSync only relays tiny timing messages — play, pause, seek position, readiness — over a hand-rolled WebSocket protocol. The actual video keeps streaming directly from the original site to each viewer's browser. KoalaSync never proxies, transcodes, uploads, or redistributes a single frame, which also means there is no legal gray zone around redistribution.
**Privacy is the default, not an upgrade.** No accounts, no emails, no telemetry, no analytics, no behavior profiling. The official relay server runs entirely in volatile RAM and keeps zero persistent state — when the room closes, the data is gone. Pick a nickname or let KoalaSync generate one for you and you're in.
### Built for people who actually want to read the code
KoalaSync is MIT-licensed open source, built by a solo developer. Audit it, fork it, change it. The extension is dependency-free with a direct Socket.IO wire implementation — no opaque libraries, no framework bloat, no surprise third-party SDKs. Want full sovereignty? Set a unique `SERVER_SALT` in the supplied Compose file, then start your own relay with a single Docker Compose command and keep all watch-party coordination traffic inside your own infrastructure. The official public relay is there when you don't care, self-hosting is there when you do.
### Little touches you'll notice
- **Episode Auto-Sync** pauses the room when someone loads the next episode and resumes only when everyone is ready — no spoilers, no one left behind on the previous cliffhanger.
- **Smart Audio Compressor** tames the modern "whisper dialogue, deafening explosion" mix with one click. Three presets or full manual control over threshold, ratio, attack, and release.
- **One-click invite links** auto-configure the server and room for your friends — they just click the link and they're in. No fumbling with server URLs or room IDs.
- **Dual-heartbeat architecture** kills ghost rooms and stale connections before they desync your session.
- **15 languages** fully translated and switchable in real time from the settings panel — English, German, French, Spanish, Portuguese (Brazil + European), Russian, Italian, Polish, Turkish, Dutch, Japanese, Korean, Chinese, Ukrainian.
### Install and start in under a minute
Install KoalaSync from the Chrome Web Store or Firefox Add-ons, click "Create Room," share the invite link, and pick a video. The official relay is ready out of the box — no setup required unless you want to self-host.
- Website: https://sync.koalastuff.net
- GitHub: https://github.com/Shik3i/KoalaSync
---
## Bonus: Taglines (for hero headlines, social bios, meta descriptions)
- Watch together. Anywhere. Privately.
- The watch-party tool that works on every site and tracks none of them.
- Sync play, pause, and seek on any video — no accounts, no logs, no lock-in.
- Self-hostable, open-source watch parties for the post-"3, 2, 1, play" era.
- Press play once. Stay together anywhere.
+46 -26
View File
@@ -1,37 +1,57 @@
KoalaSync: The Privacy-First Watch Party Extension
KoalaSync: Private Watch Parties for Emby, Jellyfin, Plex, Netflix & YouTube
Private watch parties for Emby, Jellyfin, Plex, Netflix, YouTube & more. No account needed. Real-time sync. Supports 13 languages. 100% free.
Tired of counting down "3, 2, 1, Play" over voice chat? KoalaSync keeps you and your friends perfectly in sync. Whether you are streaming from your own self-hosted media server like Emby, Jellyfin or Plex, or watching on a major platform like Netflix, Prime Video or YouTube, KoalaSync is designed for smooth, browser-based watch parties.
Whether you're binge-watching Netflix, streaming from your own self-hosted Emby, Jellyfin, or Plex servers, or watching on virtually any other website with a video player — KoalaSync keeps everyone perfectly in sync.
Lightweight, open-source, and built for absolute data sovereignty with a clean, minimalist UI.
✨ CORE FEATURES
✨ CORE FEATURES
No account required. No tracking. Just create a room, invite your friends, and start watching together.
No Account Needed: Just create a room and share the link. No emails, no passwords, no hassle. A random nickname is assigned automatically, or pick your own.
Universal HTML5 Sync: Flawlessly synchronizes play, pause, and seeking on Netflix, Jellyfin, Emby, YouTube, and almost any website hosting a standard HTML5 <video> tag.
• Smart Binge-Watching (Auto-Sync): When a new episode loads, KoalaSync automatically pauses playback in a smart lobby until everyone's video is ready. No spoilers, no one left behind.
Fully Localized: Enjoy a native experience with a user interface fully translated into 13 different languages.
One-Click Invitations: Generate a smart invite link. Once clicked, the extension instantly configures room credentials and server settings for your guests.
Zero-Latency Protocol: Powered by a custom WebSocket architecture designed for real-time coordination without buffering loops.
Real-Time Video Sync: Play, pause, seek, and watch together with fast synchronized playback across everyone in your room.
No Account Needed: Create a room and share the invite link. No emails, no passwords, no sign-ups. Pick a nickname or let KoalaSync generate one for you.
• Works Almost Everywhere: If the website uses a standard HTML5 video player, KoalaSync can usually sync it. Perfect for streaming sites, self-hosted media servers, and other websites.
Smart Binge-Watching: When a new episode loads, KoalaSync automatically pauses the lobby until everyone is ready. No spoilers, no one left behind.
Smart Audio Compressor: Tired of quiet dialogue and suddenly loud action scenes? Balance whispering, explosions, and music with a single click while you watch.
One-Click Invites: Send a smart invite link to your friends. When they open it, KoalaSync automatically configures the room so they can join instantly.
• 15 Languages: Enjoy a native experience in English, German, French, Spanish, Brazilian and European Portuguese, Russian, Italian, Polish, Turkish, Dutch, Japanese, Korean, Simplified Chinese, and Ukrainian.
🛡️ UNCOMPROMISING PRIVACY & SECURITY
No data collection. No compromises.
• 100% Anonymous: Zero tracking, zero analytics, zero telemetry.
• Ready Out-Of-The-Box: No technical knowledge needed. Install and start watching immediately using the official public server.
• RAM-Only Public Server: The official relay server operates entirely in volatile RAM. No databases, no logs, no watch history — nothing persists. It also features built-in rate-limiting and brute-force protection to prevent room-id spamming.
• Self-Hostable: Take total control by deploying a private relay server in seconds via Docker. Optional, never required.
🚀 HOW IT WORKS
🛡️ PRIVACY & SECURITY
KoalaSync is built for private watch parties without unnecessary data collection.
1. Install KoalaSync and click "Create Room" to spin up a secure lobby.
2. Copy the invite link and send it to your friends.
3. Open your favorite streaming site and set the video tab as the active target.
4. Press play — everyone stays perfectly in sync.
• No Tracking: Zero analytics, zero telemetry, and absolutely no behavior profiling.
• Anonymous by Design: No accounts needed. Rooms can be joined with a simple nickname.
• Ready Out of the Box: Install KoalaSync and start watching immediately using the official public relay server. No technical setup required.
• RAM-Only Public Server: The official relay server operates entirely in volatile RAM. No databases, no stored watch history, no persistent room data. Room data exists only temporarily and disappears when the room closes.
• Self-Hostable: Want full control? You can run your own private KoalaSync relay server via Docker in seconds. Self-hosting is optional and never required.
💻 OPEN SOURCE
KoalaSync was built by a solo developer who needed a fast, secure way to watch movies with friends — and builds tools worth using. The code is fully transparent under the MIT license: audit it, fork it, improve it. Found a bug or have a feature idea? Open an issue on GitHub. Contributions and code reviews are always welcome.
• Website: https://sync.koalastuff.net
• GitHub: https://github.com/Shik3i/KoalaSync
🚀 HOW IT WORKS
1. Install KoalaSync.
2. Click "Create Room" to start a private watch party.
3. Share the invite link with your friends.
4. Open your favorite streaming site or media server.
5. Select the active video tab.
6. Press play. Everyone stays perfectly in sync.
⚙️ UNDER THE HOOD
KoalaSync is lightweight, transparent, and built with privacy in mind.
• On-Demand Relay: Playback state is synchronized through a custom WebSocket-based relay server. There is no permanent background connection. The relay connection is maintained only while you're in a room and closes when you leave or automatically after two hours without a selected-video heartbeat.
• No Media Streaming: KoalaSync does not stream, proxy, upload, download, or redistribute any video content. Everyone watches from their own browser on the original website.
• Temporary Room State Only: The relay server only coordinates room state such as play, pause, seek position, active target, nickname, and readiness status.
• Docker Self-Hosting: The relay server can be self-hosted with Docker if you prefer to run your own private instance.
• Open Architecture: The project is designed to be inspectable, forkable, and easy to review.
💻 OPEN SOURCE
KoalaSync was built by a solo developer who needed a fast, secure way to watch movies with friends. The code is fully transparent under the MIT license: audit it, fork it, improve it, or self-host your own relay server.
Found a bug or have a feature idea? Open an issue on GitHub. Contributions and code reviews are always welcome.
• Website: https://sync.koalastuff.net
• GitHub: https://github.com/Shik3i/KoalaSync
+33 -31
View File
@@ -7,7 +7,7 @@ Welcome to the KoalaSync project. This file is the primary entry point for any d
> - **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.
> **Communication Standard**: Be concise, concrete, and professional. Match the user's language when practical, explain risky changes before making them, and report exactly what was changed and verified. Do not use joke protocols or intentionally broken language in contributor-facing work.
---
@@ -21,28 +21,30 @@ 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`).
- **`build.cjs`**: Static site compiler using the repository's build dependencies. Translates `template.html` + `locales/*.json``www/`, generates the sitemap, and minifies HTML/CSS/JS.
- **`www/` is auto-generated**: Never edit files in `www/` directly. Always edit source files (`template.html`, `style.css`, `styles/*.css`, `app.js`, `lang-init.js`, `locales/*.json`) and run `node website/build.cjs` to regenerate. `style.css` is the development manifest; `style.legacy.css` is an unloaded, byte-identical migration reference. Production creates page-specific and render-priority CSS bundles from `styles/*.css`; CSS/JS are output as `.min.*` files and stale generated assets are removed on each build.
- `shared/`: **Single Source of Truth** for protocol constants, event names, blacklist data, and generated usernames.
- `scripts/`: Development utilities (e.g., `build-extension.cjs`).
- `docker-compose.yml`: Root-level orchestration for the relay server.
> [!IMPORTANT]
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `node scripts/build-extension.js`.
> **Single Source of Truth**: `shared/constants.js`, `shared/blacklist.js`, and `shared/names.js` are the master shared files. They must be synchronized to the `extension/shared/` directory using `node scripts/build-extension.cjs`.
> - **Extension Modules** (`background.js`, `popup.js`) import directly from `./shared/constants.js`.
> - **Content Scripts** (`content.js`) use a **marker-injected synchronous copy** of the constants. The build script automatically replaces the marked blocks — no manual mirroring needed.
## 3. Mandatory Reading
Before touching any code, you MUST read the following documents in order:
1. [ARCHITECTURE.md](ARCHITECTURE.md) Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
2. [extension/README.md](../extension/README.md) Extension components, tab structure, and loading process.
3. [SYNC_GUIDE.md](SYNC_GUIDE.md) Protocol constants and synchronization requirements.
1. [../README.md](../README.md) User, developer, and self-hosting overview.
2. [README.md](README.md) Documentation map by role.
3. [ARCHITECTURE.md](ARCHITECTURE.md) Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
4. [../extension/README.md](../extension/README.md) Extension components, tab structure, and loading process.
5. [SYNC_GUIDE.md](SYNC_GUIDE.md) Protocol constants and synchronization requirements.
## 4. The "Vanilla JS Mirror" Pattern
To avoid boot-time race conditions in Manifest V3 without a bundler, the following architectural trade-off is enforced:
- **Synchronous Execution**: `content.js` MUST execute synchronously to catch early media events.
- **Automated Injection**: The build script (`node scripts/build-extension.js`) automatically injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` using marker-based replacement (see `../scripts/README.md` for marker details).
- **Maintenance**: After modifying `shared/constants.js`, simply run the build script. No manual mirroring is required.
- **Automated Injection**: The build script (`node scripts/build-extension.cjs`) automatically injects `EVENTS`, `HEARTBEAT_INTERVAL`, and episode utility functions into `content.js` using marker-based replacement (see `../scripts/README.md` for marker details).
- **Maintenance**: After modifying any root `shared/` file, run the build script. No manual mirroring is required.
## 5. File Responsibility Map
@@ -52,26 +54,24 @@ To avoid boot-time race conditions in Manifest V3 without a bundler, the followi
| `content.js` | Video element detection, media control, event origin detection (loop prevention) |
| `popup.js` | UI rendering, user input handling, peer display, invitation link generation |
| `bridge.js` | Landing page ↔ extension communication for invitation join flow |
| `episode-utils.js` | Shared episode parsing, imported by background and injected into content |
| `title-privacy.js` | Tab/media title privacy normalization and sanitization |
| `page-api-seek-overrides.js` | Page-level seek bridge for site-specific player APIs |
| `server/index.js` | Room management, message relay, rate limiting, authentication, peer lifecycle |
## 6. Design Guidelines
The popup UI follows a strict design system. Do not modify these variables or the layout structure without explicit approval.
- **Font**: System font stack. **MANDATORY**: No external CDNs or Google Fonts to ensure 100% privacy.
- **Popup Width**: Fixed at `320px`.
- **Tab Structure**: Must maintain the **Room**, **Sync**, **Settings**, and **Dev** tabs.
- **CSS Variables**:
| Variable | Value | Purpose |
| :--- | :--- | :--- |
| `--bg` | `#0f172a` | Main background |
| `--card` | `#1e293b` | Form and info cards |
| `--accent` | `#6366f1` | Primary actions and branding |
| `--success` | `#22c55e` | Success states / Online dot |
| `--error` | `#ef4444` | Errors / Offline dot |
- **Tab Structure**: Must maintain the visible **Room**, **Sync**, **Settings**, and **Status** tabs. The hidden **Dev** tab is available only for developer diagnostics.
- **Appearance modes**: Preserve `system`, `light`, and `dark` behavior and the Eucalyptus, Cyber, and Graphite palettes.
- **CSS variables**: Use the semantic tokens defined in `popup.html` (`--bg`, `--card`, `--accent`, `--text`, `--text-muted`, `--success`, `--warning`, and `--error`). Do not hard-code the retired indigo/slate palette or bypass palette-specific token overrides.
## 7. Non-Negotiables (Core Logic)
The following features are critical and must not be removed or fundamentally altered:
- **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.
- **Host Control Mode**: In `host-only` rooms, only the host and promoted controllers may initiate room-moving playback events. Both client-side UX and server-side gates must remain intact.
- **Dual Heartbeat**:
- **Background Heartbeat (1m)**: Ensures session persistence even without a video element.
- **Content Heartbeat (15s)**: Transmits current video metadata (time, title).
@@ -80,8 +80,9 @@ The following features are critical and must not be removed or fundamentally alt
- **Platform Specifics**: Specialized click-logic for YouTube (`.ytp-play-button`) and Twitch.
- **pollSeekReady()**: Polling mechanism that checks `video.readyState` before acknowledging sync.
- **SW Keep-alive**: Use of `chrome.alarms` to prevent the Manifest V3 Service Worker from suspending.
- **Diagnostics**: The "Dev" tab provides real-time access to the underlying `<video>` state for troubleshooting.
- **Diagnostics**: The visible **Status** tab provides real-time access to connection state, ping, logs, history, and underlying `<video>` state for troubleshooting.
- **Persistence**: `peerId` and `username` must be stored to remain stable across sessions.
- **Title Privacy**: Do not accidentally reintroduce tab/media title sharing when users disabled or reduced it.
- **Room ID Format**: Room IDs are restricted to `[a-zA-Z0-9-]` only (alphanumeric + hyphens). This is enforced server-side.
## 8. Technical Constraints
@@ -89,7 +90,8 @@ The following features are critical and must not be removed or fundamentally alt
- **Manual Protocol**: `background.js` implements a subset of the Socket.IO wire protocol natively.
- **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`.
- **Content Script Scope**: `bridge.js` is the only static content script in the manifest and runs on `https://sync.koalastuff.net/*` at `document_start`. Video control scripts are injected only into the selected tab via `chrome.scripting`; do not add broad persistent video content-script matches.
- **Self-Hosting Salt**: Production/self-hosted relay examples must include a unique `SERVER_SALT` so room-password hashes are not derived with the public fallback salt.
- **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
@@ -117,7 +119,7 @@ Before starting any task, committing, or pushing, you **MUST** run `git pull --r
> **AI AGENTS MUST FOLLOW THIS EXACT SEQUENCE WHEN RELEASING A NEW VERSION OR TAGGING.**
>
> **🚫 NO MANUAL VERSION BUMPING**: You MUST **NEVER** manually modify the version strings in `package.json`, `extension/manifest.base.json`, or `website/version.json`. The GitHub Actions CI pipeline automatically extracts the version from the git tag (e.g. `v2.0.5` -> `2.0.5`), injects it into all target files, and commits the updates back to `main` with `[skip ci]`. Manual bumps will cause merge conflicts and build failures.
> - **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.
> - **Website Versioning**: **NEVER** manually modify generated version strings in `website/www/`. The website build injects version data from `website/version.json` into generated output.
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.
@@ -137,22 +139,22 @@ Before starting any task, committing, or pushing, you **MUST** run `git pull --r
### Adding a Protocol Event
1. Add the event name to `shared/constants.js`.
2. Run the build script (`node scripts/build-extension.js`).
2. Run the build script (`node scripts/build-extension.cjs`).
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`.
1. Edit source files in `website/` (`template.html`, `style.css`, `styles/*.css`, `app.js`, `lang-init.js`, or `locales/*.json`).
2. Run the compiler: `node website/build.cjs`. This generates the multilingual pages in `www/` and minifies CSS/JS.
3. Verify the sources and generated contract: `node --check website/app.js`, `node --check website/lang-init.js`, `node scripts/test-website-locales.mjs`, and `node scripts/test-website-theme.mjs`.
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.
5. Commit the source changes only. `website/www/` is generated and gitignored.
### Testing Locally
1. Run the build script: `node scripts/build-extension.js`.
1. Run the build script: `node scripts/build-extension.cjs`.
2. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (or `dist/firefox/` in Firefox).
3. Start the server from the root: `docker-compose up --build`.
3. Start the server from the root: `docker compose up --build`.
4. Use **different browser profiles** or vendors to test multi-peer logic.
5. Use the **Dev tab** to verify real-time video element metadata.
5. Use the **Status tab** to verify real-time connection state, logs, and video element metadata.
### Locking Old Versions
1. Update `MIN_VERSION` in the server's `.env` file to the minimum acceptable version.
+6 -5
View File
@@ -2,9 +2,10 @@
This document describes the communication flows and internal logic of the KoalaSync system.
## 1. Extension Startup & Connection
- **Initialization**: On startup, `background.js` reads settings (Server URL, Username, Last Room) from `chrome.storage.sync`.
- **WebSocket Handshake**:
## 1. Extension Connection (Lazy Connect)
- **Initialization**: On startup, `background.js` reads settings (Server URL, Username, Last Room) from `chrome.storage.sync`. No WebSocket connection is established at this point.
- **On-Demand Connection**: The extension only connects when needed — either the user opens the popup with saved room credentials, or when actively in a room. When not in a room, no connection exists. This improves privacy (IP not exposed while idle) and reduces battery/network usage.
- **WebSocket Handshake (when connecting)**:
1. Background creates a `new WebSocket` to `/socket.io/?EIO=4&transport=websocket&version=1.0.0`.
2. Server performs security checks:
- **IP Rate Limit**: Checks if the IP has exceeded connection limits.
@@ -45,7 +46,7 @@ To maintain a clean room state and eliminate "Ghost Peers":
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
- **Reconnect Strategy**: Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin.
- **Reconnect Strategy (while in room)**: Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin. When not in a room, no reconnection occurs.
> [!CAUTION]
> **Identity Rule**: Differentiate between `peerId` and `socket.id`. Use `socket.id` exclusively for ephemeral transport routing on the server. Use `peerId` exclusively for identity, state management, and room tracking across the stack.
@@ -70,5 +71,5 @@ KoalaSync uses a megaphone routing approach to minimize server logic:
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
- **Relay Server & Extension Modules**: `background.js` and `popup.js` import constants directly from `shared/constants.js`.
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a synchronized copy of `EVENTS` and constants.
- **Automation**: The `node scripts/build-extension.js` script automatically injects these constants into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
- **Automation**: The `npm run build:extension` script automatically injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
- **Verification**: Any protocol change is automatically propagated across the stack by running the build script.
+299 -2
View File
@@ -4,15 +4,312 @@ All notable changes to the KoalaSync browser extension and relay server.
---
## [v3.0.1] — 2026-07-26
### Added
- **Extension: Google Drive workaround** — Detects and controls videos opened on `drive.google.com` through the site-specific embedded-player path.
- **Extension: Chat event messages** — Optionally shows timestamped room and playback events in chat while keeping chat-message notifications enabled by default and event notifications disabled by default.
### Fixed
- **Extension: Chat interactions** — Prevents video-player keyboard shortcuts while typing, makes the floating chat draggable, restores left/right docking, updates unread notifications, and keeps chat timestamps visible.
- **Extension: Embedded-player selection** — Avoids selecting a stale hidden media frame when multiple nested frames are temporarily ambiguous.
- **Extension: Popup layout** — Moves the video-selection hint lower and reduces its glow.
- **Website and extension copy** — Corrects malformed translations, separates chat and playback notification labels, removes stale version references and unsupported comparison claims, and accurately documents encrypted live chat and operational logging.
## [v3.0.0] — 2026-07-26
### Added
- **Extension: Optional encrypted room chat** — Rooms now support live-only, end-to-end encrypted text messages. Chat is disabled and hidden by default and can be enabled explicitly in the extension options.
- **Extension: Persistent room chat key** — Every room generates and retains a chat key even while chat is disabled, so chat can be enabled later without creating a new room.
- **Extension: Floating player chat** — When enabled, chat is available from a floating control over the selected player and opens in an overlay without replacing the synchronized video.
- **Extension: Dedicated chat settings** — Chat now has its own settings section for enablement, left/right/free-floating placement, compact/standard/large/custom sizing, and bubble-versus-open startup behavior.
### Security
- **Ciphertext-only relay** — The relay receives and forwards encrypted message payloads without storing chat history or accepting client-supplied plaintext, sender identities, timestamps, or message IDs as authoritative.
- **Backward-compatible rollout** — Versioned `chat-v1` capabilities ensure old non-chat extensions never receive unknown chat events, while current extensions continue to work with older chatless relay versions.
### Changed
- **Build dependencies** — Updated the supported build and validation toolchain and refreshed compatible transitive dependencies.
- **Relay container runtime** — Moved the production container from end-of-life Node.js 20 to Node.js 24 LTS and made production dependency installation deterministic with `npm ci --omit=dev`.
## [v2.6.4] — 2026-07-16
### Changed
- **Extension: Localized store metadata** — Renamed the public extension title to `Watch Party - KoalaSync` and added natural, store-ready descriptions for all 15 supported locales while preserving regional language variants.
- **Extension: Locale-based manifest metadata** — The extension name and description now resolve through Chrome/Firefox `_locales` messages instead of being hardcoded in the manifest.
### Fixed
- **Build: Extension metadata validation** — Extension builds now fail clearly for invalid locale JSON, missing metadata, an incorrect extension title, or store titles and descriptions that exceed their character limits.
## [v2.6.3] — 2026-07-15
### Fixed
- **Extension: Host-access recovery races** — Prevents stale permission grants, reinjection retries, closed tabs, and rapid target changes from reactivating or clearing the wrong video tab.
- **Extension: Cross-browser host patterns** — Uses Firefox-compatible match patterns for local servers while preserving exact Chromium port scopes.
- **Extension: Force-sync target integrity** — Keeps sampled playback time and acknowledgements bound to the selected tab during recovery and target changes.
## [v2.6.2] — 2026-07-15
### Added
- **Website: Site-access recovery guide** — Added an English help page and a localized banner across every landing-page language.
### Fixed
- **Extension: Withheld website access recovery** — Detects browser-withheld host access, shows a localized allow-access action, requests only the selected website origin, and resumes the selected tab after access is granted.
- **Extension: Target-tab reliability** — Hardened permission recovery against rapid tab changes, navigation, closed tabs, service-worker restoration, and stale pending state.
- **Browser compatibility** — Uses Chrome's toolbar access request only when available, with a direct user-gesture permission fallback for Firefox and older Chromium browsers.
---
## [v2.6.1] — 2026-07-15
### Fixed
- **Extension: Episode Lobby peer list** — Peer names in the lobby are now rendered as text instead of markup. A peer could previously put HTML in their username and have it rendered in everyone else's popup, which allowed loading remote images (leaking viewer IP addresses) and spoofing the readiness badges. Scripts were already blocked by the extension's content security policy.
### Changed
- **Release checks: AMO validation** — `npm run verify` now runs Mozilla's `addons-linter` against the built Firefox artifact with `--warnings-as-errors`, and ESLint enforces `no-unsanitized`, so upload-blocking issues surface locally instead of at submission time.
---
## [v2.6.0] — 2026-07-15
### Added
- **Extension: Appearance controls** — Added localized system, light, and dark theme options with an early theme initializer to avoid flashes during popup startup.
### Changed
- **Extension: Popup and settings redesign** — Unified controls, status surfaces, colors, icons, badges, and accessibility behavior; settings are now organized into mutually exclusive accordion groups.
### Fixed
- **Extension: Episode Lobby reliability** — Prevented the lobby from remaining stuck in a loading state and stopped episode transitions from triggering on non-episodic media.
- **Extension: Tab-title normalization** — Notification counters such as `(14)`, `[7]`, and `(99+)` are removed reliably without stripping legitimate large numeric titles.
---
## [v2.5.4] — 2026-07-08
### Changed
- **Extension: Refined clutter blacklist** — Removed `localhost` and cloud storage providers (`drive.google.com`, `dropbox.com`, `onedrive.live.com`, `icloud.com`) from the domains blacklist so that local web development servers and tabs hosting video files in cloud storage are no longer hidden under "Hide Clutter Tabs".
---
## [v2.5.3] — 2026-07-02
### Fixed
- **Extension: Disney+ force sync and seeking** — Fixed force sync on Disney+ failing or jumping to wrong positions. The extension now relies solely on accurate player-API time and fails cleanly when it isn't available yet, instead of falling back to unreliable raw video data.
- **Extension: Force-sync accuracy** — When syncing to the group, peers whose current position isn't known yet (e.g. a Disney+ peer that just loaded) no longer pull the sync target toward the start of the video.
- **Extension: Disney+ Host Control Mode** — Regular Disney+ content is no longer misclassified as a live stream, which had silently disabled the desync dialog and snap-back for guests in host-controlled rooms. YouTube and Twitch live detection is unchanged.
- **Extension: Disney+ episode auto-sync** — Episode transitions and the "waiting for peers" lobby flow now work reliably on Disney+ again.
---
## [v2.5.2] — 2026-07-02
### Added
- **Extension: Privacy title controls** - Advanced users can now disable sending browser tab titles separately from media titles. Media titles can still be sent in full, reduced to detected episode identifiers such as `S01E04`, or hidden entirely. Defaults remain full titles for backwards compatibility.
- **Relay: Cleaner restart handling** — Connected clients are now disconnected explicitly during relay shutdown so reconnects recover more predictably.
- **Relay: Stronger abuse protection** — Rapid room-leave spam is now rate-limited.
- **Extension: Hidden remote seek diagnostics** — KoalaDev can use the hidden Dev tab to simulate remote seeks and inspect precise native/page-API timing while debugging playback integrations.
### Changed
- **Extension: Shared page-API seek bridge** — Netflix and Disney+ now use a common page-level seek bridge so private player APIs can be invoked from the page context while the default HTML5 path stays unchanged.
- **Build: Release build timestamp** — Extension builds now inject a build timestamp into the hidden Dev tab for easier local package verification.
### Fixed
- **Extension: Disney+ precise sync** — Disney+ now reads time and seeks through the real page media-player API, and the temporary DOM timeline/button scraping fallback has been removed.
- **Extension: Netflix seek reliability** — Netflix seeking keeps using the page player API with a safer session lookup path.
- **Extension: Tab-title counter cleanup** — Leading browser notification counters such as `(14)` or `[7]` are removed from shared tab titles and matching logic without changing the existing privacy controls.
- **Extension: Tab navigation reinjection** — Reinjecting the content script after selected-tab navigation now uses the same page-API-aware injection path.
---
## [v2.5.0] — 2026-06-29
### Added
- **Extension + Relay: Host Control Mode** — Room owners can now switch a room between open playback control and host-controlled playback. In host-only mode, guests stay synchronized but their local play, pause, and seek actions are not rebroadcast to the room.
- **Backward-compatible Host Control rollout** — The extension only shows Host Control when the connected relay supports it, so users on older self-hosted servers do not see controls that cannot work yet.
- **Extension: Clear host and guest states** — The popup shows the current control mode, host status, peer roles, and localized guest guidance so participants understand when playback is controlled by the host.
- **Website: FAQ clarification for streaming access** — The landing page and FAQ structured data now state clearly that KoalaSync does not stream, host, share, or bypass access to video content. Every participant watches locally and needs their own access to services such as Netflix.
### Changed
- **Playback sync now follows the room's control setting** — When Host Control is enabled, only the host can drive room-wide playback changes; guests can still watch in sync without accidentally changing playback for everyone.
---
## [v2.4.6] — 2026-06-23
### Fixed
- **Room and settings are no longer stored in `chrome.storage.sync`** — Room ID, password, and username were being resurrected from synced storage on a fresh install (sync survives an uninstall in the user's Google account), which made the extension silently auto-connect to a dead room and appear permanently connected. `getSettings()` and all settings reads are now local-only, and legacy keys are actively purged from sync on install/update/startup. Only `onboardingComplete` and `dismissedHints` remain in sync.
- **No server traffic while alone in a room** — When you are the only peer, heartbeats, force-sync, and episode auto-sync are now fully suppressed (previously the keepAlive heartbeat, force-sync, and episode lobby were still broadcast to an empty room). The solo state is re-evaluated live on every event — never cached — so the instant another peer joins, syncing resumes immediately, including an instant state push so the newcomer sees your current position without waiting for the next heartbeat.
## [v2.4.4] — 2026-06-23
### Changed
- **Server: Event rate limit raised 30 → 50 per 10s**, and all connection/event/health rate-limit thresholds and windows extracted into named constants.
- **Extension: Reconnect backoff tuned and jittered** — capped at ~8 attempts/60s (under the per-IP connection limit) with ±20% jitter to de-synchronize reconnect herds after a server blip.
- **CI: Added a verification workflow** running lint, tests, audits, and builds on every push/PR; the release build now uses `npm ci`.
### Fixed
- **Extension: Offline event-queue flush is now paced** (small batches instead of one synchronous burst) so a reconnect after a long outage no longer trips the server event limit and gets disconnected on rejoin.
- **Extension: Ping liveness tolerates one missed PONG** — a reconnect is forced only after 2 consecutive misses (~20s) instead of a single 5s timeout, avoiding spurious drops under transient load.
- **Extension: `socket.send()` failures are caught and re-queued** instead of losing the event on a disconnect race.
## [v2.4.3] — 2026-06-19
### Added
- **Two new languages: Ukrainian (`uk`) and Chinese (`zh`, Simplified)** — added across the extension (UI strings + Chrome `_locales`) and the website (localized pages, hreflang/Open Graph/schema tags, language selector), bringing the total to 15 languages.
### Changed
- **Play/pause sync coalescing** — The content script now collapses rapid bursts of native play/pause events (source swaps, ABR/quality switches, ad transitions, page teardown) into a single relayed command: the first event is sent instantly and a short 150ms window absorbs the rest. This cuts redundant relay traffic and stops bursts from tripping the server's per-socket event rate limit.
### Fixed
- **zh/uk translation quality** — Corrected systematic machine-translation word-sense errors in the two new locales (e.g. "Play", "Status", "Leave Room", "Clear", "Open", "peers", and audio compressor terms) and translated the remaining English leftovers.
- **Relay logging** — An `EVENT_ACK` aimed at a peer that already left is now logged quietly instead of as a `[SECURITY]` cross-room event, so genuine cross-room attempts stand out in the logs.
## [v2.4.2] — 2026-06-19
### Changed
- **Extension: Optimized uninstall URL registration** — Extracted registration into a reusable, race-condition-protected `initUninstallURL()` helper. It registers the uninstall feedback URL with browser context on both extension installation/update and browser startup to prevent state loss, without storing or sending an installation token.
## [v2.4.1] — 2026-06-19
### Added
- **Extension: Onboarding tour now has a closing step** — The first-run tour ends on a dedicated "You're all set!" card (the `ONBOARDING_5` copy that already existed in all 13 locales but was never shown). The tour no longer stops abruptly on the username step.
- **Extension: One-click invite from the empty peer list** — The "No peers yet" state now shows a **📋 Invite Link** button that copies the invite link to the clipboard, so users can share it without hunting for the field.
### Changed
- **Extension: Cleaner onboarding welcome** — Step 1 is now a centered welcome card instead of spotlighting the logo title. Added a guard so target-less tour steps center cleanly.
- **Website: Mobile comparison table** — The KoalaSync vs Teleparty table stacks into per-feature cards on phones instead of forcing horizontal scrolling; feature descriptions are shown again on mobile.
### Fixed
- **Extension: Onboarding step counter/progress placeholders** — Static `Step 1 of 3` / 33% fallbacks in `popup.html` corrected to match the actual 5-step tour (`Step 1 of 5` / 20%).
- **Website: Mobile navigation restored** — The header hamburger menu was hidden by a `display:none !important` rule, leaving the nav links unreachable on phones. Re-enabled, with spacing kept comfortable down to ~320px.
- **Website: Hero alignment on mobile** — A fixed-width extension mockup forced the hero grid column wider than the container, shifting all hero content off-center (larger left margin than right). The mockup is now responsive (`width:100%/max-width` + `minmax(0,1fr)` grid track).
- **Website: Reveal-animation fallback** — Added a `<noscript>` style fallback and `IntersectionObserver` feature guards so scroll-revealed content can never stay invisible if JavaScript is disabled or unsupported.
## [v2.4.0] — 2026-06-16
### Added
- **Extension: Lazy WebSocket connection** — The extension no longer maintains a permanent WebSocket connection to the relay server. Instead, the connection is established only when actively in a room or when the popup is opened with a saved room configuration. This improves privacy (IP is not exposed while idle), reduces battery/network usage, and prevents the server from tracking online status of inactive users. Automatic reconnect is guaranteed while in a room — zero behavior change during active sync sessions. See `connectIntent` flag in `background.js`.
- **Extension: Episode title regex unification** — `extractEpisodeId()` had inconsistent regex patterns between `background.js` and `content.js`. The content script correctly matched Crunchyroll-style separators (`S01/E01`) while the service worker's stricter pattern (`[\s\-\.]*`) silently rejected them, causing episode lobby sync failures. Now unified to `[^a-zA-Z0-9]*` via shared `episode-utils.js`.
- **Unit tests: `rate-limiter` and `episode-utils`** — 12 test groups for rate-limit functions and 30+ assertions for episode title parsing, covering all 6 separator types (dash, dot, slash, colon, comma, space). Run automatically via `npm run verify`.
### Changed
- **Server: Rate limiter extracted to `rate-limiter.js`** — 6 rate-limit functions, all rate-limit Maps, and cleanup intervals moved from `index.js` (149 lines). `index.js` now imports via facade pattern with re-exports for backward compatibility.
- **Extension: Episode utilities extracted to `episode-utils.js`** — `extractEpisodeId()` and `sameEpisode()` deduplicated from `background.js` and `content.js`. The shared module is imported as an ES module by the service worker and injected into the content script IIFE by the build script.
- **Build: `"type": "module"` in root `package.json`** — All scripts standardized to ESM (`.mjs`) or explicitly CommonJS (`.cjs`). Eliminated Node.js `MODULE_TYPELESS_PACKAGE_JSON` warnings.
- **Build: 4 CJS scripts renamed to `.cjs`** — `build-extension.js`, `test-content-video-finder.js`, `test-locales.js`, `website/build.js`.
### Fixed
- **Server: npm audit resolved** — `ws` package vulnerability (CVE-2024-37890) fixed. Zero vulnerabilities in production dependencies.
- **Pop-up: Connection status flicker fixed** — Removed hardcoded `disconnected` state on every pop-up open. Status now reflects actual background state from the first frame.
- **Pop-up: Join button timeout improved** — No longer blindly re-enables after 15s. Polls connection status and extends window if still connecting.
- **Pop-up: Validation failure state cleanup** — Custom server URL validation errors now properly reset `isProcessingConnection` and `joinBtnTimeout`.
- **Extension: `WEB_JOIN_REQUEST` channel leak fixed** — Missing `sendResponse()` call when already in the target room.
- **Extension: `LEAVE_ROOM` now clears `roomId` from storage** — Prevents phantom auto-reconnect on browser restart after explicit leave.
- **Extension: Reconnect attempt counters reset on leave** — Prevents stale `reconnecting` status display after intentional disconnect.
## [v2.3.2] — 2026-06-16
### Changed
- **Extension: Refined Spanish, Italian, and Portuguese translations**: Complete manual review and improvement of all Spanish (`es`), Italian (`it`), Portuguese — Brazil (`pt-BR`), and Portuguese — Portugal (`pt`) locale files for both the extension UI and the landing website. Thanks to [@Kaia-Alenia](https://github.com/Kaia-Alenia) for the native-quality translations.
### Fixed
- **Extension: Locale typos and corrupted characters fixed**: Repaired a Korean refresh button label (`Refreschi``새로고침`), a corrupted Korean connection status string (`연kel``연결`), a Korean character contaminating a Japanese string (`의``の`), and a Dutch typo (`cmmuniceren``communiceren`).
- **Server: Admin token length leak fixed (timing side-channel)**: `isAdminMetricsAuthorized()` returned early when the provided buffer had a different length than the expected token, leaking the token length via response timing. Now `crypto.timingSafeEqual` runs in constant time on every attempt regardless of length match. Reported by [@Kaia-Alenia](https://github.com/Kaia-Alenia).
---
## [v2.3.1] — 2026-06-15
### Fixed
- **Server: Concurrent peer join race condition and teardown error handling**
### Changed
- **Server: Smart unhandled rejection handling (exits after 5/min instead of 1)**
- **Server: Optimized admin health metrics allocation**
---
## [v2.3.0] — 2026-06-14
### Added
- **Extension: New Interactive Onboarding Tour**: A fully redesigned, interactive step-by-step onboarding experience.
- **Extension: Auto-Switch to Sync Tab**: The UI now intelligently switches to the Sync tab when you join a room to guide video selection.
- **Extension: Uninstall URL Integration**: Prepared an uninstall URL setup that works natively across Chrome and Firefox, cleanly attaching browser context for analytics.
### Fixed
- **Extension: Infinite Seek Loop Prevention**: Replaced the fragile time-based seek suppression with an exact target-time verification mechanism, entirely eliminating infinite seek loops on slow buffers.
- **Extension: Zombie Connections Resolved**: Implemented a forced disconnect upon ping timeouts, ensuring the extension reliably auto-reconnects when the WebSocket hangs in a half-open state.
- **Extension: Room Switching Architecture**: Joining a new room while already connected now explicitly severs the old connection first, preventing state cross-contamination.
- **Extension: Join/Leave Race Conditions**: Added UI locks to prevent users from accidentally sending conflicting connection commands via rapid double-clicking.
- **Extension: Same-Room Invite Bypass**: Clicking an invite link for the room you are currently in no longer triggers a redundant reconnect, instead instantly confirming the join.
- **Extension: Audio settings now propagate immediately to video tabs**: Changes made in the audio options page are now instantly applied to the active video tab. Previously, settings saved to `chrome.storage.local` were not picked up by the background listener, which only watched `chrome.storage.sync`.
- **Extension: Audio compressor now logs enable/disable state and resume failures**: The compressor reports when it is activated or bypassed, and warns if the `AudioContext` cannot be resumed (e.g. browser autoplay policy requires a user gesture on the page first).
- **Extension: Video heartbeat no longer sent when alone in a room**: The full media metadata `PEER_STATUS` is now only emitted when other peers are present. The session keepalive (background heartbeat) continues to run unaffected, preventing the server reaper from disconnecting idle peers.
- **Server: Increased `failedAuthAttempts` eviction threshold from 50k to 200k**: Reduces frequency of expensive batch evictions under high auth-failure volumes, smoothing heap usage.
---
## [v2.2.4] — 2026-06-10
### Fixed
- **Extension: Error notifications now respect `browserNotifications` setting**: Server error events (e.g. "Server is restarting") no longer trigger a browser notification when the user has disabled notifications in the extension settings.
- **Server: Misleading reconnect message corrected**: The graceful shutdown message no longer tells users to manually reconnect — the extension handles this automatically.
---
## [v2.2.3] — 2026-06-10
### Added
- **Artifact Attestations (Supply Chain Security)**: All release artifacts (Docker images, extension ZIPs) are now published with signed [SLSA provenance attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations) via `actions/attest@v4`. Anyone can verify that an artifact was built from this repository using `gh attestation verify`.
- **Admin health: `rateLimits.denied` counters**: New rolling counters track actual rate-limit denials (429 responses), separate from `rateLimits.trackedClients` which reports unique IPs in the tracking window.
- **Docker HEALTHCHECK**: Container health is now checked every 30s via `GET /health`.
- **`npm start` script**: Server can now be started with `npm start`.
### Fixed
- **Server: `activeLobby` no longer silently overwritten**: If a second peer sends `EPISODE_LOBBY` while a lobby is already active, the request is now ignored instead of destroying the first peer's lobby.
- **CORS log sanitization**: Rejected origin headers are sanitized (`\r\n` stripped) to prevent log injection.
- **Extension: pagehide resource leak**: `keepAlivePort`, `lobbyPollTimer`, heartbeats, and `MutationObserver` are now properly cleaned up when a tab is hidden or enters bfcache.
- **Extension: unhandled storage rejections**: `chrome.storage.session.set()` calls in the disconnect handler now have `.catch(() => {})`.
- **`'Pixel'` duplicate in name generator**: Second occurrence replaced with `'Nitro'` for better name diversity.
- **`'opposum'` typo**: Corrected to `'opossum'` in the emoji map and added `'Opossum'` to `USERNAME_NOUNS`.
- **Test reliability**: `test-server-routes.mjs` now sets `ADMIN_METRICS_TOKEN` before importing the server module, fixing standalone test execution.
- **`MAX_PEERS_PER_ROOM` konsistent**: `.env` auf 25 gesetzt (wie `.env.example`).
- **pt-BR.json duplicate removed**: Duplicate `FOOTER_DISCLAIMER` key removed from website locale.
### Changed
- **Admin health: `rateLimitEntries` renamed to `rateLimits.trackedClients`**: The field now accurately describes that it tracks unique clients in the rate-limit window, not denial counts. Update your json_exporter/Grafana config accordingly.
- **README restructured**: Sections reordered by progressive technical depth. New "Supply Chain Security" subsection under "For Developers & Self-Hosters" with verification commands.
---
## [v2.2.2] — 2026-06-09
### Added
- **Chrome Web Store i18n Support**: Added `default_locale: "en"` to manifest and created `_locales/*/messages.json` for all 13 supported languages. This unlocks the language selection dropdown in the Chrome Web Store dashboard, allowing translated store listings (title, description) per locale. The extension's own UI translations (`locales/*.json` + `i18n.js`) remain unchanged.
- **Locale test coverage**: Extended `scripts/test-locales.js` to validate all `_locales/*/messages.json` files (correct format, required keys, no duplicates) and verify `default_locale` is set in the manifest.
### Fixed
- **Copy Logs button alignment**: Removed stray `margin-top: 8px` inherited from `.secondary` class that pushed the button 8px down in the connection status row.
---
## [v2.2.1] — 2026-06-09
### Added
- **Server Ping Display**: Measures round-trip latency to the relay server via application-level ping/pong events. The extension sends `PING { t }` every 15 seconds; the server responds with `PONG { t }`. Round-trip time is calculated client-side and displayed in the Status tab, color-coded (<50ms green, 50150ms yellow, >150ms red). No ping value is shown when disconnected or if the server does not respond within 5 seconds.
- **Peer Ping Response (Future-Proof)**: The extension can now respond to incoming `PING { t, sender }` events from other peers by sending back `PONG { t, target: sender }`. The relay server forwards `PING` to the target peer and routes `PONG` back to the original sender. Both client and server validate that peers are in the same room before forwarding/routing. Peer-to-peer ping initiation will be activated in a future extension update without requiring a server restart.
---
## [v2.2.0] — 2026-06-08
### Added
- **Web Audio API Compressor**: Built-in audio dynamic range compression with four presets (Recommended, Dynamic Range, Vocal Enhancement, Smooth) and fully customizable sliders (threshold, ratio, knee, attack, release, makeup gain). Uses dry/wet crossfade (40ms linear ramp) to avoid clicks. Configured via the new Audio Options page accessible from the Settings tab.
- **Web Audio API Compressor**: Built-in audio dynamic range compression with four presets (Recommended, Dynamic Range, Vocal Enhancement, Smooth) and fully customizable sliders (threshold, ratio, knee, attack, release). Uses dry/wet crossfade (40ms linear ramp) to avoid clicks. Configured via the new Audio Options page accessible from the Settings tab.
- **Audio Options Page** (`audio-options.html`): Dedicated settings page with master toggle, compressor preset selector, real-time custom sliders, and equalizer placeholder. Dark theme matching the popup design.
- **Feature Hint System**: Generic `dismissedHints` array in sync storage for announcing new features. First hint highlights the Audio Options entry in Settings. Extensible for future features.
### Changed
- **Ko-Fi Support Links**: Static footer badges on the Settings and Status tabs linking to the developer's support page. README and website footer updated with Ko-Fi badge.
- **Support Links**: Static footer badges on the Settings and Status tabs linking to the developer's support page. README and website footer updated with a Support KoalaSync badge.
### Fixed
- **Portuguese (PT) locale**: Removed Italian contamination — "sincronizzazione" → "sincronização", "tempo reale" → "tempo real", "Link di Invito" → "Link de Convite", "Sair della Sala" → "Sair da Sala".
+85
View File
@@ -0,0 +1,85 @@
# Ephemeral end-to-end encrypted chat
KoalaSync chat is an optional, live-only text channel rendered on the selected
streaming tab. It is not a messenger and does not add chat UI to the extension
popup.
## Security boundary
- The relay receives ciphertext only and never receives the chat secret.
- The relay stores no messages in RAM or on disk and sends no backlog.
- A late joiner sees only messages sent after joining.
- Room authentication is unchanged. The room password remains separate from the
chat secret and continues to be sent to the relay during `join_room`.
- Message timing and ciphertext length remain visible to the relay. Same-room
replay is accepted by the threat model.
## Invite format and key lifecycle
New invitations use a named fragment format:
```text
#j2:r=<roomId>&p=<password>&k=<base64url-secret>[&u=<relayUrl>]
```
The room creator generates 16 random bytes and encodes them as 22 unpadded
base64url characters. URL fragments are not sent in HTTP requests. The website
parses the fragment and passes structured fields through `bridge.js` to the
extension. `chatKey` must never be included in a relay event.
Legacy `#join:` links remain supported and join normally without chat. Manual
room/password entry also joins without chat. The extension clears any prior chat
secret when joining without a key, switching rooms, or leaving.
Deployment order is website first, extension second. An old extension ignores the
additional structured `chatKey` field and continues joining normally.
## Cryptography
- Secret: 16 cryptographically random bytes.
- KDF: HKDF-SHA256 with `roomId` as salt and a fixed KoalaSync chat info label.
- Encryption: AES-256-GCM using WebCrypto.
- IV: fresh random 12-byte value for every message, prepended to the ciphertext.
- AAD: `${roomId}|${senderId}`.
- The derived `CryptoKey` is cached once per active room.
The relay stamps `id`, `senderId`, and `timestamp` on the ciphertext envelope.
Changing `senderId` causes AES-GCM authentication to fail because the receiver uses
the stamped value as AAD.
## Client policy
- Maximum plaintext length: 500 Unicode code points.
- Decrypted text is untrusted. Escape HTML before applying the supported limited
Markdown formatting.
- No read receipts and no typing indicators.
- The local message DOM is bounded; this is presentation state, not server history.
## Overlay behavior
- Default dock: right.
- Live modes: right, left, and detached.
- Detached mode is draggable and moderately resizable. Size and position are stored
per origin and clamped to the current viewport.
- The overlay uses a Shadow DOM and never changes host-page layout.
- On `fullscreenchange`, its host moves into `document.fullscreenElement` and remains
visible.
- It follows all eucalyptus, cyber, and graphite light/dark theme combinations.
- Without a key, the panel stays closed and a disabled chat control explains that a
current invite link is required.
- Chat display is a local option and defaults to off. Enabling or disabling it never
deletes the room chat secret, so it can be enabled later without creating a room.
- Without the relay `chat` capability, no chat control is shown.
## Mixed-version rollout
- New extensions announce `chat-v1` in `join_room.clientCapabilities`.
- Old non-chat extensions omit the optional field and receive no `chat_message`
events. Their playback and room protocol remains unchanged.
- The first chat beta omitted the capability. When it sends one valid v1 chat
frame, the relay treats that socket as chat-capable for the rest of the
connection.
- New extensions accept the first beta's unversioned server `chat` capability as
well as `chat-v1`, so a server-first or extension-first rollout degrades safely.
Peer removal and role management are outside chat scope.
+5 -5
View File
@@ -16,9 +16,9 @@ This guide walks through the complete user flow of KoalaSync, from creating a ro
## Step 2: Connecting to the Relay Server
When you open the extension popup, the background service worker connects to the relay server:
When you open the extension popup (with saved room credentials) or when a saved room configuration exists from a previous session, the background service worker connects to the relay server:
1. **WebSocket Handshake**: `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket`.
1. **WebSocket Handshake** (on demand): `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket` only when needed (popup opened or active room).
2. **Security Checks** (server-side):
- The server checks the client's **IP rate limit** (max 10 connections per 60 seconds).
- The server validates the **authentication token** (hardcoded in `shared/constants.js`) to verify this is a legitimate KoalaSync client.
@@ -155,8 +155,8 @@ While in a room, two heartbeats keep the session alive:
| Heartbeat | Interval | Source | Purpose |
|:----------|:---------|:-------|:--------|
| **Background** | 30 seconds | `background.js` | Signals "I'm still connected" and triggers aggressive reconnect (500ms base, max 5s) |
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, `mediaTitle`, `playbackState`, `volume`, `muted` |
| **Background** | 30 seconds | `background.js` | While connected, signals "I'm still connected" and triggers automatic reconnect (500ms base, max 5s). No heartbeats fire when idle (lazy connect). |
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, privacy-filtered `mediaTitle`, `playbackState`, `volume`, `muted` |
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
- **Room Cleanup**: Empty rooms are deleted immediately. Inactive rooms are pruned after 2 hours.
@@ -184,7 +184,7 @@ When a user clicks **"Leave"** or closes their browser:
When watching a series and an episode ends:
1. `content.js` monitors the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) for title changes.
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected new title.
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected title after applying the local media title privacy setting. In episode-only mode this is an identifier such as `S01E04`; when media titles are not sent, the client does not create a new episode lobby.
3. All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
4. Each peer's `content.js` polls for the new title to appear in the `<video>` element's metadata.
5. Once a peer detects the matching title, they send `EPISODE_READY`.
+104
View File
@@ -0,0 +1,104 @@
# Known Limitations & Accepted Non-Issues (NOFIX)
This file records design decisions that are **intentional** and **out of scope** for
KoalaSync's threat model. They are frequently re-discovered by reviewers, linters, and
AI agents and re-reported as "bugs" or "vulnerabilities." They are not. Please read the
threat model below before flagging any of them.
> **For reviewers & agents:** Do **not** re-report the NOFIX items below. They are
> deliberate. If you think the *threat model itself* should change (e.g. KoalaSync
> starts handling accounts, payments, or sensitive data), raise **that** discussion
> first — these items only stand because the threat model holds.
---
## Threat model
KoalaSync is a **privacy-preserving watch-party sync tool**, not a security product.
- **Rooms are ephemeral.** They exist for a few hours and are auto-reaped. There are no
accounts, no persistent storage, no money, and no sensitive content on the relay.
- **The relay is a zero-persistence, in-memory message bus.** It temporarily holds room
and peer state while forwarding play/pause/seek between peers who *chose* to watch
together and joined via an invite link shared out-of-band.
- **Participants are invited.** Anyone in a room was let in. The social contract is
"we're watching a video together," not "mutually distrusting parties."
### What we DO defend against
- **Accidental disruption** — the entire point of Host Control Mode.
- **Spam / DoS** that degrades the relay for everyone — rate limits, 4 KB payload cap,
server-side gating, lazy-cached health responses.
- **Resource exhaustion / memory leaks** — bounded maps, periodic cleanup, room/peer reaping.
- **Crashes from malformed input** — strict sanitization and clamping of every field.
- **Genuine boundary breaches** — admin-metrics auth (constant-time), CORS, WSS upgrade,
invite-hash isolation, strict CSP. Reports here are very welcome (see `SECURITY.md`).
### What we explicitly DO NOT defend against
A **determined participant who modifies their own client to misbehave inside a room they
were invited to.** The worst they achieve is sending playback commands or seizing the
"host" role in a temporary room they could already disrupt by other means. That is a
**social** problem, solved socially: kick them, or start a new room. Engineering real
identity/auth to prevent it would destroy the account-less, frictionless, privacy-first
design — a bad trade for an ad-hoc movie night.
---
## NOFIX entries
### NOFIX-1 — `peerId` is unauthenticated; a crafted client can impersonate or seize the host
**Flag:** `peerId` is client-asserted and broadcast to every peer (in `ROOM_DATA` /
`PEER_STATUS`). A modified client can join with the host's `peerId`, dedupe-kick the real
host, and become host — controlling or locking `host-only` mode.
**Why NOFIX:** Requires a *modified client* + an *invited* participant + a `peerId` that is
only meaningful inside that one *temporary* room. The payoff is sending play/pause or
locking a room the attacker is already in — pure trolling, instantly reversible (kick /
new room). Cryptographic per-user identity is wildly disproportionate for an ad-hoc,
account-less, ephemeral watch party. **Out of threat model.**
Do **not** "fix" with accounts, signed peerIds, or per-user tokens — that breaks the
core design.
### NOFIX-2 — Room-password comparison is not constant-time
**Flag:** room password hashes are compared with `!==` (`server/index.js`), so the compare
is theoretically timing-attackable.
**Why NOFIX:** The compared value is an **HMAC-SHA256 hash that never leaves the server**
an attacker cannot observe it to mount a timing attack. Even a hypothetical success only
lets someone join a *temporary* room to send playback commands. Not worth defending.
(The admin-metrics bearer token — a real boundary — **does** use `crypto.timingSafeEqual`.
That is the line we actually guard.)
### NOFIX-3 — `OFFICIAL_SERVER_TOKEN` is public in the repo
**Flag:** the connection token in `shared/constants.js` is committed, so anyone can connect.
**Why NOFIX:** It is a **coarse filter** to keep random scanners off the relay, **not
authentication**. The relay is a public message bus by design; rate limits and per-room
behavior are the real protections.
### NOFIX-4 — Room IDs are enumerable via `GET_ROOMS`
**Flag:** any connected client can list all room IDs (and whether each has a password).
**Why NOFIX:** This is the intended **"Public Rooms"** feature. Rooms wanting privacy set a
password; listing the IDs of password-less rooms only lets someone join a watch party —
the same as being handed the invite link.
### NOFIX-5 — A pause/seek can only be reverted, not prevented
**Flag:** in `host-only` mode a guest's pause still fires locally before the extension can
react, so there is a brief flicker before snap-back.
**Why NOFIX:** A content script cannot intercept a `<video>` event before the element
acts. Reacting (snap-back) is the only option and is by design; the ~½s flicker is
acceptable. Not a bug.
---
## Not NOFIX — just deferred (may be revisited)
These are *not* accepted-forever; they are scoped out of v1 and tracked separately
(see the host-control-mode design docs in `docs/`):
- **Host grace on a long disconnect (EC-10).** A brief reconnect/second-tab keeps the host
(handled), but a long real disconnect still falls back to `everyone`. A ~30s host-reserve
grace could be added later.
- **Intent-classifier / snap-back tuning.** Thresholds are first-pass; real-device testing
may adjust them.
+2 -2
View File
@@ -8,7 +8,7 @@ KoalaSync is designed with a **Security-First & Volatile** architecture. This me
## 1. Data Processing (In-Memory Only)
KoalaSync does not use a database. All active session data exists only in the server's RAM and is purged immediately when no longer needed.
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId`, `username`, and the `title` of the video you are watching. Additionally, playback metadata (`mediaTitle`, `playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId` and `username`. By default, KoalaSync also shares the selected tab title and media title with the room so peers can identify matching videos and coordinate episode transitions. Privacy Settings let you disable sending the tab title separately, and choose whether media titles are sent in full, reduced to a detected episode identifier (for example `S01E04`), or not sent. Playback metadata (`playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
- **Room Passwords**: If you set a room password, it is stored only as an in-memory **keyed SHA-256 HMAC hash**. The server receives the plaintext password only during join validation, never stores it, and keeps only the hash for the short room lifetime.
- **Routing Maps**: The server maintains ephemeral lookup tables (`socketToRoom`, `peerToSocket`) to route messages between peers. These contain only transport identifiers and are purged on disconnect.
@@ -33,7 +33,7 @@ The browser extension requires the following permissions:
- `storage`: To remember your local preferences (username, server URL, room settings).
- `tabs` & `scripting`: To detect and control video elements on the pages you choose to sync.
- `<all_urls>` (host permission): Required to detect `<video>` elements on any website the user chooses to synchronize. The extension only activates on the specific tab the user has actively selected — it does not scan, monitor, or interact with any other tabs or pages.
- `alarms`: To keep the background service worker alive during active sync sessions.
- `alarms`: To keep the background service worker alive during active sync sessions (the extension only stays connected while you are in a room).
- `notifications`: To display sync status updates (e.g., "Peer joined", "Force Sync initiated").
- **No History Access**: We do not read, store, or transmit your browsing history. We only interact with the specific tab you have actively selected for synchronization.
+428
View File
@@ -0,0 +1,428 @@
# WebSocket Protocol Reference
This document describes the relay behavior implemented by `server/index.js` and
the event names defined in `shared/constants.js`.
## Transport
- The relay uses Socket.IO v4 events over WebSocket.
- Long-polling is disabled (`transports: ['websocket']`, `allowUpgrades: false`).
- Messages are Socket.IO event packets whose payload is an event name plus an
object payload.
- The relay caps incoming Socket.IO message size at 4 KB.
## Invite fragments
Current invitations use named URL-fragment fields:
```text
#j2:r=<roomId>&p=<password>&k=<base64url-chat-secret>[&u=<relayUrl>]
```
The fragment is parsed by the website and forwarded to the extension as structured
fields. `k` is client-only and must never appear in a relay payload. Legacy
`#join:<roomId>:<password>[:1:<relayUrl>]` fragments remain valid but provide no chat
secret.
## Connection Handshake
The Socket.IO handshake must include:
- `token`: must match `OFFICIAL_SERVER_TOKEN`.
- `version`: optional app version. If present, it must be a valid semver-like
string and not older than `MIN_VERSION` (default `1.0.0`).
If the token is invalid, the relay emits `error` and disconnects the socket.
If `version` is invalid or too old, the relay emits `error` and disconnects the
socket.
After the socket is connected, `join_room` must include `protocolVersion`.
It must equal `PROTOCOL_VERSION` exactly. A mismatch emits `error` and rejects
the join attempt; it does not currently disconnect the socket.
## Room Join
### `join_room` (client -> server)
Payload:
```json
{
"roomId": "string, sanitized to [A-Za-z0-9-], max 64",
"peerId": "string, max 16",
"username": "string, max 30",
"password": "string, max 128, optional",
"tabTitle": "string, max 100, optional",
"mediaTitle": "string, max 100, optional",
"clientCapabilities": ["chat-v1"],
"protocolVersion": "string, max 16"
}
```
Behavior:
- Creates the room if it does not exist and capacity allows it.
- The first peer becomes `hostPeerId`.
- Rooms may have an optional password hash.
- Joining with a duplicate `peerId` disconnects the previous socket for that peer.
- Joining the same room with the same socket and peer is ignored as a no-op.
- Switching rooms removes the socket from the old room first.
On success, the joining socket receives `room_data`.
Other room members receive `peer_status` with `status: "joined"`.
### `room_data` (server -> client)
Payload:
```json
{
"roomId": "string",
"peers": ["peer state objects"],
"activeLobby": "object or null",
"hostPeerId": "string or null",
"controlMode": "everyone | host-only",
"controllers": ["peerId"],
"capabilities": ["host-control", "co-host", "chat", "chat-v1"]
}
```
`room_data` is sent to the joining socket. It is not the general broadcast used
for every later room update.
## Ephemeral encrypted chat
Relays advertise chat support with `"chat-v1"` in `room_data.capabilities` and keep
the initial beta's `"chat"` flag during the transition. New clients announce
`"chat-v1"` in optional `join_room.clientCapabilities` (at most the first 16 entries
are inspected; unknown or malformed values are ignored). Old clients omit the field
and continue using the pre-chat protocol unchanged.
The relay sends `chat_message` only to sockets that announced `"chat-v1"`. As a
transition for the first chat beta, a socket that sends a valid v1 ciphertext is
marked capable for the rest of that connection. This prevents old non-chat
extensions from receiving unknown events while preserving the first beta's send
path.
### `chat_message`
Client to relay:
```json
{ "ciphertext": "<unpadded-base64url>" }
```
`ciphertext` contains a 12-byte AES-GCM IV followed by ciphertext and the 16-byte
authentication tag. The relay validates only canonical base64url and byte bounds.
It cannot inspect plaintext.
Relay to every chat-capable current room peer, including the sender:
```json
{
"id": "<server-generated UUID>",
"senderId": "<server-stamped peer ID>",
"timestamp": 1710000000000,
"ciphertext": "<unpadded-base64url>"
}
```
Client-provided `id`, `senderId`, `timestamp`, or plaintext fields are discarded.
The relay keeps no message collection and `room_data` contains no chat history.
Messages are limited to 10 per socket per 10 seconds in addition to the global event
budget. There are no typing, read-receipt, history, or chat-specific peer-management
events.
## Room Leave
### `leave_room` (client -> server)
Payload: none.
Behavior:
- Rate-limited to 10 events per socket per minute.
- If the socket is mapped to a room, the relay removes it from that room.
- Remaining room members receive `peer_status` with `status: "left"` when the
peer is no longer represented by another socket.
- Empty rooms are deleted.
- If the host leaves and peers remain, the relay assigns the next peer as host,
falls back to `controlMode: "everyone"`, resets controllers to the new host,
and broadcasts `control_mode`.
Exceeding the `leave_room` limit is logged and the socket is disconnected.
## Relayed Room Events
The relay accepts and sanitizes these events, then emits the same event to other
peers in the room:
- `play`
- `pause`
- `seek`
- `peer_status`
- `force_sync_prepare`
- `force_sync_ack`
- `force_sync_execute`
- `episode_lobby`
- `episode_ready`
- `episode_lobby_cancel`
Relayed payload fields are sanitized and may include:
```json
{
"senderId": "peerId of sender",
"seq": "number",
"currentTime": "number 0..86400 or null",
"targetTime": "number 0..86400",
"playbackState": "playing | paused",
"username": "string, max 30",
"tabTitle": "string, max 100 or null",
"mediaTitle": "string, max 100 or null",
"volume": "number 0..1",
"muted": "boolean",
"desynced": "boolean",
"peerId": "sender peerId",
"status": "string, max 16",
"expectedTitle": "string, max 100",
"title": "string, max 100",
"actionTimestamp": "number"
}
```
Undefined fields are removed before relay. Raw client payloads are not forwarded.
## Media Control
### `play`, `pause`, `seek`
These are room-moving actions. In `host-only` mode, the relay drops them unless
the sender is a controller.
Common payload fields:
- `currentTime` for `play`/`pause`.
- `targetTime` for `seek`.
- `seq` and `actionTimestamp` when the extension needs stale-command or ACK
handling.
The content script applies additional client-side filtering for noisy native
player events before it sends these events.
## Peer Status
### `peer_status`
Used for heartbeats and peer state updates. The extension sends it every
`HEARTBEAT_INTERVAL` while syncing is active.
Typical fields:
- `peerId`
- `username`
- `tabTitle`
- `mediaTitle`
- `playbackState`
- `currentTime`
- `volume`
- `muted`
- `desynced`
- `status`
The relay stores sanitized peer state and relays the sanitized update to other
peers.
## Force Sync
Force sync coordination is implemented primarily in the extension. The relay
sanitizes and relays the events.
### `force_sync_prepare`
Payload includes `targetTime`. The initiator waits for ACKs or for
`FORCE_SYNC_TIMEOUT` before sending `force_sync_execute`.
In `host-only` mode, only controllers may initiate it.
### `force_sync_ack`
The extension sends ACKs with peer identity and sequence data. The relay relays
them with the same sanitized relay envelope as other room events, including
`senderId`.
### `force_sync_execute`
Payload includes `targetTime`. In `host-only` mode, only controllers may send it.
The relay also allows a matching initiator's execute event after that initiator
started the prepare step, even if their controller state changed before execute.
## Episode Lobby
Episode lobby coordination is implemented primarily in the extension. The relay
tracks enough state to include `activeLobby` in `room_data` for later joiners.
### `episode_lobby`
Payload uses `expectedTitle`. The relay creates `activeLobby` when this field is
present and no lobby is already active.
In `host-only` mode, only controllers may initiate it.
### `episode_ready`
Payload may include `title`. The relay adds the sender to the active lobby's
ready list when a lobby exists.
### `episode_lobby_cancel`
Clears the active lobby and is relayed to peers. In `host-only` mode, only
controllers may initiate it.
## Host Control Mode
### `set_control_mode` (client -> server)
Payload:
```json
{
"controlMode": "everyone | host-only"
}
```
Only the room host may change the mode. Non-host attempts are ignored and the
sender receives the current `control_mode` snapshot.
Mode changes are debounced per room with `CONTROL_MODE_MIN_INTERVAL_MS` (500 ms).
### `set_peer_role` (client -> server)
Payload:
```json
{
"peerId": "string, max 16",
"controller": "boolean"
}
```
Only the room host may promote or demote controllers. The host cannot demote
themself. Role changes use the same 500 ms per-room debounce as mode changes.
### `control_mode` (server -> client)
Payload:
```json
{
"controlMode": "everyone | host-only",
"hostPeerId": "string or null",
"controllers": ["peerId"]
}
```
Sent when mode or controller state changes, when host migration changes room
authority, and when unauthorized role/mode attempts need to resync the sender.
## Room List
### `get_rooms` (client -> server)
Payload: none.
No admin token is required for this Socket.IO event.
Limits:
- Counts against the per-socket event limit.
- Also has a 10 second per-socket cooldown.
### `room_list` (server -> client)
Payload:
```json
{
"rooms": [
{
"id": "room id",
"peerCount": 2,
"hasPassword": false
}
]
}
```
## Ping, Pong, and ACK
### `ping`
Payload:
```json
{
"t": 1234567890,
"target": "peerId, optional"
}
```
If `target` is omitted, the relay responds to the sender with `pong`.
If `target` is another peer in the same room, the relay sends `ping` to that peer
with `{ "t": ..., "sender": "senderPeerId" }`.
### `pong`
Payload:
```json
{
"t": 1234567890,
"target": "peerId, optional"
}
```
If `target` is a peer in the same room, the relay sends `pong` to that peer with
`{ "t": ... }`.
### `event_ack`
Client payload:
```json
{
"targetId": "peerId",
"actionTimestamp": 1234567890
}
```
If sender and target are still in the same room, the relay emits:
```json
{
"senderId": "sender peerId",
"actionTimestamp": 1234567890
}
```
## Rate Limits
- Connections: 10 per IP per minute; excess connections are disconnected.
- Relayed/events: 50 per socket per 10 seconds; excess disconnects the socket.
- `get_rooms`: 10 second cooldown per socket plus the event limit.
- `leave_room`: 10 per socket per minute; excess disconnects the socket.
- Invalid room passwords: tracked per IP and room. Five recent failures block
more password attempts for that room until the failure window ages out.
- HTTP health and admin-metrics endpoints have their own rate limits outside this
Socket.IO protocol.
## Capabilities
`room_data.capabilities` advertises server-backed features:
- `host-control`
- `co-host`
- `chat`
- `chat-v1`
Clients should treat a missing or unknown capabilities list as unsupported.
+37 -4
View File
@@ -1,7 +1,40 @@
# Technical Documentation
This directory contains deep-dives into the KoalaSync protocol and architecture.
This directory contains deep-dives into the KoalaSync protocol, architecture, roadmap, and operational guidelines.
- [HOW_IT_WORKS.md](HOW_IT_WORKS.md): Step-by-step walkthrough of every user flow, from room creation to synchronized playback. Ideal for store reviewers and manual testers.
- [ARCHITECTURE.md](ARCHITECTURE.md): Communication flows, Dual Heartbeat, and Sync logic.
- [SYNC_GUIDE.md](SYNC_GUIDE.md): Protocol constants and sync requirements.
## Start Here by Role
- **Users and reviewers**: Start with [HOW_IT_WORKS.md](HOW_IT_WORKS.md), then [PRIVACY.md](PRIVACY.md) and [TESTED_SERVICES.md](TESTED_SERVICES.md).
- **Self-hosters**: Start with the root [README.md](../README.md), then [devops.md](devops.md), [PROTOCOL.md](PROTOCOL.md), and the examples in `../examples/`.
- **Contributors**: Start with [../CONTRIBUTING.md](../CONTRIBUTING.md), then [ARCHITECTURE.md](ARCHITECTURE.md), [SYNC_GUIDE.md](SYNC_GUIDE.md), and the README for the subdirectory you are editing.
- **AI agents**: Start with [AI_INIT.md](AI_INIT.md), then read the relevant subdirectory README before changing files.
- **Security reviewers**: Start with [SECURITY.md](../SECURITY.md), [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md), [PRIVACY.md](PRIVACY.md), and [PROTOCOL.md](PROTOCOL.md).
## 🏗️ Core Architecture & Design
- **[ARCHITECTURE.md](ARCHITECTURE.md)**: Overview of the communication flows, Dual Heartbeat architecture, and synchronization logic.
- **[HOW_IT_WORKS.md](HOW_IT_WORKS.md)**: Step-by-step walkthrough of every user flow, from room creation to synchronized playback. Ideal for store reviewers and manual testers.
- **[host-control-mode.md](host-control-mode.md)**: Design, requirements, and edge cases of the Host Control feature.
- **[AI_INIT.md](AI_INIT.md)**: Maintainer and AI-agent onboarding: non-negotiables, workflow order, and safety checks.
## 📡 Protocol & Synchronization
- **[PROTOCOL.md](PROTOCOL.md)**: Low-level message format and payload descriptions for the KoalaSync sync protocol.
- **[SYNC_GUIDE.md](SYNC_GUIDE.md)**: Guide on keeping protocol constants synchronized across the workspace.
## 📋 Compatibility, Roadmap & Contribution
- **[TESTED_SERVICES.md](TESTED_SERVICES.md)**: Status of compatibility with major streaming services and contribution guidelines for testing new platforms.
- **[KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md)**: Threat model and accepted design limitations (NOFIX entries) for security audits.
- **[PRIVACY.md](PRIVACY.md)**: Privacy model and data-handling policy for users, reviewers, and contributors.
- **[ROADMAP.md](ROADMAP.md)**: Planned features, backlog items, and rejected proposals.
- **[TRANSLATION.md](TRANSLATION.md)**: Guide for native speakers to contribute and audit dynamic extension/website translations.
## 🚀 DevOps & Releases
- **[devops.md](devops.md)**: Guide on the automated tag-based release pipeline.
- **[CHANGELOG.md](CHANGELOG.md)**: Detailed history of releases and changes.
---
*For high-level project information and developer setup instructions, refer to the root [README.md](../README.md).*
+148 -8
View File
@@ -1,14 +1,154 @@
# KoalaSync Roadmap
This document tracks future technical plans and optimizations for the KoalaSync system.
> Feature priorities, planned work, backlog, and rejected ideas for KoalaSync.
---
## Planned Optimizations & Technical Roadmap
## Status Legend
| Badge | Meaning |
|---|---|
| 🚧 In Progress | Currently being developed |
| 📋 Planned | Prioritized for an upcoming phase |
| 💡 Backlog | Under evaluation, not yet prioritized |
| ❌ Rejected | Declined (with rationale) |
| ✅ Completed | Shipped |
---
## 🚧 In Progress
*Currently being worked on.*
| Feature | Priority | Area |
|---|---|---|
| *(none yet)* | | |
---
## 📋 Planned
*Prioritized for upcoming phases.*
### v3.0 — Modern Room Chat
- **Release date:** Not scheduled
- **Priority:** P1
- **Category:** Social / Communication / Privacy
- **Status:** Planned. This is a roadmap target, not a release announcement.
- **Existing foundation:** Opt-in, live-only end-to-end encrypted room chat; a floating
chat bubble over the selected player opens a dockable, detachable, resizable overlay.
The relay stores no chat history and mixed extension versions use capability-gated
delivery.
#### Core experience
- Floating bubble with an accessible unread badge (`1`, `2`, `3`, …, `99+`), cleared
only when the chat is actually viewed.
- Subtle new-message pulse with reduced-motion support; no animation or sound while
muted or during Do Not Disturb.
- Drag-and-snap bubble positioning, remembered per site, with reset-to-default.
- Dock left/right, detached overlay, compact mode, fullscreen-safe placement, responsive
narrow-player layout, and keyboard-only operation.
- Per-room mute, mention-only mode, notification sound controls, browser notifications,
and a visible mute state.
- Unread state survives overlay close/reopen and tab focus changes, but room messages
remain memory-only unless a future storage option is explicitly enabled.
#### Messaging
- Replies with quoted context, emoji reactions, emoji picker, mentions, and typing
indicators.
- Edit and delete own messages with clear local tombstones; no silent mutation.
- Delivery states (`sending`, `sent`, `failed`, `retry`) with idempotent retries and
duplicate suppression after reconnects.
- Message timestamps, date separators, jump-to-latest, unread divider, search within
the current live session, copy, and selectable text.
- Link detection with safe external opening, explicit scheme allowlist, and no automatic
previews or remote-media loading.
- Optional image/GIF/file sharing only after encrypted size limits, malware-risk UX,
relay bandwidth limits, and privacy behavior are designed and tested.
#### Presence, safety, and control
- Online/reconnecting status, join/leave events, and room-member presence without
exposing IP addresses or cross-room identity.
- Local mute/block, host moderation controls, slow mode, spam limits, and configurable
maximum message length.
- Optional read receipts and activity indicators default off; never infer or expose
viewing behavior without explicit consent.
- Clear live-only/no-history explanation, encrypted-chat indicator, room-key fingerprint,
and key-rotation/rejoin UX.
#### Reliability and compatibility
- Versioned chat capabilities for every new wire feature; unknown fields/events remain
harmless to older extensions and old non-chat clients receive no chat traffic.
- Server-first and extension-first rollout tests, including current, first-chat-beta,
old non-chat, reconnect, duplicate-frame, and malformed-capability clients.
- Bounded queues, payloads, unread counters, typing events, reactions, and attachment
metadata; backpressure and rate limits on both client and relay.
- Real Chrome and Firefox extension E2E coverage for bubble, unread count, overlay,
fullscreen, reconnect, and mixed-version rooms before v3.0 can ship.
#### Accessibility and localization
- Screen-reader announcements that do not read every busy-room message by default.
- Complete focus management, logical tab order, Escape behavior, high contrast,
zoom/reflow, reduced motion, and touch target coverage.
- All chat UI, notification text, moderation states, and errors localized across every
supported locale before release.
#### Explicit non-goals for the first v3.0 release
- No server-readable plaintext.
- No mandatory accounts, public room directory, advertising, tracking, or presence
across rooms.
- No unencrypted server-side history. Any future encrypted history requires a separate
threat model, retention controls, export/delete behavior, and explicit opt-in.
### 2. Invite link with target URL for auto-redirect
- **Priority:** P2
- **Category:** UX / Ease of Sharing
- **Background:** The invite link currently only contains the room ID. The invited person has to manually open the page. Ideally, the link would include the shared tab's URL so the invitee gets redirected to the right page and the tab is auto-selected (auto-matching via tab title already exists).
- **Known challenges:**
- Many streaming sites (e.g., Emby, Jellyfin) don't have unique URLs per content — once inside the player, the URL stays the same.
- Dozens of such edge cases exist; a generic solution is difficult.
- Would likely need site-specific extractor logic (similar to the existing sync service adapters).
- **Possible approaches:**
- Fallback: if no unique URL can be determined, only pass the tab title.
- Site-specific URL extraction for known services.
---
## 💡 Backlog
*Ideas and feature requests under evaluation.*
### Cross-frame video detection and control
- **Priority:** P3
- **Category:** Compatibility / Embedded Players
- **Background:** KoalaSync currently injects on demand into the selected tab's top frame. This works for normal top-frame players, including current Emby/Jellyfin usage, but does not cover cases where the real `<video>` lives inside a cross-origin iframe or an `about:blank`/`srcdoc` player frame.
- **Possible approach:** Add an opt-in frame bridge where child frames announce detected videos to the top frame, and the top frame routes remote play/pause/seek commands to the active child video.
- **Status:** Future compatibility work, not needed for current Emby behavior.
### Local extension E2E smoke tests
- **Priority:** P2
- **Category:** Testing / Release Confidence
- **Background:** The release verification covers unit tests, server integration, syntax, lint, audits, and builds, but it does not currently run a real browser extension flow. A small local E2E smoke suite would catch regressions in content-script injection, tab navigation reinjection, remote seek handling, and iframe player support.
- **Possible approach:** Add a separate local-only Playwright smoke command that loads the unpacked extension, opens two controlled video pages, and verifies play/pause/seek through the actual extension path. Keep it outside `npm run verify` until it is stable enough for CI.
- **Status:** Backlog, recommended before larger content-script or frame-bridge changes.
---
## ❌ Rejected
*Declined features with rationale — keeps decisions documented so they don't get re-debated.*
| Feature | Reason |
|---|---|
| *(none yet)* | |
### 1. Split large JavaScript files (> 800 lines) into smaller modules
* **Category**: Maintainability / AI Context Optimization
* **Background**: Core files like `background.js` and `popup.js` have grown large and exceed 800 lines. This makes manual debugging harder and wastes context window space for AI models.
* **Planned solution**:
- Structurally split logic into separate focused modules (e.g., UI Renderer, Message Router, Storage Manager, Socket Client).
- Use ES modules for clean separation and better reusability.
+11 -7
View File
@@ -10,22 +10,26 @@ You MUST run the build script in any of the following scenarios:
1. **After a fresh `git clone` or `git pull`** (as the synced files are ignored by git).
2. **After modifying** `shared/constants.js`.
3. **After modifying** `shared/blacklist.js`.
4. **Before committing** changes to the repository if any protocol-related files were touched.
5. **Before deploying** the server or releasing the extension.
4. **After modifying** `shared/names.js`.
5. **Before committing** changes to the repository if any shared protocol or extension-mirrored files were touched.
6. **Before deploying** the server or releasing the extension.
## How to sync
Run the Node.js build script from the repository root:
```bash
node scripts/build-extension.js
node scripts/build-extension.cjs
# or simply:
npm run build:extension
```
## What does it do?
The build script performs the following actions:
1. Synchronizes protocol constants by copying `shared/constants.js`, `shared/blacklist.js`, and `shared/README.md` into `extension/shared/`.
2. Injects `EVENTS` and `HEARTBEAT_INTERVAL` into `content.js` via marker-based replacement.
3. Compiles browser-specific manifest files.
4. Packages the final ready-to-publish extension artifacts for Chrome and Firefox into the `dist/` directory.
1. Synchronizes shared files by copying `shared/constants.js`, `shared/blacklist.js`, `shared/names.js`, and `shared/README.md` into `extension/shared/`.
2. Injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions (`extractEpisodeId`, `sameEpisode`) into `content.js` via marker-based replacement.
3. Injects browser-specific uninstall URL constants into `background.js` and a build timestamp into `popup.html`.
4. Compiles browser-specific manifest files.
5. Packages the final ready-to-publish extension artifacts for Chrome and Firefox into the `dist/` directory.
## Protocol Versioning
The system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake.
+45 -25
View File
@@ -1,45 +1,65 @@
# Tested Streaming Services
# 🎬 Tested Streaming Services & Compatibility
This document tracks which streaming platforms and media servers have been tested with KoalaSync.
This document tracks which streaming platforms and media servers are supported by the KoalaSync extension.
| 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. |
> [!TIP]
> **Contributions are highly welcome!** 🤝 Anyone can easily update this list. If you have tested a streaming service (whether it works, has issues, or is not yet listed), please help the project by submitting a quick Pull Request. See the [How to Contribute](#how-to-contribute) guide below!
## Legend
---
## Compatibility Matrix
| Service | Sync Works | Media Title | Episode Auto-Sync | Last Tested | Tested By | Extension Version | Notes |
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :--- |
| **YouTube** | ✅ Full | ✅ Full | ❌ N/A | — | — | — | Individual videos, not episodes. |
| **Twitch** | ✅ Full | ✅ Full | ❌ N/A | — | — | — | Individual streams/VODs. |
| **Netflix** | ✅ Full | ❌ | ❌ | — | — | — | No media title exposed. |
| **Emby** | ✅ Full | ✅ Full | ✅ Full | — | — | — | Best-in-class support. |
| **Jellyfin** | ✅ Full | ✅ Full | ✅ Full | — | — | — | — |
| **Plex** | Not tested | Not tested | Not tested | — | — | — | — |
| **Disney+** | ✅ Full | ⚠️ Partial | ❌ | — | — | — | Series title only (e.g. "The Simpsons"), no episode info. |
| **Prime Video** | ✅ Full | ✅ Full | ❌ | — | — | — | — |
| **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 | — | — | — | — |
| **Vix** | ✅ Full | ✅ Full | ✅ Full | — | — | — | Everything works correctly. |
### Legend
| Symbol | Meaning |
|--------|---------|
| :---: | :--- |
| ✅ Full | Works without limitations. |
| ⚠️ Partial | Works with caveats (see Notes). |
| ❌ N/A | Not applicable or not supported. |
| ❌ | Not supported / does not work. |
| ❌ N/A | Not applicable (feature does not exist on the platform). |
| **Not tested** | Has not been tested yet. |
---
## How to Contribute
Tested a service that's not listed? Found different behavior than documented?
Updating this compatibility list is quick and easy! You don't need deep coding skills to contribute:
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
1. **Fork the Repository**: Click the **Fork** button at the top of the [KoalaSync GitHub Repository](https://github.com/Shik3i/KoalaSync).
2. **Edit this File**: Open [docs/TESTED_SERVICES.md](TESTED_SERVICES.md) in your fork's browser editor (or clone it locally) and update the table with your testing details.
3. **Commit & Push**: Commit your changes with a clear message (e.g., `docs: update Netflix compatibility status`).
4. **Create a Pull Request**: Submit the Pull Request (PR) from your fork to our `main` branch.
> [!NOTE]
> **Reporting Problems:** If you notice a bug or partial support on a service, please open a [GitHub Issue](https://github.com/Shik3i/KoalaSync/issues) describing the problem, and link it in the **Notes** column of the table.
>
> _If you are unsure how to create/link an issue, don't worry! Simply submit the PR anyway, and the maintainers will gladly create and link the issue for you._
---
## 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
- **DRM/Copy Protection** (e.g., Widevine on Netflix) 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
+121
View File
@@ -0,0 +1,121 @@
# KoalaSync Translation & Localization Guide
Welcome to the **KoalaSync** translation guide. We rely on the open-source community to make KoalaSync accessible to users worldwide.
KoalaSync is split into two independent translation areas. You can translate either one, or both:
1. **The Browser Extension** (`extension/locales/`): The core product that users interact with daily.
2. **The Website** (`website/locales/`): The landing page and invitation bridge.
---
## Supported Languages Dashboard
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 `100% Manually Verified` status.
| Language Code | Language Name | Verification Status | Rationale / Context |
| :--- | :--- | :--- | :--- |
| `en` | **English** | `100% Manually Verified` | Global default language (verified by developer) |
| `de` | **German** | `100% Manually Verified` | Developer's native language |
| `fr` | **French** | `Auto-Generated` | Needs manual native review and polishing |
| `es` | **Spanish** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `pt-BR` | **Portuguese (Brazil)** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `ru` | **Russian** | `Auto-Generated` | Needs manual native review and polishing |
| `it` | **Italian** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `pl` | **Polish** | `Auto-Generated` | Needs manual native review and polishing |
| `tr` | **Turkish** | `Auto-Generated` | Needs manual native review and polishing |
| `nl` | **Dutch** | `Auto-Generated` | Needs manual native review and polishing |
| `ja` | **Japanese** | `Auto-Generated` | Needs manual native review and polishing |
| `ko` | **Korean** | `Auto-Generated` | Needs manual native review and polishing |
| `pt` | **European Portuguese** | `100% Manually Verified` | Manual native review by Alenia Studios |
| `zh` | **Chinese (Simplified)** | `Auto-Generated` | Needs manual native review and polishing |
| `uk` | **Ukrainian** | `Auto-Generated` | Needs manual native review and polishing |
> [!WARNING]
> **Autogeneration Quality Rule**
> Any newly contributed languages must be marked as `Auto-Generated` in this table until fully reviewed and signed off by a native speaker in a pull request.
---
## How to Translate KoalaSync
Here is the exact step-by-step process for contributing translations.
### Step 1: Fork and Clone the Repository
If you are an external contributor, start with the standard open-source workflow:
1. Click the "Fork" button on GitHub to create your own copy of the repository.
2. Clone your fork locally: `git clone https://github.com/YOUR-USERNAME/KoalaSync.git`
3. Create a branch: `git checkout -b translation/my-language`
### Step 2: Translate the Extension
The browser extension handles real-time syncing, settings, and popups.
1. Navigate to `extension/locales/`.
2. Edit an existing `[lang].json` or copy `en.json` to create a new one (for example, `it.json`).
3. Translate all string values. **Do not change the JSON keys.**
### Step 3: Translate the Website
The website hosts the landing page and invitation bridge.
1. Navigate to `website/locales/`.
2. Edit an existing `[lang].json` or copy `en.json` to create a new one.
3. Translate all string values. **Do not change the JSON keys.**
4. If creating a brand new language, configure the metadata at the top of your JSON file:
```json
{
"LANG_CODE": "it",
"HTML_CLASS": "lang-it",
"CANONICAL_PATH": "it/",
"LANG_TOGGLE_URL": "../",
"LANG_TOGGLE_TEXT": "EN"
}
```
5. If creating a brand new language, register it in `website/build.cjs` by adding it to the `languages` array.
### Step 4: Verify Locally
Ensure your JSON files are valid and all keys match the English baseline. Open your terminal in the KoalaSync root folder and run:
```bash
# Tests the extension locales for missing keys or syntax errors
node scripts/test-locales.cjs
# Tests the website locales for missing keys or syntax errors
node scripts/test-website-locales.mjs
# Builds the website with your new translations
node website/build.cjs
```
If you receive any errors about missing keys or placeholder strings, fix them before submitting.
### Step 5: Commit and Pull Request
1. Open this `TRANSLATION.md` file and add or update your language in the **Supported Languages Dashboard** above. Mark it as `100% Manually Verified` only if it has been reviewed by a native speaker.
2. Commit your changes: `git commit -m "Update Italian translations"`
3. Push to your fork: `git push origin translation/my-language`
4. Open a pull request on the main KoalaSync repository on GitHub.
---
## 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 (`website/impressum.html`) and privacy policy (`website/datenschutz.html`) **MUST remain exclusively in English and German**.
>
> **Rationale:** Legal compliance under the European Union General Data Protection Regulation (GDPR) and the German Digital Services Act (DDG). Offering automated translations of legally binding notices introduces compliance risks due to potential mistranslations of liability limits.
>
> **Technical fallback:** Our system automatically falls back to English for legal pages if a user visits them in an unsupported language, so you do not need to translate them.
+346
View File
@@ -0,0 +1,346 @@
# E2E-verschlüsselter In-Page-Chat für KoalaSync
## Auftrag
Baue einen privacy-first, Ende-zu-Ende-verschlüsselten Text-Chat, der direkt auf der
Streaming-Seite neben dem Video sitzt. Neuer Branch von `main`, Vorschlag
`feature/chat-e2e`.
### Harte Anforderungen
1. **Der Server darf Nachrichten nie lesen können.** Schlüssel nur clientseitig, der
Relay sieht ausschließlich Ciphertext.
2. **Der Server speichert keine Nachrichten.** Reines Live-Relay, kein RAM-Backlog,
keine History. Wer zu spät kommt, verpasst den bisherigen Chat. Das ist gewollt.
3. **Chat läuft auf der Streaming-Seite**, nicht im Extension-Popup.
4. **Themes werden respektiert** (3 Paletten x light/dark).
5. **Keine Read Receipts.** Bewusst gestrichen.
6. **Volle Abwärtskompatibilität**: alte Extensions müssen weiter normal joinen können.
### Bedrohungsmodell (bestimmt alle Krypto-Entscheidungen)
Das hier ist eine Video-Sync-Extension mit optionalem, flüchtigem Chat, **keine
sicherheitskritische Messenger-App**. Ziel ist: ein bösartiger Betreiber eines
Custom-Relays soll nicht einfach mitlesen können. Ob etwas mit sehr viel Aufwand
theoretisch knackbar wäre, ist egal.
Daraus folgt, und das ist bindend:
- **Usability und Performance haben Vorrang** vor kryptografischer Maximalhärte.
- Raum-Passwörter sind kurz, zufällig generiert (z.B. `0XUK3C`) und ständig neu. Wer
eines knackt, kann trollen (`play`/`pause` senden). Nervig, nicht kritisch. Deshalb
nutzt der Server bewusst simples SHA256/HMAC statt bcrypt. **Diese Linie beibehalten,
nicht "verbessern".**
- Der globale `SERVER_SALT` ist bekannt und kein Issue. Räume sind flüchtig, es gibt
keine Datenbank.
### Explizite Nicht-Ziele
- Kein Chat-Tab im Popup. Nicht als Fallback, nicht übergangsweise.
- Keine serverseitige `chatHistory` in irgendeiner Form.
- **Die Auth nicht anfassen.** Das Klartext-Passwort an den Relay ist bewusst so und
wird nicht umgebaut (Begründung unten).
- `website/` Theme-Redesign auf `main` nicht anfassen.
---
## Krypto-Design (entschieden, nicht neu diskutieren)
### Woher der Schlüssel kommt
Das Raum-Passwort ist **unbrauchbar** als Schlüsselmaterial: der Client sendet es im
Klartext an den Relay (`join_room` -> `payload.password`, `server/index.js:349`), der
Server HMACt es erst selbst (`hashPassword`, Zeile 52-55). Der Server könnte jeden
daraus abgeleiteten Schlüssel mitberechnen.
Es zu ändern würde auch nichts bringen: Raum-Passwörter haben ~31 Bit Entropie
(6 Zeichen A-Z0-9). Ein bösartiger Server könnte sie offline durchprobieren und den
Chat-Schlüssel ableiten.
**Lösung: ein eigenes, zufälliges Secret im URL-Fragment.** Fragmente werden nie an
einen Server gesendet, und das Secret hat volle Entropie per Konstruktion. Der
Einladungslink transportiert bereits heute das Passwort im Fragment, der Mechanismus
existiert also schon.
### Primitive
- **16 zufällige Bytes** (128 Bit), base64url, 22 Zeichen. Beispiel:
`R5Ti1nxp0crfAFHf3gVncw`
- Ableitung: **HKDF-SHA256**(secret, salt=roomId) -> AES-256-GCM-Key.
**Kein PBKDF2, kein Stretching.** Slow KDFs existieren nur, um schwache
Menschen-Passwörter zu strecken. Das Secret ist zufällig mit voller Entropie, HKDF
ist ein einziger HMAC (Mikrosekunden). Das ist dieselbe Logik wie sha256-statt-bcrypt
beim Server.
- **Key genau einmal pro Raum ableiten und den `CryptoKey` cachen.** Nicht pro
Nachricht neu ableiten oder importieren.
- Pro Nachricht: **zufälliger 12-Byte-IV**, dem Ciphertext vorangestellt.
- Alles über **WebCrypto**, keine Fremdabhängigkeit.
### AAD: senderId gegen Umetikettierung binden (Pflicht)
`senderId` wird vom Server gestempelt und liegt damit **außerhalb** des Ciphertexts.
Ohne Gegenmaßnahme kann ein bösartiger Relay Alices Ciphertext als Bob weiterreichen.
Das ist exakt der Angreifer, gegen den dieses Feature gebaut wird.
Deshalb: **AES-GCM mit AAD** verschlüsseln.
```
AAD = `${roomId}|${senderId}` // senderId = eigene peerId beim Verschlüsseln,
// envelope.senderId beim Entschlüsseln
```
Etikettiert der Server um, schlägt die Auth-Tag-Prüfung fehl und die Nachricht wird
verworfen. Kostet null Performance und bindet die Nachricht zusätzlich an den Raum
(kein Cross-Room-Replay).
Bewusst **nicht** abgedeckt und akzeptiert: Nachrichtenlängen und Timing bleiben für den
Server sichtbar (kein Padding). Ein Relay kann eine Nachricht innerhalb desselben Raums
erneut abspielen. Für das Bedrohungsmodell irrelevant.
### Sanitization wandert auf den Client (Pflicht)
Der Server sieht nur noch Ciphertext und **kann Text nicht mehr prüfen**. Die Bedrohung
verschwindet dadurch nicht, sie verschiebt sich: entschlüsselter Text stammt von einem
**Peer, der den Key besitzt**.
- Entschlüsselter Text ist **untrusted input**. Vor dem Rendern zwingend durch
`escapeChatHtml`/`formatChatText` (escapen, dann Markdown). Niemals rohes `innerHTML`.
- Die Längengrenze (500 Codepoints) muss der **Client** durchsetzen, vor dem
Verschlüsseln. Der Server kann nur noch Bytes zählen.
- **Bestehende Schranke beachten:** `maxHttpBufferSize: 4096` (`server/index.js:142`)
gilt global pro Socket-Nachricht. Nachgerechnet: 500 Codepoints als 4-Byte-Emoji
ergeben 2000 B Klartext, +16 B GCM-Tag +12 B IV = 2028 B, base64 = 2704 Zeichen,
socket.io-Frame = **2731 B**. Passt, Headroom 1365 B. Bei 700 Codepoints wären es
3799 B. Wer die Zeichengrenze anhebt, muss diese Rechnung neu machen, sonst reißt das
Limit.
---
## Link-Format und Abwärtskompatibilität (entschieden)
### Warum das alte Format nicht erweiterbar ist
Aktuell (`popup.js:559-563`):
```
offiziell: #join:<roomId>:<password>
custom: #join:<roomId>:<password>:1:<encodedUrl>
```
Der Extension-Parser (`popup.js:1294-1313`) nimmt `roomId` von vorne, das Paar
`(flag, url)` von hinten, und **alles dazwischen ist das Passwort** (`parts.join(':')`).
Es gibt keine Position, die er verwirft. Verifiziert durch Ausführen der echten Parser
gegen echte Links:
| Link | Alte Extension | Alte Website |
|---|---|---|
| `#join:SILENT-EAGLE-90:30PXPD:1:wss%3A%2F%2F…` (Kontrolle) | `password:"30PXPD"` OK | `serverFlag:"1"` OK |
| Key inline: `…:30PXPD:<KEY>:1:wss%3A%2F%2F…` | `password:"30PXPD:R5Ti1nxp…"` **korrumpiert** | `serverFlag:"R5Ti…"`, `serverUrl:"1"` -> **falscher Server** |
Zwei verschiedene, irreführende Fehler aus derselben Zeile. Deshalb: neues Präfix.
### Neues Format
```
#j2:r=<roomId>&p=<password>&k=<key>[&u=<encodedRelayUrl>]
```
- `URLSearchParams`, kein positionales Parsen. Behebt nebenbei einen bestehenden Bug:
heute zerschießt ein Doppelpunkt im Passwort den Website-Parser (`parts[2]`), während
der Extension-Parser (`parts.join(':')`) damit klarkommt.
- **`s=1` entfällt.** Es existierte nur wegen des positionalen Parsens. Mit benannten
Parametern gilt: `u` vorhanden bedeutet Custom Relay.
- Das Präfix darf `#join:` **nicht als Teilstring enthalten**, sonst greift
`includes('#join:')` in alten Extensions doch.
Beispiele:
```
offiziell: …/join.html#j2:r=SAPPHIRE-DUCK-49&p=0XUK3C&k=R5Ti1nxp0crfAFHf3gVncw
custom: …/join.html#j2:r=SILENT-EAGLE-90&p=30PXPD&k=R5Ti1nxp0crfAFHf3gVncw
&u=wss%3A%2F%2Fsync.shik3i.net
```
### Warum das alte Extensions nicht bricht
**Die Website ist der Hauptpfad, nicht die Extension.** `website/app.js:452-458` parst
das Fragment selbst, prüft via `document.documentElement.dataset.koalasyncInstalled`
(gesetzt von `bridge.js:9`), ob die Extension da ist, und dispatcht dann automatisch
(`app.js:515-526`, "AUTO-TRIGGER JOIN") ein `KOALASYNC_JOIN_REQUEST` mit fertig
geparsten, **strukturierten Feldern**. `bridge.js` reicht es als `WEB_JOIN_REQUEST` an
den background weiter. In diesem Pfad liest die Extension die URL nie an.
Altes `bridge.js` destrukturiert nur, was es kennt:
```js
const { roomId, password, useCustomServer, serverUrl } = e.detail;
```
Ein zusätzliches `chatKey` fällt dort stillschweigend auf den Boden.
**Daraus folgt die Deploy-Reihenfolge: Website zuerst.** Sie ist die einzige Stelle, die
das neue Format kennen muss, und sie ist sofort deploybar. Die Extension darf beliebig
hinterherhinken (Store-Review, Update-Zyklen der Nutzer).
| Kombination | Ergebnis |
|---|---|
| Neue Website + neue Ext | Join + Chat |
| Neue Website + alte Ext | Join normal, kein Chat, `chatKey` ignoriert |
| Neue Ext + alter `#join:`-Link | Join, kein Key, Chat aus. Legacy-Parser bleibt erhalten |
| Alte Website + neuer Link | Präfix unbekannt, Join-Seite tot. **Existiert nach dem Website-Deploy nicht mehr** |
`checkInviteLink()` (`popup.js:1289`) ist nur der Komfort-Pfad "Popup auf der Join-Seite
öffnen und Felder vorausfüllen". Bei `#j2:` greift er in alten Extensions nicht mehr,
das Feld bleibt leer statt falsch befüllt. Die neue Extension muss dort **beide**
Formate parsen (`#j2:` und Legacy `#join:`).
**`history.replaceState` auf das Legacy-Format: nicht tun.** Naheliegende Idee, um das
Autofill alter Extensions zu retten, aber ein Eigentor: es entfernt `k` aus der
Adressleiste. Nutzer kopieren die URL aus der Adressleiste, um sie weiterzuteilen. Der
weitergegebene Link joint dann zwar, hat aber stillschweigend keinen Chat mehr. Der
Fragment-Inhalt muss unangetastet bleiben.
### Lebenszyklus des Keys
- **Erzeugt wird er vom Raum-Ersteller**, einmal, beim Anlegen des Raums. Er lebt im
background neben `roomId`/`password` und geht in den Invite-Link.
- **Er darf niemals in einem Relay-Payload landen.** Nicht in `join_room`, nirgends.
Dafür einen Test schreiben, der alle ausgehenden Events gegen den Key prüft.
- **Krypto gehört in den background**, nicht in den content script. Der Key wird damit
gar nicht erst in den Kontext einer fremden Seite ausgeliefert. Das Overlay schickt
Klartext an den background und bekommt Klartext zurück, verschlüsselt wird
ausschließlich dort.
- **Kanten, die korrekt fallen müssen:**
- Raum-Ersteller hat eine **alte** Extension: es existiert kein Key, niemand chattet.
Korrektes Verhalten, kein Fehlerfall.
- Ein Peer mit **alter** Extension teilt den Invite aus seinem eigenen Popup: der Link
trägt keinen Key. Wer darüber joint, chattet nicht, während die anderen chatten.
- Jemand tippt Raum und Passwort **manuell**: kein Key, kein Chat.
- Relay **ohne** Chat-Capability: Chat-UI gar nicht erst anzeigen.
---
## Vorgeschichte
Vorgängerbranch `feature/soonTMChat` (Stand `c706513`) ist **verworfen**, bleibt als
Referenz liegen. Gründe:
- Chat lag nur im Popup. Ein Chrome-Popup schließt beim Fokusverlust, man kann nicht
gleichzeitig Video schauen und chatten. Strukturell unbrauchbar.
- Der Server speicherte bis zu 500 Klartext-Nachrichten pro Raum und schickte jedem
Joiner das Backlog.
- Verifizierter Folgebug: wer einen Raum mit >120 Nachrichten Historie betrat, wurde
**rausgeworfen**. `renderChatHistory()` feuerte eine ungebündelte `chat_read`-Quittung
pro Nachricht und riss `CHAT_READ_RATE_LIMIT` (120/10s). Reproduziert bei
`CHAT_HISTORY_LIMIT=200`, dokumentiert erlaubt sind 500.
**Lehre: kein ungebündeltes Event-pro-Nachricht-Muster. Jedes Client-Verhalten gegen die
Rate-Limits gegenrechnen, bevor es eingebaut wird.**
### Wiederverwendbar (per `git checkout feature/soonTMChat -- <pfad>`, kritisch prüfen)
| Datei | Was | Einschränkung |
|---|---|---|
| `extension/chat.js` | `escapeChatHtml`, `formatChatText`, `insertEmoji`, `createTypingTracker`, `createRemoteTypingTracker` | reine Funktionen, storage-frei, getestet. `createReceiptTracker` weglassen |
| `extension/chat.test.mjs` | Unit-Tests | Receipt-Tests weg |
| `extension/locales/*.json` | `CHAT_*`-Keys, 15 Sprachen | Receipt-Keys weg, neue Keys für Overlay nötig |
| `server/chat.js` | `sanitizeChatUsername`, `canKickPeer` | `sanitizeChatText`/`createChatMessage` greifen auf Klartext zu, bei E2E unmöglich. `parseChatHistoryLimit`, `appendChatHistory`, `canRelayReadReceipt` entfallen |
| `server/rate-limiter.js` | `checkChatMessageRate` (10/10s) | `checkChatReadRate` entfällt |
| `shared/constants.js` | `CAPABILITIES.CHAT`, Event-Namen | **muss neu hinzu.** main kennt nur `HOST_CONTROL` und `CO_HOST` (`shared/constants.js:78`, `server/index.js:174`) |
**Server-Design, das überleben soll:** Der Server vergab Message-ID, `senderId` und
`timestamp` selbst und ignorierte Client-Angaben (Spoofing-Schutz). Bleibt richtig, auch
wenn der Text jetzt Ciphertext ist.
---
## Technische Randbedingungen
### Overlay-Kontext
- `content.js` wird **programmatisch** injiziert:
`chrome.scripting.executeScript({ target: {tabId}, files: ['content.js'] })`
(`background.js:1775`). Nicht über `manifest.content_scripts`, dort steht nur
`bridge.js` für `https://sync.koalastuff.net/*`.
- `host_permissions: ["<all_urls>"]`, MV3.
- Das Overlay lebt im DOM fremder Seiten (Netflix, YouTube, Emby, Jellyfin).
**Shadow DOM ist Pflicht**, sonst bluten fremde Styles rein und umgekehrt.
- Die Theme-Variablen aus `popup.html` existieren im Page-Kontext **nicht** und müssen
in den Shadow Root injiziert werden.
- **Risiko, vorab prüfen:** Netflix-Vollbild ist ein eigener Fullscreen-Element-Kontext.
Ein Overlay im normalen DOM verschwindet dort möglicherweise. Das kann die
Positionierung grundlegend beeinflussen.
### Performance auf der Host-Seite
Das Video ist das Produkt, der Chat ist Beiwerk. Ein Overlay, das die Wiedergabe ruckeln
lässt, ist schlechter als kein Overlay.
- Nur im **Ziel-Tab** injizieren (`currentTabId` im background), nicht auf jeder Seite.
- `position: fixed`, eigener Compositing-Layer, **kein Eingriff in das Layout der
Host-Seite**. Kein Layout-Thrashing, keine erzwungenen Reflows während der Wiedergabe.
- Nachrichtenliste beim Rendern nicht unbegrenzt wachsen lassen (DOM-Knoten deckeln).
### Firefox
`scripts/build-extension.cjs:199` baut ein eigenes **Firefox-Target**, und `bridge.js`
enthält bereits Firefox-spezifisches `cloneInto`-Handling für CustomEvent-Details
(isolierte Welten in FF MV3). Overlay, Shadow DOM und WebCrypto müssen dort ebenfalls
laufen. Neue Website-zu-Extension-Felder (`chatKey`) gehen durch dieselbe
`cloneInto`-Stelle.
### Theme-System (aus `main`, verbindlich)
- `extension/theme-init.js` setzt auf `<html>`: `data-theme` (`light`/`dark`),
`data-palette` (`eucalyptus`/`cyber`/`graphite`), Klasse `theme-light`.
- Quelle: `chrome.storage.local`, Keys `themeMode` und `themePalette`, plus
`chrome.storage.onChanged` für Live-Updates. Das Overlay nutzt denselben Mechanismus
im Shadow Root.
- **Kein `prefers-color-scheme`** für Theme-Farben. Das System ist explizit, nur der
Modus `system` liest die OS-Präferenz und das erledigt `theme-init.js`.
- Tokens: `--bg`, `--card`, `--surface-alt`, `--surface-deep`, `--accent`, `--text`,
`--text-muted`, `--border-soft`, `--border-strong`, `--text-on-green`.
Text auf `--accent` immer `--text-on-green`, nie `white`.
- Getönte `oklch`-Schatten sind pro Palette hartcodiert und brauchen
`html[data-palette=...]`-Overrides. Neutrale Schwarz-Alpha-Schatten sind
palettenunabhängig und der einfachere Weg.
- Bekannte Alt-Last, nicht Aufgabe dieses Branches: `--text-on-green` auf `--accent`
erreicht in eucalyptus/light 3.76 und cyber/dark 4.24, unter WCAG AA.
### Qualitäts-Gates
- `npm run lint`, `npx vitest run`, `npm run verify` müssen grün sein.
- i18n: alle 15 Sprachen in `extension/locales/`, `en` ist Baseline-Fallback. Keine
hartcodierten UI-Strings, `getMessage(key, { placeholder })` aus `i18n.js`.
- Bei einem Release-Tag: Eintrag in `docs/CHANGELOG.md`.
- Keine Em-Dashes in nutzersichtbaren Texten.
---
## Offene Punkte (mit dem Nutzer klären, nicht selbst entscheiden)
1. **Overlay-Positionierung**: fest andockbar rechts/links, oder frei verschiebbar?
Position pro Seite in `chrome.storage.local` merken? Verhalten im Vollbild?
2. **Peers ohne Key**: Chat-UI ganz ausblenden mit Hinweis, oder anzeigen und
Nachrichten als "nicht lesbar" markieren? (Die Fälle, in denen das eintritt, stehen
oben unter Lebenszyklus.)
3. **Metadaten**: Username bleibt heute für den Server sichtbar (er sanitized ihn).
Mitverschlüsseln? Typing-Indikatoren verraten dem Server, wer wann aktiv ist.
Behalten, verschlüsseln oder streichen?
4. **Kick-Funktion**: `canKickPeer` existiert (Host/Controller-Rechte). Behalten?
5. **Nachrichtenlängen-Limit**: 500 Codepoints beibehalten, oder angesichts des
4096-B-Frames anders wählen?
---
## Vorgehen
1. Offene Punkte klären.
2. Krypto- und Linkformat-Design in `docs/CHAT.md` festhalten. Das bestehende Dokument
beschreibt das verworfene Konzept und wird neu geschrieben. Chat-Events in
`docs/PROTOCOL.md` ergänzen.
3. Website-Parser (`#j2:`, Legacy `#join:` weiter unterstützen) und Bridge-Feld
`chatKey` zuerst, weil davon die Abwärtskompatibilität hängt.
4. Dann Relay (reines Relay ohne Storage), dann Krypto im background, dann Overlay, dann
i18n, dann Tests.
5. **Verifizieren, nicht annehmen:**
- Overlay auf mindestens zwei echten Seiten in allen 6 Theme-Kombinationen, nicht nur
in einer isolierten HTML-Datei.
- Chrome **und** Firefox-Build.
- Ein Test, der beweist, dass der Key in keinem ausgehenden Relay-Payload vorkommt.
- Ein Test, der eine umetikettierte Nachricht (fremde `senderId`) als ungültig
zurückweist (AAD-Bindung).
- Alte Extension gegen neue Website: Join funktioniert weiter, Chat ist stumm.
+51
View File
@@ -0,0 +1,51 @@
# DevOps Release Workflow
This document describes the deployment and release process for KoalaSync.
## Tag-Based Releases
KoalaSync uses a fully automated release pipeline triggered by Git tags.
> [!IMPORTANT]
> **DO NOT** manually bump the version numbers in any files (such as `package.json`, `manifest.base.json`, `shared/constants.js`, etc.) before creating a release.
> Bumping versions manually is redundant, leads to conflicts, and is completely handled by the CI/CD pipeline.
### How it Works
When you push a Git tag matching `v*` (e.g., `v2.5.1`), the GitHub Actions release workflow (`.github/workflows/release.yml`) is triggered. The workflow performs the following actions:
1. **Extracts the version** from the tag (e.g., `2.5.1` from `v2.5.1`).
2. **Injects the version** automatically into the following files:
- `extension/manifest.base.json`
- `shared/constants.js` (updates `APP_VERSION`)
- `package.json`
- `website/version.json`
- `website/template.html` (updates `softwareVersion` schema)
- `README.md` (updates badge and announcement banner)
- `website/sitemap.xml` (updates `lastmod` dates)
3. **Commits and pushes** these version updates back to the `main` branch automatically with the commit message `chore(release): update versions to vX.X.X [skip ci]`.
4. **Builds the extension** for both Chrome and Firefox and publishes the zipped archives.
5. **Builds the website** and uploads website artifacts.
6. **Builds and publishes** the Docker image for the relay server to the GitHub Container Registry (`ghcr.io`).
---
## Steps to Deploy a New Release
To release a new version (e.g., `v2.5.1`), follow these steps:
1. Make sure your local repository is synced on `main`:
```bash
git checkout main
git pull origin main
```
2. Create a local Git tag:
```bash
git tag v2.5.1
```
3. Push the tag to GitHub:
```bash
git push origin v2.5.1
```
The release pipeline will take care of the rest! You can monitor the progress under the **Actions** tab of the GitHub repository.
+151
View File
@@ -0,0 +1,151 @@
# Host Control Mode
This document describes the Host Control Mode implementation in the relay and
extension. It only covers behavior implemented in the current codebase.
## Modes
### `everyone`
- Default room mode.
- Any peer may send room-moving playback events.
### `host-only`
- Only controllers may send room-moving playback events.
- The host is always a controller.
- The host can promote additional peers to controllers.
- Guests can keep watching locally in solo/desynced mode, but their local actions
still do not drive the shared room.
Room-moving events are:
- `play`
- `pause`
- `seek`
- `force_sync_prepare`
- `force_sync_execute`
- `episode_lobby`
- `episode_lobby_cancel`
Heartbeats, force-sync ACKs, episode-ready events, ping/pong, and command ACKs
remain allowed for guests.
## Server State
Rooms store Host Control state in memory:
```js
{
hostPeerId,
controlMode,
controllers,
lastControlModeChangeAt,
lastRoleChangeAt,
forceSyncInitiator
}
```
`controllers` is a `Set` on the server and is serialized as an array in
`room_data` and `control_mode`.
State is not persisted across relay restarts.
## Authority Rules
### Changing mode
Only `hostPeerId` may send `set_control_mode`.
Valid values:
- `everyone`
- `host-only`
Invalid values are ignored. Non-host attempts are ignored and the sender receives
the current `control_mode` snapshot so optimistic UI can revert.
Mode changes are debounced per room for 500 ms.
### Promoting and demoting controllers
Only `hostPeerId` may send `set_peer_role`.
The host cannot demote themself. No-op role changes are ignored. Role changes are
debounced per room for 500 ms.
### Host leaving
When the host leaves and peers remain:
- the next peer becomes `hostPeerId`;
- `controlMode` falls back to `everyone`;
- `controllers` is reset to the new host;
- the relay broadcasts `control_mode`.
When a non-host controller leaves, the relay removes that peer from
`controllers` and broadcasts `control_mode`.
## Enforcement
The implementation has two enforcement points:
- The extension background script blocks local guest attempts in `host-only` and
sends `HOST_BLOCKED` to the content script for local UX.
- The relay drops room-moving events from non-controllers in `host-only`, so old
or modified clients cannot drive the room.
The relay is the authority for room-wide effects.
## Guest UX
When a guest action is blocked locally, the content script classifies it:
- deliberate user action: show the host-control dialog;
- likely involuntary player action (buffering, tab refocus, no recent gesture):
silently snap back when safe;
- live/DVR stream: degrade without forcing snap-back.
The dialog offers:
- stay in sync: resync to the host;
- watch on my own: enter solo/desynced mode.
In solo/desynced mode:
- the guest can control their local video;
- host room commands are ignored locally, except force-sync preparation is ACKed
so the host's flow can continue;
- the guest can resync to the host.
The extension reports `desynced` in peer status so the host UI can show that a
guest is watching solo.
## Force Sync Edge Case
The relay tracks `forceSyncInitiator` after a controller sends
`force_sync_prepare`.
This allows that same initiator's `force_sync_execute` through even if their
controller role changes before execute arrives. Without this, a demotion in the
middle of a force-sync flow could leave peers waiting after prepare.
The relay clears `forceSyncInitiator` after execute or when the initiator leaves.
## Capabilities
The relay advertises Host Control support in `room_data.capabilities`:
- `host-control`
- `co-host`
The extension hides or disables matching UI when capabilities are missing.
## Related Events
See [PROTOCOL.md](PROTOCOL.md) for payloads and relay behavior for:
- `set_control_mode`
- `control_mode`
- `set_peer_role`
- host-only gated relay events
+17 -3
View File
@@ -1,6 +1,8 @@
import noUnsanitized from "eslint-plugin-no-unsanitized";
export default [
{
ignores: ["dist/**", "node_modules/**", "scratch/**"]
ignores: ["coverage/**", "dist/**", "node_modules/**", "scratch/**", "website/www/**"]
},
{
languageOptions: {
@@ -8,6 +10,7 @@ export default [
sourceType: "module",
globals: {
chrome: "readonly",
browser: "readonly",
window: "readonly",
document: "readonly",
navigator: "readonly",
@@ -17,6 +20,8 @@ export default [
setInterval: "readonly",
clearTimeout: "readonly",
clearInterval: "readonly",
requestAnimationFrame: "readonly",
cancelAnimationFrame: "readonly",
fetch: "readonly",
CustomEvent: "readonly",
MutationObserver: "readonly",
@@ -36,11 +41,20 @@ export default [
URL: "readonly",
URLSearchParams: "readonly",
WebSocket: "readonly",
history: "readonly",
location: "readonly",
self: "readonly",
process: "readonly"
process: "readonly",
}
},
plugins: {
"no-unsanitized": noUnsanitized
},
rules: {
// Mirrors the AMO validator's "Unsafe assignment to innerHTML" check, so a
// rejection at upload time surfaces here instead.
"no-unsanitized/property": "error",
"no-unsanitized/method": "error",
"no-undef": "error",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
"no-unreachable": "error",
@@ -55,7 +69,7 @@ export default [
}
},
{
files: ["server/**/*.js", "scripts/**/*.js", "website/build.js"],
files: ["server/**/*.js", "scripts/**/*.js", "scripts/**/*.cjs", "website/build.cjs", "website/**/*.cjs"],
languageOptions: {
globals: {
require: "readonly",
+16
View File
@@ -18,6 +18,14 @@
# # Clean URLs support (resolves /join to join.html, etc.)
# try_files {path} {path}.html {path}/
# file_server
#
# # Serve the themed 404 page for unknown URLs (without this block,
# # Caddy returns an empty 404 response)
# handle_errors {
# @notfound expression {err.status_code} == 404
# rewrite @notfound /404.html
# file_server
# }
# }
#
# syncserver.koalastuff.net {
@@ -57,6 +65,14 @@ sync.koalastuff.net {
try_files {path} {path}.html {path}/
file_server
# Serve the themed 404 page for unknown URLs (without this block,
# Caddy returns an empty 404 response)
handle_errors {
@notfound expression {err.status_code} == 404
rewrite @notfound /404.html
file_server
}
# Static Caching for high-performance PageSpeed (1 year with validation)
@static {
file
@@ -9,6 +9,7 @@ services:
- 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=25 # Maximum number of peers allowed per room
- SERVER_SALT=CHANGE_ME_GENERATE_WITH_OPENSSL_RAND_BASE64_32 # Required: unique random salt for room-password hashes
- ADMIN_METRICS_TOKEN= # Optional: 32+ char random token for aggregate-only /health metrics
pids_limit: 2048 # Limits the container to 2048 process IDs for safety
networks: # Attaches the service to the networks listed below
+1
View File
@@ -11,6 +11,7 @@ services:
- 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=25 # Maximum number of peers allowed per room
- SERVER_SALT=CHANGE_ME_GENERATE_WITH_OPENSSL_RAND_BASE64_32 # Required: unique random salt for room-password hashes
- ADMIN_METRICS_TOKEN= # Optional: 32+ char random token for aggregate-only /health metrics
pids_limit: 2048 # Limits the container to 2048 process IDs for safety
networks: # Attaches the service to the networks listed below
+37 -12
View File
@@ -54,26 +54,51 @@ modules:
path: '{.memory.heapTotal}'
help: "V8 engine heap total in bytes"
# Rate limiter tracking — unique clients currently in each tracking window
# (not rate-limit denials; these include legitimate traffic too)
- name: koalasync_rate_limit_connections
path: '{.rateLimitEntries.connections}'
help: "Number of entries tracked in the connection rate limiter"
path: '{.rateLimits.trackedClients.connections}'
help: "Unique clients tracked in the connection rate limiter window"
- name: koalasync_rate_limit_events
path: '{.rateLimitEntries.events}'
help: "Number of entries in the event rate limiter"
path: '{.rateLimits.trackedClients.events}'
help: "Unique sockets tracked in the event rate limiter window"
- name: koalasync_rate_limit_health
path: '{.rateLimitEntries.health}'
help: "Number of entries in the health rate limiter"
path: '{.rateLimits.trackedClients.health}'
help: "Unique IPs tracked in the health endpoint rate limiter window"
- name: koalasync_rate_limit_admin_metrics_auth
path: '{.rateLimitEntries.adminMetricsAuth}'
help: "Number of entries in the admin metrics auth rate limiter"
path: '{.rateLimits.trackedClients.adminMetricsAuth}'
help: "Unique IPs tracked in the admin metrics auth rate limiter window"
- name: koalasync_rate_limit_auth_failures
path: '{.rateLimitEntries.authFailures}'
help: "Number of entries in the authentication failures cache"
path: '{.rateLimits.trackedClients.authFailures}'
help: "Unique IPs tracked in the authentication failures cache"
- name: koalasync_rate_limit_room_list
path: '{.rateLimitEntries.roomList}'
help: "Number of entries in the room list cooldown cache"
path: '{.rateLimits.trackedClients.roomList}'
help: "Unique sockets in the room list cooldown cache"
# Actual rate-limit denials — incremented only when a 429 is served
- name: koalasync_rate_limit_denied_connections
path: '{.rateLimits.denied.connections}'
help: "Total connection attempts denied by rate limiter"
- name: koalasync_rate_limit_denied_events
path: '{.rateLimits.denied.events}'
help: "Total socket events denied by rate limiter"
- name: koalasync_rate_limit_denied_health
path: '{.rateLimits.denied.health}'
help: "Total health endpoint requests denied by rate limiter"
- name: koalasync_rate_limit_denied_admin_metrics_auth
path: '{.rateLimits.denied.adminMetricsAuth}'
help: "Total admin metrics auth attempts denied by rate limiter"
- name: koalasync_rate_limit_denied_room_list
path: '{.rateLimits.denied.roomList}'
help: "Total room list refresh requests denied by rate limiter"
+92 -26
View File
@@ -1,40 +1,106 @@
# KoalaSync Browser Extension
A Manifest V3 Browser Extension (Chrome & Firefox) for synchronized video playback across any website.
This directory contains the Manifest V3 browser extension for Chrome and Firefox. It owns the popup UI, background service worker, content-script video control, invitation bridge, audio processing, and all browser-local settings.
## Where You Are
- `manifest.base.json` is the source manifest. The build script creates browser-specific `manifest.json` files in `dist/chrome/` and `dist/firefox/`.
- `background.js` is the long-lived coordinator: WebSocket client, room state, host-control authority, heartbeat, reconnects, tab selection, and content-script injection.
- `content.js` runs in the selected video tab. It detects video state, applies remote play/pause/seek, handles episode transitions, and applies local audio processing.
- `popup.html` and `popup.js` implement the visible extension UI.
- `extension/shared/` is generated by `npm run build:extension` from the root `shared/` directory. Do not edit it directly.
## Key Features
- **Manifest V3**: Optimized Service Worker architecture with session persistence.
- **Pure Vanilla JS**: No external dependencies or heavy libraries.
- **Smart Peer IDs**: Hexadecimal IDs combined with customizable Usernames for easy identification.
- **Dual Heartbeat**: Advanced session tracking (Background) and video synchronization (Content) to prevent ghost sessions.
- **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.
- **Manifest V3**: Service-worker architecture with session persistence and explicit keep-alive handling.
- **Pure Vanilla JS**: No extension runtime dependencies and no bundler inside `extension/`.
- **On-Demand Connection**: The service worker connects only while the user intends to be in a room.
- **Host Control & Co-Hosts**: Hosts can switch a room into `host-only` mode and grant controller rights to trusted peers.
- **Episode Auto-Sync**: Title/episode changes can open a lobby so peers resume together once everyone is ready.
- **Smart Matching & Title Privacy**: Matching video tabs are highlighted, while tab/media title sharing can be reduced or disabled.
- **Audio Processing**: Optional local compressor settings live in the dedicated audio options page.
- **Status Diagnostics**: The Status tab exposes connection state, ping, video debug data, action history, and copyable logs.
- **Dynamic i18n**: 15 languages are supported: `en`, `de`, `fr`, `es`, `it`, `nl`, `pl`, `pt`, `pt-BR`, `tr`, `ru`, `ja`, `ko`, `zh`, and `uk`.
## 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, toggle domain-based Noise Filtering, and switch the App Language.
4. **Dev**: Monitor connection status and view real-time video element metadata for debugging.
1. **Room**: Select official/custom server, create or join rooms, view peers, share invite links, and manage Host Control when supported by the relay.
2. **Sync**: Select the video tab, send play/pause/seek/force-sync actions, and view episode lobby state.
3. **Settings**: Configure username, title sharing, noise filtering, auto episode sync, notifications, language, and audio options.
4. **Status**: Inspect connection state, latency, video debug info, history, and logs for bug reports.
5. **Dev**: Hidden developer-only controls shown for the `KoalaDev` username.
## Privacy & Permissions
KoalaSync requires `<all_urls>` permission to detect and interact with video elements (`<video>`) on websites.
- **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 performance and privacy.
KoalaSync requires `<all_urls>` host permission so it can detect and control `<video>` elements on arbitrary sites.
- No browsing history is collected or uploaded.
- Room credentials and user settings are stored locally with `chrome.storage`.
- No analytics, external scripts, external fonts, or tracking libraries are used by the extension.
- Audio processing is local to the selected tab.
- Title privacy controls decide whether tab/media titles are shared with room peers.
## Installation
1. **Prepare Extension**: From the repository root, run:
```bash
node scripts/build-extension.js
```
2. Open Chrome and go to `chrome://extensions/`.
3. Enable **Developer mode** (top right).
4. Click **Load unpacked** and select the `dist/chrome` folder.
From the repository root:
```bash
npm install
npm run build:extension
```
Then load the generated bundle:
- Chrome/Chromium: open `chrome://extensions/`, enable Developer Mode, and load `dist/chrome`.
- Firefox: open `about:debugging`, choose **This Firefox**, and load `dist/firefox/manifest.json`.
## Development
If you modify `shared/constants.js`, you must synchronize the changes by running the build script from the root:
Run the build whenever shared protocol files or extension packaging inputs change:
```bash
node scripts/build-extension.js
npm run build:extension
```
This ensures that the `extension/shared` folder is updated with the latest protocol constants.
The build copies `shared/constants.js`, `shared/blacklist.js`, `shared/names.js`, and `shared/README.md` into `extension/shared/`, injects synchronous constants into `content.js`, generates browser manifests, and creates zip artifacts in `dist/`.
Useful focused checks from the repository root:
```bash
node -c extension/background.js
node -c extension/content.js
node -c extension/popup.js
node scripts/test-episode-utils.mjs
node scripts/test-title-privacy.mjs
node scripts/test-audio-settings.mjs
node scripts/test-locales.cjs
```
For the full suite, run:
```bash
npm run verify
```
## Do Not Break
- Keep `content.js` synchronous and IIFE-based; it cannot import ES modules directly.
- Keep the injection markers used by `scripts/build-extension.cjs`.
- Keep protocol names in `shared/constants.js` as the source of truth.
- Keep extension runtime dependencies at zero unless the project explicitly decides to introduce a bundler.
- Keep all extension assets self-hosted.
## Module Structure
| File | Purpose |
|---|---|
| `background.js` | Service worker: WebSocket protocol, room state, host control, tab/content routing, reconnects |
| `content.js` | Video detection/control, audio processing, episode transition, host-only guest behavior |
| `popup.js` | Popup UI: room join/create, tabs, settings, peer list, status, diagnostics |
| `popup.html` | Popup markup, tabs, onboarding, status/debug surfaces |
| `bridge.js` | Invitation bridge injected into `sync.koalastuff.net` |
| `episode-utils.js` | Shared episode-title parser imported by background and injected into content at build time |
| `title-privacy.js` | Tab/media title privacy modes and sanitization helpers |
| `audio-options.html` / `audio-options.js` / `audio-options.css` | Dedicated local audio-processing settings page |
| `page-api-seek-overrides.js` | Page-level seek bridge for site-specific player APIs |
| `modules/tab-manager.js` | Tab lifecycle helper used by the background service worker |
| `i18n.js` | Dynamic locale loader and DOM translation helper |
| `locales/` | Runtime popup translations |
| `_locales/` | Browser-store manifest translations |
| `shared/` | Generated mirror of root shared constants, blacklist, names, and README |
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Erstelle private Watch Partys und synchronisiere Videos mit Freunden auf YouTube, Netflix, Jellyfin, Emby und fast jeder Website."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Create private watch parties and sync videos with friends on YouTube, Netflix, Jellyfin, Emby, and almost any HTML5 website."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Crea fiestas privadas y sincroniza vídeos con amigos en YouTube, Netflix, Jellyfin, Emby y casi cualquier sitio HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Créez des watch parties privées et regardez en synchro entre amis sur YouTube, Netflix, Jellyfin, Emby et presque tout site HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Crea watch party private e sincronizza i video con gli amici su YouTube, Netflix, Jellyfin, Emby e quasi ogni sito HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "プライベートなウォッチパーティーを作成し、YouTube、Netflix、Jellyfin、EmbyやほぼすべてのHTML5サイトで友達と動画を同期再生できます。"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "비공개 Watch Party를 만들고 YouTube, Netflix, Jellyfin, Emby 및 거의 모든 HTML5 사이트에서 친구들과 영상을 동기화하세요."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Maak privéwatchparty's en kijk synchroon met vrienden op YouTube, Netflix, Jellyfin, Emby en vrijwel elke HTML5-website."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Twórz prywatne wspólne seanse i synchronizuj filmy ze znajomymi na YouTube, Netflix, Jellyfin, Emby i niemal każdej stronie HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Crie watch parties privadas e sincronize vídeos com amigos no YouTube, Netflix, Jellyfin, Emby e em quase qualquer site HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Crie sessões privadas e veja vídeos sincronizados com amigos no YouTube, Netflix, Jellyfin, Emby e em quase qualquer site HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Создавайте закрытые Watch Party и смотрите синхронно с друзьями на YouTube, Netflix, Jellyfin, Emby и почти любом сайте HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Özel izleme partileri kurun; YouTube, Netflix, Jellyfin, Emby ve çoğu HTML5 sitesinde arkadaşlarınızla senkron izleyin."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "Створюйте закриті Watch Party й дивіться синхронно з друзями на YouTube, Netflix, Jellyfin, Emby та майже будь-якому сайті HTML5."
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"appName": {
"message": "Watch Party - KoalaSync"
},
"appDesc": {
"message": "创建私人观影派对,与好友在 YouTube、Netflix、Jellyfin、Emby 及几乎任何 HTML5 视频网站同步观看。"
}
}
+54 -17
View File
@@ -1,13 +1,46 @@
:root {
--bg: #0f172a;
--card: #1e293b;
--panel: #172033;
--accent: #6366f1;
--accent-hover: #818cf8;
--text: #f8fafc;
--text-muted: #94a3b8;
--border: #334155;
/* KoalaSync nature system: forest neutrals, eucalyptus brand, warm supporting accents. */
--bg-base: oklch(0.23 0.028 155);
--surface: oklch(0.30 0.035 150);
--surface-deep: oklch(0.205 0.025 155);
--surface-alt: oklch(0.265 0.032 150);
--border-soft: oklch(0.88 0.025 145 / 0.09);
--border-strong: oklch(0.39 0.038 145);
--text-primary: oklch(0.96 0.01 90);
--text-secondary: oklch(0.78 0.02 90);
--accent-green: oklch(0.73 0.13 155);
--accent-green-hover: oklch(0.79 0.12 155);
--accent-terracotta: oklch(0.62 0.14 45);
--success-green: oklch(0.72 0.14 135);
--warning-amber: oklch(0.76 0.14 80);
--danger: oklch(0.55 0.15 25);
--text-on-green: oklch(0.14 0.02 90);
--bg: var(--bg-base);
--card: var(--surface);
--panel: var(--surface-alt);
--accent: var(--accent-green);
--accent-hover: var(--accent-green-hover);
--text: var(--text-primary);
--text-muted: var(--text-secondary);
--border: var(--border-strong);
--radius: 8px;
color-scheme: dark;
}
html.theme-light {
--bg-base: oklch(0.965 0.012 105);
--surface: oklch(0.995 0.006 100);
--surface-deep: oklch(0.935 0.018 110);
--surface-alt: oklch(0.91 0.022 115);
--border-soft: oklch(0.28 0.035 140 / 0.10);
--border-strong: oklch(0.73 0.035 125);
--text-primary: oklch(0.21 0.03 140);
--text-secondary: oklch(0.43 0.03 135);
--accent-green: oklch(0.52 0.13 155);
--accent-green-hover: oklch(0.45 0.12 155);
--text-on-green: oklch(0.98 0.005 100);
color-scheme: light;
}
* {
@@ -55,7 +88,7 @@ body {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
letter-spacing: 0.04em;
}
h1,
@@ -83,6 +116,10 @@ h2 {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
html.theme-light .panel {
box-shadow: 0 12px 30px oklch(0.22 0.035 140 / 0.09);
}
.muted-panel {
color: var(--text-muted);
}
@@ -101,7 +138,7 @@ h2 {
color: var(--text-muted);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.01em;
}
.preset-group {
@@ -125,12 +162,12 @@ h2 {
transition: border-color 0.2s, box-shadow 0.2s;
}
.preset-card:hover {
border-color: rgba(99, 102, 241, 0.4);
border-color: color-mix(in oklch, var(--accent), transparent 60%);
}
.preset-card:has(input:checked) {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35);
box-shadow: 0 0 0 1px color-mix(in oklch, var(--accent), transparent 65%);
}
.preset-card input {
@@ -141,8 +178,8 @@ h2 {
display: grid;
gap: 12px;
padding: 16px;
background: rgba(15, 23, 42, 0.58);
border: 1px solid rgba(148, 163, 184, 0.16);
background: var(--surface-deep);
border: 1px solid var(--border-soft);
border-radius: var(--radius);
transition: opacity 0.3s;
}
@@ -158,7 +195,7 @@ h2 {
color: var(--text-muted);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.01em;
}
input[type="range"] {
@@ -204,7 +241,7 @@ input[type="number"]:focus {
position: absolute;
cursor: pointer;
inset: 0;
background-color: #334155;
background-color: var(--border-strong);
transition: .3s;
border-radius: 22px;
}
@@ -216,7 +253,7 @@ input[type="number"]:focus {
width: 16px;
left: 3px;
bottom: 3px;
background-color: #94a3b8;
background-color: var(--text-secondary);
transition: .3s;
border-radius: 50%;
}
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>KoalaSync Audio Settings</title>
<script src="theme-init.js"></script>
<link rel="stylesheet" href="audio-options.css">
</head>
<body>
+4 -8
View File
@@ -126,17 +126,13 @@ function setCustomParam(param, value) {
}
async function init() {
let audioData = (await chrome.storage.local.get(['audioSettings'])).audioSettings;
const syncData = await chrome.storage.sync.get(['audioSettings', 'locale']);
if (!audioData && syncData.audioSettings) {
audioData = syncData.audioSettings;
await chrome.storage.local.set({ audioSettings: audioData });
}
const lang = syncData.locale || getSystemLanguage();
// Local-only: audioSettings/locale are never read from storage.sync.
const { audioSettings, locale } = await chrome.storage.local.get(['audioSettings', 'locale']);
const lang = locale || getSystemLanguage();
await loadLocale(lang);
translateDOM();
currentSettings = mergeAudioSettings(audioData);
currentSettings = mergeAudioSettings(audioSettings);
render();
}
+2045 -290
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -11,11 +11,12 @@ document.documentElement.dataset.koalasyncInstalled = 'true';
// 2. Listen for Join Requests from the Website
window.addEventListener('KOALASYNC_JOIN_REQUEST', (e) => {
if (!e || !e.detail) return;
const { roomId, password, useCustomServer, serverUrl } = e.detail;
const { roomId, password, chatKey, useCustomServer, serverUrl } = e.detail;
chrome.runtime.sendMessage({
type: 'WEB_JOIN_REQUEST',
roomId,
password,
chatKey,
useCustomServer,
serverUrl
}).catch(() => {});
+122
View File
@@ -0,0 +1,122 @@
const CHAT_INFO = 'KoalaSync E2E Chat v1';
const SECRET_BYTES = 16;
const IV_BYTES = 12;
const MIN_ENCRYPTED_BYTES = 29;
const MAX_ENCRYPTED_BYTES = 2028;
let cachedKeyPromise = null;
let cachedRoomId = '';
let cachedSecret = '';
let cacheGeneration = 0;
function bytesToBase64Url(bytes) {
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
return globalThis.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
function base64UrlToBytes(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) return null;
const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
try {
const binary = globalThis.atob(padded);
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
return bytesToBase64Url(bytes) === value ? bytes : null;
} catch (_) {
return null;
}
}
export function generateChatSecret(cryptoImpl = globalThis.crypto) {
const bytes = new Uint8Array(SECRET_BYTES);
cryptoImpl.getRandomValues(bytes);
return bytesToBase64Url(bytes);
}
export function validateChatSecret(value) {
const bytes = base64UrlToBytes(value);
return bytes?.length === SECRET_BYTES ? value : '';
}
export function countCodePoints(value) {
return [...String(value ?? '')].length;
}
export function normalizeOutgoingChatText(value) {
if (typeof value !== 'string') throw new TypeError('Chat message must be text');
const text = value.trim();
if (!text) throw new TypeError('Chat message must not be empty');
if (countCodePoints(text) > 500) throw new RangeError('Chat message exceeds 500 Unicode code points');
return text;
}
export function clearChatKeyCache() {
cacheGeneration++;
cachedKeyPromise = null;
cachedRoomId = '';
cachedSecret = '';
}
export async function deriveChatKey(roomId, secret, cryptoImpl = globalThis.crypto) {
const validSecret = validateChatSecret(secret);
if (!roomId || !validSecret) throw new TypeError('Valid roomId and chat secret are required');
if (cachedKeyPromise && cachedRoomId === roomId && cachedSecret === validSecret) return cachedKeyPromise;
const encoder = new globalThis.TextEncoder();
const generation = cacheGeneration;
const keyPromise = (async () => {
const material = await cryptoImpl.subtle.importKey(
'raw', base64UrlToBytes(validSecret), { name: 'HKDF' }, false, ['deriveKey']
);
return cryptoImpl.subtle.deriveKey({
name: 'HKDF',
hash: 'SHA-256',
salt: encoder.encode(roomId),
info: encoder.encode(CHAT_INFO)
}, material, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
})();
cachedKeyPromise = keyPromise;
cachedRoomId = roomId;
cachedSecret = validSecret;
try {
return await keyPromise;
} catch (error) {
if (generation === cacheGeneration && cachedKeyPromise === keyPromise) clearChatKeyCache();
throw error;
}
}
export async function encryptChatMessage({ text, roomId, senderId, secret }, cryptoImpl = globalThis.crypto) {
const plaintext = normalizeOutgoingChatText(text);
if (!senderId) throw new TypeError('senderId is required');
const key = await deriveChatKey(roomId, secret, cryptoImpl);
const encoder = new globalThis.TextEncoder();
const iv = new Uint8Array(IV_BYTES);
cryptoImpl.getRandomValues(iv);
const encrypted = new Uint8Array(await cryptoImpl.subtle.encrypt({
name: 'AES-GCM',
iv,
additionalData: encoder.encode(`${roomId}|${senderId}`)
}, key, encoder.encode(plaintext)));
const envelope = new Uint8Array(iv.length + encrypted.length);
envelope.set(iv);
envelope.set(encrypted, iv.length);
return bytesToBase64Url(envelope);
}
export async function decryptChatMessage({ ciphertext, roomId, senderId, secret }, cryptoImpl = globalThis.crypto) {
if (!senderId) throw new TypeError('senderId is required');
const bytes = base64UrlToBytes(ciphertext);
if (!bytes || bytes.length < MIN_ENCRYPTED_BYTES || bytes.length > MAX_ENCRYPTED_BYTES) {
throw new TypeError('Invalid chat ciphertext');
}
const key = await deriveChatKey(roomId, secret, cryptoImpl);
const encoder = new globalThis.TextEncoder();
const plaintext = await cryptoImpl.subtle.decrypt({
name: 'AES-GCM',
iv: bytes.slice(0, IV_BYTES),
additionalData: encoder.encode(`${roomId}|${senderId}`)
}, key, bytes.slice(IV_BYTES));
const text = new globalThis.TextDecoder('utf-8', { fatal: true }).decode(plaintext);
return normalizeOutgoingChatText(text);
}
+114
View File
@@ -0,0 +1,114 @@
import { webcrypto } from 'node:crypto';
import { Buffer } from 'node:buffer';
import { afterEach, describe, expect, it } from 'vitest';
import {
clearChatKeyCache,
countCodePoints,
decryptChatMessage,
deriveChatKey,
encryptChatMessage,
generateChatSecret,
normalizeOutgoingChatText,
validateChatSecret
} from './chat-crypto.js';
afterEach(clearChatKeyCache);
describe('chat crypto', () => {
it('generates canonical 128-bit secrets', () => {
const secret = generateChatSecret(webcrypto);
expect(secret).toMatch(/^[A-Za-z0-9_-]{22}$/);
expect(validateChatSecret(secret)).toBe(secret);
expect(validateChatSecret(`${secret}=`)).toBe('');
});
it('round-trips Unicode with a cached room key and fresh IVs', async () => {
const secret = generateChatSecret(webcrypto);
const firstKey = await deriveChatKey('ROOM-1', secret, webcrypto);
const secondKey = await deriveChatKey('ROOM-1', secret, webcrypto);
expect(secondKey).toBe(firstKey);
const input = { text: 'Hello 🐨 **world**', roomId: 'ROOM-1', senderId: 'alice', secret };
const first = await encryptChatMessage(input, webcrypto);
const second = await encryptChatMessage(input, webcrypto);
expect(second).not.toBe(first);
await expect(decryptChatMessage({ ciphertext: first, ...input }, webcrypto)).resolves.toBe(input.text);
});
it('deduplicates in-flight key derivations without restoring a cleared cache', async () => {
const secret = generateChatSecret(webcrypto);
let deriveCalls = 0;
let releaseDerive;
const delayedCrypto = {
...webcrypto,
subtle: {
importKey: (...args) => webcrypto.subtle.importKey(...args),
deriveKey: async (...args) => {
deriveCalls++;
await new Promise(resolve => { releaseDerive = resolve; });
return webcrypto.subtle.deriveKey(...args);
}
}
};
const first = deriveChatKey('ROOM-1', secret, delayedCrypto);
const second = deriveChatKey('ROOM-1', secret, delayedCrypto);
await Promise.resolve();
expect(deriveCalls).toBe(1);
clearChatKeyCache();
releaseDerive();
await Promise.all([first, second]);
const fresh = deriveChatKey('ROOM-1', secret, webcrypto);
expect(fresh).not.toBe(first);
await fresh;
});
it('rejects relabeling, cross-room replay, and the wrong secret', async () => {
const secret = generateChatSecret(webcrypto);
const ciphertext = await encryptChatMessage({ text: 'secret', roomId: 'ROOM-1', senderId: 'alice', secret }, webcrypto);
clearChatKeyCache();
await expect(decryptChatMessage({ ciphertext, roomId: 'ROOM-1', senderId: 'bob', secret }, webcrypto)).rejects.toThrow();
clearChatKeyCache();
await expect(decryptChatMessage({ ciphertext, roomId: 'ROOM-2', senderId: 'alice', secret }, webcrypto)).rejects.toThrow();
clearChatKeyCache();
await expect(decryptChatMessage({ ciphertext, roomId: 'ROOM-1', senderId: 'alice', secret: generateChatSecret(webcrypto) }, webcrypto)).rejects.toThrow();
});
it('enforces 500 Unicode code points before encryption', () => {
expect(countCodePoints('😀'.repeat(500))).toBe(500);
expect(normalizeOutgoingChatText(` ${'😀'.repeat(500)} `)).toBe('😀'.repeat(500));
expect(() => normalizeOutgoingChatText('😀'.repeat(501))).toThrow(RangeError);
expect(() => normalizeOutgoingChatText(' ')).toThrow(TypeError);
});
it('keeps the worst-case 500-codepoint payload within the relay byte bound', async () => {
const secret = generateChatSecret(webcrypto);
const ciphertext = await encryptChatMessage({
text: '😀'.repeat(500), roomId: 'ROOM-1', senderId: 'alice', secret
}, webcrypto);
const bytes = Buffer.from(ciphertext, 'base64url');
expect(bytes).toHaveLength(2028);
});
it('rejects non-canonical plaintext and malformed UTF-8 after authentication', async () => {
const secret = generateChatSecret(webcrypto);
const roomId = 'ROOM-1';
const senderId = 'alice';
const key = await deriveChatKey(roomId, secret, webcrypto);
const encoder = new globalThis.TextEncoder();
async function encryptRaw(plaintext) {
const iv = webcrypto.getRandomValues(new Uint8Array(12));
const encrypted = new Uint8Array(await webcrypto.subtle.encrypt({
name: 'AES-GCM',
iv,
additionalData: encoder.encode(`${roomId}|${senderId}`)
}, key, plaintext));
return Buffer.concat([Buffer.from(iv), Buffer.from(encrypted)]).toString('base64url');
}
const whitespace = await encryptRaw(encoder.encode(' '));
await expect(decryptChatMessage({ ciphertext: whitespace, roomId, senderId, secret }, webcrypto)).rejects.toThrow(TypeError);
const malformed = await encryptRaw(Uint8Array.of(0xff));
await expect(decryptChatMessage({ ciphertext: malformed, roomId, senderId, secret }, webcrypto)).rejects.toThrow();
});
});
+33
View File
@@ -0,0 +1,33 @@
(function exposeChatFormat(root) {
function escapeChatHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatChatText(value) {
return escapeChatHtml(value)
.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>')
.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
}
function tokenizeChatText(value) {
const text = String(value ?? '');
const tokens = [];
const pattern = /\*\*([^*\n]+)\*\*|\*([^*\n]+)\*/g;
let cursor = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
if (match.index > cursor) tokens.push({ type: 'text', text: text.slice(cursor, match.index) });
tokens.push({ type: match[1] !== undefined ? 'strong' : 'em', text: match[1] ?? match[2] });
cursor = pattern.lastIndex;
}
if (cursor < text.length) tokens.push({ type: 'text', text: text.slice(cursor) });
return tokens;
}
root.KoalaSyncChatFormat = Object.freeze({ escapeChatHtml, formatChatText, tokenizeChatText });
})(globalThis);
+22
View File
@@ -0,0 +1,22 @@
import { beforeAll, describe, expect, it } from 'vitest';
beforeAll(async () => {
await import('./chat-format.js');
});
describe('chat formatting', () => {
it('escapes executable HTML before applying limited Markdown', () => {
const result = globalThis.KoalaSyncChatFormat.formatChatText('<img src=x onerror=alert(1)> **bold** *italic*');
expect(result).toBe('&lt;img src=x onerror=alert(1)&gt; <strong>bold</strong> <em>italic</em>');
expect(result).not.toContain('<img');
});
it('tokenizes only text, strong, and emphasis nodes', () => {
expect(globalThis.KoalaSyncChatFormat.tokenizeChatText('<script>x</script> **bold** *italic*')).toEqual([
{ type: 'text', text: '<script>x</script> ' },
{ type: 'strong', text: 'bold' },
{ type: 'text', text: ' ' },
{ type: 'em', text: 'italic' }
]);
});
});
+152
View File
@@ -0,0 +1,152 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
const overlaySource = fs.readFileSync(path.join(extensionDir, 'chat-overlay.js'), 'utf8');
const backgroundSource = fs.readFileSync(path.join(extensionDir, 'background.js'), 'utf8');
const popupSource = fs.readFileSync(path.join(extensionDir, 'popup.js'), 'utf8');
const localeDir = path.join(extensionDir, 'locales');
const chatKeys = [
'LABEL_CHAT_ENABLED',
'LABEL_CHAT_ENABLED_TOOLTIP',
'CHAT_TITLE',
'CHAT_LIVE_ONLY',
'CHAT_OPEN',
'CHAT_CLOSE',
'CHAT_DOCK_LEFT',
'CHAT_DOCK_RIGHT',
'CHAT_DETACHED',
'CHAT_PLACEHOLDER',
'CHAT_SEND',
'CHAT_MISSING_KEY',
'CHAT_TOO_LONG',
'CHAT_SEND_FAILED',
'CHAT_EMPTY'
];
describe('chat overlay contract', () => {
it('isolates the overlay and never renders markup as HTML', () => {
expect(overlaySource).toContain("attachShadow({ mode: 'open' })");
expect(overlaySource).not.toMatch(/\.innerHTML\s*=|insertAdjacentHTML|\.outerHTML\s*=/);
expect(overlaySource).toContain('document.createTextNode(token.text)');
});
it('injects formatting and overlay code only with the selected-tab content script', () => {
expect(backgroundSource).toContain("files: ['chat-format.js', 'chat-overlay.js', 'content.js']");
expect(overlaySource).toContain("const storageKey = `chatOverlayLayout:${location.origin}`");
expect(overlaySource).toContain('document.fullscreenElement || document.documentElement');
});
it('supports all layout and theme combinations with bounded message DOM', () => {
expect(overlaySource).toContain("['left', 'right', 'detached']");
expect(overlaySource).toContain('!layout.detachedInitialized');
expect(overlaySource).toContain('const rect = panel.getBoundingClientRect()');
expect(overlaySource).not.toContain('contentRect');
expect(overlaySource).toContain("#app[data-palette=\"cyber\"][data-theme=\"light\"]");
expect(overlaySource).toContain("#app[data-palette=\"graphite\"][data-theme=\"light\"]");
expect(overlaySource).toContain('const MAX_MESSAGES = 200');
expect(overlaySource).toContain('while (messages.querySelectorAll(\'.message\').length > MAX_MESSAGES)');
expect(overlaySource).toContain('Math.max(1, window.innerWidth)');
expect(overlaySource).toContain('min(${MIN_WIDTH}px, calc(100vw - 16px))');
});
it('blocks page playback shortcuts while the chat composer owns keyboard input', () => {
expect(overlaySource).toContain("for (const eventName of ['keydown', 'keyup', 'keypress'])");
expect(overlaySource).toContain('window.addEventListener(eventName, stopPageKeyboardShortcut, true)');
expect(overlaySource).toContain('path.includes(textarea)');
expect(overlaySource).toContain('event.stopImmediatePropagation()');
expect(overlaySource).toContain('window.removeEventListener(eventName, stopPageKeyboardShortcut, true)');
});
it('keeps the launcher draggable independently from dock mode', () => {
expect(overlaySource).toContain("launcher.addEventListener('pointerdown'");
expect(overlaySource).toContain("launcher.addEventListener('pointermove'");
expect(overlaySource).toContain('layout.launcherX = launcherDrag.x + deltaX');
expect(overlaySource).toContain('layout.launcherY = launcherDrag.y + deltaY');
expect(overlaySource).toContain('suppressLauncherClick = launcherDrag.moved');
expect(overlaySource).not.toMatch(/launcher\.style\.left = `\$\{layout\.x\}px`/);
});
it('reserves page space for real left and right dock modes', () => {
expect(overlaySource).toContain("const PAGE_DOCK_ATTRIBUTE = 'data-koalasync-chat-dock'");
expect(overlaySource).toContain('padding-left: var(${PAGE_DOCK_WIDTH}) !important');
expect(overlaySource).toContain('padding-right: var(${PAGE_DOCK_WIDTH}) !important');
expect(overlaySource).toContain('document.documentElement.setAttribute(PAGE_DOCK_ATTRIBUTE, side)');
expect(overlaySource).toContain('applyPageDock(layout.mode, dockWidth)');
expect(overlaySource).toContain('clearPageDock()');
});
it('counts unread remote messages on the bubble and clears the badge when opened', () => {
expect(overlaySource).toContain("const unreadBadge = element('span', 'unread')");
expect(overlaySource).toContain('if (!opened && !own) setUnreadCount(unreadCount + 1)');
expect(overlaySource).toMatch(/if \(opened\) \{\s*setUnreadCount\(0\)/);
expect(overlaySource).toContain("unreadBadge.classList.toggle('visible', unreadCount > 0)");
expect(backgroundSource).toMatch(/received\.senderId !== peerId[\s\S]*showChatNotification\([\s\S]*senderPeer\.username[\s\S]*received\.senderId/);
expect(backgroundSource).toContain("chrome.notifications.create(`chat_${Date.now()}`");
expect(backgroundSource).toContain("if (settings.chatNotifications === false) return");
});
it('renders timestamped room activity and keeps activity notifications opt-in', () => {
expect(overlaySource).toContain("if (message?.type === 'CHAT_EVENT') appendEvent(message.event)");
expect(overlaySource).toContain("const time = element('time', 'time')");
expect(overlaySource).toContain("date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })");
expect(overlaySource).toContain('if (!opened && context.eventNotifications) setUnreadCount(unreadCount + 1)');
expect(backgroundSource).toContain("eventNotifications: localeData.browserNotifications === true");
expect(backgroundSource).toContain("sendChatActivity(message.action, peerId, timestamp)");
expect(backgroundSource).toContain("sendChatActivity(event, data.senderId, data.actionTimestamp)");
expect(backgroundSource).toContain("sendChatActivity('joined', data.peerId, Date.now())");
expect(backgroundSource).toContain("sendChatActivity('left', data.peerId, Date.now())");
});
it('guards async refresh/send work and clears all composer state on room reset', () => {
expect(overlaySource).toContain('generation !== refreshGeneration');
expect(overlaySource).toContain('if (sending || !context?.enabled) return');
expect(overlaySource).toContain('textarea.value === submittedValue');
expect(overlaySource).toMatch(/CHAT_RESET[\s\S]*resetComposer\(\)/);
expect(overlaySource).toContain('setTimeout(() => finish(null), timeoutMs)');
expect(backgroundSource).toContain('chatReceiveQueue = chatReceiveQueue.catch(() => {}).then');
expect(backgroundSource).toContain("status: 'rate_limited'");
});
it('keeps chat hidden by default without discarding the room chat key', () => {
expect(popupSource).toContain('localData.chatEnabled === true');
expect(backgroundSource).toContain('chatEnabled: data.chatEnabled === true');
expect(backgroundSource).toContain('clientCapabilities: CLIENT_CAPABILITIES');
expect(overlaySource).toContain('all:initial;display:none;position:fixed');
expect(overlaySource).toContain("host.style.display = supported && optedIn ? '' : 'none'");
expect(overlaySource).toContain('supported && optedIn && hasKey && connected');
expect(popupSource).toContain("chrome.storage.local.set({ chatEnabled: elements.chatEnabled.checked })");
expect(popupSource).toMatch(/if \(isCreating\) \{[\s\S]*?type: 'CREATE_CHAT_KEY'[\s\S]*?chatKey = normalizeChatKey/);
expect(popupSource).not.toMatch(/chatEnabled[\s\S]{0,120}chatKey:\s*''/);
});
it('keeps unavailable chat controls discoverable to assistive technology', () => {
expect(overlaySource).toContain("launcher.setAttribute('aria-disabled'");
expect(overlaySource).not.toContain('launcher.disabled =');
expect(overlaySource).toContain("launcher.setAttribute('aria-describedby', launcherHint.id)");
expect(overlaySource).toContain("textarea.setAttribute('aria-describedby', 'chat-composer-count chat-composer-status')");
expect(overlaySource).toContain("status.setAttribute('role', 'status')");
});
it('creates a chat key for both generated-room entry points', () => {
expect(popupSource).toContain('let pendingRoomCreation = false');
expect(popupSource).toContain('const isCreating = pendingRoomCreation || !roomIdInput');
expect(popupSource).toMatch(/function handleCreateRoom\(\)[\s\S]*pendingRoomCreation = true[\s\S]*elements\.joinBtn\.click\(\)/);
expect(popupSource).toContain("type: 'CREATE_CHAT_KEY'");
});
it('contains every chat string in all 15 extension locales', () => {
const localeFiles = fs.readdirSync(localeDir).filter(file => file.endsWith('.json')).sort();
expect(localeFiles).toHaveLength(15);
for (const file of localeFiles) {
const messages = JSON.parse(fs.readFileSync(path.join(localeDir, file), 'utf8'));
for (const key of chatKeys) {
expect(messages[key], `${file}: ${key}`).toBeTypeOf('string');
expect(messages[key], `${file}: ${key}`).not.toBe('');
expect(messages[key], `${file}: ${key}`).not.toMatch(/[—–]/);
}
}
});
});
+808
View File
@@ -0,0 +1,808 @@
(function initKoalaSyncChatOverlay() {
if (window.koalaSyncChatOverlay?.refresh) {
window.koalaSyncChatOverlay.refresh();
return;
}
const MAX_MESSAGES = 200;
const MIN_WIDTH = 300;
const MIN_HEIGHT = 320;
const DEFAULT_WIDTH = 360;
const DEFAULT_HEIGHT = 520;
const LAUNCHER_SIZE = 48;
const EDGE_INSET = 8;
const DOCK_MIN_PAGE_WIDTH = 480;
const PAGE_DOCK_ATTRIBUTE = 'data-koalasync-chat-dock';
const PAGE_DOCK_WIDTH = '--koalasync-chat-dock-width';
const SIZE_PRESETS = Object.freeze({
compact: Object.freeze({ width: 320, height: 400 }),
standard: Object.freeze({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }),
large: Object.freeze({ width: 440, height: 640 })
});
const storageKey = `chatOverlayLayout:${location.origin}`;
const systemTheme = window.matchMedia('(prefers-color-scheme: light)');
let context = null;
let opened = false;
let destroyed = false;
let saveTimer = null;
let applyingLayout = false;
let preferencesLoaded = false;
let startStateApplied = false;
let refreshGeneration = 0;
let sendGeneration = 0;
let sending = false;
let unreadCount = 0;
let layout = {
mode: 'right',
x: 24,
y: 72,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
customWidth: DEFAULT_WIDTH,
customHeight: DEFAULT_HEIGHT,
detachedInitialized: false,
launcherX: null,
launcherY: null,
launcherInitialized: false
};
let chatPosition = 'right';
let chatSize = 'standard';
let chatStartMode = 'bubble';
let themeMode = 'system';
let themePalette = 'eucalyptus';
const host = document.createElement('div');
host.id = 'koalasync-chat-overlay-host';
host.style.cssText = 'all:initial;display:none;position:fixed;inset:0;z-index:2147483647;pointer-events:none;contain:layout style paint;transform:translateZ(0);';
const shadow = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
:host { all: initial; }
* { box-sizing: border-box; }
#app {
--bg: oklch(0.205 0.025 155); --card: oklch(0.30 0.035 150);
--surface-alt: oklch(0.265 0.032 150); --surface-deep: oklch(0.205 0.025 155);
--accent: oklch(0.73 0.13 155); --accent-hover: oklch(0.79 0.12 155);
--text: oklch(0.96 0.01 90); --text-muted: oklch(0.78 0.02 90);
--border-soft: oklch(0.88 0.025 145 / 0.09); --border-strong: oklch(0.39 0.038 145);
--text-on-green: oklch(0.14 0.02 90); --danger: oklch(0.63 0.18 25);
position: fixed; inset: 0; pointer-events: none; color: var(--text);
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#app[data-theme="light"] {
--bg: oklch(0.935 0.018 110); --card: oklch(0.995 0.006 100);
--surface-alt: oklch(0.91 0.022 115); --surface-deep: oklch(0.935 0.018 110);
--accent: oklch(0.52 0.13 155); --accent-hover: oklch(0.45 0.12 155);
--text: oklch(0.21 0.03 140); --text-muted: oklch(0.43 0.03 135);
--border-soft: oklch(0.28 0.035 140 / 0.10); --border-strong: oklch(0.73 0.035 125);
--text-on-green: oklch(0.16 0.03 140);
}
#app[data-palette="cyber"] {
--bg: oklch(0.185 0.04 265); --card: oklch(0.285 0.05 265);
--surface-alt: oklch(0.25 0.048 265); --surface-deep: oklch(0.185 0.04 265);
--accent: oklch(0.59 0.20 275); --accent-hover: oklch(0.69 0.17 275);
--text: oklch(0.97 0.012 265); --text-muted: oklch(0.74 0.03 265);
--border-soft: oklch(0.9 0.03 265 / 0.10); --border-strong: oklch(0.42 0.05 265);
--text-on-green: oklch(0.99 0.006 275);
}
#app[data-palette="cyber"][data-theme="light"] {
--bg: oklch(0.945 0.014 265); --card: oklch(0.995 0.005 265);
--surface-alt: oklch(0.915 0.02 265); --surface-deep: oklch(0.945 0.014 265);
--accent: oklch(0.53 0.21 275); --accent-hover: oklch(0.46 0.20 275);
--text: oklch(0.26 0.05 265); --text-muted: oklch(0.48 0.045 265);
--border-soft: oklch(0.30 0.05 265 / 0.12); --border-strong: oklch(0.79 0.03 265);
--text-on-green: oklch(0.99 0.005 275);
}
#app[data-palette="graphite"] {
--bg: oklch(0.165 0.004 260); --card: oklch(0.255 0.005 260);
--surface-alt: oklch(0.225 0.005 260); --surface-deep: oklch(0.165 0.004 260);
--accent: oklch(0.93 0.006 260); --accent-hover: oklch(0.99 0.003 260);
--text: oklch(0.97 0.003 260); --text-muted: oklch(0.72 0.005 260);
--border-soft: oklch(0.92 0.008 260 / 0.11); --border-strong: oklch(0.42 0.006 260);
--text-on-green: oklch(0.20 0.004 260);
}
#app[data-palette="graphite"][data-theme="light"] {
--bg: oklch(0.928 0.003 260); --card: oklch(1 0 0);
--surface-alt: oklch(0.90 0.004 260); --surface-deep: oklch(0.928 0.003 260);
--accent: oklch(0.28 0.006 260); --accent-hover: oklch(0.18 0.005 260);
--text: oklch(0.22 0.005 260); --text-muted: oklch(0.46 0.006 260);
--border-soft: oklch(0.20 0.006 260 / 0.13); --border-strong: oklch(0.77 0.004 260);
--text-on-green: oklch(0.98 0.002 260);
}
button, textarea { font: inherit; }
button { color: inherit; }
.launcher {
position: fixed; width: 48px; height: 48px; border: 1px solid var(--border-strong);
border-radius: 16px; background: var(--card); color: var(--text); cursor: pointer;
pointer-events: auto; box-shadow: 0 10px 28px rgb(0 0 0 / .32); font-size: 22px;
touch-action: none; user-select: none;
}
.launcher:hover:not([aria-disabled="true"]) { border-color: var(--accent); transform: translateY(-1px); }
.launcher[aria-disabled="true"] { cursor: not-allowed; opacity: .58; }
.launcher-icon { pointer-events: none; }
.unread {
position: absolute; top: -7px; right: -7px; display: none; min-width: 22px; height: 22px;
align-items: center; justify-content: center; padding: 0 6px; border: 2px solid var(--card);
border-radius: 999px; background: var(--danger); color: white; font-size: 11px; font-weight: 800;
line-height: 1; pointer-events: none;
}
.unread.visible { display: flex; }
.panel {
position: fixed; display: none; flex-direction: column; overflow: hidden;
min-width: min(${MIN_WIDTH}px, calc(100vw - 16px)); min-height: min(${MIN_HEIGHT}px, calc(100vh - 16px)); max-width: calc(100vw - 16px);
max-height: calc(100vh - 16px); pointer-events: auto; color: var(--text);
background: var(--card); border: 1px solid var(--border-strong); border-radius: 18px;
box-shadow: 0 18px 55px rgb(0 0 0 / .38); transform: translateZ(0);
}
.panel.open { display: flex; }
.panel.docked { border-radius: 0; max-height: 100vh; }
.header { display: flex; align-items: center; gap: 10px; padding: 12px; border-bottom: 1px solid var(--border-soft); user-select: none; }
.header.detached { cursor: move; }
.heading { min-width: 0; flex: 1; }
.title { font-size: 14px; font-weight: 760; }
.subtitle { color: var(--text-muted); font-size: 11px; }
.modes { display: flex; gap: 4px; }
.icon-button { width: 30px; height: 30px; border: 1px solid var(--border-soft); border-radius: 9px; background: var(--surface-alt); cursor: pointer; }
.icon-button:hover, .icon-button.active { border-color: var(--accent); color: var(--accent); }
.messages { flex: 1; min-height: 0; overflow-y: auto; padding: 12px; background: var(--bg); overscroll-behavior: contain; }
.empty { color: var(--text-muted); text-align: center; padding: 32px 12px; font-size: 12px; }
.message { margin-bottom: 11px; overflow-wrap: anywhere; }
.meta { display: flex; align-items: baseline; gap: 7px; margin-bottom: 2px; }
.sender { color: var(--accent); font-weight: 700; font-size: 12px; }
.time { color: var(--text-muted); font-size: 10px; }
.body { white-space: pre-wrap; color: var(--text); }
.event-message {
display: flex; align-items: center; justify-content: center; gap: 7px; margin: 8px 0;
color: var(--text-muted); font-size: 11px; text-align: center;
}
.event-message::before, .event-message::after { content: ""; flex: 1; height: 1px; background: var(--border-soft); }
.event-text { max-width: 78%; }
.composer { padding: 10px; border-top: 1px solid var(--border-soft); background: var(--card); }
textarea { width: 100%; min-height: 62px; max-height: 132px; resize: vertical; border: 1px solid var(--border-strong); border-radius: 11px; padding: 9px; background: var(--surface-deep); color: var(--text); outline: none; }
textarea:focus { border-color: var(--accent); }
textarea::placeholder { color: var(--text-muted); }
.composer-row { display: flex; align-items: center; gap: 8px; margin-top: 7px; }
.count { flex: 1; color: var(--text-muted); font-size: 10px; }
.status { color: var(--danger); font-size: 10px; min-height: 14px; }
.send { border: 0; border-radius: 10px; padding: 8px 13px; background: var(--accent); color: var(--text-on-green); font-weight: 750; cursor: pointer; }
.send:hover:not(:disabled) { background: var(--accent-hover); }
.send:disabled { opacity: .55; cursor: wait; }
.visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@media (max-width: 520px) { .panel { max-width: calc(100vw - 12px); } }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
`;
function element(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
const app = element('div');
app.id = 'app';
const launcher = element('button', 'launcher');
launcher.type = 'button';
const launcherIcon = element('span', 'launcher-icon', '💬');
const unreadBadge = element('span', 'unread');
unreadBadge.setAttribute('aria-hidden', 'true');
launcher.append(launcherIcon, unreadBadge);
const launcherHint = element('span', 'visually-hidden');
launcherHint.id = 'chat-launcher-hint';
const panel = element('section', 'panel');
panel.setAttribute('role', 'dialog');
const header = element('header', 'header');
const heading = element('div', 'heading');
const title = element('div', 'title');
const subtitle = element('div', 'subtitle');
heading.append(title, subtitle);
const modes = element('div', 'modes');
const leftButton = element('button', 'icon-button', '←');
const detachedButton = element('button', 'icon-button', '✥');
const rightButton = element('button', 'icon-button', '→');
const closeButton = element('button', 'icon-button', '×');
for (const button of [leftButton, detachedButton, rightButton, closeButton]) button.type = 'button';
modes.append(leftButton, detachedButton, rightButton, closeButton);
header.append(heading, modes);
const messages = element('div', 'messages');
messages.setAttribute('aria-live', 'polite');
const empty = element('div', 'empty');
messages.append(empty);
const composer = element('form', 'composer');
const textareaLabel = element('label', 'visually-hidden');
textareaLabel.htmlFor = 'chat-composer-input';
const textarea = element('textarea');
textarea.id = 'chat-composer-input';
textarea.setAttribute('aria-describedby', 'chat-composer-count chat-composer-status');
const composerRow = element('div', 'composer-row');
const count = element('span', 'count', '0/500');
count.id = 'chat-composer-count';
const sendButton = element('button', 'send');
sendButton.type = 'submit';
composerRow.append(count, sendButton);
const status = element('div', 'status');
status.id = 'chat-composer-status';
status.setAttribute('role', 'status');
composer.append(textareaLabel, textarea, composerRow, status);
panel.append(header, messages, composer);
app.append(launcherHint, launcher, panel);
shadow.append(style, app);
document.documentElement.append(host);
const pageDockStyle = document.createElement('style');
pageDockStyle.id = 'koalasync-chat-page-dock-style';
pageDockStyle.textContent = `
html[${PAGE_DOCK_ATTRIBUTE}="left"] {
box-sizing: border-box !important;
padding-left: var(${PAGE_DOCK_WIDTH}) !important;
}
html[${PAGE_DOCK_ATTRIBUTE}="right"] {
box-sizing: border-box !important;
padding-right: var(${PAGE_DOCK_WIDTH}) !important;
}
`;
document.documentElement.append(pageDockStyle);
function messageRuntime(payload, timeoutMs = 5000) {
return new Promise(resolve => {
let settled = false;
const finish = response => {
if (settled) return;
settled = true;
clearTimeout(timeout);
resolve(response || null);
};
const timeout = setTimeout(() => finish(null), timeoutMs);
try {
chrome.runtime.sendMessage(payload, response => {
if (chrome.runtime.lastError) finish(null);
else finish(response);
});
} catch (_) {
finish(null);
}
});
}
function strings() {
return context?.strings || {};
}
function applyStrings() {
const text = strings();
title.textContent = text.title || '';
subtitle.textContent = text.liveOnly || '';
launcher.setAttribute('aria-label', text.open || '');
const unavailableHint = context?.supported && !context?.hasKey
? (text.missingKey || '')
: context?.supported && !context?.connected ? (text.sendFailed || '') : '';
launcher.title = unavailableHint || text.open || '';
launcherHint.textContent = unavailableHint;
if (unavailableHint) launcher.setAttribute('aria-describedby', launcherHint.id);
else launcher.removeAttribute('aria-describedby');
panel.setAttribute('aria-label', text.title || '');
textarea.placeholder = text.placeholder || '';
textareaLabel.textContent = text.placeholder || text.title || '';
sendButton.textContent = text.send || '';
empty.textContent = text.empty || '';
leftButton.title = text.dockLeft || '';
detachedButton.title = text.detached || '';
rightButton.title = text.dockRight || '';
closeButton.title = text.close || '';
leftButton.setAttribute('aria-label', text.dockLeft || '');
detachedButton.setAttribute('aria-label', text.detached || '');
rightButton.setAttribute('aria-label', text.dockRight || '');
closeButton.setAttribute('aria-label', text.close || '');
applyUnreadCount();
}
function applyTheme() {
const light = themeMode === 'light' || (themeMode === 'system' && systemTheme.matches);
app.dataset.theme = light ? 'light' : 'dark';
app.dataset.palette = ['eucalyptus', 'cyber', 'graphite'].includes(themePalette) ? themePalette : 'eucalyptus';
}
function viewportBounds() {
return { width: Math.max(1, window.innerWidth), height: Math.max(1, window.innerHeight) };
}
function normalizePosition(value) {
return ['left', 'right', 'detached'].includes(value) ? value : 'right';
}
function normalizeSize(value) {
return ['compact', 'standard', 'large', 'custom'].includes(value) ? value : 'standard';
}
function preferredSize() {
return SIZE_PRESETS[chatSize] || {
width: Number(layout.customWidth) || DEFAULT_WIDTH,
height: Number(layout.customHeight) || DEFAULT_HEIGHT
};
}
function clampLauncher() {
const viewport = viewportBounds();
const maxX = Math.max(0, viewport.width - LAUNCHER_SIZE - EDGE_INSET);
const maxY = Math.max(0, viewport.height - LAUNCHER_SIZE - EDGE_INSET);
layout.launcherX = Math.min(Math.max(Number(layout.launcherX) || EDGE_INSET, EDGE_INSET), maxX);
layout.launcherY = Math.min(Math.max(Number(layout.launcherY) || EDGE_INSET, EDGE_INSET), maxY);
}
function initializeLauncher() {
if (layout.launcherInitialized) return;
const viewport = viewportBounds();
layout.launcherX = chatPosition === 'left'
? Math.min(16, Math.max(EDGE_INSET, viewport.width - LAUNCHER_SIZE - EDGE_INSET))
: Math.max(EDGE_INSET, viewport.width - LAUNCHER_SIZE - 16);
layout.launcherY = Math.max(EDGE_INSET, Math.round(viewport.height / 2 - LAUNCHER_SIZE / 2));
layout.launcherInitialized = true;
clampLauncher();
}
function clearPageDock() {
document.documentElement.removeAttribute(PAGE_DOCK_ATTRIBUTE);
document.documentElement.style.removeProperty(PAGE_DOCK_WIDTH);
}
function applyPageDock(side, width) {
const viewport = viewportBounds();
if (!opened || document.fullscreenElement || viewport.width - width < DOCK_MIN_PAGE_WIDTH) {
clearPageDock();
return;
}
document.documentElement.setAttribute(PAGE_DOCK_ATTRIBUTE, side);
document.documentElement.style.setProperty(PAGE_DOCK_WIDTH, `${width}px`);
}
function applyUnreadCount() {
unreadBadge.textContent = unreadCount > 99 ? '99+' : String(unreadCount);
unreadBadge.classList.toggle('visible', unreadCount > 0);
const openLabel = strings().open || '';
launcher.setAttribute('aria-label', unreadCount > 0 ? `${openLabel} (${unreadCount})` : openLabel);
}
function setUnreadCount(next) {
unreadCount = Math.max(0, Number.isFinite(next) ? Math.trunc(next) : 0);
applyUnreadCount();
}
function clampDetached() {
const viewport = viewportBounds();
const maxWidth = Math.max(1, Math.min(600, viewport.width - 16));
const maxHeight = Math.max(1, viewport.height - 16);
const horizontalInset = Math.min(8, Math.max(0, Math.floor((viewport.width - 1) / 2)));
const verticalInset = Math.min(8, Math.max(0, Math.floor((viewport.height - 1) / 2)));
layout.width = Math.min(Math.max(Number(layout.width) || DEFAULT_WIDTH, Math.min(MIN_WIDTH, maxWidth)), maxWidth);
layout.height = Math.min(Math.max(Number(layout.height) || DEFAULT_HEIGHT, Math.min(MIN_HEIGHT, maxHeight)), maxHeight);
layout.x = Math.min(Math.max(Number(layout.x) || horizontalInset, horizontalInset), Math.max(horizontalInset, viewport.width - layout.width - horizontalInset));
layout.y = Math.min(Math.max(Number(layout.y) || verticalInset, verticalInset), Math.max(verticalInset, viewport.height - layout.height - verticalInset));
}
function saveLayout() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
saveTimer = null;
chrome.storage.local.set({ [storageKey]: layout }).catch(() => {});
}, 150);
}
function applyLayout() {
applyingLayout = true;
const size = preferredSize();
initializeLauncher();
clampLauncher();
launcher.style.left = `${layout.launcherX}px`;
launcher.style.right = 'auto';
launcher.style.top = `${layout.launcherY}px`;
panel.style.right = 'auto';
panel.style.left = 'auto';
panel.style.bottom = 'auto';
panel.style.resize = 'none';
panel.classList.toggle('docked', layout.mode !== 'detached');
header.classList.toggle('detached', layout.mode === 'detached');
leftButton.classList.toggle('active', layout.mode === 'left');
detachedButton.classList.toggle('active', layout.mode === 'detached');
rightButton.classList.toggle('active', layout.mode === 'right');
if (layout.mode === 'detached') {
clearPageDock();
clampDetached();
panel.style.left = `${layout.x}px`;
panel.style.top = `${layout.y}px`;
panel.style.width = `${layout.width}px`;
panel.style.height = `${layout.height}px`;
panel.style.resize = 'both';
} else {
const viewport = viewportBounds();
const dockWidth = Math.max(1, Math.min(size.width, viewport.width - 16));
panel.style.top = '0';
panel.style.bottom = '0';
panel.style.width = `${dockWidth}px`;
panel.style.height = `${viewport.height}px`;
panel.style[layout.mode] = '0';
applyPageDock(layout.mode, dockWidth);
}
globalThis.queueMicrotask(() => { applyingLayout = false; });
}
function setMode(mode, persistPreference = true) {
mode = normalizePosition(mode);
if (mode === 'detached' && !layout.detachedInitialized) {
const size = preferredSize();
layout.x = Math.max(8, Math.round((window.innerWidth - size.width) / 2));
layout.y = Math.max(8, Math.round((window.innerHeight - size.height) / 2));
layout.detachedInitialized = true;
}
chatPosition = mode;
layout.mode = mode;
applyLayout();
saveLayout();
if (persistPreference) chrome.storage.local.set({ chatPosition: mode }).catch(() => {});
}
function setSize(size, persistPreference = true) {
chatSize = normalizeSize(size);
const preset = SIZE_PRESETS[chatSize];
if (preset) {
layout.width = preset.width;
layout.height = preset.height;
} else {
layout.width = Number(layout.customWidth) || DEFAULT_WIDTH;
layout.height = Number(layout.customHeight) || DEFAULT_HEIGHT;
}
if (layout.mode === 'detached') clampDetached();
applyLayout();
saveLayout();
if (persistPreference) chrome.storage.local.set({ chatSize }).catch(() => {});
}
function setOpened(next) {
opened = !!next && !!context?.enabled;
panel.classList.toggle('open', opened);
launcher.style.display = opened ? 'none' : '';
if (opened) {
setUnreadCount(0);
textarea.focus();
messages.scrollTop = messages.scrollHeight;
}
applyLayout();
}
function applyContext(next) {
const previousRoomId = context?.roomId;
context = next || null;
const supported = !!context?.supported;
const optedIn = !!context?.enabled;
const hasKey = !!context?.hasKey;
const connected = !!context?.connected;
context = context ? { ...context, enabled: supported && optedIn && hasKey && connected } : null;
if (previousRoomId && previousRoomId !== context?.roomId) {
clearMessages();
startStateApplied = false;
}
host.style.display = supported && optedIn ? '' : 'none';
launcher.setAttribute('aria-disabled', String(!context?.enabled));
if (!optedIn) startStateApplied = false;
if (!context?.enabled) {
setOpened(false);
} else if (preferencesLoaded && !startStateApplied) {
startStateApplied = true;
setOpened(chatStartMode === 'open');
}
applyStrings();
applyLayout();
}
async function refresh() {
if (destroyed) return;
const generation = ++refreshGeneration;
const next = await messageRuntime({ type: 'GET_CHAT_CONTEXT' });
if (destroyed || generation !== refreshGeneration) return;
applyContext(next);
}
function appendMessage(message) {
if (!context?.enabled || !message || typeof message.text !== 'string') return;
if (empty.isConnected) empty.remove();
const wrapper = element('article', 'message');
const meta = element('div', 'meta');
const own = message.senderId === context.peerId;
if (!opened && !own) setUnreadCount(unreadCount + 1);
const sender = element('span', 'sender', own ? (strings().you || '') : (message.username || message.senderId || ''));
const time = element('time', 'time');
const date = new Date(message.timestamp);
time.textContent = Number.isFinite(date.getTime()) ? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
meta.append(sender, time);
const body = element('div', 'body');
const tokens = globalThis.KoalaSyncChatFormat?.tokenizeChatText(message.text) || [{ type: 'text', text: message.text }];
for (const token of tokens) {
const node = token.type === 'strong' ? document.createElement('strong') : token.type === 'em' ? document.createElement('em') : document.createTextNode(token.text);
if (node.nodeType === 1) node.textContent = token.text;
body.append(node);
}
wrapper.append(meta, body);
messages.append(wrapper);
while (messages.querySelectorAll('.message').length > MAX_MESSAGES) messages.querySelector('.message')?.remove();
messages.scrollTop = messages.scrollHeight;
}
function appendEvent(event) {
if (!context?.enabled || !event || typeof event.action !== 'string') return;
const own = event.senderId === context.peerId;
const displayName = own ? (strings().you || '') : (event.username || event.senderId || '');
const labels = {
play: strings().eventPlay,
pause: strings().eventPause,
seek: strings().eventSeek,
force_sync_prepare: strings().eventForcePrepare,
force_sync_execute: strings().eventForceExecute
};
let eventText = '';
if (event.action === 'joined' || event.action === 'left') {
const template = event.action === 'joined' ? strings().eventJoined : strings().eventLeft;
eventText = (template || '{name}').replace('{name}', displayName);
} else if (labels[event.action]) {
eventText = (strings().eventAction || '{name} {action}')
.replace('{name}', displayName)
.replace('{action}', labels[event.action]);
}
if (!eventText) return;
if (empty.isConnected) empty.remove();
if (!opened && context.eventNotifications) setUnreadCount(unreadCount + 1);
const wrapper = element('article', 'message event-message');
const body = element('span', 'event-text', eventText);
const time = element('time', 'time');
const date = new Date(event.timestamp);
time.textContent = Number.isFinite(date.getTime()) ? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
wrapper.append(body, time);
messages.append(wrapper);
while (messages.querySelectorAll('.message').length > MAX_MESSAGES) messages.querySelector('.message')?.remove();
messages.scrollTop = messages.scrollHeight;
}
function clearMessages() {
messages.replaceChildren(empty);
setUnreadCount(0);
}
function resetComposer() {
sendGeneration++;
sending = false;
textarea.value = '';
count.textContent = '0/500';
count.style.color = '';
status.textContent = '';
sendButton.disabled = false;
}
let launcherDrag = null;
let suppressLauncherClick = false;
launcher.addEventListener('click', event => {
if (suppressLauncherClick && event.detail !== 0) {
suppressLauncherClick = false;
return;
}
suppressLauncherClick = false;
setOpened(true);
});
launcher.addEventListener('pointerdown', event => {
launcherDrag = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
x: layout.launcherX,
y: layout.launcherY,
moved: false
};
launcher.setPointerCapture(event.pointerId);
});
launcher.addEventListener('pointermove', event => {
if (!launcherDrag || event.pointerId !== launcherDrag.pointerId) return;
const deltaX = event.clientX - launcherDrag.startX;
const deltaY = event.clientY - launcherDrag.startY;
if (Math.hypot(deltaX, deltaY) >= 4) launcherDrag.moved = true;
layout.launcherX = launcherDrag.x + deltaX;
layout.launcherY = launcherDrag.y + deltaY;
clampLauncher();
launcher.style.left = `${layout.launcherX}px`;
launcher.style.top = `${layout.launcherY}px`;
});
launcher.addEventListener('pointerup', event => {
if (!launcherDrag || event.pointerId !== launcherDrag.pointerId) return;
suppressLauncherClick = launcherDrag.moved;
launcherDrag = null;
saveLayout();
});
launcher.addEventListener('pointercancel', () => {
launcherDrag = null;
suppressLauncherClick = false;
});
closeButton.addEventListener('click', () => setOpened(false));
leftButton.addEventListener('click', () => setMode('left'));
detachedButton.addEventListener('click', () => setMode('detached'));
rightButton.addEventListener('click', () => setMode('right'));
textarea.addEventListener('input', () => {
const length = [...textarea.value].length;
count.textContent = `${length}/500`;
count.style.color = length > 500 ? 'var(--danger)' : '';
status.textContent = length > 500 ? (strings().tooLong || '') : '';
});
const stopPageKeyboardShortcut = event => {
const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
if (!path.includes(textarea) && !path.includes(composer)) return;
if (event.type === 'keydown' && event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (!sending) composer.requestSubmit();
}
event.stopImmediatePropagation();
};
for (const eventName of ['keydown', 'keyup', 'keypress']) {
window.addEventListener(eventName, stopPageKeyboardShortcut, true);
}
composer.addEventListener('submit', async event => {
event.preventDefault();
if (sending || !context?.enabled) return;
const submittedValue = textarea.value;
const text = textarea.value.trim();
if (!text) return;
if ([...text].length > 500) {
status.textContent = strings().tooLong || '';
return;
}
const generation = ++sendGeneration;
sending = true;
sendButton.disabled = true;
status.textContent = '';
const response = await messageRuntime({ type: 'CHAT_SEND', text });
if (destroyed || generation !== sendGeneration) return;
sending = false;
sendButton.disabled = false;
if (response?.status === 'ok') {
if (textarea.value === submittedValue) {
textarea.value = '';
count.textContent = '0/500';
}
} else {
status.textContent = response?.status === 'too_long' ? (strings().tooLong || '') : (strings().sendFailed || '');
}
});
let drag = null;
header.addEventListener('pointerdown', event => {
if (layout.mode !== 'detached' || event.target.closest('button')) return;
drag = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, x: layout.x, y: layout.y };
header.setPointerCapture(event.pointerId);
});
header.addEventListener('pointermove', event => {
if (!drag || event.pointerId !== drag.pointerId) return;
layout.x = drag.x + event.clientX - drag.startX;
layout.y = drag.y + event.clientY - drag.startY;
clampDetached();
applyLayout();
});
header.addEventListener('pointerup', event => {
if (!drag || event.pointerId !== drag.pointerId) return;
drag = null;
saveLayout();
});
const resizeObserver = new globalThis.ResizeObserver(() => {
if (applyingLayout || layout.mode !== 'detached') return;
const rect = panel.getBoundingClientRect();
if (!opened || !rect?.width || !rect.height) return;
if (Math.abs(rect.width - layout.width) < 1 && Math.abs(rect.height - layout.height) < 1) return;
layout.width = rect.width;
layout.height = rect.height;
layout.customWidth = rect.width;
layout.customHeight = rect.height;
if (chatSize !== 'custom') {
chatSize = 'custom';
chrome.storage.local.set({ chatSize }).catch(() => {});
}
clampDetached();
saveLayout();
});
resizeObserver.observe(panel);
function moveIntoFullscreen() {
const parent = document.fullscreenElement || document.documentElement;
if (parent && host.parentNode !== parent) parent.append(host);
globalThis.requestAnimationFrame(applyLayout);
}
function handleResize() {
clampLauncher();
applyLayout();
saveLayout();
if (layout.mode === 'detached') {
clampDetached();
}
}
function handleSystemTheme() {
if (themeMode === 'system') applyTheme();
}
function handleStorage(changes, area) {
if (area !== 'local') return;
if (changes.themeMode) themeMode = changes.themeMode.newValue || 'system';
if (changes.themePalette) themePalette = changes.themePalette.newValue || 'eucalyptus';
if (changes.themeMode || changes.themePalette) applyTheme();
if (changes.chatPosition) setMode(changes.chatPosition.newValue, false);
if (changes.chatSize) setSize(changes.chatSize.newValue, false);
if (changes.chatStartMode) {
chatStartMode = changes.chatStartMode.newValue === 'open' ? 'open' : 'bubble';
if (context?.enabled) {
startStateApplied = true;
setOpened(chatStartMode === 'open');
}
}
if (changes.locale) refresh();
}
function handleRuntime(message) {
if (message?.type === 'CHAT_MESSAGE') appendMessage(message.message);
if (message?.type === 'CHAT_EVENT') appendEvent(message.event);
if (message?.type === 'CHAT_CONTEXT_UPDATE' || message?.type === 'CONNECTION_STATUS') refresh();
if (message?.type === 'CHAT_RESET') {
clearMessages();
resetComposer();
refresh();
}
if (message?.type === 'CHAT_DESTROY') destroy();
}
function destroy() {
if (destroyed) return;
destroyed = true;
refreshGeneration++;
sendGeneration++;
if (saveTimer) clearTimeout(saveTimer);
resizeObserver.disconnect();
clearPageDock();
pageDockStyle.remove();
for (const eventName of ['keydown', 'keyup', 'keypress']) {
window.removeEventListener(eventName, stopPageKeyboardShortcut, true);
}
document.removeEventListener('fullscreenchange', moveIntoFullscreen);
window.removeEventListener('resize', handleResize);
systemTheme.removeEventListener('change', handleSystemTheme);
chrome.storage.onChanged.removeListener(handleStorage);
chrome.runtime.onMessage.removeListener(handleRuntime);
host.remove();
window.koalaSyncChatOverlay = null;
}
document.addEventListener('fullscreenchange', moveIntoFullscreen);
window.addEventListener('resize', handleResize, { passive: true });
systemTheme.addEventListener('change', handleSystemTheme);
chrome.storage.onChanged.addListener(handleStorage);
chrome.runtime.onMessage.addListener(handleRuntime);
chrome.storage.local.get([storageKey, 'themeMode', 'themePalette', 'chatPosition', 'chatSize', 'chatStartMode'], data => {
const storedLayout = data[storageKey];
if (storedLayout && typeof storedLayout === 'object') {
layout = { ...layout, ...storedLayout };
layout.customWidth = Number(storedLayout.customWidth) || Number(storedLayout.width) || DEFAULT_WIDTH;
layout.customHeight = Number(storedLayout.customHeight) || Number(storedLayout.height) || DEFAULT_HEIGHT;
}
chatPosition = normalizePosition(data.chatPosition);
chatSize = normalizeSize(data.chatSize);
chatStartMode = data.chatStartMode === 'open' ? 'open' : 'bubble';
layout.mode = chatPosition;
if (layout.mode === 'detached') layout.detachedInitialized = true;
const preset = SIZE_PRESETS[chatSize];
if (preset) {
layout.width = preset.width;
layout.height = preset.height;
}
themeMode = data.themeMode || 'system';
themePalette = data.themePalette || 'eucalyptus';
preferencesLoaded = true;
applyTheme();
applyLayout();
refresh();
});
window.koalaSyncChatOverlay = { refresh, destroy };
})();
+58
View File
@@ -0,0 +1,58 @@
export const MAX_ROOM_ID_LENGTH = 64;
export const CHAT_SEND_LIMIT = 9;
export const CHAT_SEND_WINDOW_MS = 10000;
export function normalizeRoomId(value) {
if (typeof value !== 'string') return '';
return value.trim().replace(/[^a-zA-Z0-9\-]/g, '').slice(0, MAX_ROOM_ID_LENGTH);
}
export function createChatSendLimiter({
limit = CHAT_SEND_LIMIT,
windowMs = CHAT_SEND_WINDOW_MS,
now = () => Date.now()
} = {}) {
let timestamps = [];
return {
take() {
const current = now();
timestamps = timestamps.filter(timestamp => current - timestamp < windowMs);
if (timestamps.length >= limit) {
return {
allowed: false,
retryAfterMs: Math.max(1, windowMs - (current - timestamps[0]))
};
}
timestamps.push(current);
return { allowed: true, retryAfterMs: 0 };
},
reset() {
timestamps = [];
}
};
}
export function createLatestTaskQueue() {
let generation = 0;
let tail = Promise.resolve();
return {
invalidate() {
generation++;
},
async run(task) {
const requestGeneration = ++generation;
const previous = tail;
let release;
tail = new Promise(resolve => { release = resolve; });
await previous.catch(() => {});
const isCurrent = () => requestGeneration === generation;
try {
return await task(isCurrent);
} finally {
release();
}
}
};
}
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';
import {
CHAT_SEND_LIMIT,
createChatSendLimiter,
createLatestTaskQueue,
MAX_ROOM_ID_LENGTH,
normalizeRoomId
} from './chat-session.js';
describe('chat session boundaries', () => {
it('matches the relay room ID sanitizer and length limit', () => {
expect(normalizeRoomId(' ROOM_!42 ')).toBe('ROOM42');
expect(normalizeRoomId('A'.repeat(MAX_ROOM_ID_LENGTH + 10))).toBe('A'.repeat(MAX_ROOM_ID_LENGTH));
expect(normalizeRoomId(null)).toBe('');
});
it('keeps client chat bursts below the relay disconnect threshold', () => {
let current = 1000;
const limiter = createChatSendLimiter({ now: () => current });
for (let index = 0; index < CHAT_SEND_LIMIT; index++) {
expect(limiter.take()).toEqual({ allowed: true, retryAfterMs: 0 });
}
expect(limiter.take()).toEqual({ allowed: false, retryAfterMs: 10000 });
current += 10000;
expect(limiter.take()).toEqual({ allowed: true, retryAfterMs: 0 });
limiter.reset();
expect(limiter.take()).toEqual({ allowed: true, retryAfterMs: 0 });
});
it('serializes join work and prevents an older request from winning storage races', async () => {
const queue = createLatestTaskQueue();
let releaseFirst;
const writes = [];
const first = queue.run(async isCurrent => {
await new Promise(resolve => { releaseFirst = resolve; });
if (isCurrent()) writes.push('first');
return { status: isCurrent() ? 'ok' : 'superseded' };
});
await new Promise(resolve => setTimeout(resolve, 0));
const second = queue.run(async isCurrent => {
if (isCurrent()) writes.push('second');
return { status: 'ok' };
});
releaseFirst();
await expect(first).resolves.toEqual({ status: 'superseded' });
await expect(second).resolves.toEqual({ status: 'ok' });
expect(writes).toEqual(['second']);
});
});
+11
View File
@@ -0,0 +1,11 @@
export function buildChatRelayPayload(ciphertext) {
return { ciphertext };
}
export function encodeSocketEvent(event, data, forbiddenSecret = '') {
const payload = JSON.stringify([event, data]);
if (forbiddenSecret && payload.includes(forbiddenSecret)) {
throw new Error('Refusing to send chat secret to relay');
}
return `42${payload}`;
}
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { buildChatRelayPayload, encodeSocketEvent } from './chat-wire.js';
describe('chat wire boundary', () => {
const secret = 'R5Ti1nxp0crfAFHf3gVncw';
it('sends only ciphertext for chat messages', () => {
expect(buildChatRelayPayload('ciphertext')).toEqual({ ciphertext: 'ciphertext' });
});
it('rejects the secret anywhere in any outgoing relay event', () => {
for (const [event, payload] of [
['join_room', { roomId: 'ROOM', password: 'PASS', chatKey: secret }],
['peer_status', { status: 'heartbeat', nested: { secret } }],
['chat_message', { ciphertext: `prefix-${secret}-suffix` }]
]) {
expect(() => encodeSocketEvent(event, payload, secret)).toThrow('Refusing to send chat secret');
}
expect(encodeSocketEvent('join_room', { roomId: 'ROOM', password: 'PASS' }, secret)).not.toContain(secret);
});
});
+1641 -1037
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
/**
* KoalaSync Episode Title Utilities
* Single source of truth synced to content.js by build-extension.cjs.
* Keep in sync with the injection block in content.js!
*/
export function extractEpisodeId(title) {
if (!title || typeof title !== 'string') return null;
const se = title.match(/S(?:eason\s*)?(\d+)[^a-zA-Z0-9]*E(?:pisode\s*)?(\d+)/i);
if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`;
const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i);
if (ep) return `EP${String(ep[1]).padStart(3, '0')}`;
return null;
}
export function sameEpisode(titleA, titleB) {
if (!titleA && !titleB) return true;
if (!titleA || !titleB) return false;
const idA = extractEpisodeId(titleA);
const idB = extractEpisodeId(titleB);
if (idA && idB) return idA === idB;
if (idA || idB) return false;
return titleA === titleB;
}
+194
View File
@@ -0,0 +1,194 @@
export const HOST_ACCESS_REQUIRED_STATUS = 'host_permission_required';
export function normalizeTabId(value) {
if (typeof value === 'number') {
return Number.isSafeInteger(value) && value > 0 ? value : null;
}
if (typeof value !== 'string') return null;
const normalized = value.trim();
if (!/^[1-9]\d*$/.test(normalized)) return null;
const tabId = Number(normalized);
return Number.isSafeInteger(tabId) ? tabId : null;
}
function browserSupportsPortMatchPatterns(chromeApi) {
// runtime.getBrowserInfo is a Firefox-only WebExtension API. Firefox does
// not accept ports in match patterns, while Chromium does.
return typeof chromeApi?.runtime?.getBrowserInfo !== 'function';
}
export function describeTabUrl(rawUrl, { includePort = true } = {}) {
if (typeof rawUrl !== 'string' || !rawUrl) return null;
try {
const url = new URL(rawUrl);
if (url.protocol === 'http:' || url.protocol === 'https:') {
const permissionHost = includePort ? url.host : url.hostname;
return {
url: rawUrl,
host: url.host,
originPattern: `${url.protocol}//${permissionHost}/*`
};
}
if (url.protocol === 'file:') {
return {
url: rawUrl,
host: 'local file',
originPattern: 'file:///*'
};
}
} catch (_e) {
// Invalid and browser-internal URLs cannot receive host access.
}
return null;
}
export async function inspectTabHostAccess(chromeApi, tabId) {
const tab = await chromeApi.tabs.get(tabId);
// executeScript targets the committed document. pendingUrl may already point
// at another origin while tab.url is still the document being injected into.
const descriptor = describeTabUrl(tab?.url || tab?.pendingUrl || '', {
includePort: browserSupportsPortMatchPatterns(chromeApi)
});
if (!descriptor) {
return {
tab,
url: tab?.url || '',
host: null,
originPattern: null,
granted: null
};
}
if (typeof chromeApi.permissions?.contains !== 'function') {
return { tab, ...descriptor, granted: null };
}
try {
const granted = await callBooleanPermissionMethod(
chromeApi,
'contains',
{ origins: [descriptor.originPattern] },
{ timeoutMs: 1000 }
);
return {
tab,
...descriptor,
granted: granted === null ? null : granted === true
};
} catch (_e) {
// Permission inspection is advisory. The actual script injection below
// remains the source of truth, including temporary activeTab access.
return { tab, ...descriptor, granted: null };
}
}
function callBooleanPermissionMethod(chromeApi, methodName, request, { timeoutMs = null } = {}) {
const method = chromeApi.permissions?.[methodName];
if (typeof method !== 'function') return Promise.resolve(null);
return new Promise((resolve, reject) => {
let settled = false;
let timeout = null;
const clearSettlementTimeout = () => {
if (timeout !== null) {
clearTimeout(timeout);
timeout = null;
}
};
const finish = (value) => {
if (settled) return;
settled = true;
clearSettlementTimeout();
resolve(value === true ? true : value === false ? false : null);
};
const fail = (error) => {
if (settled) return;
settled = true;
clearSettlementTimeout();
reject(error instanceof Error ? error : new Error(String(error || 'Permission request failed')));
};
const callback = (value) => {
const lastError = chromeApi.runtime?.lastError;
if (lastError) {
fail(new Error(lastError.message || String(lastError)));
return;
}
finish(value);
};
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
timeout = setTimeout(() => finish(null), timeoutMs);
}
try {
const result = method.call(chromeApi.permissions, request, callback);
if (result && typeof result.then === 'function') {
result.then(finish, fail);
} else if (typeof result === 'boolean') {
finish(result);
}
// Callback-based Chromium/Firefox APIs return undefined and settle
// through callback. Promise-based implementations settle above.
} catch (error) {
fail(error);
}
});
}
export function requestOriginPermission(chromeApi, originPattern) {
if (typeof originPattern !== 'string' || !originPattern) {
return Promise.resolve(null);
}
return callBooleanPermissionMethod(
chromeApi,
'request',
{ origins: [originPattern] },
{ timeoutMs: 60000 }
).catch(() => false);
}
export function isHostAccessError(error) {
const message = typeof error?.message === 'string' ? error.message : String(error || '');
return /host permission|permission to access (?:this|the|respective) host|cannot access contents of/i.test(message);
}
function hostAccessRequestDetails(tabId, originPattern) {
const request = { tabId };
if (typeof originPattern === 'string' && originPattern) {
request.pattern = originPattern;
}
return request;
}
export async function addTabHostAccessRequest(chromeApi, tabId, originPattern = null) {
if (typeof chromeApi.permissions?.addHostAccessRequest !== 'function') {
return false;
}
try {
await chromeApi.permissions.addHostAccessRequest(
hostAccessRequestDetails(tabId, originPattern)
);
return true;
} catch (_e) {
return false;
}
}
export async function removeTabHostAccessRequest(chromeApi, tabId, originPattern = null) {
if (typeof chromeApi.permissions?.removeHostAccessRequest !== 'function') {
return false;
}
try {
await chromeApi.permissions.removeHostAccessRequest(
hostAccessRequestDetails(tabId, originPattern)
);
return true;
} catch (_e) {
return false;
}
}
+6 -1
View File
@@ -1,5 +1,5 @@
// extension/i18n.js
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'pt-BR', 'ru', 'it', 'pl', 'tr', 'nl', 'ja', 'ko', 'pt'];
export const SUPPORTED_LANGUAGES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pl', 'pt', 'pt-BR', 'tr', 'ru', 'ja', 'ko', 'zh', 'uk'];
export const DEFAULT_LANGUAGE = 'en';
let activeDictionary = {};
@@ -12,6 +12,9 @@ let currentLanguage = null;
*/
export async function loadLocale(langCode) {
const resolvedLang = SUPPORTED_LANGUAGES.includes(langCode) ? langCode : DEFAULT_LANGUAGE;
if (typeof document !== 'undefined') {
document.documentElement.lang = resolvedLang;
}
if (currentLanguage === resolvedLang && Object.keys(activeDictionary).length > 0) {
return;
@@ -90,6 +93,8 @@ export function getMessage(key, placeholders = null) {
* Performs dynamic DOM replacements for elements carrying data-i18n attributes.
*/
export function translateDOM() {
if (typeof document === 'undefined') return;
// 1. Text Content
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M3 2.5h4v11H3zm6 0h4v11H9z"/>
</svg>

After

Width:  |  Height:  |  Size: 109 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M4 2.6v10.8L13 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 100 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M13.4 5.5A5.8 5.8 0 0 0 3 4.2L1.7 2.9v4.4h4.4L4.4 5.6a4 4 0 0 1 7.2.8zM2.6 10.5A5.8 5.8 0 0 0 13 11.8l1.3 1.3V8.7H9.9l1.7 1.7a4 4 0 0 1-7.2-.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 226 B

+98 -6
View File
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Teile diesen Link mit Freunden, damit sie beitreten können",
"LABEL_PEERS_IN_ROOM": "Teilnehmer im Raum",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere Benutzer, die derzeit mit diesem Raum verbunden sind",
"LABEL_HOST_CONTROL": "Host-Steuerung",
"LABEL_HOST_CONTROL_TOOLTIP": "Festlegen, wer die Wiedergabe in diesem Raum steuern darf",
"LABEL_HOST_ONLY_TOGGLE": "Nur ich darf die Wiedergabe steuern",
"NOTICE_HOST_CONTROLS": "Der Host steuert die Wiedergabe für alle.",
"NOTICE_COHOST_HINT": "Tipp: Gib einzelnen Teilnehmern unten in der Teilnehmerliste die Steuerung.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Gast",
"BADGE_CONTROLLER": "Controller",
"BTN_GIVE_CONTROL": "Steuerung geben",
"BTN_REVOKE_CONTROL": "Entziehen",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Schaut alleine — ignoriert die Befehle des Hosts",
"HCM_DIALOG_TITLE": "KoalaSync · Der Host steuert diesen Raum",
"HCM_DIALOG_BODY": "Nur der Host kann die Wiedergabe in diesem Raum steuern. Weiter gemeinsam schauen oder allein weiterschauen?",
"HCM_DIALOG_STAY": "Synchron bleiben",
"HCM_DIALOG_SOLO": "Allein weiterschauen",
"HCM_BADGE_SOLO": "Du schaust allein",
"HCM_BADGE_RESYNC": "Neu sync",
"NO_PEERS_CONNECTED": "Keine Teilnehmer verbunden",
"BTN_LEAVE_ROOM": "Raum verlassen",
"LABEL_SELECT_VIDEO": "Video auswählen",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "Fernsteuerung",
"BTN_COPY_INVITE": "📋 Einladungslink",
"BTN_COPY_INVITE_TOOLTIP": "Einladungslink kopieren",
"BTN_PLAY": "Abspielen",
"BTN_PLAY": "Abspielen",
"BTN_PLAY_TOOLTIP": "Sende einen Play-Befehl an alle",
"BTN_PAUSE": "Pause",
"BTN_PAUSE": "Pause",
"BTN_PAUSE_TOOLTIP": "Sende einen Pause-Befehl an alle",
"BTN_SYNC": "⚡ SYNC",
"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",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "Generiere deine interne ID neu und verbinde dich erneut",
"REGEN_ID_DESC": "Verwende dies, wenn du Fehler wegen doppelter Identität siehst.",
"REGEN_ID_OTHER_ISSUE": "Anderes Problem? Öffne ein GitHub Issue",
"TOAST_ID_REGENERATED": "Identität neu generiert — Verbindung wird wiederhergestellt…",
"LABEL_CONN_STATUS": "Verbindungsstatus",
"LABEL_CONN_STATUS_TOOLTIP": "Aktueller WebSocket-Verbindungsstatus",
"CONN_STATUS_DISCONNECTED": "Getrennt",
@@ -115,7 +134,7 @@
"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_1_TEXT": "Schaut Videos gemeinsam, egal wo ihr seid. Hier ist eine kurze Einführung.",
"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",
@@ -129,6 +148,8 @@
"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.",
"SITE_ACCESS_REQUIRED": "Dein Browser blockiert KoalaSync auf {host}. Klicke unten auf „Zugriff erlauben“. Falls kein Dialog erscheint, erlaube KoalaSync in den Erweiterungs- oder Websiteberechtigungen für diese Seite.",
"BTN_RETRY_ACCESS": "Zugriff erlauben",
"ERR_SELECT_VIDEO": "Bitte wähle zuerst ein Video aus!",
"TOAST_INVITE_COPIED": "Einladungslink kopiert!",
"TOAST_COPY_FAILED": "Kopieren in Zwischenablage fehlgeschlagen",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "Verbinde...",
"STATUS_FAILED": "Fehlgeschlagen",
"STATUS_DISCONNECTED": "Getrennt",
"STATUS_IDLE": "Bereit zum Verbinden",
"STATUS_IDLE_TOOLTIP": "KoalaSync ist bereit. Tritt einem Raum bei oder erstelle einen, um die Verbindung herzustellen und die Synchronisierung zu starten.",
"BTN_STATE_JOINING": "🚀 Trete bei...",
"BTN_STATE_RECONNECTING": "🔄 Verbinde erneut...",
"BTN_STATE_PLAYING": "▶ Spiele ab...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zeitüberschreitung — nicht alle Teilnehmer haben die Episode geladen",
"LOBBY_CANCEL_USER": "Vom Benutzer abgebrochen",
"NOTIF_ERROR_TITLE": "KoalaSync-Fehler",
"FOOTER_SUPPORT": "☕ Kauf mir einen Kaffee",
"FOOTER_SUPPORT": "KoalaSync unterstützen",
"FOOTER_REVIEW": "★ Bewerten",
"FOOTER_SUPPORT_PROMPT": "Gefällt dir KoalaSync? Hinterlasse eine Bewertung!",
"LABEL_AUDIO_PROCESSING": "Audio-Verarbeitung",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Wendet Audioeffekte wie Kompression auf die Videowiedergabe an",
"AUDIO_OPEN_SETTINGS": "Öffnen",
"NEW_FEATURE_AUDIO": "Neu: Audio-Verarbeitung — probiere den Kompressor aus!",
"NEW_FEATURE_THEME": "Neu: Themes — wechsle zum klassischen Look oder minimalem Graphit.",
"AUDIO_BACK": "← Zurück",
"AUDIO_PAGE_TITLE": "Audio-Einstellungen",
"AUDIO_MASTER_TOGGLE": "Audio-Verarbeitung",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Demnächst"
"AUDIO_COMING_SOON": "Demnächst",
"BTN_RESTART_TOUR": "Einführung neu starten",
"BTN_RESTART_TOUR_TOOLTIP": "Startet die Einführung für neue Benutzer erneut",
"HINT_SELECT_VIDEO": "Wähle hier dein Video aus!",
"OPTION_TITLE_PRIVACY_FULL": "Voll senden",
"OPTION_TITLE_PRIVACY_EPISODE": "Nur Episoden-ID senden",
"OPTION_TITLE_PRIVACY_HIDDEN": "Nicht senden",
"LABEL_PRIVACY_SETTINGS": "Privatsphäre-Einstellungen",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Lege fest, welche Tab- und Medientitel an den Raum gesendet werden.",
"LABEL_SEND_TAB_TITLE": "Tab-Titel senden",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Sendet den Titel des ausgewählten Browser-Tabs an den Raum.",
"LABEL_MEDIA_TITLE_PRIVACY": "Medientitel",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Lege fest, wie Medientitel an den Raum gesendet werden.",
"LABEL_SETTINGS_GROUP_PROFILE": "Profil & Aussehen",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Ändere deinen Benutzernamen, dein Design und deine Spracheinstellungen.",
"LABEL_SETTINGS_GROUP_SYNC": "Wiedergabe & Sync",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Passe Wiedergabe-, Benachrichtigungs- und Audioeinstellungen an.",
"LABEL_CHAT_ENABLED": "Raum-Chat aktivieren",
"LABEL_CHAT_ENABLED_TOOLTIP": "Verschlüsselten Raum-Chat im ausgewählten Video-Tab anzeigen.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Darstellung und Position des verschlüsselten Raum-Chats festlegen.",
"CHAT_SETTINGS_NOTE": "Optional und Ende-zu-Ende verschlüsselt. Nachrichten sind nur live sichtbar und werden vom Relay nicht gespeichert.",
"LABEL_CHAT_POSITION": "Position",
"LABEL_CHAT_POSITION_TOOLTIP": "Lege fest, wo der Chat am ausgewählten Player angezeigt wird.",
"LABEL_CHAT_SIZE": "Overlay-Größe",
"LABEL_CHAT_SIZE_TOOLTIP": "Wähle die Standardgröße des Chat-Overlays.",
"OPTION_CHAT_SIZE_COMPACT": "Kompakt",
"OPTION_CHAT_SIZE_STANDARD": "Standard",
"OPTION_CHAT_SIZE_LARGE": "Groß",
"OPTION_CHAT_SIZE_CUSTOM": "Letzte eigene Größe",
"LABEL_CHAT_START_MODE": "Startansicht",
"LABEL_CHAT_START_MODE_TOOLTIP": "Lege fest, ob der Chat als schwebende Blase oder geöffnetes Overlay startet.",
"OPTION_CHAT_START_BUBBLE": "Schwebende Chatblase",
"OPTION_CHAT_START_OPEN": "Geöffnetes Overlay",
"CHAT_TITLE": "Raum-Chat",
"CHAT_LIVE_ONLY": "Nur live. Kein Verlauf.",
"CHAT_OPEN": "Raum-Chat öffnen",
"CHAT_CLOSE": "Chat schließen",
"CHAT_DOCK_LEFT": "Chat links andocken",
"CHAT_DOCK_RIGHT": "Chat rechts andocken",
"CHAT_DETACHED": "Chat frei positionieren",
"CHAT_PLACEHOLDER": "Nachricht schreiben...",
"CHAT_SEND": "Senden",
"CHAT_MISSING_KEY": "Chat ist nur über einen aktuellen Einladungslink verfügbar.",
"CHAT_TOO_LONG": "Maximal 500 Zeichen.",
"CHAT_SEND_FAILED": "Nachricht konnte nicht gesendet werden.",
"CHAT_EMPTY": "Noch keine Nachrichten. Der Chat ist nur live.",
"LABEL_CHAT_NOTIFICATIONS": "Chat-Benachrichtigungen",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Zeigt neue Raum-Chatnachrichten als Systembenachrichtigung, wenn der Video-Tab nicht fokussiert ist.",
"LABEL_EVENT_NOTIFICATIONS": "Wiedergabe- & Raumbenachrichtigungen",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Zeigt Systembenachrichtigungen, wenn jemand beitritt, den Raum verlässt, abspielt oder pausiert.",
"DEBUG_SECTION_VIDEO_DETECTION": "Videoerkennung",
"DEBUG_VIDEO_NOT_FOUND": "KEIN VIDEOELEMENT GEFUNDEN",
"DEBUG_SECTION_MEDIA_SESSION": "Media Session API",
"DEBUG_VIDEO_HINT": "Läuft gerade ein Video? KoalaSync erkennt Browser-Videoelemente, nachdem das Medium geladen wurde.",
"DEBUG_SECTION_PLAYBACK": "Wiedergabe",
"DEBUG_STATE_PAUSED": "PAUSIERT",
"DEBUG_STATE_PLAYING": "LÄUFT",
"DEBUG_SECTION_PROPERTIES": "Eigenschaften",
"DEBUG_SECTION_DIMENSIONS": "Abmessungen",
"DEBUG_DIMENSIONS_HINT": "0×0 = Videoelement ist ausgeblendet oder noch nicht geladen",
"DEBUG_SECTION_MEDIA_ERROR": "Medienfehler",
"DEBUG_SECTION_DETECTION": "Erkennung",
"DEBUG_SECTION_IDENTIFICATION": "Identifikation",
"DEBUG_SECTION_MEDIA_SOURCE": "Medienquelle",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Datenattribute",
"DEBUG_YES": "JA",
"DEBUG_NO": "NEIN",
"DEBUG_YES_CHECKED": "JA (geprüft)",
"DEBUG_NA": "k. A."
}
+98 -6
View File
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Share this link with friends so they can join",
"LABEL_PEERS_IN_ROOM": "Peers in Room",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Other users currently connected to this room",
"LABEL_HOST_CONTROL": "Host Control",
"LABEL_HOST_CONTROL_TOOLTIP": "Restrict who can control playback in this room",
"LABEL_HOST_ONLY_TOGGLE": "Only I can control playback",
"NOTICE_HOST_CONTROLS": "The host controls playback for everyone.",
"NOTICE_COHOST_HINT": "Tip: grant control to individual viewers in the participant list below.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Guest",
"BADGE_CONTROLLER": "Controller",
"BTN_GIVE_CONTROL": "Give control",
"BTN_REVOKE_CONTROL": "Revoke",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Watching on their own — host's commands are ignored",
"HCM_DIALOG_TITLE": "KoalaSync · Host controls this room",
"HCM_DIALOG_BODY": "Only the host can control playback in this room. Keep watching together, or watch on your own?",
"HCM_DIALOG_STAY": "Stay in sync",
"HCM_DIALOG_SOLO": "Watch on my own",
"HCM_BADGE_SOLO": "Watching on your own",
"HCM_BADGE_RESYNC": "Resync",
"NO_PEERS_CONNECTED": "No peers connected",
"BTN_LEAVE_ROOM": "Leave Room",
"LABEL_SELECT_VIDEO": "Select Video",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "Remote Control",
"BTN_COPY_INVITE": "📋 Invite Link",
"BTN_COPY_INVITE_TOOLTIP": "Copy Invite Link",
"BTN_PLAY": "Play",
"BTN_PLAY": "Play",
"BTN_PLAY_TOOLTIP": "Send a Play command to everyone",
"BTN_PAUSE": "Pause",
"BTN_PAUSE": "Pause",
"BTN_PAUSE_TOOLTIP": "Send a Pause command to everyone",
"BTN_SYNC": "⚡ SYNC",
"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",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "Regenerate your internal ID and reconnect",
"REGEN_ID_DESC": "Use this if you see \"Duplicate Identity\" errors.",
"REGEN_ID_OTHER_ISSUE": "Other issue? Open a GitHub Issue",
"TOAST_ID_REGENERATED": "Identity regenerated — reconnecting…",
"LABEL_CONN_STATUS": "Connection Status",
"LABEL_CONN_STATUS_TOOLTIP": "Current WebSocket connection state",
"CONN_STATUS_DISCONNECTED": "Disconnected",
@@ -115,7 +134,7 @@
"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_1_TEXT": "Watch videos together wherever you are. Here is 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",
@@ -129,6 +148,8 @@
"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.",
"SITE_ACCESS_REQUIRED": "Your browser is blocking KoalaSync on {host}. Click \"Allow access\" below. If no dialog appears, allow KoalaSync on this site in your extension or site permissions.",
"BTN_RETRY_ACCESS": "Allow access",
"ERR_SELECT_VIDEO": "Please select a video first!",
"TOAST_INVITE_COPIED": "Invite link copied!",
"TOAST_COPY_FAILED": "Failed to copy to clipboard",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "Connecting...",
"STATUS_FAILED": "Failed",
"STATUS_DISCONNECTED": "Disconnected",
"STATUS_IDLE": "Ready to connect",
"STATUS_IDLE_TOOLTIP": "KoalaSync is ready. Join or create a room to connect and start syncing.",
"BTN_STATE_JOINING": "🚀 Joining...",
"BTN_STATE_RECONNECTING": "🔄 Reconnecting...",
"BTN_STATE_PLAYING": "▶ Playing...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — not all peers loaded the episode",
"LOBBY_CANCEL_USER": "Cancelled by user",
"NOTIF_ERROR_TITLE": "KoalaSync Error",
"FOOTER_SUPPORT": "☕ Buy me a coffee",
"FOOTER_SUPPORT": "Support KoalaSync",
"FOOTER_REVIEW": "★ Rate us",
"FOOTER_SUPPORT_PROMPT": "Enjoying KoalaSync? Leave a review!",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"NEW_FEATURE_THEME": "New: Themes — switch to the classic look or minimal graphite.",
"AUDIO_BACK": "← Back",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
"AUDIO_COMING_SOON": "Coming soon",
"BTN_RESTART_TOUR": "Restart Tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Restart the onboarding tutorial",
"HINT_SELECT_VIDEO": "Select your video here!",
"OPTION_TITLE_PRIVACY_FULL": "Send full title",
"OPTION_TITLE_PRIVACY_EPISODE": "Send episode ID",
"OPTION_TITLE_PRIVACY_HIDDEN": "Do not send",
"LABEL_PRIVACY_SETTINGS": "Privacy Settings",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choose which tab and media titles are sent to the room.",
"LABEL_SEND_TAB_TITLE": "Send tab title",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Send the selected browser tab title to the room.",
"LABEL_MEDIA_TITLE_PRIVACY": "Media title",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choose how media titles are sent to the room.",
"LABEL_SETTINGS_GROUP_PROFILE": "Profile & Appearance",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Change your username, theme, and language preferences.",
"LABEL_SETTINGS_GROUP_SYNC": "Playback & Sync",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Customize playback, notifications, and audio preferences.",
"LABEL_CHAT_ENABLED": "Enable Room Chat",
"LABEL_CHAT_ENABLED_TOOLTIP": "Show encrypted room chat on the selected video tab.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Configure encrypted room chat appearance and placement.",
"CHAT_SETTINGS_NOTE": "Optional and end-to-end encrypted. Messages are live only and are not stored by the relay.",
"LABEL_CHAT_POSITION": "Position",
"LABEL_CHAT_POSITION_TOOLTIP": "Choose where chat is attached to the selected player.",
"LABEL_CHAT_SIZE": "Overlay size",
"LABEL_CHAT_SIZE_TOOLTIP": "Choose the default size of the chat overlay.",
"OPTION_CHAT_SIZE_COMPACT": "Compact",
"OPTION_CHAT_SIZE_STANDARD": "Standard",
"OPTION_CHAT_SIZE_LARGE": "Large",
"OPTION_CHAT_SIZE_CUSTOM": "Last custom size",
"LABEL_CHAT_START_MODE": "Start as",
"LABEL_CHAT_START_MODE_TOOLTIP": "Choose whether chat starts as a floating bubble or an open overlay.",
"OPTION_CHAT_START_BUBBLE": "Floating bubble",
"OPTION_CHAT_START_OPEN": "Open overlay",
"CHAT_TITLE": "Room Chat",
"CHAT_LIVE_ONLY": "Live only. No history.",
"CHAT_OPEN": "Open room chat",
"CHAT_CLOSE": "Close chat",
"CHAT_DOCK_LEFT": "Dock chat left",
"CHAT_DOCK_RIGHT": "Dock chat right",
"CHAT_DETACHED": "Detach chat",
"CHAT_PLACEHOLDER": "Write a message...",
"CHAT_SEND": "Send",
"CHAT_MISSING_KEY": "Chat is available only through a current invite link.",
"CHAT_TOO_LONG": "Maximum 500 characters.",
"CHAT_SEND_FAILED": "Message could not be sent.",
"CHAT_EMPTY": "No messages yet. Chat is live only.",
"LABEL_CHAT_NOTIFICATIONS": "Chat Notifications",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Shows a system notification for new room-chat messages when the video tab is not focused.",
"LABEL_EVENT_NOTIFICATIONS": "Playback & Room Notifications",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Shows system notifications when someone joins, leaves, plays, or pauses.",
"DEBUG_SECTION_VIDEO_DETECTION": "Video Detection",
"DEBUG_VIDEO_NOT_FOUND": "NO VIDEO ELEMENT FOUND",
"DEBUG_SECTION_MEDIA_SESSION": "Media Session API",
"DEBUG_VIDEO_HINT": "Is a video currently playing? KoalaSync detects browser video elements after the media has loaded.",
"DEBUG_SECTION_PLAYBACK": "Playback",
"DEBUG_STATE_PAUSED": "PAUSED",
"DEBUG_STATE_PLAYING": "PLAYING",
"DEBUG_SECTION_PROPERTIES": "Properties",
"DEBUG_SECTION_DIMENSIONS": "Dimensions",
"DEBUG_DIMENSIONS_HINT": "0×0 = video element is hidden or not loaded yet",
"DEBUG_SECTION_MEDIA_ERROR": "Media Error",
"DEBUG_SECTION_DETECTION": "Detection",
"DEBUG_SECTION_IDENTIFICATION": "Identification",
"DEBUG_SECTION_MEDIA_SOURCE": "Media Source",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Data Attributes",
"DEBUG_YES": "YES",
"DEBUG_NO": "NO",
"DEBUG_YES_CHECKED": "YES (checked)",
"DEBUG_NA": "n/a"
}
+121 -29
View File
@@ -6,7 +6,7 @@
"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": "Configuración",
"TAB_SETTINGS_TOOLTIP": "Preferencias de la extensión",
"TAB_STATUS": "Estado",
"TAB_STATUS_TOOLTIP": "Diagnósticos avanzados y registros",
@@ -15,7 +15,7 @@
"LABEL_SERVER": "Servidor",
"BTN_SERVER_OFFICIAL": "Oficial",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar el servidor oficial confiable",
"BTN_SERVER_CUSTOM": "Perso",
"BTN_SERVER_CUSTOM": "Personalizado",
"BTN_SERVER_CUSTOM_TOOLTIP": "Conectarse a su propio servidor auto-alojado",
"PLACEHOLDER_SERVER_URL": "wss://tu-servidor:3000",
"LABEL_ROOM_ID": "ID de sala",
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Comparte este enlace con amigos para que se unan",
"LABEL_PEERS_IN_ROOM": "Participantes en la sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Otros usuarios conectados actualmente a esta sala",
"LABEL_HOST_CONTROL": "Control del anfitrión",
"LABEL_HOST_CONTROL_TOOLTIP": "Restringir quién puede controlar la reproducción en esta sala",
"LABEL_HOST_ONLY_TOGGLE": "Solo yo puedo controlar la reproducción",
"NOTICE_HOST_CONTROLS": "El anfitrión controla la reproducción para todos.",
"NOTICE_COHOST_HINT": "Consejo: concede el control a usuarios concretos en la lista de participantes de abajo.",
"BADGE_HOST": "Anfitrión",
"BADGE_GUEST": "Invitado",
"BADGE_CONTROLLER": "Controlador",
"BTN_GIVE_CONTROL": "Dar control",
"BTN_REVOKE_CONTROL": "Quitar",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Viendo por su cuenta — ignora los comandos del anfitrión",
"HCM_DIALOG_TITLE": "KoalaSync · El anfitrión controla esta sala",
"HCM_DIALOG_BODY": "Solo el anfitrión puede controlar la reproducción en esta sala. ¿Seguir viendo juntos o ver por tu cuenta?",
"HCM_DIALOG_STAY": "Seguir sincronizado",
"HCM_DIALOG_SOLO": "Ver por mi cuenta",
"HCM_BADGE_SOLO": "Viendo por tu cuenta",
"HCM_BADGE_RESYNC": "Resincronizar",
"NO_PEERS_CONNECTED": "Sin participantes conectados",
"BTN_LEAVE_ROOM": "Salir de la sala",
"LABEL_SELECT_VIDEO": "Seleccionar video",
@@ -52,22 +70,22 @@
"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": "Reproducir",
"BTN_PLAY_TOOLTIP": "Enviar un comando de reproducción a todos",
"BTN_PAUSE": "Pausar",
"BTN_PAUSE": "Pausar",
"BTN_PAUSE_TOOLTIP": "Enviar un comando de pausa a todos",
"BTN_SYNC": "⚡ SYNC",
"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_TO_OTHERS": "Ir a la posición de otros",
"OPTION_JUMP_TO_ME": "Traerlos a mi posición",
"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",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Muestra el comando más reciente de reproducción, pausa o salto en el tiempo",
"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": "Omitir y reproducir ya",
"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.",
@@ -76,7 +94,7 @@
"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": "Ocultar pestañas sin video",
"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.",
@@ -88,10 +106,11 @@
"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": "Regenerar ID de usuario",
"BTN_REGEN_ID_TOOLTIP": "Regenerar tu ID interno y volver a conectarte",
"REGEN_ID_DESC": "Usa esto si ves errores de 'Identidad duplicada'.",
"REGEN_ID_OTHER_ISSUE": "¿Otro problema? Abre un Issue en GitHub",
"REGEN_ID_OTHER_ISSUE": "¿Tienes otro problema? Abre un reporte en GitHub",
"TOAST_ID_REGENERATED": "Identidad regenerada — reconectando…",
"LABEL_CONN_STATUS": "Estado de la conexión",
"LABEL_CONN_STATUS_TOOLTIP": "Estado actual de la conexión WebSocket",
"CONN_STATUS_DISCONNECTED": "Desconectado",
@@ -115,7 +134,7 @@
"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_1_TEXT": "Ved vídeos juntos estéis donde estéis. Aquí tienes 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",
@@ -127,22 +146,26 @@
"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_PEERS_TIME": "No hay otros participantes con posición conocida. Cambia a 'Traerlos a mi posición'.",
"ERR_NO_VIDEO_TAB": "No se pudo conectar a la pestaña de video.",
"SITE_ACCESS_REQUIRED": "Tu navegador está bloqueando KoalaSync en {host}. Haz clic en «Permitir acceso». Si no aparece ningún diálogo, permite KoalaSync en este sitio desde los permisos de extensiones o del sitio.",
"BTN_RETRY_ACCESS": "Permitir acceso",
"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_JOINED": "{name} se ha unido a la sala",
"TOAST_PEER_LEFT": "{name} ha salido de la sala",
"TOAST_PEER_ACTION": "{name} ha {action}",
"STATUS_CONNECTED": "Conectado",
"STATUS_RECONNECTING": "Reconectando...",
"STATUS_CONNECTING": "Conectando...",
"STATUS_FAILED": "Error",
"STATUS_DISCONNECTED": "Desconectado",
"STATUS_IDLE": "Listo para conectar",
"STATUS_IDLE_TOOLTIP": "KoalaSync está listo. Únete o crea una sala para conectarte y empezar a sincronizar.",
"BTN_STATE_JOINING": "🚀 Uniéndose...",
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
"BTN_STATE_PLAYING": "▶ Reproduciendo...",
@@ -150,17 +173,17 @@
"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",
"NOTIF_PLAY": "ha iniciado la reproducción",
"NOTIF_PAUSE": "ha pausado la reproducción",
"NOTIF_SEEK": "ha cambiado la posición del video",
"NOTIF_FORCE_PREPARE": "ha iniciado una sincronización forzada",
"NOTIF_FORCE_EXECUTE": "ha sincronizado 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_HISTORY_HINT": "Reproduce, pausa o salta en el tiempo 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",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tiempo de espera agotado: no todos los participantes cargaron el episodio",
"LOBBY_CANCEL_USER": "Cancelado por el usuario",
"NOTIF_ERROR_TITLE": "Error de KoalaSync",
"FOOTER_SUPPORT": "☕ Invítame a un café",
"FOOTER_SUPPORT": "Apoyar KoalaSync",
"FOOTER_REVIEW": "★ Valorar",
"FOOTER_SUPPORT_PROMPT": "¿Te gusta KoalaSync? ¡Deja una reseña!",
"LABEL_AUDIO_PROCESSING": "Procesamiento de audio",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efectos de audio como compresión a la reproducción de video",
"AUDIO_OPEN_SETTINGS": "Abrir",
"NEW_FEATURE_AUDIO": "Nuevo: Procesamiento de audio — ¡prueba el compresor!",
"NEW_FEATURE_THEME": "Nuevo: Temas — cambia al aspecto clásico o al grafito minimalista.",
"AUDIO_BACK": "← Volver",
"AUDIO_PAGE_TITLE": "Configuración de audio",
"AUDIO_MASTER_TOGGLE": "Procesamiento de audio",
@@ -199,11 +223,79 @@
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Mejora de voz",
"AUDIO_PRESET_SMOOTH": "Suave",
"AUDIO_PRESET_CUSTOM": "Personalizado",
"AUDIO_PARAM_THRESHOLD": "Umbral",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_PARAM_THRESHOLD": "Umbral (Threshold)",
"AUDIO_PARAM_KNEE": "Codo (Knee)",
"AUDIO_PARAM_RATIO": "Relación (Ratio)",
"AUDIO_PARAM_ATTACK": "Ataque (Attack)",
"AUDIO_PARAM_RELEASE": "Liberación (Release)",
"AUDIO_EQUALIZER": "Ecualizador",
"AUDIO_COMING_SOON": "Próximamente"
"AUDIO_COMING_SOON": "Próximamente",
"BTN_RESTART_TOUR": "Reiniciar tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar el tutorial de inicio",
"HINT_SELECT_VIDEO": "¡Selecciona tu video aquí!",
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID de episodio",
"OPTION_TITLE_PRIVACY_HIDDEN": "No enviar",
"LABEL_PRIVACY_SETTINGS": "Ajustes de privacidad",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Elige qué títulos de pestaña y multimedia se envían a la sala.",
"LABEL_SEND_TAB_TITLE": "Enviar título de pestaña",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envía a la sala el título de la pestaña del navegador seleccionada.",
"LABEL_MEDIA_TITLE_PRIVACY": "Título multimedia",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Elige cómo se envían los títulos multimedia a la sala.",
"LABEL_SETTINGS_GROUP_PROFILE": "Perfil y Apariencia",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Cambia tu nombre de usuario, tema y preferencias de idioma.",
"LABEL_SETTINGS_GROUP_SYNC": "Reproducción y Sincronización",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personaliza la reproducción, las notificaciones y las preferencias de audio.",
"LABEL_CHAT_ENABLED": "Activar chat de la sala",
"LABEL_CHAT_ENABLED_TOOLTIP": "Mostrar el chat cifrado de la sala en la pestaña de vídeo seleccionada.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Configura la apariencia y la posición del chat cifrado de la sala.",
"CHAT_SETTINGS_NOTE": "Opcional y cifrado de extremo a extremo. Los mensajes solo se muestran en directo y el relé no los almacena.",
"LABEL_CHAT_POSITION": "Posición",
"LABEL_CHAT_POSITION_TOOLTIP": "Elige dónde se acopla el chat al reproductor seleccionado.",
"LABEL_CHAT_SIZE": "Tamaño de la superposición",
"LABEL_CHAT_SIZE_TOOLTIP": "Elige el tamaño predeterminado de la superposición del chat.",
"OPTION_CHAT_SIZE_COMPACT": "Compacto",
"OPTION_CHAT_SIZE_STANDARD": "Estándar",
"OPTION_CHAT_SIZE_LARGE": "Grande",
"OPTION_CHAT_SIZE_CUSTOM": "Último tamaño personalizado",
"LABEL_CHAT_START_MODE": "Iniciar como",
"LABEL_CHAT_START_MODE_TOOLTIP": "Elige si el chat comienza como una burbuja flotante o una superposición abierta.",
"OPTION_CHAT_START_BUBBLE": "Burbuja flotante",
"OPTION_CHAT_START_OPEN": "Superposición abierta",
"CHAT_TITLE": "Chat de la sala",
"CHAT_LIVE_ONLY": "Solo en directo. Sin historial.",
"CHAT_OPEN": "Abrir chat de la sala",
"CHAT_CLOSE": "Cerrar chat",
"CHAT_DOCK_LEFT": "Acoplar chat a la izquierda",
"CHAT_DOCK_RIGHT": "Acoplar chat a la derecha",
"CHAT_DETACHED": "Separar chat",
"CHAT_PLACEHOLDER": "Escribe un mensaje...",
"CHAT_SEND": "Enviar",
"CHAT_MISSING_KEY": "El chat solo está disponible mediante un enlace de invitación actual.",
"CHAT_TOO_LONG": "Máximo 500 caracteres.",
"CHAT_SEND_FAILED": "No se pudo enviar el mensaje.",
"CHAT_EMPTY": "Aún no hay mensajes. El chat solo es en directo.",
"LABEL_CHAT_NOTIFICATIONS": "Notificaciones del chat",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Muestra una notificación del sistema para mensajes nuevos cuando la pestaña de vídeo no está activa.",
"LABEL_EVENT_NOTIFICATIONS": "Notificaciones de reproducción y sala",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Muestra notificaciones cuando alguien entra, sale, reproduce o pausa.",
"DEBUG_SECTION_VIDEO_DETECTION": "Detección de vídeo",
"DEBUG_VIDEO_NOT_FOUND": "NO SE ENCONTRÓ NINGÚN ELEMENTO DE VÍDEO",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "¿Hay un vídeo reproduciéndose? KoalaSync detecta elementos de vídeo cuando el contenido está cargado.",
"DEBUG_SECTION_PLAYBACK": "Reproducción",
"DEBUG_STATE_PAUSED": "EN PAUSA",
"DEBUG_STATE_PLAYING": "REPRODUCIENDO",
"DEBUG_SECTION_PROPERTIES": "Propiedades",
"DEBUG_SECTION_DIMENSIONS": "Dimensiones",
"DEBUG_DIMENSIONS_HINT": "0×0 = el elemento de vídeo está oculto o aún no se ha cargado",
"DEBUG_SECTION_MEDIA_ERROR": "Error multimedia",
"DEBUG_SECTION_DETECTION": "Detección",
"DEBUG_SECTION_IDENTIFICATION": "Identificación",
"DEBUG_SECTION_MEDIA_SOURCE": "Fuente multimedia",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Atributos de datos",
"DEBUG_YES": "SÍ",
"DEBUG_NO": "NO",
"DEBUG_YES_CHECKED": "SÍ (comprobado)",
"DEBUG_NA": "n/d"
}
+98 -6
View File
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Partagez ce lien avec vos amis pour qu'ils vous rejoignent",
"LABEL_PEERS_IN_ROOM": "Membres dans le salon",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Autres utilisateurs connectés à ce salon",
"LABEL_HOST_CONTROL": "Contrôle de l'hôte",
"LABEL_HOST_CONTROL_TOOLTIP": "Définir qui peut contrôler la lecture dans ce salon",
"LABEL_HOST_ONLY_TOGGLE": "Moi seul peux contrôler la lecture",
"NOTICE_HOST_CONTROLS": "L'hôte contrôle la lecture pour tout le monde.",
"NOTICE_COHOST_HINT": "Astuce : donnez le contrôle à des participants précis dans la liste ci-dessous.",
"BADGE_HOST": "Hôte",
"BADGE_GUEST": "Invité",
"BADGE_CONTROLLER": "Contrôleur",
"BTN_GIVE_CONTROL": "Donner le contrôle",
"BTN_REVOKE_CONTROL": "Retirer",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Regarde de son côté — ignore les commandes de l'hôte",
"HCM_DIALOG_TITLE": "KoalaSync · L'hôte contrôle ce salon",
"HCM_DIALOG_BODY": "Seul l'hôte peut contrôler la lecture dans ce salon. Continuer à regarder ensemble, ou regarder de votre côté ?",
"HCM_DIALOG_STAY": "Rester synchronisé",
"HCM_DIALOG_SOLO": "Regarder de mon côté",
"HCM_BADGE_SOLO": "Vous regardez seul",
"HCM_BADGE_RESYNC": "Resynchroniser",
"NO_PEERS_CONNECTED": "Aucun membre connecté",
"BTN_LEAVE_ROOM": "Quitter le salon",
"LABEL_SELECT_VIDEO": "Choisir une vidéo",
@@ -52,11 +70,11 @@
"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": "Lecture",
"BTN_PLAY_TOOLTIP": "Envoyer une commande Lecture à tout le monde",
"BTN_PAUSE": "Pause",
"BTN_PAUSE": "Pause",
"BTN_PAUSE_TOOLTIP": "Envoyer une commande Pause à tout le monde",
"BTN_SYNC": "⚡ SYNC",
"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",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "Régénérer votre identifiant interne et vous reconnecter",
"REGEN_ID_DESC": "Utilisez cette option si vous rencontrez des erreurs de 'Double identité'.",
"REGEN_ID_OTHER_ISSUE": "Autre problème? Ouvrez un Issue GitHub",
"TOAST_ID_REGENERATED": "Identité régénérée — reconnexion…",
"LABEL_CONN_STATUS": "Statut de la connexion",
"LABEL_CONN_STATUS_TOOLTIP": "Statut actuel de la connexion WebSocket",
"CONN_STATUS_DISCONNECTED": "Déconnecté",
@@ -115,7 +134,7 @@
"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_1_TEXT": "Regardez des vidéos ensemble où que vous soyez. Voici une présentation rapide.",
"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",
@@ -129,6 +148,8 @@
"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.",
"SITE_ACCESS_REQUIRED": "Votre navigateur bloque KoalaSync sur {host}. Cliquez sur « Autoriser laccès ». Si aucune boîte de dialogue napparaît, autorisez KoalaSync sur ce site dans les permissions des extensions ou du site.",
"BTN_RETRY_ACCESS": "Autoriser laccès",
"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",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "Connexion...",
"STATUS_FAILED": "Échec",
"STATUS_DISCONNECTED": "Déconnecté",
"STATUS_IDLE": "Prêt à se connecter",
"STATUS_IDLE_TOOLTIP": "KoalaSync est prêt. Rejoignez ou créez un salon pour vous connecter et commencer la synchronisation.",
"BTN_STATE_JOINING": "🚀 Connexion...",
"BTN_STATE_RECONNECTING": "🔄 Reconnexion...",
"BTN_STATE_PLAYING": "▶ Lecture...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Délai dépassé — tous les membres n'ont pas chargé l'épisode",
"LOBBY_CANCEL_USER": "Annulé par l'utilisateur",
"NOTIF_ERROR_TITLE": "Erreur KoalaSync",
"FOOTER_SUPPORT": "☕ Offre-moi un café",
"FOOTER_SUPPORT": "Soutenir KoalaSync",
"FOOTER_REVIEW": "★ Évaluer",
"FOOTER_SUPPORT_PROMPT": "Tu aimes KoalaSync? Laisse un avis!",
"LABEL_AUDIO_PROCESSING": "Traitement audio",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Applique des effets audio comme la compression à la lecture vidéo",
"AUDIO_OPEN_SETTINGS": "Ouvrir",
"NEW_FEATURE_AUDIO": "Nouveau : Traitement audio — essayez le compresseur !",
"NEW_FEATURE_THEME": "Nouveau : Thèmes — passez au look classique ou au graphite minimal.",
"AUDIO_BACK": "← Retour",
"AUDIO_PAGE_TITLE": "Paramètres audio",
"AUDIO_MASTER_TOGGLE": "Traitement audio",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Égaliseur",
"AUDIO_COMING_SOON": "Bientôt disponible"
"AUDIO_COMING_SOON": "Bientôt disponible",
"BTN_RESTART_TOUR": "Redémarrer le tutoriel",
"BTN_RESTART_TOUR_TOOLTIP": "Recommencer le tutoriel d'intégration",
"HINT_SELECT_VIDEO": "Sélectionnez votre vidéo ici !",
"OPTION_TITLE_PRIVACY_FULL": "Envoyer le titre complet",
"OPTION_TITLE_PRIVACY_EPISODE": "Envoyer lidentifiant d’épisode",
"OPTION_TITLE_PRIVACY_HIDDEN": "Ne pas envoyer",
"LABEL_PRIVACY_SETTINGS": "Paramètres de confidentialité",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choisissez quels titres donglet et de média sont envoyés au salon.",
"LABEL_SEND_TAB_TITLE": "Envoyer le titre de longlet",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envoie au salon le titre de longlet de navigateur sélectionné.",
"LABEL_MEDIA_TITLE_PRIVACY": "Titre du média",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choisissez comment les titres des médias sont envoyés au salon.",
"LABEL_SETTINGS_GROUP_PROFILE": "Profil & Apparence",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Modifiez votre nom d'utilisateur, votre thème et vos préférences linguistiques.",
"LABEL_SETTINGS_GROUP_SYNC": "Lecture & Sync",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personnalisez la lecture, les notifications et les préférences audio.",
"LABEL_CHAT_ENABLED": "Activer le chat du salon",
"LABEL_CHAT_ENABLED_TOOLTIP": "Afficher le chat chiffré du salon dans l'onglet vidéo sélectionné.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Configurez l'apparence et l'emplacement du chat chiffré du salon.",
"CHAT_SETTINGS_NOTE": "Facultatif et chiffré de bout en bout. Les messages sont uniquement en direct et ne sont pas stockés par le relais.",
"LABEL_CHAT_POSITION": "Position",
"LABEL_CHAT_POSITION_TOOLTIP": "Choisissez où le chat est ancré au lecteur sélectionné.",
"LABEL_CHAT_SIZE": "Taille de la fenêtre",
"LABEL_CHAT_SIZE_TOOLTIP": "Choisissez la taille par défaut de la fenêtre de chat.",
"OPTION_CHAT_SIZE_COMPACT": "Compacte",
"OPTION_CHAT_SIZE_STANDARD": "Standard",
"OPTION_CHAT_SIZE_LARGE": "Grande",
"OPTION_CHAT_SIZE_CUSTOM": "Dernière taille personnalisée",
"LABEL_CHAT_START_MODE": "Affichage initial",
"LABEL_CHAT_START_MODE_TOOLTIP": "Choisissez si le chat démarre sous forme de bulle flottante ou de fenêtre ouverte.",
"OPTION_CHAT_START_BUBBLE": "Bulle flottante",
"OPTION_CHAT_START_OPEN": "Fenêtre ouverte",
"CHAT_TITLE": "Chat du salon",
"CHAT_LIVE_ONLY": "En direct uniquement. Aucun historique.",
"CHAT_OPEN": "Ouvrir le chat du salon",
"CHAT_CLOSE": "Fermer le chat",
"CHAT_DOCK_LEFT": "Ancrer le chat à gauche",
"CHAT_DOCK_RIGHT": "Ancrer le chat à droite",
"CHAT_DETACHED": "Détacher le chat",
"CHAT_PLACEHOLDER": "Écrire un message...",
"CHAT_SEND": "Envoyer",
"CHAT_MISSING_KEY": "Le chat est disponible uniquement via un lien d'invitation actuel.",
"CHAT_TOO_LONG": "500 caractères maximum.",
"CHAT_SEND_FAILED": "Le message n'a pas pu être envoyé.",
"CHAT_EMPTY": "Aucun message. Le chat est uniquement en direct.",
"LABEL_CHAT_NOTIFICATIONS": "Notifications du chat",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Affiche une notification système pour les nouveaux messages lorsque longlet vidéo nest pas actif.",
"LABEL_EVENT_NOTIFICATIONS": "Notifications de lecture et de salon",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Affiche des notifications quand une personne arrive, part, lance ou met en pause.",
"DEBUG_SECTION_VIDEO_DETECTION": "Détection vidéo",
"DEBUG_VIDEO_NOT_FOUND": "AUCUN ÉLÉMENT VIDÉO DÉTECTÉ",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "Une vidéo est-elle en cours de lecture ? KoalaSync détecte les éléments vidéo une fois le média chargé.",
"DEBUG_SECTION_PLAYBACK": "Lecture",
"DEBUG_STATE_PAUSED": "EN PAUSE",
"DEBUG_STATE_PLAYING": "EN LECTURE",
"DEBUG_SECTION_PROPERTIES": "Propriétés",
"DEBUG_SECTION_DIMENSIONS": "Dimensions",
"DEBUG_DIMENSIONS_HINT": "0×0 = l’élément vidéo est masqué ou pas encore chargé",
"DEBUG_SECTION_MEDIA_ERROR": "Erreur multimédia",
"DEBUG_SECTION_DETECTION": "Détection",
"DEBUG_SECTION_IDENTIFICATION": "Identification",
"DEBUG_SECTION_MEDIA_SOURCE": "Source multimédia",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Attributs de données",
"DEBUG_YES": "OUI",
"DEBUG_NO": "NON",
"DEBUG_YES_CHECKED": "OUI (vérifié)",
"DEBUG_NA": "n/d"
}
-204
View File
@@ -1,204 +0,0 @@
# 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.
+197 -105
View File
@@ -3,8 +3,8 @@
"HTML_CLASS": "lang-it",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Stanza",
"TAB_ROOM_TOOLTIP": "Impostazioni della stanza e connessione",
"TAB_SYNC": "Sincronizza",
"TAB_ROOM_TOOLTIP": "Impostazioni della stanza y connessione",
"TAB_SYNC": "Sincro",
"TAB_SYNC_TOOLTIP": "Controlli di sincronizzazione video e azioni remote",
"TAB_SETTINGS": "Impostazioni",
"TAB_SETTINGS_TOOLTIP": "Preferenze dell'estensione",
@@ -16,155 +16,178 @@
"BTN_SERVER_OFFICIAL": "Ufficiale",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usa il server ufficiale affidabile",
"BTN_SERVER_CUSTOM": "Personalizzato",
"BTN_SERVER_CUSTOM_TOOLTIP": "Connettiti al tuo server ospitato autonomamente",
"BTN_SERVER_CUSTOM_TOOLTIP": "Connettiti al tuo server privato",
"PLACEHOLDER_SERVER_URL": "wss://tuo-server:3000",
"LABEL_ROOM_ID": "ID Stanza",
"LABEL_ROOM_ID_TOOLTIP": "L'identificatore univoco per la tua stanza di sincronizzazione",
"LABEL_ROOM_ID_TOOLTIP": "L'identificatore univoco per la tua stanza",
"PLACEHOLDER_ROOM_ID": "Inserisci ID Stanza",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "L'ID univoco della stanza a cui vuoi unirti",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "L'ID della stanza a cui vuoi unirti",
"LABEL_PASSWORD": "Password (Opzionale)",
"LABEL_PASSWORD_TOOLTIP": "Password opzionale per limitare l'accesso alla stanza",
"LABEL_PASSWORD_TOOLTIP": "Password per limitare l'accesso alla stanza",
"PLACEHOLDER_PASSWORD": "Password della stanza (opzionale)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Password per la stanza (lascia vuoto se nessuna)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Lascia vuoto se non c'è una password",
"BTN_JOIN_ROOM": "Entra / Crea Stanza",
"BTN_JOIN_ROOM_TOOLTIP": "Connettiti alla stanza",
"BTN_JOIN_ROOM_TOOLTIP": "Entra nella stanza",
"LABEL_PUBLIC_ROOMS": "Stanze Pubbliche",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Elenco delle stanze pubblicamente disponibili su questo server",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Elenco delle stanze pubbliche su questo server",
"BTN_REFRESH": "AGGIORNA",
"BTN_REFRESH_TOOLTIP": "Aggiorna l'elenco delle stanze pubbliche",
"BTN_REFRESH_TOOLTIP": "Aggiorna l'elenco delle stanze",
"PUBLIC_ROOMS_REFRESHING": "Aggiornamento...",
"BTN_REFRESH_COOLDOWN": "ATTENDI {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "L'aggiornamento dell'elenco è in pausa. Riprova tra {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Aggiornamento stanze pubbliche. Prossimo aggiornamento disponibile tra {seconds}s.",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Lelenco delle stanze è in attesa. Riprova tra {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Aggiornamento in corso. Disponibile tra {seconds}s.",
"LABEL_ACTIVE_ROOM": "Stanza Attiva",
"LABEL_ACTIVE_ROOM_TOOLTIP": "La stanza a cui sei attualmente connesso",
"LABEL_ACTIVE_ROOM_TOOLTIP": "La stanza a cui sei connesso",
"ACTIVE_ROOM_NONE": "NESSUNA",
"ACTIVE_SERVER_OFFICIAL": "Server Ufficiale",
"LABEL_INVITE_LINK": "Link di Invito",
"LABEL_INVITE_LINK_TOOLTIP": "Condividi questo link con gli amici per farli entrare",
"LABEL_PEERS_IN_ROOM": "Partecipanti nella Stanza",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Altri utenti attualmente connessi a questa stanza",
"LABEL_INVITE_LINK_TOOLTIP": "Condividi questo link per invitare i tuoi amici",
"LABEL_PEERS_IN_ROOM": "Partecipanti",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Utenti connessi a questa stanza",
"LABEL_HOST_CONTROL": "Controllo host",
"LABEL_HOST_CONTROL_TOOLTIP": "Limita chi può controllare la riproduzione in questa stanza",
"LABEL_HOST_ONLY_TOGGLE": "Solo io posso controllare la riproduzione",
"NOTICE_HOST_CONTROLS": "L'host controlla la riproduzione per tutti.",
"NOTICE_COHOST_HINT": "Suggerimento: concedi il controllo ai singoli partecipanti nell'elenco qui sotto.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Ospite",
"BADGE_CONTROLLER": "Controller",
"BTN_GIVE_CONTROL": "Dai il controllo",
"BTN_REVOKE_CONTROL": "Revoca",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Guarda per conto proprio — ignora i comandi dell'host",
"HCM_DIALOG_TITLE": "KoalaSync · L'host controlla questa stanza",
"HCM_DIALOG_BODY": "Solo l'host può controllare la riproduzione in questa stanza. Continuare a guardare insieme o guardare per conto tuo?",
"HCM_DIALOG_STAY": "Resta sincronizzato",
"HCM_DIALOG_SOLO": "Guarda per conto mio",
"HCM_BADGE_SOLO": "Stai guardando da solo",
"HCM_BADGE_RESYNC": "Risincronizza",
"NO_PEERS_CONNECTED": "Nessun partecipante connesso",
"BTN_LEAVE_ROOM": "Lascia Stanza",
"LABEL_SELECT_VIDEO": "Seleziona Video",
"LABEL_SELECT_VIDEO_TOOLTIP": "Scegli la scheda del browser contenente il video da sincronizzare",
"LABEL_SELECT_VIDEO_TOOLTIP": "Scegli la scheda con il video da sincronizzare",
"OPTION_SELECT_TAB": "-- Seleziona una Scheda --",
"LABEL_REMOTE_CONTROL": "Controllo Remoto",
"LABEL_REMOTE_CONTROL": "Telecomando",
"BTN_COPY_INVITE": "📋 Link di Invito",
"BTN_COPY_INVITE_TOOLTIP": "Copia Link di Invito",
"BTN_PLAY": "Riproduci",
"BTN_PLAY_TOOLTIP": "Invia un comando di riproduzione a tutti",
"BTN_PAUSE": "Pausa",
"BTN_PAUSE_TOOLTIP": "Invia un comando di pausa a tutti",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forza la sincronizzazione di tutti gli utenti",
"OPTION_JUMP_TO_OTHERS": "Salto dagli Altri",
"OPTION_JUMP_TO_ME": "Salto da Me",
"OPTION_JUMP_MODE_TOOLTIP": "Scegli il target di sincronizzazione",
"LABEL_LAST_ACTIVITY": "Stato Ultima Attività",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra il comando più recente di riproduzione, pausa o ricerca",
"BTN_COPY_INVITE_TOOLTIP": "Copia il link di invito",
"BTN_PLAY": "Riproduci",
"BTN_PLAY_TOOLTIP": "Avvia la riproduzione per tutti",
"BTN_PAUSE": "Pausa",
"BTN_PAUSE_TOOLTIP": "Metti in pausa per tutti",
"BTN_SYNC": "Sincro",
"BTN_SYNC_TOOLTIP": "Forza la sincronizzazione per tutti",
"OPTION_JUMP_TO_OTHERS": "Vai alla posizione degli altri",
"OPTION_JUMP_TO_ME": "Portali alla mia posizione",
"OPTION_JUMP_MODE_TOOLTIP": "Scegli l'obiettivo di sincronizzazione",
"LABEL_LAST_ACTIVITY": "Ultima Attività",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra l'ultimo comando inviato",
"NO_RECENT_COMMANDS": "Nessun comando recente",
"LOBBY_HEADER": "LOBBY EPISODIO",
"LOBBY_HEADER": "SALA D'ATTESA EPISODIO",
"LOBBY_WAITING_FOR": "🎬 In attesa di: \"{title}\"",
"LOBBY_WAITING_PEERS": "In attesa dei partecipanti...",
"BTN_SKIP_PLAY": "Salta e riproduci comunque",
"BTN_SKIP_PLAY_TOOLTIP": "Annulla la lobby e riproduci comunque",
"LOBBY_CONNECT_FIRST": "Connettiti prima a una stanza",
"LOBBY_CONNECT_FIRST_DESC": "Devi unirti a una stanza tramite un link di invito o crearne una nuova per sincronizzare i video.",
"BTN_SKIP_PLAY": "Salta e riproduci ora",
"BTN_SKIP_PLAY_TOOLTIP": "Annulla l'attesa e riproduci comunque",
"LOBBY_CONNECT_FIRST": "Entra prima in una stanza",
"LOBBY_CONNECT_FIRST_DESC": "Devi unirti a una stanza o crearne una nuova per sincronizzare i video.",
"BTN_CREATE_ROOM_ALT": "Crea Nuova Stanza",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Crea una nuova stanza casuale ed entra",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Crea una stanza casuale ed entra",
"LABEL_USERNAME": "Tuo Nome Utente",
"LABEL_USERNAME_TOOLTIP": "Il nome utente aiuta gli altri a identificarti.",
"LABEL_USERNAME_TOOLTIP": "Il tuo nome visibile agli altri.",
"PLACEHOLDER_USERNAME": "Koala Anonimo",
"LABEL_HIDE_CLUTTER": "Nascondi Schede Inutili",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra le schede senza video e i domini non correlati per mantenere pulito l'elenco",
"LABEL_AUTO_SYNC_NEXT": "Auto-Sync Prossimo Episodio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e attende tutti i partecipanti al cambio episodio, poi si avvia in sincro.",
"LABEL_AUTO_COPY_INVITE": "Copia Automatica Invito",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automaticamente il link di invito negli appunti quando crei una nuova stanza.",
"LABEL_HIDE_CLUTTER": "Nascondi schede senza video",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Mantiene pulito l'elenco filtrando le schede non pertinenti",
"LABEL_AUTO_SYNC_NEXT": "Sincro auto prossimo episodio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Attende tutti i partecipanti prima di avviare il prossimo episodio.",
"LABEL_AUTO_COPY_INVITE": "Copia automatica invito",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia il link negli appunti quando crei una nuova stanza.",
"LABEL_NOTIFICATIONS": "Notifiche del Browser",
"LABEL_NOTIFICATIONS_TOOLTIP": "Mostra le notifiche di sistema quando qualcuno entra/esce o riproduce/mette in pausa.",
"LABEL_NOTIFICATIONS_TOOLTIP": "Mostra notifiche quando qualcuno entra, esce o cambia stato.",
"LABEL_LANGUAGE": "Lingua Applicazione",
"LABEL_LANGUAGE_TOOLTIP": "Scegli la lingua preferita per l'estensione",
"LABEL_LANGUAGE_TOOLTIP": "Scegli la lingua dell'estensione",
"LABEL_TROUBLESHOOTING": "Risoluzione Problemi",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Strumenti per risolvere problemi di connessione",
"BTN_REGEN_ID": "Rigenera ID Peer",
"BTN_REGEN_ID_TOOLTIP": "Rigenera il tuo ID interno e riconnettiti",
"REGEN_ID_DESC": "Usa questo se riscontri errori di \"Identità Duplicata\".",
"REGEN_ID_OTHER_ISSUE": "Altro problema? Apri una Issue su GitHub",
"BTN_REGEN_ID": "Rigenera ID Utente",
"BTN_REGEN_ID_TOOLTIP": "Genera un nuovo ID interno e riconnettiti",
"REGEN_ID_DESC": "Usa questa opzione se riscontri errori di 'Identità Duplicata'.",
"REGEN_ID_OTHER_ISSUE": "Hai altri problemi? Apri una segnalazione su GitHub",
"TOAST_ID_REGENERATED": "Identità rigenerata — riconnessione…",
"LABEL_CONN_STATUS": "Stato Connessione",
"LABEL_CONN_STATUS_TOOLTIP": "Stato corrente della connessione WebSocket",
"LABEL_CONN_STATUS_TOOLTIP": "Stato attuale della connessione",
"CONN_STATUS_DISCONNECTED": "Disconnesso",
"BTN_RETRY": "RIPROVA",
"BTN_RETRY_TOOLTIP": "Tenta di riconnettersi al server",
"BTN_COPY_LOGS": "Copia Log",
"BTN_COPY_LOGS_TOOLTIP": "Copia i log negli appunti per la condivisione",
"LABEL_VIDEO_DEBUG": "Info Debug Video",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Dettagli tecnici sull'elemento video attualmente selezionato",
"BTN_COPY_LOGS_TOOLTIP": "Copia i log negli appunti",
"LABEL_VIDEO_DEBUG": "Debug Video",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Dettagli tecnici sul video selezionato",
"VIDEO_DEBUG_EMPTY": "Nessuna scheda selezionata o video rilevato.",
"LABEL_HISTORY": "Cronologia Azioni Completa",
"LABEL_HISTORY_TOOLTIP": "Registro cronologico di tutti i comandi di sincronizzazione nella stanza",
"LABEL_HISTORY": "Cronologia Completa",
"LABEL_HISTORY_TOOLTIP": "Registro di tutti i comandi inviati nella stanza",
"HISTORY_EMPTY": "Ancora nessuna attività",
"LABEL_LOGS": "Log (Ultimi 50)",
"LABEL_LOGS_TOOLTIP": "Log di connessione tecnica per il debug",
"LABEL_LOGS_TOOLTIP": "Log tecnici per il debug",
"BTN_CLEAR": "PULISCI",
"BTN_CLEAR_TOOLTIP": "Cancella l'output del log",
"BTN_CLEAR_TOOLTIP": "Cancella i log",
"LABEL_GITHUB": "Repository GitHub",
"BTN_ONBOARDING_SKIP": "Salta",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Salta il tutorial",
"BTN_ONBOARDING_NEXT": "Avanti",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Vai al passaggio successivo",
"ONBOARDING_1_TITLE": "Benvenuto in KoalaSync!",
"ONBOARDING_1_TEXT": "Guarda i video insieme in perfetta sincronia, non importa dove ti trovi. Facciamo un rapido tour!",
"ONBOARDING_1_TEXT": "Guarda video insieme ovunque vi troviate. Ecco una breve panoramica.",
"ONBOARDING_2_TITLE": "1. Crea una Stanza",
"ONBOARDING_2_TEXT": "Inizia qui. Crea una stanza e condividi il link di invito con i tuoi amici.",
"ONBOARDING_2_TEXT": "Inizia da qui. Crea una stanza e condividi il link con i tuoi amici.",
"ONBOARDING_3_TITLE": "2. Seleziona Video",
"ONBOARDING_3_TEXT": "Naviga qui per selezionare il video che vuoi sincronizzare. Riproduci, metti in pausa e cerca: tutti rimangono sincronizzati.",
"ONBOARDING_3_TEXT": "Scegli il video che vuoi sincronizzare. Play, pausa o salta: tutti vedranno lo stesso.",
"ONBOARDING_4_TITLE": "3. Personalizza",
"ONBOARDING_4_TEXT": "Scegli un nome utente divertente così i tuoi amici sanno chi sei.",
"ONBOARDING_4_TEXT": "Scegli un nome utente per farti riconoscere dai tuoi amici.",
"ONBOARDING_5_TITLE": "Tutto pronto!",
"ONBOARDING_5_TEXT": "È ora di prendere i popcorn. Buona visione insieme!",
"ONBOARDING_5_TEXT": "È ora di prendere i popcorn. Buona visione!",
"ERR_CONN_TIMEOUT": "Connessione scaduta. Riprova.",
"ERR_INVALID_SERVER_URL": "Formato URL del server non valido.",
"ERR_IDENTITY_NOT_LOADED": "Identità non ancora caricata. Attendi un momento e riprova.",
"ERR_NO_PEERS_TIME": "Nessun altro peer con un orario noto. Passa a 'Salto da Me'.",
"ERR_IDENTITY_NOT_LOADED": "Identità non ancora caricata. Attendi un momento.",
"ERR_NO_PEERS_TIME": "Nessun altro partecipante con posizione nota. Passa a 'Portali alla mia posizione'.",
"ERR_NO_VIDEO_TAB": "Impossibile connettersi alla scheda del video.",
"SITE_ACCESS_REQUIRED": "Il browser sta bloccando KoalaSync su {host}. Fai clic su «Consenti accesso». Se non appare alcuna finestra, consenti KoalaSync su questo sito nelle autorizzazioni dellestensione o del sito.",
"BTN_RETRY_ACCESS": "Consenti accesso",
"ERR_SELECT_VIDEO": "Seleziona prima un video!",
"TOAST_INVITE_COPIED": "Link di invito copiato!",
"TOAST_COPY_FAILED": "Impossibile copiare negli appunti",
"TOAST_LOBBY_SKIPPED": "Lobby dell'episodio saltata.",
"TOAST_LOBBY_SKIP_FAILED": "Impossibile saltare la lobby.",
"TOAST_LOBBY_SKIPPED": "Sala d'attesa saltata.",
"TOAST_LOBBY_SKIP_FAILED": "Impossibile saltare la sala d'attesa.",
"TOAST_LOGS_COPIED": "Copiato!",
"TOAST_PEER_JOINED": "{name} è entrato nella stanza",
"TOAST_PEER_LEFT": "{name} ha lasciato la stanza",
"TOAST_PEER_ACTION": "{name} ha {action}",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Connesso",
"STATUS_RECONNECTING": "Riconnessione...",
"STATUS_CONNECTING": "Connessione in corso...",
"STATUS_FAILED": "Fallito",
"STATUS_FAILED": "Errore",
"STATUS_DISCONNECTED": "Disconnesso",
"STATUS_IDLE": "Pronto a connettersi",
"STATUS_IDLE_TOOLTIP": "KoalaSync è pronto. Entra o crea una stanza per connetterti e avviare la sincronizzazione.",
"BTN_STATE_JOINING": "🚀 Entrando...",
"BTN_STATE_RECONNECTING": "🔄 Riconnessione...",
"BTN_STATE_PLAYING": "▶ Riproduzione...",
"BTN_STATE_PLAYING": "▶ In riproduzione...",
"BTN_STATE_PAUSING": "⏸ In pausa...",
"BTN_STATE_SYNCING_GROUP": "Sincronizzazione al gruppo ({time})...",
"BTN_STATE_SYNCING_GROUP": "Sincronizzazione di gruppo ({time})...",
"BTN_STATE_SYNCING": "Sincronizzazione...",
"BTN_STATE_SYNCED": "✅ Sincronizzato!",
"NOTIF_PLAY": "avviato la riproduzione",
"NOTIF_PAUSE": "messo in pausa la riproduzione",
"NOTIF_SEEK": "cercato nel video",
"NOTIF_FORCE_PREPARE": "avviato la sincronizzazione forzata",
"NOTIF_FORCE_EXECUTE": "sincronizzato tutti",
"DEBUG_NO_TAB": "Nessuna scheda di destinazione selezionata.",
"DEBUG_COMM_FAIL": "Impossibile comunicare con il video della scheda.",
"EMPTY_PEERS_TITLE": "Ancora nessun partecipante",
"EMPTY_PEERS_HINT": "Condividi il tuo link di invito per iniziare",
"EMPTY_HISTORY_TITLE": "Ancora nessuna attività",
"EMPTY_HISTORY_HINT": "Riproduci, metti in pausa o cerca per vedere la cronologia",
"NOTIF_PLAY": "ha avviato la riproduzione",
"NOTIF_PAUSE": "ha messo in pausa",
"NOTIF_SEEK": "ha cambiato posizione",
"NOTIF_FORCE_PREPARE": "ha avviato una sincronizzazione forzata",
"NOTIF_FORCE_EXECUTE": "ha sincronizzato tutti",
"DEBUG_NO_TAB": "Nessuna scheda selezionata.",
"DEBUG_COMM_FAIL": "Errore di comunicazione con il video.",
"EMPTY_PEERS_TITLE": "Nessun partecipante",
"EMPTY_PEERS_HINT": "Condividi il tuo link per iniziare",
"EMPTY_HISTORY_TITLE": "Nessuna attività",
"EMPTY_HISTORY_HINT": "Usa i comandi per vedere la cronologia",
"EMPTY_LOGS_TITLE": "Nessun log",
"EMPTY_LOGS_HINT": "Gli eventi di connessione appariranno qui",
"EMPTY_LOGS_HINT": "Gli eventi appariranno qui",
"EMPTY_ROOMS_TITLE": "Nessuna stanza attiva",
"EMPTY_ROOMS_HINT": "Crea una stanza o aggiorna per trovare quelle pubbliche",
"EMPTY_ROOMS_HINT": "Crea una stanza o aggiorna l'elenco",
"LABEL_YOU": "Tu",
"ONBOARDING_DONE": "Fatto!",
"LABEL_LOBBY_PEER_READY": "Pronto",
@@ -173,37 +196,106 @@
"LABEL_PEERS_COUNT": "{count} partecipanti",
"LABEL_CUSTOM_SERVER": "Server Personalizzato",
"BTN_STATE_CREATING": "🚀 Creazione Stanza...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Sincronizzazione Episodio Fallita",
"NOTIF_LOBBY_CANCEL_MSG": "Auto-sync annullato: {reason}. Potrebbe essere necessaria una sincronizzazione manuale.",
"LOBBY_CANCEL_TIMEOUT": "Timeout",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Timeout (ripristinato)",
"LOBBY_CANCEL_PEERS_LEFT": "Tutti gli altri partecipanti hanno lasciato la stanza",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — non tutti i partecipanti hanno caricato l'episodio",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Sincro Episodio Fallita",
"NOTIF_LOBBY_CANCEL_MSG": "Sincro auto annullata: {reason}. Usa la sincronizzazione manuale.",
"LOBBY_CANCEL_TIMEOUT": "Tempo scaduto",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tempo scaduto (ripristinato)",
"LOBBY_CANCEL_PEERS_LEFT": "Tutti gli altri partecipanti sono usciti",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo scaduto: caricamento incompleto per alcuni partecipanti",
"LOBBY_CANCEL_USER": "Annullato dall'utente",
"NOTIF_ERROR_TITLE": "Errore KoalaSync",
"FOOTER_SUPPORT": "☕ Offrimi un caffè",
"FOOTER_REVIEW": "★ Valuta",
"FOOTER_SUPPORT": "Sostieni KoalaSync",
"FOOTER_REVIEW": "★ Valutaci",
"FOOTER_SUPPORT_PROMPT": "Ti piace KoalaSync? Lascia una recensione!",
"LABEL_AUDIO_PROCESSING": "Elaborazione audio",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Applica effetti audio come la compressione alla riproduzione video",
"LABEL_AUDIO_PROCESSING": "Elaborazione Audio",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Applica effetti sonori (come il compressore) ai video",
"AUDIO_OPEN_SETTINGS": "Apri",
"NEW_FEATURE_AUDIO": "Novità: Elaborazione audio — prova il compressore!",
"NEW_FEATURE_AUDIO": "Novità: Elaborazione Audio — prova il compressore!",
"NEW_FEATURE_THEME": "Nuovo: Temi — passa all'aspetto classico o al grafite minimale.",
"AUDIO_BACK": "← Indietro",
"AUDIO_PAGE_TITLE": "Impostazioni audio",
"AUDIO_MASTER_TOGGLE": "Elaborazione audio",
"AUDIO_PAGE_TITLE": "Impostazioni Audio",
"AUDIO_MASTER_TOGGLE": "Elaborazione Audio",
"AUDIO_COMPRESSOR": "Compressore",
"AUDIO_COMPRESSOR_ENABLE": "Attivato",
"AUDIO_COMPRESSOR_ENABLE": "Attivo",
"AUDIO_PRESET": "Preimpostazione",
"AUDIO_PRESET_RECOMMENDED": "Consigliato",
"AUDIO_PRESET_DYNAMIC_RANGE": "Gamma dinamica",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Miglioramento vocale",
"AUDIO_PRESET_DYNAMIC_RANGE": "Gamma Dinamica",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Miglioramento Vocale",
"AUDIO_PRESET_SMOOTH": "Morbido",
"AUDIO_PRESET_CUSTOM": "Personalizzato",
"AUDIO_PARAM_THRESHOLD": "Soglia",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_PARAM_THRESHOLD": "Soglia (Threshold)",
"AUDIO_PARAM_KNEE": "Ginocchio (Knee)",
"AUDIO_PARAM_RATIO": "Rapporto (Ratio)",
"AUDIO_PARAM_ATTACK": "Attacco (Attack)",
"AUDIO_PARAM_RELEASE": "Rilascio (Release)",
"AUDIO_EQUALIZER": "Equalizzatore",
"AUDIO_COMING_SOON": "Prossimamente"
"AUDIO_COMING_SOON": "Prossimamente",
"BTN_RESTART_TOUR": "Riavvia Tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Riavvia il tutorial introduttivo",
"HINT_SELECT_VIDEO": "Scegli il tuo video qui!",
"OPTION_TITLE_PRIVACY_FULL": "Invia titolo completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Invia ID episodio",
"OPTION_TITLE_PRIVACY_HIDDEN": "Non inviare",
"LABEL_PRIVACY_SETTINGS": "Impostazioni privacy",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Scegli quali titoli di scheda e contenuto multimediale vengono inviati alla stanza.",
"LABEL_SEND_TAB_TITLE": "Invia titolo scheda",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Invia alla stanza il titolo della scheda del browser selezionata.",
"LABEL_MEDIA_TITLE_PRIVACY": "Titolo multimediale",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Scegli come inviare alla stanza i titoli multimediali.",
"LABEL_SETTINGS_GROUP_PROFILE": "Profilo & Aspetto",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Modifica il tuo nome utente, tema e preferenze di lingua.",
"LABEL_SETTINGS_GROUP_SYNC": "Riproduzione & Sync",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personalizza la riproduzione, le notifiche e le preferenze audio.",
"LABEL_CHAT_ENABLED": "Attiva chat della stanza",
"LABEL_CHAT_ENABLED_TOOLTIP": "Mostra la chat crittografata della stanza nella scheda video selezionata.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Configura l'aspetto e la posizione della chat crittografata della stanza.",
"CHAT_SETTINGS_NOTE": "Opzionale e crittografata end-to-end. I messaggi sono solo in diretta e non vengono memorizzati dal relay.",
"LABEL_CHAT_POSITION": "Posizione",
"LABEL_CHAT_POSITION_TOOLTIP": "Scegli dove agganciare la chat al lettore selezionato.",
"LABEL_CHAT_SIZE": "Dimensione overlay",
"LABEL_CHAT_SIZE_TOOLTIP": "Scegli la dimensione predefinita dell'overlay della chat.",
"OPTION_CHAT_SIZE_COMPACT": "Compatto",
"OPTION_CHAT_SIZE_STANDARD": "Standard",
"OPTION_CHAT_SIZE_LARGE": "Grande",
"OPTION_CHAT_SIZE_CUSTOM": "Ultima dimensione personalizzata",
"LABEL_CHAT_START_MODE": "Avvio come",
"LABEL_CHAT_START_MODE_TOOLTIP": "Scegli se la chat si avvia come bolla mobile o overlay aperto.",
"OPTION_CHAT_START_BUBBLE": "Bolla fluttuante",
"OPTION_CHAT_START_OPEN": "Overlay aperto",
"CHAT_TITLE": "Chat della stanza",
"CHAT_LIVE_ONLY": "Solo in diretta. Nessuna cronologia.",
"CHAT_OPEN": "Apri la chat della stanza",
"CHAT_CLOSE": "Chiudi chat",
"CHAT_DOCK_LEFT": "Aggancia chat a sinistra",
"CHAT_DOCK_RIGHT": "Aggancia chat a destra",
"CHAT_DETACHED": "Sgancia chat",
"CHAT_PLACEHOLDER": "Scrivi un messaggio...",
"CHAT_SEND": "Invia",
"CHAT_MISSING_KEY": "La chat è disponibile solo tramite un link di invito attuale.",
"CHAT_TOO_LONG": "Massimo 500 caratteri.",
"CHAT_SEND_FAILED": "Impossibile inviare il messaggio.",
"CHAT_EMPTY": "Nessun messaggio. La chat è solo in diretta.",
"LABEL_CHAT_NOTIFICATIONS": "Notifiche chat",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Mostra una notifica di sistema per i nuovi messaggi quando la scheda video non è attiva.",
"LABEL_EVENT_NOTIFICATIONS": "Notifiche di riproduzione e stanza",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Mostra notifiche quando qualcuno entra, esce, avvia o mette in pausa.",
"DEBUG_SECTION_VIDEO_DETECTION": "Rilevamento video",
"DEBUG_VIDEO_NOT_FOUND": "NESSUN ELEMENTO VIDEO TROVATO",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "È in riproduzione un video? KoalaSync rileva gli elementi video dopo il caricamento del contenuto.",
"DEBUG_SECTION_PLAYBACK": "Riproduzione",
"DEBUG_STATE_PAUSED": "IN PAUSA",
"DEBUG_STATE_PLAYING": "IN RIPRODUZIONE",
"DEBUG_SECTION_PROPERTIES": "Proprietà",
"DEBUG_SECTION_DIMENSIONS": "Dimensioni",
"DEBUG_DIMENSIONS_HINT": "0×0 = elemento video nascosto o non ancora caricato",
"DEBUG_SECTION_MEDIA_ERROR": "Errore multimediale",
"DEBUG_SECTION_DETECTION": "Rilevamento",
"DEBUG_SECTION_IDENTIFICATION": "Identificazione",
"DEBUG_SECTION_MEDIA_SOURCE": "Sorgente multimediale",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Attributi dati",
"DEBUG_YES": "SÌ",
"DEBUG_NO": "NO",
"DEBUG_YES_CHECKED": "SÌ (verificato)",
"DEBUG_NA": "n/d"
}
+99 -7
View File
@@ -29,7 +29,7 @@
"BTN_JOIN_ROOM": "ルームに参加 / 作成",
"BTN_JOIN_ROOM_TOOLTIP": "ルームに接続",
"LABEL_PUBLIC_ROOMS": "公開ルーム",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "このサーバー上の公開ルームリスト",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "このサーバー上の公開ルームリスト",
"BTN_REFRESH": "更新",
"BTN_REFRESH_TOOLTIP": "公開ルームのリストを更新",
"PUBLIC_ROOMS_REFRESHING": "更新中...",
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "友達とこのリンクを共有して参加してもらいましょう",
"LABEL_PEERS_IN_ROOM": "ルーム内のメンバー",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "現在このルームに接続している他のユーザー",
"LABEL_HOST_CONTROL": "ホスト操作",
"LABEL_HOST_CONTROL_TOOLTIP": "このルームで再生を操作できる人を制限します",
"LABEL_HOST_ONLY_TOGGLE": "再生を操作できるのは自分だけ",
"NOTICE_HOST_CONTROLS": "ホストが全員の再生を操作します。",
"NOTICE_COHOST_HINT": "ヒント: 下の参加者リストで個別の参加者に操作権限を付与できます。",
"BADGE_HOST": "ホスト",
"BADGE_GUEST": "ゲスト",
"BADGE_CONTROLLER": "操作権あり",
"BTN_GIVE_CONTROL": "操作権を付与",
"BTN_REVOKE_CONTROL": "取り消す",
"BADGE_DESYNCED": "ソロ",
"TOOLTIP_PEER_DESYNCED": "単独で視聴中 — ホストのコマンドを無視しています",
"HCM_DIALOG_TITLE": "KoalaSync · ホストがこのルームを操作中",
"HCM_DIALOG_BODY": "このルームではホストのみが再生を操作できます。一緒に視聴を続けますか、それとも自分だけで視聴しますか?",
"HCM_DIALOG_STAY": "同期を維持",
"HCM_DIALOG_SOLO": "自分だけで視聴",
"HCM_BADGE_SOLO": "自分だけで視聴中",
"HCM_BADGE_RESYNC": "再同期",
"NO_PEERS_CONNECTED": "接続しているメンバーはいません",
"BTN_LEAVE_ROOM": "ルームを退室",
"LABEL_SELECT_VIDEO": "ビデオを選択",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "リモートコントロール",
"BTN_COPY_INVITE": "📋 招待リンク",
"BTN_COPY_INVITE_TOOLTIP": "招待リンクをコピー",
"BTN_PLAY": "再生",
"BTN_PLAY": "再生",
"BTN_PLAY_TOOLTIP": "全員に再生コマンドを送信",
"BTN_PAUSE": "一時停止",
"BTN_PAUSE": "一時停止",
"BTN_PAUSE_TOOLTIP": "全員に一時停止コマンドを送信",
"BTN_SYNC": "同期",
"BTN_SYNC": "同期",
"BTN_SYNC_TOOLTIP": "すべてのユーザーを強制的に同期",
"OPTION_JUMP_TO_OTHERS": "他の人に合わせる",
"OPTION_JUMP_TO_ME": "自分に合わせる",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "内部IDを再生成して再接続します",
"REGEN_ID_DESC": "「Duplicate Identity」(ID重複)エラーが表示される場合に使用します。",
"REGEN_ID_OTHER_ISSUE": "他の問題?GitHub Issueを開く",
"TOAST_ID_REGENERATED": "IDを再生成しました — 再接続中…",
"LABEL_CONN_STATUS": "接続状態",
"LABEL_CONN_STATUS_TOOLTIP": "現在のWebSocket接続状態",
"CONN_STATUS_DISCONNECTED": "切断されました",
@@ -115,7 +134,7 @@
"BTN_ONBOARDING_NEXT": "次へ",
"BTN_ONBOARDING_NEXT_TOOLTIP": "次のステップに進む",
"ONBOARDING_1_TITLE": "KoalaSyncへようこそ!",
"ONBOARDING_1_TEXT": "どこにいても、完璧に同期してビデオを一緒に楽しめます。簡単なツアーを始めましょう!",
"ONBOARDING_1_TEXT": "離れた場所から一緒に動画を視聴できます。簡単に機能を紹介します。",
"ONBOARDING_2_TITLE": "1. ルームの作成",
"ONBOARDING_2_TEXT": "ここからスタート。ルームを作成し、招待リンクを友達に共有します。",
"ONBOARDING_3_TITLE": "2. ビデオの選択",
@@ -129,6 +148,8 @@
"ERR_IDENTITY_NOT_LOADED": "IDがまだロードされていません。しばらく待ってから再試行してください。",
"ERR_NO_PEERS_TIME": "既知の時間を持つ他のメンバーがいません。「自分に合わせる」に切り替えてください。",
"ERR_NO_VIDEO_TAB": "ビデオタブに接続できませんでした。",
"SITE_ACCESS_REQUIRED": "ブラウザーが {host} で KoalaSync をブロックしています。下の「アクセスを許可」をクリックしてください。ダイアログが表示されない場合は、拡張機能またはサイトの権限でこのサイトの KoalaSync を許可してください。",
"BTN_RETRY_ACCESS": "アクセスを許可",
"ERR_SELECT_VIDEO": "最初にビデオを選択してください!",
"TOAST_INVITE_COPIED": "招待リンクをコピーしました!",
"TOAST_COPY_FAILED": "クリップボードへのコピーに失敗しました",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "接続中...",
"STATUS_FAILED": "失敗",
"STATUS_DISCONNECTED": "切断されました",
"STATUS_IDLE": "接続準備完了",
"STATUS_IDLE_TOOLTIP": "KoalaSyncの準備ができました。ルームに参加するか作成して接続し、同期を開始してください。",
"BTN_STATE_JOINING": "🚀 参加中...",
"BTN_STATE_RECONNECTING": "🔄 再接続中...",
"BTN_STATE_PLAYING": "▶ 再生中...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "タイムアウト — 一部のメンバーがエピソードを読み込めませんでした",
"LOBBY_CANCEL_USER": "ユーザーによってキャンセルされました",
"NOTIF_ERROR_TITLE": "KoalaSyncエラー",
"FOOTER_SUPPORT": "☕ コーヒーをおごってね",
"FOOTER_SUPPORT": "KoalaSyncを支援",
"FOOTER_REVIEW": "★ 評価する",
"FOOTER_SUPPORT_PROMPT": "KoalaSyncはいかが?レビューを書いてください!",
"LABEL_AUDIO_PROCESSING": "オーディオ処理",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "動画再生に圧縮などのオーディオエフェクトを適用します",
"AUDIO_OPEN_SETTINGS": "開く",
"NEW_FEATURE_AUDIO": "新機能: オーディオ処理 — コンプレッサーを試してみよう!",
"NEW_FEATURE_THEME": "新機能:テーマ — クラシックな外観やミニマルなグラファイトに切り替え。",
"AUDIO_BACK": "← 戻る",
"AUDIO_PAGE_TITLE": "オーディオ設定",
"AUDIO_MASTER_TOGGLE": "オーディオ処理",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "イコライザー",
"AUDIO_COMING_SOON": "近日公開"
"AUDIO_COMING_SOON": "近日公開",
"BTN_RESTART_TOUR": "チュートリアルを再起動",
"BTN_RESTART_TOUR_TOOLTIP": "オンボーディングチュートリアルを再起動する",
"HINT_SELECT_VIDEO": "ここで動画を選択してください!",
"OPTION_TITLE_PRIVACY_FULL": "完全なタイトルを送信",
"OPTION_TITLE_PRIVACY_EPISODE": "エピソードIDを送信",
"OPTION_TITLE_PRIVACY_HIDDEN": "送信しない",
"LABEL_PRIVACY_SETTINGS": "プライバシー設定",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "ルームに送信するタブタイトルとメディアタイトルを選択します。",
"LABEL_SEND_TAB_TITLE": "タブタイトルを送信",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "選択中のブラウザータブのタイトルをルームに送信します。",
"LABEL_MEDIA_TITLE_PRIVACY": "メディアタイトル",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "メディアタイトルをルームに送信する方法を選択します。",
"LABEL_SETTINGS_GROUP_PROFILE": "プロフィールと外観",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "ユーザー名、テーマ、言語設定を変更します。",
"LABEL_SETTINGS_GROUP_SYNC": "再生と同期",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "再生、通知、オーディオ設定をカスタマイズします。",
"LABEL_CHAT_ENABLED": "ルームチャットを有効化",
"LABEL_CHAT_ENABLED_TOOLTIP": "選択した動画タブに暗号化されたルームチャットを表示します。",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "暗号化されたルームチャットの外観と位置を設定します。",
"CHAT_SETTINGS_NOTE": "任意で利用でき、エンドツーエンドで暗号化されます。メッセージはリアルタイムのみで、リレーには保存されません。",
"LABEL_CHAT_POSITION": "位置",
"LABEL_CHAT_POSITION_TOOLTIP": "選択したプレーヤーのどこにチャットを配置するか選択します。",
"LABEL_CHAT_SIZE": "オーバーレイサイズ",
"LABEL_CHAT_SIZE_TOOLTIP": "チャットオーバーレイの標準サイズを選択します。",
"OPTION_CHAT_SIZE_COMPACT": "コンパクト",
"OPTION_CHAT_SIZE_STANDARD": "標準",
"OPTION_CHAT_SIZE_LARGE": "大",
"OPTION_CHAT_SIZE_CUSTOM": "前回のカスタムサイズ",
"LABEL_CHAT_START_MODE": "開始表示",
"LABEL_CHAT_START_MODE_TOOLTIP": "フローティングバブルまたは開いたオーバーレイで開始するか選択します。",
"OPTION_CHAT_START_BUBBLE": "フローティングバブル",
"OPTION_CHAT_START_OPEN": "開いたオーバーレイ",
"CHAT_TITLE": "ルームチャット",
"CHAT_LIVE_ONLY": "ライブのみ。履歴はありません。",
"CHAT_OPEN": "ルームチャットを開く",
"CHAT_CLOSE": "チャットを閉じる",
"CHAT_DOCK_LEFT": "チャットを左に固定",
"CHAT_DOCK_RIGHT": "チャットを右に固定",
"CHAT_DETACHED": "チャットを切り離す",
"CHAT_PLACEHOLDER": "メッセージを入力...",
"CHAT_SEND": "送信",
"CHAT_MISSING_KEY": "チャットは最新の招待リンクからのみ利用できます。",
"CHAT_TOO_LONG": "最大500文字です。",
"CHAT_SEND_FAILED": "メッセージを送信できませんでした。",
"CHAT_EMPTY": "まだメッセージはありません。チャットはライブのみです。",
"LABEL_CHAT_NOTIFICATIONS": "チャット通知",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "動画タブがアクティブでないとき、新しいルームチャットをシステム通知で表示します。",
"LABEL_EVENT_NOTIFICATIONS": "再生とルームの通知",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "参加、退出、再生、一時停止をシステム通知で表示します。",
"DEBUG_SECTION_VIDEO_DETECTION": "動画検出",
"DEBUG_VIDEO_NOT_FOUND": "動画要素が見つかりません",
"DEBUG_SECTION_MEDIA_SESSION": "Media Session API",
"DEBUG_VIDEO_HINT": "動画を再生していますか?メディアの読み込み後にKoalaSyncが動画要素を検出します。",
"DEBUG_SECTION_PLAYBACK": "再生",
"DEBUG_STATE_PAUSED": "一時停止",
"DEBUG_STATE_PLAYING": "再生中",
"DEBUG_SECTION_PROPERTIES": "プロパティ",
"DEBUG_SECTION_DIMENSIONS": "サイズ",
"DEBUG_DIMENSIONS_HINT": "0×0 = 動画要素が非表示、または未読み込みです",
"DEBUG_SECTION_MEDIA_ERROR": "メディアエラー",
"DEBUG_SECTION_DETECTION": "検出",
"DEBUG_SECTION_IDENTIFICATION": "識別情報",
"DEBUG_SECTION_MEDIA_SOURCE": "メディアソース",
"DEBUG_SECTION_DATA_ATTRIBUTES": "データ属性",
"DEBUG_YES": "はい",
"DEBUG_NO": "いいえ",
"DEBUG_YES_CHECKED": "はい(確認済み)",
"DEBUG_NA": "なし"
}
+100 -8
View File
@@ -30,7 +30,7 @@
"BTN_JOIN_ROOM_TOOLTIP": "방에 연결",
"LABEL_PUBLIC_ROOMS": "공개 방",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "이 서버의 공개 방 목록",
"BTN_REFRESH": "Refreschi",
"BTN_REFRESH": "새로고침",
"BTN_REFRESH_TOOLTIP": "공개 방 목록 새로고침",
"PUBLIC_ROOMS_REFRESHING": "새로고침 중...",
"BTN_REFRESH_COOLDOWN": "{seconds}초 대기",
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "친구들이 참여할 수 있도록 이 링크를 공유하세요",
"LABEL_PEERS_IN_ROOM": "방의 참여자",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "현재 이 방에 연결되어 있는 다른 사용자들",
"LABEL_HOST_CONTROL": "호스트 제어",
"LABEL_HOST_CONTROL_TOOLTIP": "이 방에서 재생을 제어할 수 있는 사람 제한",
"LABEL_HOST_ONLY_TOGGLE": "나만 재생을 제어할 수 있음",
"NOTICE_HOST_CONTROLS": "호스트가 모두의 재생을 제어합니다.",
"NOTICE_COHOST_HINT": "팁: 아래 참가자 목록에서 개별 참가자에게 제어 권한을 부여하세요.",
"BADGE_HOST": "호스트",
"BADGE_GUEST": "게스트",
"BADGE_CONTROLLER": "제어자",
"BTN_GIVE_CONTROL": "제어 권한 부여",
"BTN_REVOKE_CONTROL": "취소",
"BADGE_DESYNCED": "단독",
"TOOLTIP_PEER_DESYNCED": "혼자 시청 중 — 호스트 명령을 무시합니다",
"HCM_DIALOG_TITLE": "KoalaSync · 호스트가 이 방을 제어합니다",
"HCM_DIALOG_BODY": "이 방에서는 호스트만 재생을 제어할 수 있습니다. 계속 함께 보시겠어요, 아니면 혼자 보시겠어요?",
"HCM_DIALOG_STAY": "동기화 유지",
"HCM_DIALOG_SOLO": "혼자 보기",
"HCM_BADGE_SOLO": "혼자 시청 중",
"HCM_BADGE_RESYNC": "다시 동기화",
"NO_PEERS_CONNECTED": "연결된 참여자가 없습니다",
"BTN_LEAVE_ROOM": "방 나가기",
"LABEL_SELECT_VIDEO": "비디오 선택",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "원격 제어",
"BTN_COPY_INVITE": "📋 초대 링크",
"BTN_COPY_INVITE_TOOLTIP": "초대 링크 복사",
"BTN_PLAY": "재생",
"BTN_PLAY": "재생",
"BTN_PLAY_TOOLTIP": "모든 사람에게 재생 명령 전송",
"BTN_PAUSE": "일시정지",
"BTN_PAUSE": "일시정지",
"BTN_PAUSE_TOOLTIP": "모든 사람에게 일시정지 명령 전송",
"BTN_SYNC": "동기화",
"BTN_SYNC": "동기화",
"BTN_SYNC_TOOLTIP": "모든 사용자의 강제 동기화",
"OPTION_JUMP_TO_OTHERS": "다른 사람에게 이동",
"OPTION_JUMP_TO_ME": "나에게 이동",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "내부 ID를 재생성하고 다시 연결합니다",
"REGEN_ID_DESC": "\"중복된 ID\" 오류가 발생할 경우에 사용하세요.",
"REGEN_ID_OTHER_ISSUE": "다른 문제? GitHub Issue 열기",
"TOAST_ID_REGENERATED": "ID가 재생성되었습니다 — 다시 연결 중…",
"LABEL_CONN_STATUS": "연결 상태",
"LABEL_CONN_STATUS_TOOLTIP": "현재 WebSocket 연결 상태",
"CONN_STATUS_DISCONNECTED": "연결 끊김",
@@ -115,7 +134,7 @@
"BTN_ONBOARDING_NEXT": "다음",
"BTN_ONBOARDING_NEXT_TOOLTIP": "다음 단계로 이동",
"ONBOARDING_1_TITLE": "KoalaSync에 오신 것을 환영합니다!",
"ONBOARDING_1_TEXT": "어디에 있든 완벽하게 동기화된 비디오를 함께 시청하세요. 간단히 둘러볼까요!",
"ONBOARDING_1_TEXT": "어디서든 함께 비디오를 시청하세요. 주요 기능을 간단히 살펴봅니다.",
"ONBOARDING_2_TITLE": "1. 방 만들기",
"ONBOARDING_2_TEXT": "여기서 시작하세요. 방을 만들고 친구들에게 초대 링크를 공유하세요.",
"ONBOARDING_3_TITLE": "2. 비디오 선택",
@@ -129,6 +148,8 @@
"ERR_IDENTITY_NOT_LOADED": "ID가 아직 로드되지 않았습니다. 잠시 후 다시 시도해 주세요.",
"ERR_NO_PEERS_TIME": "시간을 알고 있는 다른 피어가 없습니다. '나에게 이동'으로 전환하세요.",
"ERR_NO_VIDEO_TAB": "비디오 탭에 연결할 수 없습니다.",
"SITE_ACCESS_REQUIRED": "브라우저가 {host}에서 KoalaSync를 차단하고 있습니다. 아래의 ‘액세스 허용’을 클릭하세요. 대화상자가 표시되지 않으면 확장 프로그램 또는 사이트 권한에서 이 사이트의 KoalaSync를 허용하세요.",
"BTN_RETRY_ACCESS": "액세스 허용",
"ERR_SELECT_VIDEO": "먼저 비디오를 선택하세요!",
"TOAST_INVITE_COPIED": "초대 링크가 복사되었습니다!",
"TOAST_COPY_FAILED": "클립보드 복사에 실패했습니다",
@@ -140,9 +161,11 @@
"TOAST_PEER_ACTION": "{name} 님이 {action}",
"STATUS_CONNECTED": "연결됨",
"STATUS_RECONNECTING": "재연결 중...",
"STATUS_CONNECTING": "연kel 중...",
"STATUS_CONNECTING": "연 중...",
"STATUS_FAILED": "실패",
"STATUS_DISCONNECTED": "연결 끊김",
"STATUS_IDLE": "연결 준비 완료",
"STATUS_IDLE_TOOLTIP": "KoalaSync가 준비되었습니다. 방에 참여하거나 생성하여 연결하고 동기화를 시작하세요.",
"BTN_STATE_JOINING": "🚀 참여 중...",
"BTN_STATE_RECONNECTING": "🔄 재연결 중...",
"BTN_STATE_PLAYING": "▶ 재생 중...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "시간 초과 — 모든 피어가 에피소드를 로드하지 못했습니다",
"LOBBY_CANCEL_USER": "사용자에 의해 취소됨",
"NOTIF_ERROR_TITLE": "KoalaSync 오류",
"FOOTER_SUPPORT": "☕ 커피 한 잔 사주세요",
"FOOTER_SUPPORT": "KoalaSync 후원",
"FOOTER_REVIEW": "★ 평가하기",
"FOOTER_SUPPORT_PROMPT": "KoalaSync가 마음에 드세요? 리뷰를 남겨주세요!",
"LABEL_AUDIO_PROCESSING": "오디오 처리",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "비디오 재생에 압축과 같은 오디오 효과를 적용합니다",
"AUDIO_OPEN_SETTINGS": "열기",
"NEW_FEATURE_AUDIO": "신규: 오디오 처리 — 컴프레서를 사용해보세요!",
"NEW_FEATURE_THEME": "신규: 테마 — 클래식 룩이나 미니멀 그래파이트로 전환하세요.",
"AUDIO_BACK": "← 뒤로",
"AUDIO_PAGE_TITLE": "오디오 설정",
"AUDIO_MASTER_TOGGLE": "오디오 처리",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "이퀄라이저",
"AUDIO_COMING_SOON": "출시 예정"
"AUDIO_COMING_SOON": "출시 예정",
"BTN_RESTART_TOUR": "튜토리얼 다시 시작",
"BTN_RESTART_TOUR_TOOLTIP": "온보딩 튜토리얼을 다시 시작합니다",
"HINT_SELECT_VIDEO": "여기에서 비디오를 선택하세요!",
"OPTION_TITLE_PRIVACY_FULL": "전체 제목 보내기",
"OPTION_TITLE_PRIVACY_EPISODE": "에피소드 ID 보내기",
"OPTION_TITLE_PRIVACY_HIDDEN": "보내지 않음",
"LABEL_PRIVACY_SETTINGS": "개인정보 설정",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "방에 보낼 탭 제목과 미디어 제목을 선택합니다.",
"LABEL_SEND_TAB_TITLE": "탭 제목 보내기",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "선택한 브라우저 탭 제목을 방에 보냅니다.",
"LABEL_MEDIA_TITLE_PRIVACY": "미디어 제목",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "미디어 제목을 방에 보내는 방식을 선택합니다.",
"LABEL_SETTINGS_GROUP_PROFILE": "프로필 및 테마",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "사용자 이름, 테마 및 언어 설정을 변경합니다.",
"LABEL_SETTINGS_GROUP_SYNC": "재생 및 동기화",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "재생, 알림 및 오디오 설정을 맞춤설정합니다.",
"LABEL_CHAT_ENABLED": "방 채팅 사용",
"LABEL_CHAT_ENABLED_TOOLTIP": "선택한 동영상 탭에 암호화된 방 채팅을 표시합니다.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "암호화된 방 채팅의 모양과 위치를 설정합니다.",
"CHAT_SETTINGS_NOTE": "선택 기능이며 종단 간 암호화됩니다. 메시지는 실시간으로만 표시되고 릴레이에 저장되지 않습니다.",
"LABEL_CHAT_POSITION": "위치",
"LABEL_CHAT_POSITION_TOOLTIP": "선택한 플레이어에서 채팅을 고정할 위치를 선택합니다.",
"LABEL_CHAT_SIZE": "오버레이 크기",
"LABEL_CHAT_SIZE_TOOLTIP": "채팅 오버레이의 기본 크기를 선택합니다.",
"OPTION_CHAT_SIZE_COMPACT": "작게",
"OPTION_CHAT_SIZE_STANDARD": "표준",
"OPTION_CHAT_SIZE_LARGE": "크게",
"OPTION_CHAT_SIZE_CUSTOM": "마지막 사용자 지정 크기",
"LABEL_CHAT_START_MODE": "시작 화면",
"LABEL_CHAT_START_MODE_TOOLTIP": "플로팅 버블 또는 열린 오버레이로 시작할지 선택합니다.",
"OPTION_CHAT_START_BUBBLE": "플로팅 채팅 버블",
"OPTION_CHAT_START_OPEN": "열린 오버레이",
"CHAT_TITLE": "방 채팅",
"CHAT_LIVE_ONLY": "실시간 전용. 기록 없음.",
"CHAT_OPEN": "방 채팅 열기",
"CHAT_CLOSE": "채팅 닫기",
"CHAT_DOCK_LEFT": "채팅을 왼쪽에 고정",
"CHAT_DOCK_RIGHT": "채팅을 오른쪽에 고정",
"CHAT_DETACHED": "채팅 분리",
"CHAT_PLACEHOLDER": "메시지를 입력하세요...",
"CHAT_SEND": "보내기",
"CHAT_MISSING_KEY": "채팅은 최신 초대 링크를 통해서만 사용할 수 있습니다.",
"CHAT_TOO_LONG": "최대 500자입니다.",
"CHAT_SEND_FAILED": "메시지를 보낼 수 없습니다.",
"CHAT_EMPTY": "아직 메시지가 없습니다. 채팅은 실시간 전용입니다.",
"LABEL_CHAT_NOTIFICATIONS": "채팅 알림",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "비디오 탭이 활성 상태가 아닐 때 새 방 채팅 메시지를 시스템 알림으로 표시합니다.",
"LABEL_EVENT_NOTIFICATIONS": "재생 및 방 알림",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "참여, 퇴장, 재생 또는 일시정지를 시스템 알림으로 표시합니다.",
"DEBUG_SECTION_VIDEO_DETECTION": "비디오 감지",
"DEBUG_VIDEO_NOT_FOUND": "비디오 요소를 찾지 못했습니다",
"DEBUG_SECTION_MEDIA_SESSION": "Media Session API",
"DEBUG_VIDEO_HINT": "비디오가 재생 중인가요? 미디어가 로드된 후 KoalaSync가 비디오 요소를 감지합니다.",
"DEBUG_SECTION_PLAYBACK": "재생",
"DEBUG_STATE_PAUSED": "일시정지",
"DEBUG_STATE_PLAYING": "재생 중",
"DEBUG_SECTION_PROPERTIES": "속성",
"DEBUG_SECTION_DIMENSIONS": "크기",
"DEBUG_DIMENSIONS_HINT": "0×0 = 비디오 요소가 숨겨져 있거나 아직 로드되지 않음",
"DEBUG_SECTION_MEDIA_ERROR": "미디어 오류",
"DEBUG_SECTION_DETECTION": "감지",
"DEBUG_SECTION_IDENTIFICATION": "식별 정보",
"DEBUG_SECTION_MEDIA_SOURCE": "미디어 소스",
"DEBUG_SECTION_DATA_ATTRIBUTES": "데이터 속성",
"DEBUG_YES": "예",
"DEBUG_NO": "아니요",
"DEBUG_YES_CHECKED": "예(확인됨)",
"DEBUG_NA": "없음"
}
+100 -8
View File
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Deel deze link met vrienden zodat ze kunnen deelnemen",
"LABEL_PEERS_IN_ROOM": "Deelnemers in kamer",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Andere gebruikers die momenteel zijn verbonden met deze kamer",
"LABEL_HOST_CONTROL": "Hostbeheer",
"LABEL_HOST_CONTROL_TOOLTIP": "Bepaal wie de weergave in deze kamer mag bedienen",
"LABEL_HOST_ONLY_TOGGLE": "Alleen ik mag de weergave bedienen",
"NOTICE_HOST_CONTROLS": "De host bedient de weergave voor iedereen.",
"NOTICE_COHOST_HINT": "Tip: geef afzonderlijke deelnemers de besturing in de lijst hieronder.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Gast",
"BADGE_CONTROLLER": "Bediener",
"BTN_GIVE_CONTROL": "Bediening geven",
"BTN_REVOKE_CONTROL": "Intrekken",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Zelfstandig aan het kijken — negeert de commando's van de host",
"HCM_DIALOG_TITLE": "KoalaSync · De host bedient deze kamer",
"HCM_DIALOG_BODY": "Alleen de host kan de weergave in deze kamer bedienen. Samen blijven kijken of zelf verder kijken?",
"HCM_DIALOG_STAY": "Gesynchroniseerd blijven",
"HCM_DIALOG_SOLO": "Zelf verder kijken",
"HCM_BADGE_SOLO": "Je kijkt zelfstandig",
"HCM_BADGE_RESYNC": "Opnieuw sync",
"NO_PEERS_CONNECTED": "Geen deelnemers verbonden",
"BTN_LEAVE_ROOM": "Kamer verlaten",
"LABEL_SELECT_VIDEO": "Selecteer video",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "Afstandsbediening",
"BTN_COPY_INVITE": "📋 Uitnodigingslink",
"BTN_COPY_INVITE_TOOLTIP": "Uitnodigingslink kopiëren",
"BTN_PLAY": "Afspelen",
"BTN_PLAY": "Afspelen",
"BTN_PLAY_TOOLTIP": "Stuur een afspeelopdracht naar iedereen",
"BTN_PAUSE": "Pauzeren",
"BTN_PAUSE": "Pauzeren",
"BTN_PAUSE_TOOLTIP": "Stuur een pauzeeropdracht naar iedereen",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC": "Sync",
"BTN_SYNC_TOOLTIP": "Dwing alle gebruikers om te synchroniseren",
"OPTION_JUMP_TO_OTHERS": "Spring naar anderen",
"OPTION_JUMP_TO_ME": "Spring naar mij",
@@ -64,7 +82,7 @@
"LABEL_LAST_ACTIVITY": "Laatste activiteitsstatus",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Toont de meest recente afspeel-, pauzeer- of zoekopdracht",
"NO_RECENT_COMMANDS": "Geen recente commando's",
"LOBBY_HEADER": "EPISODE LOBBY",
"LOBBY_HEADER": "AFLEVERINGSLOBBY",
"LOBBY_WAITING_FOR": "🎬 Wachten op: \"{title}\"",
"LOBBY_WAITING_PEERS": "Wachten op deelnemers...",
"BTN_SKIP_PLAY": "Overslaan & toch afspelen",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "Genereer uw interne ID opnieuw en maak opnieuw verbinding",
"REGEN_ID_DESC": "Gebruik dit als u fouten ziet over 'Duplicate Identity'.",
"REGEN_ID_OTHER_ISSUE": "Ander probleem? Open een GitHub Issue",
"TOAST_ID_REGENERATED": "Identiteit opnieuw gegenereerd — opnieuw verbinden…",
"LABEL_CONN_STATUS": "Verbindingsstatus",
"LABEL_CONN_STATUS_TOOLTIP": "Huidige WebSocket-verbindingsstatus",
"CONN_STATUS_DISCONNECTED": "Verbinding verbroken",
@@ -115,7 +134,7 @@
"BTN_ONBOARDING_NEXT": "Volgende",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ga naar de volgende stap",
"ONBOARDING_1_TITLE": "Welkom bij KoalaSync!",
"ONBOARDING_1_TEXT": "Bekijk video's samen in perfecte synchronisatie — waar u ook bent. Laten we een snelle rondleiding nemen!",
"ONBOARDING_1_TEXT": "Bekijk samen videos, waar je ook bent. Hier volgt een korte rondleiding.",
"ONBOARDING_2_TITLE": "1. Kamer maken",
"ONBOARDING_2_TEXT": "Begin hier. Maak een kamer en deel de uitnodigingslink met uw vrienden.",
"ONBOARDING_3_TITLE": "2. Selecteer video",
@@ -129,6 +148,8 @@
"ERR_IDENTITY_NOT_LOADED": "Identiteit nog niet geladen. Wacht een moment en probeer het opnieuw.",
"ERR_NO_PEERS_TIME": "Geen andere deelnemers met een bekende tijd. Schakel over naar 'Spring naar mij'.",
"ERR_NO_VIDEO_TAB": "Kon geen verbinding maken met videotabblad.",
"SITE_ACCESS_REQUIRED": "Je browser blokkeert KoalaSync op {host}. Klik hieronder op Toegang toestaan. Als er geen dialoog verschijnt, sta KoalaSync voor deze site toe via de extensie- of siterechten.",
"BTN_RETRY_ACCESS": "Toegang toestaan",
"ERR_SELECT_VIDEO": "Selecteer eerst een video!",
"TOAST_INVITE_COPIED": "Uitnodigingslink gekopieerd!",
"TOAST_COPY_FAILED": "Kopiëren naar klembord mislukt",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "Verbinden...",
"STATUS_FAILED": "Mislukt",
"STATUS_DISCONNECTED": "Verbinding verbroken",
"STATUS_IDLE": "Gereed om te verbinden",
"STATUS_IDLE_TOOLTIP": "KoalaSync is gereed. Neem deel aan een kamer of maak er een aan om te verbinden en te synchroniseren.",
"BTN_STATE_JOINING": "🚀 Deelnemen...",
"BTN_STATE_RECONNECTING": "🔄 Opnieuw verbinden...",
"BTN_STATE_PLAYING": "▶ Afspelen...",
@@ -156,7 +179,7 @@
"NOTIF_FORCE_PREPARE": "is een geforceerde sync gestart",
"NOTIF_FORCE_EXECUTE": "heeft iedereen gesynchroniseerd",
"DEBUG_NO_TAB": "Geen doeltabblad geselecteerd.",
"DEBUG_COMM_FAIL": "Kon niet cmmuniceren met de videotab.",
"DEBUG_COMM_FAIL": "Kon niet communiceren met de videotab.",
"EMPTY_PEERS_TITLE": "Nog geen deelnemers",
"EMPTY_PEERS_HINT": "Deel uw uitnodigingslink om te beginnen",
"EMPTY_HISTORY_TITLE": "Nog geen activiteit",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Time-out — niet alle deelnemers hebben de aflevering geladen",
"LOBBY_CANCEL_USER": "Geannuleerd door gebruiker",
"NOTIF_ERROR_TITLE": "KoalaSync-fout",
"FOOTER_SUPPORT": "☕ Trakteer op een koffie",
"FOOTER_SUPPORT": "Steun KoalaSync",
"FOOTER_REVIEW": "★ Beoordelen",
"FOOTER_SUPPORT_PROMPT": "Vind je KoalaSync leuk? Laat een review achter!",
"LABEL_AUDIO_PROCESSING": "Audioverwerking",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Past audio-effecten zoals compressie toe op video-afspelen",
"AUDIO_OPEN_SETTINGS": "Openen",
"NEW_FEATURE_AUDIO": "Nieuw: Audioverwerking — probeer de compressor!",
"NEW_FEATURE_THEME": "Nieuw: Thema's — schakel naar de klassieke look of minimaal grafiet.",
"AUDIO_BACK": "← Terug",
"AUDIO_PAGE_TITLE": "Audio-instellingen",
"AUDIO_MASTER_TOGGLE": "Audioverwerking",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Binnenkort beschikbaar"
"AUDIO_COMING_SOON": "Binnenkort beschikbaar",
"BTN_RESTART_TOUR": "Tutorial opnieuw starten",
"BTN_RESTART_TOUR_TOOLTIP": "Start de introductie-tutorial opnieuw",
"HINT_SELECT_VIDEO": "Selecteer hier je video!",
"OPTION_TITLE_PRIVACY_FULL": "Volledige titel verzenden",
"OPTION_TITLE_PRIVACY_EPISODE": "Afleverings-ID verzenden",
"OPTION_TITLE_PRIVACY_HIDDEN": "Niet verzenden",
"LABEL_PRIVACY_SETTINGS": "Privacy-instellingen",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Kies welke tab- en mediatitels naar de kamer worden gestuurd.",
"LABEL_SEND_TAB_TITLE": "Tabtitel verzenden",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Stuurt de titel van het geselecteerde browsertabblad naar de kamer.",
"LABEL_MEDIA_TITLE_PRIVACY": "Mediatitel",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Kies hoe mediatitels naar de kamer worden gestuurd.",
"LABEL_SETTINGS_GROUP_PROFILE": "Profiel & Weergave",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Wijzig uw gebruikersnaam, thema en taalvoorkeuren.",
"LABEL_SETTINGS_GROUP_SYNC": "Afspelen & Sync",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Pas afspelen, meldingen en audiovoorkeuren aan.",
"LABEL_CHAT_ENABLED": "Kamerchat inschakelen",
"LABEL_CHAT_ENABLED_TOOLTIP": "Versleutelde kamerchat tonen in het geselecteerde videotabblad.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Pas het uiterlijk en de positie van de versleutelde kamerchat aan.",
"CHAT_SETTINGS_NOTE": "Optioneel en end-to-end versleuteld. Berichten zijn alleen live en worden niet door de relay opgeslagen.",
"LABEL_CHAT_POSITION": "Positie",
"LABEL_CHAT_POSITION_TOOLTIP": "Kies waar de chat aan de geselecteerde speler wordt gekoppeld.",
"LABEL_CHAT_SIZE": "Overlaygrootte",
"LABEL_CHAT_SIZE_TOOLTIP": "Kies de standaardgrootte van de chatoverlay.",
"OPTION_CHAT_SIZE_COMPACT": "Compact",
"OPTION_CHAT_SIZE_STANDARD": "Standaard",
"OPTION_CHAT_SIZE_LARGE": "Groot",
"OPTION_CHAT_SIZE_CUSTOM": "Laatste aangepaste grootte",
"LABEL_CHAT_START_MODE": "Start als",
"LABEL_CHAT_START_MODE_TOOLTIP": "Kies of de chat start als zwevende knop of als geopende overlay.",
"OPTION_CHAT_START_BUBBLE": "Zwevende chatknop",
"OPTION_CHAT_START_OPEN": "Geopende overlay",
"CHAT_TITLE": "Kamerchat",
"CHAT_LIVE_ONLY": "Alleen live. Geen geschiedenis.",
"CHAT_OPEN": "Kamerchat openen",
"CHAT_CLOSE": "Chat sluiten",
"CHAT_DOCK_LEFT": "Chat links vastzetten",
"CHAT_DOCK_RIGHT": "Chat rechts vastzetten",
"CHAT_DETACHED": "Chat losmaken",
"CHAT_PLACEHOLDER": "Schrijf een bericht...",
"CHAT_SEND": "Verzenden",
"CHAT_MISSING_KEY": "Chat is alleen beschikbaar via een actuele uitnodigingslink.",
"CHAT_TOO_LONG": "Maximaal 500 tekens.",
"CHAT_SEND_FAILED": "Bericht kon niet worden verzonden.",
"CHAT_EMPTY": "Nog geen berichten. Chat is alleen live.",
"LABEL_CHAT_NOTIFICATIONS": "Chatmeldingen",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Toont een systeemmelding voor nieuwe chatberichten wanneer het videotabblad niet actief is.",
"LABEL_EVENT_NOTIFICATIONS": "Afspeel- en kamermeldingen",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Toont meldingen wanneer iemand deelneemt, vertrekt, afspeelt of pauzeert.",
"DEBUG_SECTION_VIDEO_DETECTION": "Videodetectie",
"DEBUG_VIDEO_NOT_FOUND": "GEEN VIDEO-ELEMENT GEVONDEN",
"DEBUG_SECTION_MEDIA_SESSION": "Media Session API",
"DEBUG_VIDEO_HINT": "Wordt er een video afgespeeld? KoalaSync detecteert video-elementen nadat de media is geladen.",
"DEBUG_SECTION_PLAYBACK": "Afspelen",
"DEBUG_STATE_PAUSED": "GEPAUZEERD",
"DEBUG_STATE_PLAYING": "WORDT AFGESPEELD",
"DEBUG_SECTION_PROPERTIES": "Eigenschappen",
"DEBUG_SECTION_DIMENSIONS": "Afmetingen",
"DEBUG_DIMENSIONS_HINT": "0×0 = video-element is verborgen of nog niet geladen",
"DEBUG_SECTION_MEDIA_ERROR": "Mediafout",
"DEBUG_SECTION_DETECTION": "Detectie",
"DEBUG_SECTION_IDENTIFICATION": "Identificatie",
"DEBUG_SECTION_MEDIA_SOURCE": "Mediabron",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Data-attributen",
"DEBUG_YES": "JA",
"DEBUG_NO": "NEE",
"DEBUG_YES_CHECKED": "JA (gecontroleerd)",
"DEBUG_NA": "n.v.t."
}
+98 -6
View File
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Udostępnij ten link znajomym, aby mogli dołączyć",
"LABEL_PEERS_IN_ROOM": "Uczestnicy w pokoju",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Inni użytkownicy aktualnie połączeni z tym pokojem",
"LABEL_HOST_CONTROL": "Kontrola hosta",
"LABEL_HOST_CONTROL_TOOLTIP": "Określ, kto może sterować odtwarzaniem w tym pokoju",
"LABEL_HOST_ONLY_TOGGLE": "Tylko ja mogę sterować odtwarzaniem",
"NOTICE_HOST_CONTROLS": "Host steruje odtwarzaniem dla wszystkich.",
"NOTICE_COHOST_HINT": "Wskazówka: przyznaj sterowanie wybranym uczestnikom na liście poniżej.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Gość",
"BADGE_CONTROLLER": "Sterujący",
"BTN_GIVE_CONTROL": "Daj sterowanie",
"BTN_REVOKE_CONTROL": "Odbierz",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Ogląda samodzielnie — ignoruje polecenia hosta",
"HCM_DIALOG_TITLE": "KoalaSync · Host steruje tym pokojem",
"HCM_DIALOG_BODY": "Tylko host może sterować odtwarzaniem w tym pokoju. Oglądać dalej razem czy oglądać samodzielnie?",
"HCM_DIALOG_STAY": "Pozostań zsynchronizowany",
"HCM_DIALOG_SOLO": "Oglądaj samodzielnie",
"HCM_BADGE_SOLO": "Oglądasz samodzielnie",
"HCM_BADGE_RESYNC": "Zsynchronizuj",
"NO_PEERS_CONNECTED": "Brak połączonych uczestników",
"BTN_LEAVE_ROOM": "Opuść pokój",
"LABEL_SELECT_VIDEO": "Wybierz wideo",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "Zdalne sterowanie",
"BTN_COPY_INVITE": "📋 Link do zaproszenia",
"BTN_COPY_INVITE_TOOLTIP": "Kopiuj link zaproszenia",
"BTN_PLAY": "Odtwórz",
"BTN_PLAY": "Odtwórz",
"BTN_PLAY_TOOLTIP": "Wyślij polecenie odtwarzania do wszystkich",
"BTN_PAUSE": "Pauza",
"BTN_PAUSE": "Pauza",
"BTN_PAUSE_TOOLTIP": "Wyślij polecenie pauzy do wszystkich",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC": "Sync",
"BTN_SYNC_TOOLTIP": "Wymuś synchronizację wszystkich użytkowników",
"OPTION_JUMP_TO_OTHERS": "Skocz do innych",
"OPTION_JUMP_TO_ME": "Skocz do mnie",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "Wygeneruj ponownie swój identyfikator i połącz się ponownie",
"REGEN_ID_DESC": "Użyj tego, jeśli widzisz błędy o zduplikowanej tożsamości.",
"REGEN_ID_OTHER_ISSUE": "Inny problem? Otwórz Issue na GitHub",
"TOAST_ID_REGENERATED": "Tożsamość wygenerowana ponownie — ponowne łączenie…",
"LABEL_CONN_STATUS": "Status połączenia",
"LABEL_CONN_STATUS_TOOLTIP": "Bieżący stan połączenia WebSocket",
"CONN_STATUS_DISCONNECTED": "Rozłączono",
@@ -115,7 +134,7 @@
"BTN_ONBOARDING_NEXT": "Dalej",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Przejdź do następnego kroku",
"ONBOARDING_1_TITLE": "Witaj w KoalaSync!",
"ONBOARDING_1_TEXT": "Oglądaj filmy w idealnej synchronizacji — bez względu na to, gdzie jesteś. Zróbmy szybką wycieczkę!",
"ONBOARDING_1_TEXT": "Oglądajcie filmy razem, gdziekolwiek jesteście. Oto krótkie wprowadzenie.",
"ONBOARDING_2_TITLE": "1. Utwórz pokój",
"ONBOARDING_2_TEXT": "Zacznij tutaj. Utwórz pokój i udostępnij link zaproszenia znajomym.",
"ONBOARDING_3_TITLE": "2. Wybierz wideo",
@@ -129,6 +148,8 @@
"ERR_IDENTITY_NOT_LOADED": "Tożsamość nie została jeszcze załadowana. Odczekaj chwilę i spróbuj ponownie.",
"ERR_NO_PEERS_TIME": "Brak innych uczestników ze znanym czasem. Przełącz na 'Skocz do mnie'.",
"ERR_NO_VIDEO_TAB": "Nie można połączyć się z kartą wideo.",
"SITE_ACCESS_REQUIRED": "Przeglądarka blokuje KoalaSync na {host}. Kliknij poniżej „Zezwól na dostęp”. Jeśli nie pojawi się okno, zezwól KoalaSync na tej stronie w uprawnieniach rozszerzenia lub witryny.",
"BTN_RETRY_ACCESS": "Zezwól na dostęp",
"ERR_SELECT_VIDEO": "Najpierw wybierz wideo!",
"TOAST_INVITE_COPIED": "Link zaproszenia skopiowany!",
"TOAST_COPY_FAILED": "Nie udało się skopiować do schowka",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "Łączenie...",
"STATUS_FAILED": "Nieudane",
"STATUS_DISCONNECTED": "Rozłączono",
"STATUS_IDLE": "Gotowy do połączenia",
"STATUS_IDLE_TOOLTIP": "KoalaSync jest gotowy. Dołącz do pokoju lub utwórz go, aby się połączyć i rozpocząć synchronizację.",
"BTN_STATE_JOINING": "🚀 Dołączanie...",
"BTN_STATE_RECONNECTING": "🔄 Ponowne łączenie...",
"BTN_STATE_PLAYING": "▶ Odtwarzanie...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Limit czasu — nie wszyscy uczestnicy załadowali odcinek",
"LOBBY_CANCEL_USER": "Anulowane przez użytkownika",
"NOTIF_ERROR_TITLE": "Błąd KoalaSync",
"FOOTER_SUPPORT": "☕ Postaw kawę",
"FOOTER_SUPPORT": "Wesprzyj KoalaSync",
"FOOTER_REVIEW": "★ Oceń",
"FOOTER_SUPPORT_PROMPT": "Podoba Ci się KoalaSync? Zostaw recenzję!",
"LABEL_AUDIO_PROCESSING": "Przetwarzanie dźwięku",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Stosuje efekty dźwiękowe, takie jak kompresja, do odtwarzania wideo",
"AUDIO_OPEN_SETTINGS": "Otwórz",
"NEW_FEATURE_AUDIO": "Nowość: Przetwarzanie dźwięku — wypróbuj kompresor!",
"NEW_FEATURE_THEME": "Nowość: Motywy — przełącz na klasyczny wygląd lub minimalny grafit.",
"AUDIO_BACK": "← Wstecz",
"AUDIO_PAGE_TITLE": "Ustawienia dźwięku",
"AUDIO_MASTER_TOGGLE": "Przetwarzanie dźwięku",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Korektor",
"AUDIO_COMING_SOON": "Wkrótce"
"AUDIO_COMING_SOON": "Wkrótce",
"BTN_RESTART_TOUR": "Uruchom samouczek ponownie",
"BTN_RESTART_TOUR_TOOLTIP": "Uruchom ponownie samouczek powitalny",
"HINT_SELECT_VIDEO": "Wybierz swoje wideo tutaj!",
"OPTION_TITLE_PRIVACY_FULL": "Wysyłaj pełny tytuł",
"OPTION_TITLE_PRIVACY_EPISODE": "Wysyłaj ID odcinka",
"OPTION_TITLE_PRIVACY_HIDDEN": "Nie wysyłaj",
"LABEL_PRIVACY_SETTINGS": "Ustawienia prywatności",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Wybierz, które tytuły kart i multimediów są wysyłane do pokoju.",
"LABEL_SEND_TAB_TITLE": "Wysyłaj tytuł karty",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Wysyła do pokoju tytuł wybranej karty przeglądarki.",
"LABEL_MEDIA_TITLE_PRIVACY": "Tytuł multimediów",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Wybierz, jak tytuły multimediów są wysyłane do pokoju.",
"LABEL_SETTINGS_GROUP_PROFILE": "Profil i Wygląd",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Zmień swoją nazwę użytkownika, motyw i preferencje językowe.",
"LABEL_SETTINGS_GROUP_SYNC": "Odtwarzanie i Sync",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Dostosuj odtwarzanie, powiadomienia i preferencje audio.",
"LABEL_CHAT_ENABLED": "Włącz czat pokoju",
"LABEL_CHAT_ENABLED_TOOLTIP": "Pokaż zaszyfrowany czat pokoju na wybranej karcie wideo.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Skonfiguruj wygląd i położenie zaszyfrowanego czatu pokoju.",
"CHAT_SETTINGS_NOTE": "Opcjonalny i szyfrowany od końca do końca. Wiadomości są tylko na żywo i nie są przechowywane przez serwer pośredniczący.",
"LABEL_CHAT_POSITION": "Położenie",
"LABEL_CHAT_POSITION_TOOLTIP": "Wybierz, gdzie czat ma być przypięty do wybranego odtwarzacza.",
"LABEL_CHAT_SIZE": "Rozmiar nakładki",
"LABEL_CHAT_SIZE_TOOLTIP": "Wybierz domyślny rozmiar nakładki czatu.",
"OPTION_CHAT_SIZE_COMPACT": "Kompaktowy",
"OPTION_CHAT_SIZE_STANDARD": "Standardowy",
"OPTION_CHAT_SIZE_LARGE": "Duży",
"OPTION_CHAT_SIZE_CUSTOM": "Ostatni własny rozmiar",
"LABEL_CHAT_START_MODE": "Uruchom jako",
"LABEL_CHAT_START_MODE_TOOLTIP": "Wybierz, czy czat ma startować jako pływający przycisk, czy otwarta nakładka.",
"OPTION_CHAT_START_BUBBLE": "Pływający przycisk",
"OPTION_CHAT_START_OPEN": "Otwarta nakładka",
"CHAT_TITLE": "Czat pokoju",
"CHAT_LIVE_ONLY": "Tylko na żywo. Bez historii.",
"CHAT_OPEN": "Otwórz czat pokoju",
"CHAT_CLOSE": "Zamknij czat",
"CHAT_DOCK_LEFT": "Przypnij czat po lewej",
"CHAT_DOCK_RIGHT": "Przypnij czat po prawej",
"CHAT_DETACHED": "Odłącz czat",
"CHAT_PLACEHOLDER": "Napisz wiadomość...",
"CHAT_SEND": "Wyślij",
"CHAT_MISSING_KEY": "Czat jest dostępny tylko przez aktualny link zaproszenia.",
"CHAT_TOO_LONG": "Maksymalnie 500 znaków.",
"CHAT_SEND_FAILED": "Nie udało się wysłać wiadomości.",
"CHAT_EMPTY": "Brak wiadomości. Czat działa tylko na żywo.",
"LABEL_CHAT_NOTIFICATIONS": "Powiadomienia czatu",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Pokazuje powiadomienie systemowe o nowej wiadomości, gdy karta wideo nie jest aktywna.",
"LABEL_EVENT_NOTIFICATIONS": "Powiadomienia o odtwarzaniu i pokoju",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Pokazuje powiadomienia, gdy ktoś dołącza, wychodzi, odtwarza lub wstrzymuje.",
"DEBUG_SECTION_VIDEO_DETECTION": "Wykrywanie wideo",
"DEBUG_VIDEO_NOT_FOUND": "NIE ZNALEZIONO ELEMENTU WIDEO",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "Czy wideo jest odtwarzane? KoalaSync wykrywa elementy wideo po załadowaniu multimediów.",
"DEBUG_SECTION_PLAYBACK": "Odtwarzanie",
"DEBUG_STATE_PAUSED": "WSTRZYMANE",
"DEBUG_STATE_PLAYING": "ODTWARZANIE",
"DEBUG_SECTION_PROPERTIES": "Właściwości",
"DEBUG_SECTION_DIMENSIONS": "Wymiary",
"DEBUG_DIMENSIONS_HINT": "0×0 = element wideo jest ukryty lub jeszcze niezaładowany",
"DEBUG_SECTION_MEDIA_ERROR": "Błąd multimediów",
"DEBUG_SECTION_DETECTION": "Wykrywanie",
"DEBUG_SECTION_IDENTIFICATION": "Identyfikacja",
"DEBUG_SECTION_MEDIA_SOURCE": "Źródło multimediów",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Atrybuty danych",
"DEBUG_YES": "TAK",
"DEBUG_NO": "NIE",
"DEBUG_YES_CHECKED": "TAK (sprawdzono)",
"DEBUG_NA": "brak"
}
+167 -75
View File
@@ -4,9 +4,9 @@
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Sala",
"TAB_ROOM_TOOLTIP": "Configurações da sala e conexão",
"TAB_SYNC": "Sincro",
"TAB_SYNC": "Sincronia",
"TAB_SYNC_TOOLTIP": "Controles de sincronia de vídeo e ações",
"TAB_SETTINGS": "Opções",
"TAB_SETTINGS": "Configurações",
"TAB_SETTINGS_TOOLTIP": "Preferências da extensão",
"TAB_STATUS": "Status",
"TAB_STATUS_TOOLTIP": "Diagnósticos avançados e logs",
@@ -15,23 +15,23 @@
"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",
"BTN_SERVER_CUSTOM": "Personalizado",
"BTN_SERVER_CUSTOM_TOOLTIP": "Conectar ao seu próprio servidor privado",
"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",
"LABEL_ROOM_ID_TOOLTIP": "O identificador exclusivo da sua sala",
"PLACEHOLDER_ROOM_ID": "Digite o ID da sala",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "O ID exclusivo da sala na qual você deseja entrar",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "O ID 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)",
"PLACEHOLDER_PASSWORD": "Senha da sala",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Deixe em branco se não houver senha",
"BTN_JOIN_ROOM": "Entrar / Criar sala",
"BTN_JOIN_ROOM_TOOLTIP": "Conectar à sala",
"BTN_JOIN_ROOM_TOOLTIP": "Entrar na 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",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas",
"PUBLIC_ROOMS_REFRESHING": "Atualizando...",
"BTN_REFRESH_COOLDOWN": "AGUARDE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "A lista de salas está em espera. Tente novamente em {seconds}s.",
@@ -41,99 +41,120 @@
"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",
"LABEL_INVITE_LINK_TOOLTIP": "Compartilhe este link com seus amigos para que eles entrem",
"LABEL_PEERS_IN_ROOM": "Participantes",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Outros usuários conectados a esta sala",
"LABEL_HOST_CONTROL": "Controle do anfitrião",
"LABEL_HOST_CONTROL_TOOLTIP": "Restringir quem pode controlar a reprodução nesta sala",
"LABEL_HOST_ONLY_TOGGLE": "Somente eu posso controlar a reprodução",
"NOTICE_HOST_CONTROLS": "O anfitrião controla a reprodução para todos.",
"NOTICE_COHOST_HINT": "Dica: conceda o controle a participantes específicos na lista abaixo.",
"BADGE_HOST": "Anfitrião",
"BADGE_GUEST": "Convidado",
"BADGE_CONTROLLER": "Controlador",
"BTN_GIVE_CONTROL": "Dar controle",
"BTN_REVOKE_CONTROL": "Revogar",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Assistindo por conta própria — ignora os comandos do anfitrião",
"HCM_DIALOG_TITLE": "KoalaSync · O anfitrião controla esta sala",
"HCM_DIALOG_BODY": "Somente o anfitrião pode controlar a reprodução nesta sala. Continuar assistindo juntos ou assistir por conta própria?",
"HCM_DIALOG_STAY": "Manter sincronizado",
"HCM_DIALOG_SOLO": "Assistir por conta própria",
"HCM_BADGE_SOLO": "Assistindo por conta própria",
"HCM_BADGE_RESYNC": "Ressincronizar",
"NO_PEERS_CONNECTED": "Sem participantes conectados",
"BTN_LEAVE_ROOM": "Sair da sala",
"LABEL_SELECT_VIDEO": "Selecionar vídeo",
"LABEL_SELECT_VIDEO_TOOLTIP": "Escolha a aba do navegador contendo o vídeo a ser sincronizado",
"LABEL_SELECT_VIDEO_TOOLTIP": "Escolha a aba do navegador com 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_PLAY": "Reproduzir",
"BTN_PLAY_TOOLTIP": "Iniciar a reprodução para todos",
"BTN_PAUSE": "Pausar",
"BTN_PAUSE_TOOLTIP": "Pausar o vídeo para todos",
"BTN_SYNC": "Sincro",
"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_TO_OTHERS": "Ir para a posição dos outros",
"OPTION_JUMP_TO_ME": "Trazer para a minha posição",
"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",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra o comando mais recente de reproduzir, pausar ou pular",
"NO_RECENT_COMMANDS": "Sem comandos recentes",
"LOBBY_HEADER": "LOBBY DE EPISÓDIO",
"LOBBY_HEADER": "SALA DE ESPERA",
"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",
"BTN_SKIP_PLAY": "Pular e reproduzir agora",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar a espera e reproduzir mesmo assim",
"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.",
"LABEL_USERNAME_TOOLTIP": "Seu nome visível para os outros.",
"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_HIDE_CLUTTER": "Ocultar abas sem vídeo",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra abas não relacionadas para manter a lista limpa",
"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_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e aguarda todos ao mudar de episódio.",
"LABEL_AUTO_COPY_INVITE": "Auto-copiar link de convite",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automaticamente o link 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_NOTIFICATIONS_TOOLTIP": "Mostra notificações quando alguém entra, sai, reproduz ou 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'.",
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um Issue no GitHub",
"LABEL_LANGUAGE_TOOLTIP": "Escolha o seu idioma preferido",
"LABEL_TROUBLESHOOTING": "Solução de problemas",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Ferramentas para corrigir problemas de conexão",
"BTN_REGEN_ID": "Regenerar ID do usuário",
"BTN_REGEN_ID_TOOLTIP": "Gerar um novo ID interno e conectar novamente",
"REGEN_ID_DESC": "Use esta opção se aparecer o erro de 'Identidade duplicada'.",
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um relatório no GitHub",
"TOAST_ID_REGENERATED": "Identidade regenerada — reconectando…",
"LABEL_CONN_STATUS": "Status da conexão",
"LABEL_CONN_STATUS_TOOLTIP": "Status atual da conexão WebSocket",
"CONN_STATUS_DISCONNECTED": "Desconectado",
"BTN_RETRY": "REENTRAR",
"BTN_RETRY": "TENTAR NOVAMENTE",
"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",
"BTN_COPY_LOGS_TOOLTIP": "Copiar logs para a área de transferência",
"LABEL_VIDEO_DEBUG": "Debug de vídeo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalhes técnicos sobre o vídeo selecionado no momento",
"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",
"LABEL_HISTORY_TOOLTIP": "Registro cronológico de todos os comandos 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",
"BTN_CLEAR_TOOLTIP": "Limpar os 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_1_TEXT": "Assista a vídeos com outras pessoas, onde vocês estiverem. Veja uma apresentação rápida.",
"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_3_TEXT": "Navegue aqui para selecionar o vídeo a ser sincronizado. Reproduza, pause e avance — todos verão o mesmo.",
"ONBOARDING_4_TITLE": "3. Personalizar",
"ONBOARDING_4_TEXT": "Escolha um nome de usuário divertido para que seus amigos saibam quem você é.",
"ONBOARDING_4_TEXT": "Escolha um nome de usuário divertido para que seus amigos reconheçam 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_CONN_TIMEOUT": "Tempo limite 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_IDENTITY_NOT_LOADED": "Identidade não carregada ainda. Aguarde um momento.",
"ERR_NO_PEERS_TIME": "Nenhum participante com posição conhecida. Mude para 'Trazer para a minha posição'.",
"ERR_NO_VIDEO_TAB": "Não foi possível conectar à aba do vídeo.",
"SITE_ACCESS_REQUIRED": "Seu navegador está bloqueando o KoalaSync em {host}. Clique em «Permitir acesso» abaixo. Se nenhuma caixa de diálogo aparecer, permita o KoalaSync neste site nas permissões da extensão ou do site.",
"BTN_RETRY_ACCESS": "Permitir acesso",
"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_LOBBY_SKIPPED": "Sala de espera ignorada.",
"TOAST_LOBBY_SKIP_FAILED": "Falha ao ignorar a sala de espera.",
"TOAST_LOGS_COPIED": "Copiado!",
"TOAST_PEER_JOINED": "{name} entrou na sala",
"TOAST_PEER_LEFT": "{name} saiu da sala",
@@ -143,51 +164,54 @@
"STATUS_CONNECTING": "Conectando...",
"STATUS_FAILED": "Falhou",
"STATUS_DISCONNECTED": "Desconectado",
"STATUS_IDLE": "Pronto para conectar",
"STATUS_IDLE_TOOLTIP": "O KoalaSync está pronto. Entre ou crie uma sala para conectar e começar a sincronizar.",
"BTN_STATE_JOINING": "🚀 Entrando...",
"BTN_STATE_RECONNECTING": "🔄 Reconectando...",
"BTN_STATE_PLAYING": "▶ Reproduzindo...",
"BTN_STATE_PAUSING": "⏸ Pausando...",
"BTN_STATE_PAUSING": "⏸ Em pausa...",
"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_PAUSE": "pausou o vídeo",
"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",
"DEBUG_NO_TAB": "Nenhuma aba selecionada.",
"DEBUG_COMM_FAIL": "Erro ao se comunicar com o vídeo.",
"EMPTY_PEERS_TITLE": "Nenhum participante",
"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_HISTORY_TITLE": "Sem atividade",
"EMPTY_HISTORY_HINT": "Reproduza ou pause 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",
"EMPTY_ROOMS_HINT": "Crie uma sala ou atualize a lista",
"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",
"LABEL_CUSTOM_SERVER": "Servidor privado",
"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.",
"NOTIF_LOBBY_CANCEL_MSG": "Sincronização automática cancelada: {reason}. Você deve sincronizar manualmente.",
"LOBBY_CANCEL_TIMEOUT": "Tempo limite esgotado",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tempo limite esgotado (recuperado)",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tempo limite (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_TIMEOUT_PEERS_LOAD": "Tempo limite — nem todos carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo usuário",
"NOTIF_ERROR_TITLE": "Erro do KoalaSync",
"FOOTER_SUPPORT": "☕ Me pague um café",
"NOTIF_ERROR_TITLE": "Erro no KoalaSync",
"FOOTER_SUPPORT": "Apoie o KoalaSync",
"FOOTER_REVIEW": "★ Avaliar",
"FOOTER_SUPPORT_PROMPT": "Gostou do KoalaSync? Deixe uma avaliação!",
"LABEL_AUDIO_PROCESSING": "Processamento de áudio",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efeitos de áudio como compressão à reprodução de vídeo",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efeitos de áudio (como compressão) aos vídeos",
"AUDIO_OPEN_SETTINGS": "Abrir",
"NEW_FEATURE_AUDIO": "Novo: Processamento de áudio — experimente o compressor!",
"NEW_FEATURE_THEME": "Novo: Temas — mude para o visual clássico ou grafite minimalista.",
"AUDIO_BACK": "← Voltar",
"AUDIO_PAGE_TITLE": "Configurações de áudio",
"AUDIO_MASTER_TOGGLE": "Processamento de áudio",
@@ -196,14 +220,82 @@
"AUDIO_PRESET": "Predefinição",
"AUDIO_PRESET_RECOMMENDED": "Recomendado",
"AUDIO_PRESET_DYNAMIC_RANGE": "Faixa dinâmica",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Aprimoramento de voz",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Aprimoramento vocal",
"AUDIO_PRESET_SMOOTH": "Suave",
"AUDIO_PRESET_CUSTOM": "Personalizado",
"AUDIO_PARAM_THRESHOLD": "Limiar",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_PARAM_THRESHOLD": "Limiar (Threshold)",
"AUDIO_PARAM_KNEE": "Joelho (Knee)",
"AUDIO_PARAM_RATIO": "Razão (Ratio)",
"AUDIO_PARAM_ATTACK": "Ataque (Attack)",
"AUDIO_PARAM_RELEASE": "Liberação (Release)",
"AUDIO_EQUALIZER": "Equalizador",
"AUDIO_COMING_SOON": "Em breve"
"AUDIO_COMING_SOON": "Em breve",
"BTN_RESTART_TOUR": "Reiniciar tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar o tutorial de introdução",
"HINT_SELECT_VIDEO": "Selecione seu vídeo aqui!",
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID do episódio",
"OPTION_TITLE_PRIVACY_HIDDEN": "Não enviar",
"LABEL_PRIVACY_SETTINGS": "Configurações de privacidade",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Escolha quais títulos de aba e mídia são enviados para a sala.",
"LABEL_SEND_TAB_TITLE": "Enviar título da aba",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envia para a sala o título da aba selecionada do navegador.",
"LABEL_MEDIA_TITLE_PRIVACY": "Título da mídia",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Escolha como os títulos de mídia são enviados para a sala.",
"LABEL_SETTINGS_GROUP_PROFILE": "Perfil e Aparência",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Altere o seu nome de usuário, tema e preferências de idioma.",
"LABEL_SETTINGS_GROUP_SYNC": "Reprodução e Sincronização",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personalize a reprodução, as notificações e as preferências de áudio.",
"LABEL_CHAT_ENABLED": "Ativar chat da sala",
"LABEL_CHAT_ENABLED_TOOLTIP": "Mostrar o chat criptografado da sala na guia de vídeo selecionada.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Configure a aparência e a posição do chat criptografado da sala.",
"CHAT_SETTINGS_NOTE": "Opcional e criptografado de ponta a ponta. As mensagens são apenas ao vivo e não são armazenadas pelo relay.",
"LABEL_CHAT_POSITION": "Posição",
"LABEL_CHAT_POSITION_TOOLTIP": "Escolha onde o chat fica fixado no player selecionado.",
"LABEL_CHAT_SIZE": "Tamanho da sobreposição",
"LABEL_CHAT_SIZE_TOOLTIP": "Escolha o tamanho padrão da sobreposição do chat.",
"OPTION_CHAT_SIZE_COMPACT": "Compacto",
"OPTION_CHAT_SIZE_STANDARD": "Padrão",
"OPTION_CHAT_SIZE_LARGE": "Grande",
"OPTION_CHAT_SIZE_CUSTOM": "Último tamanho personalizado",
"LABEL_CHAT_START_MODE": "Iniciar como",
"LABEL_CHAT_START_MODE_TOOLTIP": "Escolha se o chat começa como uma bolha flutuante ou uma sobreposição aberta.",
"OPTION_CHAT_START_BUBBLE": "Bolha flutuante",
"OPTION_CHAT_START_OPEN": "Sobreposição aberta",
"CHAT_TITLE": "Chat da sala",
"CHAT_LIVE_ONLY": "Somente ao vivo. Sem histórico.",
"CHAT_OPEN": "Abrir chat da sala",
"CHAT_CLOSE": "Fechar chat",
"CHAT_DOCK_LEFT": "Fixar chat à esquerda",
"CHAT_DOCK_RIGHT": "Fixar chat à direita",
"CHAT_DETACHED": "Desanexar chat",
"CHAT_PLACEHOLDER": "Escreva uma mensagem...",
"CHAT_SEND": "Enviar",
"CHAT_MISSING_KEY": "O chat está disponível apenas por um link de convite atual.",
"CHAT_TOO_LONG": "Máximo de 500 caracteres.",
"CHAT_SEND_FAILED": "Não foi possível enviar a mensagem.",
"CHAT_EMPTY": "Ainda não há mensagens. O chat é somente ao vivo.",
"LABEL_CHAT_NOTIFICATIONS": "Notificações do chat",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Mostra uma notificação do sistema para novas mensagens quando a aba do vídeo não está ativa.",
"LABEL_EVENT_NOTIFICATIONS": "Notificações de reprodução e sala",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Mostra notificações quando alguém entra, sai, reproduz ou pausa.",
"DEBUG_SECTION_VIDEO_DETECTION": "Detecção de vídeo",
"DEBUG_VIDEO_NOT_FOUND": "NENHUM ELEMENTO DE VÍDEO ENCONTRADO",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "Há um vídeo sendo reproduzido? O KoalaSync detecta elementos de vídeo após o carregamento da mídia.",
"DEBUG_SECTION_PLAYBACK": "Reprodução",
"DEBUG_STATE_PAUSED": "PAUSADO",
"DEBUG_STATE_PLAYING": "REPRODUZINDO",
"DEBUG_SECTION_PROPERTIES": "Propriedades",
"DEBUG_SECTION_DIMENSIONS": "Dimensões",
"DEBUG_DIMENSIONS_HINT": "0×0 = elemento de vídeo oculto ou ainda não carregado",
"DEBUG_SECTION_MEDIA_ERROR": "Erro de mídia",
"DEBUG_SECTION_DETECTION": "Detecção",
"DEBUG_SECTION_IDENTIFICATION": "Identificação",
"DEBUG_SECTION_MEDIA_SOURCE": "Fonte de mídia",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Atributos de dados",
"DEBUG_YES": "SIM",
"DEBUG_NO": "NÃO",
"DEBUG_YES_CHECKED": "SIM (verificado)",
"DEBUG_NA": "n/d"
}
+193 -101
View File
@@ -4,136 +4,157 @@
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Sala",
"TAB_ROOM_TOOLTIP": "Definições da sala e ligação",
"TAB_SYNC": "Sincronização",
"TAB_SYNC": "Sincronia",
"TAB_SYNC_TOOLTIP": "Controlo de sincronização de vídeo e ações remotas",
"TAB_SETTINGS": "Definições",
"TAB_SETTINGS_TOOLTIP": "Preferências da extensão",
"TAB_STATUS": "Estado",
"TAB_STATUS_TOOLTIP": "Diagnóstico avanzado e registos",
"TAB_STATUS_TOOLTIP": "Diagnóstico avançado e registos",
"BTN_CREATE_ROOM": "+ Criar Nova Sala",
"MANUAL_CONNECT_HEADER": "Ligação Manual / Avançado",
"LABEL_SERVER": "Servidor",
"BTN_SERVER_OFFICIAL": "Oficial",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Usar o servidor oficial fiável",
"BTN_SERVER_CUSTOM": "Personalizado",
"BTN_SERVER_CUSTOM_TOOLTIP": "Ligar ao seu próprio servidor auto-hospedado",
"BTN_SERVER_CUSTOM_TOOLTIP": "Ligar ao seu próprio servidor privado",
"PLACEHOLDER_SERVER_URL": "wss://o-seu-servidor:3000",
"LABEL_ROOM_ID": "ID da Sala",
"LABEL_ROOM_ID_TOOLTIP": "O identificador exclusivo para a sua sala de sincronização",
"LABEL_ROOM_ID_TOOLTIP": "O identificador exclusivo para a sua sala",
"PLACEHOLDER_ROOM_ID": "Introduzir ID da Sala",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "O ID exclusivo da sala à qual deseja aderir",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "O ID da sala à qual deseja aceder",
"LABEL_PASSWORD": "Palavra-passe (Opcional)",
"LABEL_PASSWORD_TOOLTIP": "Palavra-passe opcional para restringir o acesso à sala",
"PLACEHOLDER_PASSWORD": "Palavra-passe da sala (opcional)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Palavra-passe para la sala (deixar em branco se não houver)",
"LABEL_PASSWORD_TOOLTIP": "Palavra-passe para restringir o acesso à sala",
"PLACEHOLDER_PASSWORD": "Palavra-passe da sala",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Deixe em branco se não houver palavra-passe",
"BTN_JOIN_ROOM": "Entrar / Criar Sala",
"BTN_JOIN_ROOM_TOOLTIP": "Ligar à sala",
"BTN_JOIN_ROOM_TOOLTIP": "Entrar na sala",
"LABEL_PUBLIC_ROOMS": "Salas Públicas",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas disponíveis neste servidor",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Lista de salas públicas neste servidor",
"BTN_REFRESH": "ATUALIZAR",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas públicas",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas",
"PUBLIC_ROOMS_REFRESHING": "A atualizar...",
"BTN_REFRESH_COOLDOWN": "AGUARDE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "A atualização da lista de salas está em espera. Tente novamente em {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "A atualizar salas públicas. Próxima atualização disponível em {seconds}s.",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "A atualização está em espera. Tente novamente em {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "A atualizar salas. Próxima atualização em {seconds}s.",
"LABEL_ACTIVE_ROOM": "Sala Ativa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "A sala à qual está atualmente ligado",
"ACTIVE_ROOM_NONE": "NENHUMA",
"ACTIVE_SERVER_OFFICIAL": "Servidor Oficial",
"LABEL_INVITE_LINK": "Link de Invito",
"LABEL_INVITE_LINK_TOOLTIP": "Partilhar este link com os amigos para que possam entrar",
"LABEL_INVITE_LINK": "Link de Convite",
"LABEL_INVITE_LINK_TOOLTIP": "Partilhe este link com os amigos para que possam entrar",
"LABEL_PEERS_IN_ROOM": "Participantes na Sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Outros utilizadores attualmente ligados a questa sala",
"NO_PEERS_CONNECTED": "Nenhum partecipante ligado",
"BTN_LEAVE_ROOM": "Sair della Sala",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Utilizadores atualmente ligados a esta sala",
"LABEL_HOST_CONTROL": "Controlo do anfitrião",
"LABEL_HOST_CONTROL_TOOLTIP": "Restringir quem pode controlar a reprodução nesta sala",
"LABEL_HOST_ONLY_TOGGLE": "Só eu posso controlar a reprodução",
"NOTICE_HOST_CONTROLS": "O anfitrião controla a reprodução para todos.",
"NOTICE_COHOST_HINT": "Dica: conceda o controlo a participantes específicos na lista abaixo.",
"BADGE_HOST": "Anfitrião",
"BADGE_GUEST": "Convidado",
"BADGE_CONTROLLER": "Controlador",
"BTN_GIVE_CONTROL": "Dar controlo",
"BTN_REVOKE_CONTROL": "Revogar",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "A ver por conta própria — ignora os comandos do anfitrião",
"HCM_DIALOG_TITLE": "KoalaSync · O anfitrião controla esta sala",
"HCM_DIALOG_BODY": "Só o anfitrião pode controlar a reprodução nesta sala. Continuar a ver em conjunto ou ver por conta própria?",
"HCM_DIALOG_STAY": "Manter sincronizado",
"HCM_DIALOG_SOLO": "Ver por minha conta",
"HCM_BADGE_SOLO": "A ver por tua conta",
"HCM_BADGE_RESYNC": "Ressincronizar",
"NO_PEERS_CONNECTED": "Nenhum participante ligado",
"BTN_LEAVE_ROOM": "Sair da Sala",
"LABEL_SELECT_VIDEO": "Selecionar Vídeo",
"LABEL_SELECT_VIDEO_TOOLTIP": "Escolher o separador do navegador que contém o vídeo a sincronizar",
"LABEL_SELECT_VIDEO_TOOLTIP": "Escolha o separador do navegador com o vídeo a sincronizar",
"OPTION_SELECT_TAB": "-- Selecionar um Separador --",
"LABEL_REMOTE_CONTROL": "Controlo Remoto",
"BTN_COPY_INVITE": "📋 Link de Invito",
"BTN_COPY_INVITE_TOOLTIP": "Copiar Link de Invito",
"BTN_PLAY": "Reproduzir",
"BTN_PLAY_TOOLTIP": "Enviar um comando de Reproduzir para todos",
"BTN_PAUSE": "Pausa",
"BTN_PAUSE_TOOLTIP": "Enviar um comando de Pausa para todos",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC_TOOLTIP": "Forçar todos os utilizadores a sincronizarem-se",
"OPTION_JUMP_TO_OTHERS": "Ir para os Outros",
"OPTION_JUMP_TO_ME": "Ir para Mim",
"BTN_COPY_INVITE": "📋 Link de Convite",
"BTN_COPY_INVITE_TOOLTIP": "Copiar Link de Convite",
"BTN_PLAY": "Reproduzir",
"BTN_PLAY_TOOLTIP": "Iniciar a reprodução para todos",
"BTN_PAUSE": "Pausa",
"BTN_PAUSE_TOOLTIP": "Pausar o vídeo para todos",
"BTN_SYNC": "Sincro",
"BTN_SYNC_TOOLTIP": "Forçar sincronização de todos os utilizadores",
"OPTION_JUMP_TO_OTHERS": "Ir para a posição dos outros",
"OPTION_JUMP_TO_ME": "Trazer para a minha posição",
"OPTION_JUMP_MODE_TOOLTIP": "Escolher o destino de sincronização",
"LABEL_LAST_ACTIVITY": "Estado da Última Atividade",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra o comando mais recente de reproduzir, pausa ou busca",
"LABEL_LAST_ACTIVITY": "Última Atividade",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Mostra o comando mais recente",
"NO_RECENT_COMMANDS": "Nenhum comando recente",
"LOBBY_HEADER": "LOBBY DE EPISÓDIO",
"LOBBY_WAITING_FOR": "🎬 A aguardar: \"{title}\"",
"LOBBY_WAITING_PEERS": "A aguardar participantes...",
"BTN_SKIP_PLAY": "Ignorar e reproduzir mesmo assim",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar o lobby e reproduzir mesmo assim",
"LOBBY_CONNECT_FIRST": "Ligar-se primeiro a uma sala",
"LOBBY_HEADER": "SALA DE ESPERA",
"LOBBY_WAITING_FOR": "🎬 À espera de: \"{title}\"",
"LOBBY_WAITING_PEERS": "À espera dos participantes...",
"BTN_SKIP_PLAY": "Ignorar e reproduzir agora",
"BTN_SKIP_PLAY_TOOLTIP": "Cancelar a espera e reproduzir mesmo assim",
"LOBBY_CONNECT_FIRST": "Ligue-se primeiro a uma sala",
"LOBBY_CONNECT_FIRST_DESC": "Precisa de entrar numa 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 nela",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Criar uma sala aleatória e entrar nela",
"LABEL_USERNAME": "O seu Nome de Utilizador",
"LABEL_USERNAME_TOOLTIP": "O nome de utilizador ajuda os outros a identificá-lo.",
"LABEL_USERNAME_TOOLTIP": "O seu nome visível para os outros.",
"PLACEHOLDER_USERNAME": "Coala Anónimo",
"LABEL_HIDE_CLUTTER": "Ocultar Separadores Desnecessários",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra separadores sem vídeo e domínios não relacionados para manter a lista limpa",
"LABEL_AUTO_SYNC_NEXT": "Sincronização Automática do Próximo Episódio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e aguarda por todos os participantes quando um episódio muda, iniciando depois a reprodução em sincronia.",
"LABEL_AUTO_COPY_INVITE": "Copiar Automaticamente o Link de Convite",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automaticamente o link de convite para a área de transferência ao criar uma nova sala.",
"LABEL_HIDE_CLUTTER": "Ocultar separadores sem vídeo",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra separadores não relacionados para manter a lista limpa",
"LABEL_AUTO_SYNC_NEXT": "Sincro Auto do Próximo Episódio",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Pausa automaticamente e aguarda por todos ao mudar de episódio.",
"LABEL_AUTO_COPY_INVITE": "Copiar Convite Automaticamente",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Copia automaticamente o link 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 da App",
"LABEL_LANGUAGE_TOOLTIP": "Escolha o seu idioma de preferência para a extensão",
"LABEL_NOTIFICATIONS_TOOLTIP": "Mostra notificações quando alguém entra, sai, reproduz ou pausa.",
"LABEL_LANGUAGE": "Idioma da Aplicação",
"LABEL_LANGUAGE_TOOLTIP": "Escolha o seu idioma de preferência",
"LABEL_TROUBLESHOOTING": "Resolução de Problemas",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Ferramentas para corrigir problemas de ligação",
"BTN_REGEN_ID": "Regerar ID do Peer",
"BTN_REGEN_ID": "Regerar ID do Utilizador",
"BTN_REGEN_ID_TOOLTIP": "Regerar o seu ID interno e voltar a ligar",
"REGEN_ID_DESC": "Use isto se vir erros de \"Identidade Duplicata\".",
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um Issue no GitHub",
"REGEN_ID_DESC": "Use isto se encontrar erros de 'Identidade Duplicada'.",
"REGEN_ID_OTHER_ISSUE": "Outro problema? Abra um relatório no GitHub",
"TOAST_ID_REGENERATED": "Identidade regenerada — a voltar a ligar…",
"LABEL_CONN_STATUS": "Estado da Ligação",
"LABEL_CONN_STATUS_TOOLTIP": "Estado atual da ligação WebSocket",
"LABEL_CONN_STATUS_TOOLTIP": "Estado atual da ligação",
"CONN_STATUS_DISCONNECTED": "Desligado",
"BTN_RETRY": "TENTAR NOVAMENTE",
"BTN_RETRY_TOOLTIP": "Tentar voltar a ligar ao servidor",
"BTN_COPY_LOGS": "Copiar Registos",
"BTN_COPY_LOGS_TOOLTIP": "Copiar registos para a área de transferência para partilha",
"LABEL_VIDEO_DEBUG": "Info de Debug de Vídeo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalhes técnicos sobre o elemento de vídeo atualmente seleccionado",
"BTN_COPY_LOGS_TOOLTIP": "Copiar registos para a área de transferência",
"LABEL_VIDEO_DEBUG": "Debug de Vídeo",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Detalhes técnicos sobre o vídeo atualmente selecionado",
"VIDEO_DEBUG_EMPTY": "Nenhum separador selecionado ou vídeo detetado.",
"LABEL_HISTORY": "Histórico Completo de Ações",
"LABEL_HISTORY_TOOLTIP": "Registo cronológico de todos os comandos de sincronização na sala",
"LABEL_HISTORY": "Histórico Completo",
"LABEL_HISTORY_TOOLTIP": "Registo cronológico de todos os comandos na sala",
"HISTORY_EMPTY": "Nenhuma atividade ainda",
"LABEL_LOGS": "Registos (Últimos 50)",
"LABEL_LOGS_TOOLTIP": "Registos de ligação técnica para depuração",
"LABEL_LOGS_TOOLTIP": "Registos técnicos para depuração",
"BTN_CLEAR": "LIMPAR",
"BTN_CLEAR_TOOLTIP": "Limpar a saída do registo",
"BTN_CLEAR_TOOLTIP": "Limpar os registos",
"LABEL_GITHUB": "Repositório GitHub",
"BTN_ONBOARDING_SKIP": "Saltar",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Saltar o tutorial",
"BTN_ONBOARDING_NEXT": "Seguinte",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Ir para o passo seguinte",
"ONBOARDING_1_TITLE": "Bem-vindo ao KoalaSync!",
"ONBOARDING_1_TEXT": "Veja vídeos em conjunto numa sincronizzazione perfetta — não importa onde esteja. Vamos fare uma breve visita guiada!",
"ONBOARDING_1_TEXT": "Vê vídeos com outras pessoas, onde quer que estejam. Aqui tens uma breve apresentação.",
"ONBOARDING_2_TITLE": "1. Criar uma Sala",
"ONBOARDING_2_TEXT": "Comece aqui. Crie uma sala e partilhe o link de convite com os seus amigos.",
"ONBOARDING_3_TITLE": "2. Selecionar Vídeo",
"ONBOARDING_3_TEXT": "Navegue até aqui para selecionar o vídeo que deseja sincronizar. Reproduza, pause e avance — todos ficam em sincronia.",
"ONBOARDING_3_TEXT": "Navegue até aqui para selecionar o vídeo a sincronizar. Reproduza, pause e avance — todos vêm o mesmo.",
"ONBOARDING_4_TITLE": "3. Personalizar",
"ONBOARDING_4_TEXT": "Escolha um nome de utilizador divertido para que os seus amigos saibam quem você é.",
"ONBOARDING_4_TEXT": "Escolha um nome de utilizador divertido para que os seus amigos o reconheçam.",
"ONBOARDING_5_TITLE": "Está tudo pronto!",
"ONBOARDING_5_TEXT": "Altura de ir buscar pipocas. Divirtam-se a ver juntos!",
"ONBOARDING_5_TEXT": "Hora de ir buscar as pipocas. Divirtam-se!",
"ERR_CONN_TIMEOUT": "Ligação expirada. Tente novamente.",
"ERR_INVALID_SERVER_URL": "Formato de URL do Servidor inválido.",
"ERR_IDENTITY_NOT_LOADED": "Identidade ainda não carregada. Aguarde um momento e tente novamente.",
"ERR_NO_PEERS_TIME": "Nenhum outro peer com hora conhecida. Mude para 'Ir para Mim'.",
"ERR_NO_VIDEO_TAB": "Não foi possível ligar ao separador do vídeo.",
"ERR_INVALID_SERVER_URL": "Formato de URL do servidor inválido.",
"ERR_IDENTITY_NOT_LOADED": "Identidade ainda não carregada. Aguarde um momento.",
"ERR_NO_PEERS_TIME": "Nenhum participante com hora conhecida. Mude para 'Trazer para a minha posição'.",
"ERR_NO_VIDEO_TAB": "Não foi possível aceder ao separador do vídeo.",
"SITE_ACCESS_REQUIRED": "O seu navegador está a bloquear o KoalaSync em {host}. Clique em «Permitir acesso» abaixo. Se não aparecer uma caixa de diálogo, permita o KoalaSync neste site nas permissões da extensão ou do site.",
"BTN_RETRY_ACCESS": "Permitir acesso",
"ERR_SELECT_VIDEO": "Selecione primeiro um vídeo!",
"TOAST_INVITE_COPIED": "Link de convite copiado!",
"TOAST_COPY_FAILED": "Falha ao copiar para a área de transferência",
"TOAST_LOBBY_SKIPPED": "Lobby do episódio ignorado.",
"TOAST_LOBBY_SKIP_FAILED": "Falha ao ignorar o lobby.",
"TOAST_LOBBY_SKIPPED": "Sala de espera ignorada.",
"TOAST_LOBBY_SKIP_FAILED": "Falha ao ignorar a sala de espera.",
"TOAST_LOGS_COPIED": "Copiado!",
"TOAST_PEER_JOINED": "{name} entrou na sala",
"TOAST_PEER_LEFT": "{name} saiu da sala",
@@ -143,67 +164,138 @@
"STATUS_CONNECTING": "A ligar...",
"STATUS_FAILED": "Falhou",
"STATUS_DISCONNECTED": "Desligado",
"STATUS_IDLE": "Pronto para ligar",
"STATUS_IDLE_TOOLTIP": "O KoalaSync está pronto. Entre ou crie uma sala para ligar e iniciar a sincronização.",
"BTN_STATE_JOINING": "🚀 A entrar...",
"BTN_STATE_RECONNECTING": "🔄 A voltar a ligar...",
"BTN_STATE_PLAYING": "▶ A reproduzir...",
"BTN_STATE_PAUSING": "⏸ A pausar...",
"BTN_STATE_SYNCING_GROUP": "A sincronizar com o grupo ({time})....",
"BTN_STATE_PAUSING": "⏸ Em pausa...",
"BTN_STATE_SYNCING_GROUP": "A sincronizar em grupo ({time})....",
"BTN_STATE_SYNCING": "A sincronizar...",
"BTN_STATE_SYNCED": "✅ Sincronizado!",
"NOTIF_PLAY": "iniciou a reprodução",
"NOTIF_PAUSE": "pausou a reprodução",
"NOTIF_SEEK": "avançou/recuou no vídeo",
"NOTIF_FORCE_PREPARE": "iniciou a sincronização forçada",
"NOTIF_PAUSE": "pausou o vídeo",
"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": "Nenhum separador de destino selecionado.",
"DEBUG_COMM_FAIL": "Não foi possível comunicar com o vídeo do separador.",
"EMPTY_PEERS_TITLE": "Nenhum participante ainda",
"DEBUG_NO_TAB": "Nenhum separador selecionado.",
"DEBUG_COMM_FAIL": "Erro ao comunicar com o vídeo.",
"EMPTY_PEERS_TITLE": "Nenhum participante",
"EMPTY_PEERS_HINT": "Partilhe o seu link de convite para começar",
"EMPTY_HISTORY_TITLE": "Nenhuma atividade ainda",
"EMPTY_HISTORY_HINT": "Reproduza, pause ou avance para ver o histórico",
"EMPTY_LOGS_TITLE": "Nenhum registo",
"EMPTY_LOGS_HINT": "Os eventi de ligação vão aparecer aqui",
"EMPTY_HISTORY_TITLE": "Nenhuma atividade",
"EMPTY_HISTORY_HINT": "Reproduza ou pause para ver o histórico",
"EMPTY_LOGS_TITLE": "Sem registos",
"EMPTY_LOGS_HINT": "Os eventos de ligação vão aparecer aqui",
"EMPTY_ROOMS_TITLE": "Nenhuma sala ativa",
"EMPTY_ROOMS_HINT": "Crie uma sala ou atualize para encontrar salas públicas",
"EMPTY_ROOMS_HINT": "Crie uma sala ou atualize a lista",
"LABEL_YOU": "Tu",
"ONBOARDING_DONE": "Concluído!",
"LABEL_LOBBY_PEER_READY": "Pronto",
"LABEL_LOBBY_PEER_LOADING": "A carregar...",
"LABEL_PASSWORD_PROTECTED": "Protegido por Palavra-passe",
"LABEL_PEERS_COUNT": "{count} participantes",
"LABEL_CUSTOM_SERVER": "Servidor Personalizado",
"LABEL_CUSTOM_SERVER": "Servidor Privado",
"BTN_STATE_CREATING": "🚀 A Criar Sala...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Sincronização do Episódio Falhou",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Falha na Sincronização do Episódio",
"NOTIF_LOBBY_CANCEL_MSG": "Sincronização automática cancelada: {reason}. Pode ter de sincronizar manualmente.",
"LOBBY_CANCEL_TIMEOUT": "Timeout",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Timeout (recuperado)",
"LOBBY_CANCEL_TIMEOUT": "Tempo limite expirado",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Tempo limite (recuperado)",
"LOBBY_CANCEL_PEERS_LEFT": "Todos os outros participantes saíram",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — nem todos os participantes carregaram o episódio",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo limite — alguns participantes não carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo utilizador",
"NOTIF_ERROR_TITLE": "Erro do KoalaSync",
"FOOTER_SUPPORT": "☕ Oferece um café",
"NOTIF_ERROR_TITLE": "Erro no KoalaSync",
"FOOTER_SUPPORT": "Apoiar o KoalaSync",
"FOOTER_REVIEW": "★ Avaliar",
"FOOTER_SUPPORT_PROMPT": "Gostas do KoalaSync? Deixa uma avaliação!",
"LABEL_AUDIO_PROCESSING": "Processamento de áudio",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efeitos de áudio como compressão à reprodução de vídeo",
"LABEL_AUDIO_PROCESSING": "Processamento de Áudio",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Aplica efeitos de áudio (como compressão) aos vídeos",
"AUDIO_OPEN_SETTINGS": "Abrir",
"NEW_FEATURE_AUDIO": "Novo: Processamento de áudio — experimente o compressor!",
"NEW_FEATURE_THEME": "Novo: Temas — mude para o visual clássico ou grafite minimalista.",
"AUDIO_BACK": "← Voltar",
"AUDIO_PAGE_TITLE": "Configurações de áudio",
"AUDIO_MASTER_TOGGLE": "Processamento de áudio",
"AUDIO_PAGE_TITLE": "Configurações de Áudio",
"AUDIO_MASTER_TOGGLE": "Processamento de Áudio",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Ativado",
"AUDIO_PRESET": "Predefinição",
"AUDIO_PRESET_RECOMMENDED": "Recomendado",
"AUDIO_PRESET_DYNAMIC_RANGE": "Gama dinâmica",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Melhoria vocal",
"AUDIO_PRESET_DYNAMIC_RANGE": "Gama Dinâmica",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Melhoria Vocal",
"AUDIO_PRESET_SMOOTH": "Suave",
"AUDIO_PRESET_CUSTOM": "Personalizado",
"AUDIO_PARAM_THRESHOLD": "Limiar",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_PARAM_THRESHOLD": "Limiar (Threshold)",
"AUDIO_PARAM_KNEE": "Joelho (Knee)",
"AUDIO_PARAM_RATIO": "Rácio (Ratio)",
"AUDIO_PARAM_ATTACK": "Ataque (Attack)",
"AUDIO_PARAM_RELEASE": "Libertação (Release)",
"AUDIO_EQUALIZER": "Equalizador",
"AUDIO_COMING_SOON": "Em breve"
"AUDIO_COMING_SOON": "Em breve",
"BTN_RESTART_TOUR": "Reiniciar Tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar o tutorial introdutório",
"HINT_SELECT_VIDEO": "Selecione o seu vídeo aqui!",
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID do episódio",
"OPTION_TITLE_PRIVACY_HIDDEN": "Não enviar",
"LABEL_PRIVACY_SETTINGS": "Definições de privacidade",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Escolhe que títulos do separador e do conteúdo multimédia são enviados para a sala.",
"LABEL_SEND_TAB_TITLE": "Enviar título do separador",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envia para a sala o título do separador do navegador selecionado.",
"LABEL_MEDIA_TITLE_PRIVACY": "Título multimédia",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Escolhe como os títulos multimédia são enviados para a sala.",
"LABEL_SETTINGS_GROUP_PROFILE": "Perfil e Aparência",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Altere o seu nome de utilizador, tema e preferências de idioma.",
"LABEL_SETTINGS_GROUP_SYNC": "Reprodução e Sincronização",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Personalize a reprodução, as notificações e as preferências de áudio.",
"LABEL_CHAT_ENABLED": "Ativar chat da sala",
"LABEL_CHAT_ENABLED_TOOLTIP": "Mostrar o chat encriptado da sala no separador de vídeo selecionado.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Configure o aspeto e a posição do chat encriptado da sala.",
"CHAT_SETTINGS_NOTE": "Opcional e encriptado de ponta a ponta. As mensagens são apenas em direto e não são guardadas pelo relay.",
"LABEL_CHAT_POSITION": "Posição",
"LABEL_CHAT_POSITION_TOOLTIP": "Escolha onde o chat fica fixado no leitor selecionado.",
"LABEL_CHAT_SIZE": "Tamanho da sobreposição",
"LABEL_CHAT_SIZE_TOOLTIP": "Escolha o tamanho predefinido da sobreposição do chat.",
"OPTION_CHAT_SIZE_COMPACT": "Compacto",
"OPTION_CHAT_SIZE_STANDARD": "Padrão",
"OPTION_CHAT_SIZE_LARGE": "Grande",
"OPTION_CHAT_SIZE_CUSTOM": "Último tamanho personalizado",
"LABEL_CHAT_START_MODE": "Iniciar como",
"LABEL_CHAT_START_MODE_TOOLTIP": "Escolha se o chat começa como uma bolha flutuante ou uma sobreposição aberta.",
"OPTION_CHAT_START_BUBBLE": "Bolha flutuante",
"OPTION_CHAT_START_OPEN": "Sobreposição aberta",
"CHAT_TITLE": "Chat da sala",
"CHAT_LIVE_ONLY": "Apenas em direto. Sem histórico.",
"CHAT_OPEN": "Abrir chat da sala",
"CHAT_CLOSE": "Fechar chat",
"CHAT_DOCK_LEFT": "Fixar chat à esquerda",
"CHAT_DOCK_RIGHT": "Fixar chat à direita",
"CHAT_DETACHED": "Desanexar chat",
"CHAT_PLACEHOLDER": "Escreva uma mensagem...",
"CHAT_SEND": "Enviar",
"CHAT_MISSING_KEY": "O chat está disponível apenas através de um link de convite atual.",
"CHAT_TOO_LONG": "Máximo de 500 caracteres.",
"CHAT_SEND_FAILED": "Não foi possível enviar a mensagem.",
"CHAT_EMPTY": "Ainda não há mensagens. O chat funciona apenas em direto.",
"LABEL_CHAT_NOTIFICATIONS": "Notificações do chat",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Mostra uma notificação do sistema para novas mensagens quando o separador do vídeo não está ativo.",
"LABEL_EVENT_NOTIFICATIONS": "Notificações de reprodução e sala",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Mostra notificações quando alguém entra, sai, reproduz ou pausa.",
"DEBUG_SECTION_VIDEO_DETECTION": "Deteção de vídeo",
"DEBUG_VIDEO_NOT_FOUND": "NENHUM ELEMENTO DE VÍDEO ENCONTRADO",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "Está algum vídeo em reprodução? O KoalaSync deteta elementos de vídeo após o carregamento do conteúdo.",
"DEBUG_SECTION_PLAYBACK": "Reprodução",
"DEBUG_STATE_PAUSED": "EM PAUSA",
"DEBUG_STATE_PLAYING": "A REPRODUZIR",
"DEBUG_SECTION_PROPERTIES": "Propriedades",
"DEBUG_SECTION_DIMENSIONS": "Dimensões",
"DEBUG_DIMENSIONS_HINT": "0×0 = elemento de vídeo oculto ou ainda não carregado",
"DEBUG_SECTION_MEDIA_ERROR": "Erro multimédia",
"DEBUG_SECTION_DETECTION": "Deteção",
"DEBUG_SECTION_IDENTIFICATION": "Identificação",
"DEBUG_SECTION_MEDIA_SOURCE": "Fonte multimédia",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Atributos de dados",
"DEBUG_YES": "SIM",
"DEBUG_NO": "NÃO",
"DEBUG_YES_CHECKED": "SIM (verificado)",
"DEBUG_NA": "n/d"
}
+98 -6
View File
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Поделитесь этой ссылкой с друзьями, чтобы они вошли",
"LABEL_PEERS_IN_ROOM": "Участники в комнате",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Другие пользователи, подключенные к этой комнате",
"LABEL_HOST_CONTROL": "Управление ведущим",
"LABEL_HOST_CONTROL_TOOLTIP": "Ограничить, кто может управлять воспроизведением в этой комнате",
"LABEL_HOST_ONLY_TOGGLE": "Только я могу управлять воспроизведением",
"NOTICE_HOST_CONTROLS": "Ведущий управляет воспроизведением для всех.",
"NOTICE_COHOST_HINT": "Совет: предоставьте управление отдельным участникам в списке ниже.",
"BADGE_HOST": "Ведущий",
"BADGE_GUEST": "Гость",
"BADGE_CONTROLLER": "Управляющий",
"BTN_GIVE_CONTROL": "Дать управление",
"BTN_REVOKE_CONTROL": "Отозвать",
"BADGE_DESYNCED": "Соло",
"TOOLTIP_PEER_DESYNCED": "Смотрит отдельно — игнорирует команды ведущего",
"HCM_DIALOG_TITLE": "KoalaSync · Комнатой управляет ведущий",
"HCM_DIALOG_BODY": "Управлять воспроизведением в этой комнате может только ведущий. Продолжить смотреть вместе или смотреть самостоятельно?",
"HCM_DIALOG_STAY": "Оставаться синхронно",
"HCM_DIALOG_SOLO": "Смотреть самостоятельно",
"HCM_BADGE_SOLO": "Вы смотрите самостоятельно",
"HCM_BADGE_RESYNC": "Синхронизировать",
"NO_PEERS_CONNECTED": "Нет подключенных участников",
"BTN_LEAVE_ROOM": "Выйти из комнаты",
"LABEL_SELECT_VIDEO": "Выбрать видео",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "Дистанционное управление",
"BTN_COPY_INVITE": "📋 Ссылка",
"BTN_COPY_INVITE_TOOLTIP": "Копировать ссылку-приглашение",
"BTN_PLAY": "Запуск",
"BTN_PLAY": "Запуск",
"BTN_PLAY_TOOLTIP": "Отправить команду воспроизведения всем",
"BTN_PAUSE": "Пауза",
"BTN_PAUSE": "Пауза",
"BTN_PAUSE_TOOLTIP": "Отправить команду паузы всем",
"BTN_SYNC": "⚡ СИНХРО",
"BTN_SYNC": "Синхро",
"BTN_SYNC_TOOLTIP": "Принудительно синхронизировать всех участников",
"OPTION_JUMP_TO_OTHERS": "Перейти к другим",
"OPTION_JUMP_TO_ME": "Синхронизировать по мне",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "Сбросить ваш внутренний идентификатор и переподключиться",
"REGEN_ID_DESC": "Используйте при появлении ошибок дублирования идентификации.",
"REGEN_ID_OTHER_ISSUE": "Другая проблема? Откройте Issue на GitHub",
"TOAST_ID_REGENERATED": "Идентичность перегенерирована — переподключение…",
"LABEL_CONN_STATUS": "Статус подключения",
"LABEL_CONN_STATUS_TOOLTIP": "Текущий статус WebSocket-соединения с сервером",
"CONN_STATUS_DISCONNECTED": "Отключено",
@@ -115,7 +134,7 @@
"BTN_ONBOARDING_NEXT": "Далее",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Перейти к следующему шагу",
"ONBOARDING_1_TITLE": "Добро пожаловать в KoalaSync!",
"ONBOARDING_1_TEXT": "Смотрите видео в идеальной синхронизации вместе где бы вы ни находились. Давайте проведем небольшую экскурсию!",
"ONBOARDING_1_TEXT": "Смотрите видео вместе, где бы вы ни находились. Вот краткий обзор.",
"ONBOARDING_2_TITLE": "1. Создайте комнату",
"ONBOARDING_2_TEXT": "Начните здесь. Создайте комнату и поделитесь ссылкой-приглашением со своими друзьями.",
"ONBOARDING_3_TITLE": "2. Выберите видео",
@@ -129,6 +148,8 @@
"ERR_IDENTITY_NOT_LOADED": "Идентификатор еще не загружен. Подождите немного и повторите попытку.",
"ERR_NO_PEERS_TIME": "Нет других участников с известной позицией плеера. Выберите 'Синхронизировать по мне'.",
"ERR_NO_VIDEO_TAB": "Не удалось подключиться к вкладке с видео.",
"SITE_ACCESS_REQUIRED": "Браузер блокирует KoalaSync на {host}. Нажмите «Разрешить доступ» ниже. Если диалог не появится, разрешите KoalaSync для этого сайта в настройках расширения или разрешениях сайта.",
"BTN_RETRY_ACCESS": "Разрешить доступ",
"ERR_SELECT_VIDEO": "Пожалуйста, сначала выберите вкладку с видео!",
"TOAST_INVITE_COPIED": "Ссылка скопирована в буфер обмена!",
"TOAST_COPY_FAILED": "Не удалось скопировать ссылку в буфер обмена",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "Подключение...",
"STATUS_FAILED": "Ошибка",
"STATUS_DISCONNECTED": "Отключено",
"STATUS_IDLE": "Готов к подключению",
"STATUS_IDLE_TOOLTIP": "KoalaSync готов. Присоединитесь к комнате или создайте её, чтобы подключиться и начать синхронизацию.",
"BTN_STATE_JOINING": "🚀 Вход...",
"BTN_STATE_RECONNECTING": "🔄 Переподключение...",
"BTN_STATE_PLAYING": "▶ Запуск...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Время ожидания истекло — не все участники загрузили серию",
"LOBBY_CANCEL_USER": "Отменено пользователем",
"NOTIF_ERROR_TITLE": "Ошибка KoalaSync",
"FOOTER_SUPPORT": "☕ Угости кофе",
"FOOTER_SUPPORT": "Поддержать KoalaSync",
"FOOTER_REVIEW": "★ Оценить",
"FOOTER_SUPPORT_PROMPT": "Нравится KoalaSync? Оставьте отзыв!",
"LABEL_AUDIO_PROCESSING": "Обработка звука",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Применяет аудиоэффекты, такие как сжатие, к воспроизведению видео",
"AUDIO_OPEN_SETTINGS": "Открыть",
"NEW_FEATURE_AUDIO": "Новое: Обработка звука — попробуйте компрессор!",
"NEW_FEATURE_THEME": "Новое: Темы — переключитесь на классический вид или минимальный графит.",
"AUDIO_BACK": "← Назад",
"AUDIO_PAGE_TITLE": "Настройки звука",
"AUDIO_MASTER_TOGGLE": "Обработка звука",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Эквалайзер",
"AUDIO_COMING_SOON": "Скоро"
"AUDIO_COMING_SOON": "Скоро",
"BTN_RESTART_TOUR": "Перезапустить обучение",
"BTN_RESTART_TOUR_TOOLTIP": "Запустить приветственное руководство заново",
"HINT_SELECT_VIDEO": "Выберите ваше видео здесь!",
"OPTION_TITLE_PRIVACY_FULL": "Отправлять полное название",
"OPTION_TITLE_PRIVACY_EPISODE": "Отправлять ID эпизода",
"OPTION_TITLE_PRIVACY_HIDDEN": "Не отправлять",
"LABEL_PRIVACY_SETTINGS": "Настройки приватности",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Выберите, какие названия вкладки и медиа отправляются в комнату.",
"LABEL_SEND_TAB_TITLE": "Отправлять название вкладки",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Отправляет в комнату название выбранной вкладки браузера.",
"LABEL_MEDIA_TITLE_PRIVACY": "Название медиа",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Выберите, как названия медиа отправляются в комнату.",
"LABEL_SETTINGS_GROUP_PROFILE": "Профиль и Оформление",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Измените имя пользователя, тему и настройки языка.",
"LABEL_SETTINGS_GROUP_SYNC": "Воспроизведение и Синхронизация",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Настройте воспроизведение, уведомления и параметры звука.",
"LABEL_CHAT_ENABLED": "Включить чат комнаты",
"LABEL_CHAT_ENABLED_TOOLTIP": "Показывать зашифрованный чат комнаты на выбранной вкладке с видео.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Настройте внешний вид и расположение зашифрованного чата комнаты.",
"CHAT_SETTINGS_NOTE": "Необязательный чат со сквозным шифрованием. Сообщения доступны только в реальном времени и не сохраняются ретранслятором.",
"LABEL_CHAT_POSITION": "Положение",
"LABEL_CHAT_POSITION_TOOLTIP": "Выберите, где чат будет закреплён на выбранном проигрывателе.",
"LABEL_CHAT_SIZE": "Размер окна",
"LABEL_CHAT_SIZE_TOOLTIP": "Выберите стандартный размер окна чата.",
"OPTION_CHAT_SIZE_COMPACT": "Компактный",
"OPTION_CHAT_SIZE_STANDARD": "Стандартный",
"OPTION_CHAT_SIZE_LARGE": "Большой",
"OPTION_CHAT_SIZE_CUSTOM": "Последний пользовательский размер",
"LABEL_CHAT_START_MODE": "Начальный вид",
"LABEL_CHAT_START_MODE_TOOLTIP": "Выберите, запускать чат как плавающую кнопку или открытое окно.",
"OPTION_CHAT_START_BUBBLE": "Плавающая кнопка",
"OPTION_CHAT_START_OPEN": "Открытое окно",
"CHAT_TITLE": "Чат комнаты",
"CHAT_LIVE_ONLY": "Только в реальном времени. Без истории.",
"CHAT_OPEN": "Открыть чат комнаты",
"CHAT_CLOSE": "Закрыть чат",
"CHAT_DOCK_LEFT": "Закрепить чат слева",
"CHAT_DOCK_RIGHT": "Закрепить чат справа",
"CHAT_DETACHED": "Открепить чат",
"CHAT_PLACEHOLDER": "Напишите сообщение...",
"CHAT_SEND": "Отправить",
"CHAT_MISSING_KEY": "Чат доступен только по актуальной ссылке-приглашению.",
"CHAT_TOO_LONG": "Не более 500 символов.",
"CHAT_SEND_FAILED": "Не удалось отправить сообщение.",
"CHAT_EMPTY": "Сообщений пока нет. Чат работает только в реальном времени.",
"LABEL_CHAT_NOTIFICATIONS": "Уведомления чата",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Показывает системное уведомление о новых сообщениях, когда вкладка с видео не активна.",
"LABEL_EVENT_NOTIFICATIONS": "Уведомления о воспроизведении и комнате",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Показывает уведомления, когда кто-то входит, выходит, запускает или приостанавливает видео.",
"DEBUG_SECTION_VIDEO_DETECTION": "Обнаружение видео",
"DEBUG_VIDEO_NOT_FOUND": "ВИДЕОЭЛЕМЕНТ НЕ НАЙДЕН",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "Видео сейчас воспроизводится? KoalaSync обнаруживает видеоэлементы после загрузки контента.",
"DEBUG_SECTION_PLAYBACK": "Воспроизведение",
"DEBUG_STATE_PAUSED": "ПАУЗА",
"DEBUG_STATE_PLAYING": "ВОСПРОИЗВЕДЕНИЕ",
"DEBUG_SECTION_PROPERTIES": "Свойства",
"DEBUG_SECTION_DIMENSIONS": "Размеры",
"DEBUG_DIMENSIONS_HINT": "0×0 = видеоэлемент скрыт или ещё не загружен",
"DEBUG_SECTION_MEDIA_ERROR": "Ошибка медиа",
"DEBUG_SECTION_DETECTION": "Обнаружение",
"DEBUG_SECTION_IDENTIFICATION": "Идентификация",
"DEBUG_SECTION_MEDIA_SOURCE": "Источник медиа",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Атрибуты данных",
"DEBUG_YES": "ДА",
"DEBUG_NO": "НЕТ",
"DEBUG_YES_CHECKED": "ДА (проверено)",
"DEBUG_NA": "н/д"
}
+98 -6
View File
@@ -44,6 +44,24 @@
"LABEL_INVITE_LINK_TOOLTIP": "Katılabilmeleri için bu bağlantıyı arkadaşlarınızla paylaşın",
"LABEL_PEERS_IN_ROOM": "Odadaki Kişiler",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Şu anda bu odaya bağlı olan diğer kullanıcılar",
"LABEL_HOST_CONTROL": "Sunucu Kontrolü",
"LABEL_HOST_CONTROL_TOOLTIP": "Bu odada oynatmayı kimin kontrol edebileceğini belirleyin",
"LABEL_HOST_ONLY_TOGGLE": "Oynatmayı yalnızca ben kontrol edebilirim",
"NOTICE_HOST_CONTROLS": "Oynatmayı herkes için sunucu kontrol eder.",
"NOTICE_COHOST_HINT": "İpucu: aşağıdaki katılımcı listesinde tek tek katılımcılara kontrol verin.",
"BADGE_HOST": "Sunucu",
"BADGE_GUEST": "Misafir",
"BADGE_CONTROLLER": "Kontrolcü",
"BTN_GIVE_CONTROL": "Kontrol ver",
"BTN_REVOKE_CONTROL": "Geri al",
"BADGE_DESYNCED": "Solo",
"TOOLTIP_PEER_DESYNCED": "Tek başına izliyor — sunucu komutlarını yok sayar",
"HCM_DIALOG_TITLE": "KoalaSync · Bu odayı sunucu kontrol ediyor",
"HCM_DIALOG_BODY": "Bu odada oynatmayı yalnızca sunucu kontrol edebilir. Birlikte izlemeye devam mı, yoksa kendi başına mı izlemek istiyorsun?",
"HCM_DIALOG_STAY": "Senkron kal",
"HCM_DIALOG_SOLO": "Kendi başıma izle",
"HCM_BADGE_SOLO": "Kendi başına izliyorsun",
"HCM_BADGE_RESYNC": "Yeniden senkronize et",
"NO_PEERS_CONNECTED": "Bağlı kimse yok",
"BTN_LEAVE_ROOM": "Odadan Ayrıl",
"LABEL_SELECT_VIDEO": "Video Seç",
@@ -52,11 +70,11 @@
"LABEL_REMOTE_CONTROL": "Uzaktan Kumanda",
"BTN_COPY_INVITE": "📋 Davet Linki",
"BTN_COPY_INVITE_TOOLTIP": "Davet Linkini Kopyala",
"BTN_PLAY": "Oynat",
"BTN_PLAY": "Oynat",
"BTN_PLAY_TOOLTIP": "Herkese Oynat komutu gönder",
"BTN_PAUSE": "Duraklat",
"BTN_PAUSE": "Duraklat",
"BTN_PAUSE_TOOLTIP": "Herkese Duraklat komutu gönder",
"BTN_SYNC": "⚡ SYNC",
"BTN_SYNC": "Sync",
"BTN_SYNC_TOOLTIP": "Tüm kullanıcıları senkronize etmeye zorla",
"OPTION_JUMP_TO_OTHERS": "Diğerlerine Atla",
"OPTION_JUMP_TO_ME": "Bana Atla",
@@ -92,6 +110,7 @@
"BTN_REGEN_ID_TOOLTIP": "Dahili kimliğinizi yeniden oluşturun ve tekrar bağlanın",
"REGEN_ID_DESC": "\"Duplicate Identity\" (Mükerrer Kimlik) hataları görüyorsanız bunu kullanın.",
"REGEN_ID_OTHER_ISSUE": "Başka bir sorun? GitHub Issue açın",
"TOAST_ID_REGENERATED": "Kimlik yeniden oluşturuldu — yeniden bağlanılıyor…",
"LABEL_CONN_STATUS": "Bağlantı Durumu",
"LABEL_CONN_STATUS_TOOLTIP": "Mevcut WebSocket bağlantı durumu",
"CONN_STATUS_DISCONNECTED": "Bağlantı Kesildi",
@@ -115,7 +134,7 @@
"BTN_ONBOARDING_NEXT": "İleri",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Bir sonraki adıma git",
"ONBOARDING_1_TITLE": "KoalaSync'e Hoş Geldiniz!",
"ONBOARDING_1_TEXT": "Nerede olursanız olun videoları mükemmel bir senkronizasyonla birlikte izleyin. Hızlı bir tura çıkalım!",
"ONBOARDING_1_TEXT": "Nerede olursanız olun videoları birlikte izleyin. İşte kısa bir tanıtım.",
"ONBOARDING_2_TITLE": "1. Oda Oluştur",
"ONBOARDING_2_TEXT": "Buradan başlayın. Bir oda oluşturun ve davet bağlantısını arkadaşlarınızla paylaşın.",
"ONBOARDING_3_TITLE": "2. Video Seç",
@@ -129,6 +148,8 @@
"ERR_IDENTITY_NOT_LOADED": "Kimlik henüz yüklenmedi. Biraz bekleyin ve tekrar deneyin.",
"ERR_NO_PEERS_TIME": "Bilinen bir zamanı olan başka bağlantı yok. 'Bana Atla' seçeneğine geçin.",
"ERR_NO_VIDEO_TAB": "Video sekmesine bağlanılamadı.",
"SITE_ACCESS_REQUIRED": "Tarayıcınız {host} üzerinde KoalaSync'i engelliyor. Aşağıdaki “Erişime izin ver” düğmesine tıklayın. İletişim kutusu görünmezse uzantı veya site izinlerinden bu site için KoalaSync'e izin verin.",
"BTN_RETRY_ACCESS": "Erişime izin ver",
"ERR_SELECT_VIDEO": "Lütfen önce bir video seçin!",
"TOAST_INVITE_COPIED": "Davet bağlantısı kopyalandı!",
"TOAST_COPY_FAILED": "Panoya kopyalanamadı",
@@ -143,6 +164,8 @@
"STATUS_CONNECTING": "Bağlanılıyor...",
"STATUS_FAILED": "Başarısız",
"STATUS_DISCONNECTED": "Bağlantı Kesildi",
"STATUS_IDLE": "Bağlanmaya hazır",
"STATUS_IDLE_TOOLTIP": "KoalaSync hazır. Bağlanmak ve senkronizasyonu başlatmak için bir odaya katılın veya oluşturun.",
"BTN_STATE_JOINING": "🚀 Katılınıyor...",
"BTN_STATE_RECONNECTING": "🔄 Yeniden bağlanılıyor...",
"BTN_STATE_PLAYING": "▶ Oynatılıyor...",
@@ -181,13 +204,14 @@
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zaman aşımı — tüm bağlantılar bölümü yüklemedi",
"LOBBY_CANCEL_USER": "Kullanıcı tarafından iptal edildi",
"NOTIF_ERROR_TITLE": "KoalaSync Hatası",
"FOOTER_SUPPORT": "☕ Bana kahve ısmarla",
"FOOTER_SUPPORT": "KoalaSynci destekle",
"FOOTER_REVIEW": "★ Değerlendir",
"FOOTER_SUPPORT_PROMPT": "KoalaSync'i beğendin mi? Bir yorum bırak!",
"LABEL_AUDIO_PROCESSING": "Ses işleme",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Video oynatımına sıkıştırma gibi ses efektleri uygular",
"AUDIO_OPEN_SETTINGS": "Aç",
"NEW_FEATURE_AUDIO": "Yeni: Ses işleme — kompresörü deneyin!",
"NEW_FEATURE_THEME": "Yeni: Temalar — klasik görünüme veya minimal grafite geçin.",
"AUDIO_BACK": "← Geri",
"AUDIO_PAGE_TITLE": "Ses ayarları",
"AUDIO_MASTER_TOGGLE": "Ses işleme",
@@ -205,5 +229,73 @@
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Ekolayzer",
"AUDIO_COMING_SOON": "Çok yakında"
"AUDIO_COMING_SOON": "Çok yakında",
"BTN_RESTART_TOUR": "Eğitimi Yeniden Başlat",
"BTN_RESTART_TOUR_TOOLTIP": "Tanıtım eğitimini yeniden başlat",
"HINT_SELECT_VIDEO": "Videonuzu buradan seçin!",
"OPTION_TITLE_PRIVACY_FULL": "Tam başlığı gönder",
"OPTION_TITLE_PRIVACY_EPISODE": "Bölüm IDsini gönder",
"OPTION_TITLE_PRIVACY_HIDDEN": "Gönderme",
"LABEL_PRIVACY_SETTINGS": "Gizlilik ayarları",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Odaya hangi sekme ve medya başlıklarının gönderileceğini seçin.",
"LABEL_SEND_TAB_TITLE": "Sekme başlığını gönder",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Seçili tarayıcı sekmesinin başlığını odaya gönderir.",
"LABEL_MEDIA_TITLE_PRIVACY": "Medya başlığı",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Medya başlıklarının odaya nasıl gönderileceğini seçin.",
"LABEL_SETTINGS_GROUP_PROFILE": "Profil ve Görünüm",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Kullanıcı adınızı, temanızı ve dil tercihlerinizi değiştirin.",
"LABEL_SETTINGS_GROUP_SYNC": "Oynatma ve Eşitleme",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Oynatma, bildirim ve ses tercihlerini özelleştirin.",
"LABEL_CHAT_ENABLED": "Oda sohbetini etkinleştir",
"LABEL_CHAT_ENABLED_TOOLTIP": "Şifreli oda sohbetini seçili video sekmesinde göster.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Şifreli oda sohbetinin görünümünü ve konumunu yapılandırın.",
"CHAT_SETTINGS_NOTE": "İsteğe bağlı ve uçtan uca şifrelidir. Mesajlar yalnızca canlıdır ve aktarıcı tarafından saklanmaz.",
"LABEL_CHAT_POSITION": "Konum",
"LABEL_CHAT_POSITION_TOOLTIP": "Sohbetin seçili oynatıcıda nereye sabitleneceğini seçin.",
"LABEL_CHAT_SIZE": "Kaplama boyutu",
"LABEL_CHAT_SIZE_TOOLTIP": "Sohbet kaplamasının varsayılan boyutunu seçin.",
"OPTION_CHAT_SIZE_COMPACT": "Kompakt",
"OPTION_CHAT_SIZE_STANDARD": "Standart",
"OPTION_CHAT_SIZE_LARGE": "Büyük",
"OPTION_CHAT_SIZE_CUSTOM": "Son özel boyut",
"LABEL_CHAT_START_MODE": "Başlangıç görünümü",
"LABEL_CHAT_START_MODE_TOOLTIP": "Sohbetin kayan balon veya açık kaplama olarak başlamasını seçin.",
"OPTION_CHAT_START_BUBBLE": "Kayan sohbet balonu",
"OPTION_CHAT_START_OPEN": "Açık kaplama",
"CHAT_TITLE": "Oda Sohbeti",
"CHAT_LIVE_ONLY": "Yalnızca canlı. Geçmiş yok.",
"CHAT_OPEN": "Oda sohbetini aç",
"CHAT_CLOSE": "Sohbeti kapat",
"CHAT_DOCK_LEFT": "Sohbeti sola sabitle",
"CHAT_DOCK_RIGHT": "Sohbeti sağa sabitle",
"CHAT_DETACHED": "Sohbeti ayır",
"CHAT_PLACEHOLDER": "Bir mesaj yazın...",
"CHAT_SEND": "Gönder",
"CHAT_MISSING_KEY": "Sohbet yalnızca güncel bir davet bağlantısıyla kullanılabilir.",
"CHAT_TOO_LONG": "En fazla 500 karakter.",
"CHAT_SEND_FAILED": "Mesaj gönderilemedi.",
"CHAT_EMPTY": "Henüz mesaj yok. Sohbet yalnızca canlıdır.",
"LABEL_CHAT_NOTIFICATIONS": "Sohbet bildirimleri",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Video sekmesi etkin değilken yeni oda mesajlarını sistem bildirimi olarak gösterir.",
"LABEL_EVENT_NOTIFICATIONS": "Oynatma ve oda bildirimleri",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Biri katıldığında, ayrıldığında, oynattığında veya duraklattığında bildirim gösterir.",
"DEBUG_SECTION_VIDEO_DETECTION": "Video algılama",
"DEBUG_VIDEO_NOT_FOUND": "VİDEO ÖĞESİ BULUNAMADI",
"DEBUG_SECTION_MEDIA_SESSION": "Media Session API",
"DEBUG_VIDEO_HINT": "Şu anda video oynatılıyor mu? KoalaSync medya yüklendikten sonra video öğelerini algılar.",
"DEBUG_SECTION_PLAYBACK": "Oynatma",
"DEBUG_STATE_PAUSED": "DURAKLATILDI",
"DEBUG_STATE_PLAYING": "OYNATILIYOR",
"DEBUG_SECTION_PROPERTIES": "Özellikler",
"DEBUG_SECTION_DIMENSIONS": "Boyutlar",
"DEBUG_DIMENSIONS_HINT": "0×0 = video öğesi gizli veya henüz yüklenmedi",
"DEBUG_SECTION_MEDIA_ERROR": "Medya hatası",
"DEBUG_SECTION_DETECTION": "Algılama",
"DEBUG_SECTION_IDENTIFICATION": "Tanımlama",
"DEBUG_SECTION_MEDIA_SOURCE": "Medya kaynağı",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Veri öznitelikleri",
"DEBUG_YES": "EVET",
"DEBUG_NO": "HAYIR",
"DEBUG_YES_CHECKED": "EVET (kontrol edildi)",
"DEBUG_NA": "yok"
}
+301
View File
@@ -0,0 +1,301 @@
{
"LANG_CODE": "uk",
"HTML_CLASS": "lang-uk",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "Кімната",
"TAB_ROOM_TOOLTIP": "Налаштування кімнати та підключення",
"TAB_SYNC": "Синхронізувати",
"TAB_SYNC_TOOLTIP": "Елементи керування синхронізацією відео та віддалені дії",
"TAB_SETTINGS": "Налаштування",
"TAB_SETTINGS_TOOLTIP": "Параметри розширення",
"TAB_STATUS": "Статус",
"TAB_STATUS_TOOLTIP": "Розширена діагностика та журнали",
"BTN_CREATE_ROOM": "+ Створити нову кімнату",
"MANUAL_CONNECT_HEADER": "Ручне підключення / Додатково",
"LABEL_SERVER": "Сервер",
"BTN_SERVER_OFFICIAL": "Офіційний",
"BTN_SERVER_OFFICIAL_TOOLTIP": "Використовуйте офіційний надійний сервер",
"BTN_SERVER_CUSTOM": "Власний",
"BTN_SERVER_CUSTOM_TOOLTIP": "Підключіться до власного серверу, розміщеного на власному хостингу",
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
"LABEL_ROOM_ID": "ID кімнати",
"LABEL_ROOM_ID_TOOLTIP": "Унікальний ідентифікатор вашої кімнати синхронізації",
"PLACEHOLDER_ROOM_ID": "Введіть Room ID",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "Унікальний ідентифікатор кімнати, до якої ви хочете приєднатися",
"LABEL_PASSWORD": "Пароль (необов'язково)",
"LABEL_PASSWORD_TOOLTIP": "Додатковий пароль для обмеження доступу до кімнати",
"PLACEHOLDER_PASSWORD": "Пароль кімнати (необов'язково)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "Пароль для кімнати (залиште порожнім, якщо немає)",
"BTN_JOIN_ROOM": "Приєднатися / Створити кімнату",
"BTN_JOIN_ROOM_TOOLTIP": "Підключіться до кімнати",
"LABEL_PUBLIC_ROOMS": "Громадські кімнати",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "Список загальнодоступних кімнат на цьому сервері",
"BTN_REFRESH": "ОНОВИТИ",
"BTN_REFRESH_TOOLTIP": "Оновити список громадських кімнат",
"PUBLIC_ROOMS_REFRESHING": "Оновлення...",
"BTN_REFRESH_COOLDOWN": "ЧЕКАЙТЕ {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Оновлення списку кімнат охолоджується. Повторіть спробу в {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Оновлення громадських кімнат. Наступне оновлення доступне в {seconds}s.",
"LABEL_ACTIVE_ROOM": "Активна кімната",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Кімната, до якої ви зараз підключені",
"ACTIVE_ROOM_NONE": "ЖОДНОГО",
"ACTIVE_SERVER_OFFICIAL": "Офіційний сервер",
"LABEL_INVITE_LINK": "Посилання на запрошення",
"LABEL_INVITE_LINK_TOOLTIP": "Поділіться цим посиланням з друзями, щоб вони могли приєднатися",
"LABEL_PEERS_IN_ROOM": "Учасники в кімнаті",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "Інші користувачі зараз підключені до цієї кімнати",
"LABEL_HOST_CONTROL": "Керування ведучим",
"LABEL_HOST_CONTROL_TOOLTIP": "Обмежити, хто може керувати відтворенням у цій кімнаті",
"LABEL_HOST_ONLY_TOGGLE": "Лише я можу керувати відтворенням",
"NOTICE_HOST_CONTROLS": "Ведучий керує відтворенням для всіх.",
"NOTICE_COHOST_HINT": "Порада: надайте керування окремим учасникам у списку нижче.",
"BADGE_HOST": "Ведучий",
"BADGE_GUEST": "Гість",
"BADGE_CONTROLLER": "Керуючий",
"BTN_GIVE_CONTROL": "Дати керування",
"BTN_REVOKE_CONTROL": "Відкликати",
"BADGE_DESYNCED": "Соло",
"TOOLTIP_PEER_DESYNCED": "Дивиться окремо — ігнорує команди ведучого",
"HCM_DIALOG_TITLE": "KoalaSync · Кімнатою керує ведучий",
"HCM_DIALOG_BODY": "Керувати відтворенням у цій кімнаті може лише ведучий. Продовжити дивитися разом чи дивитися самостійно?",
"HCM_DIALOG_STAY": "Залишатися синхронно",
"HCM_DIALOG_SOLO": "Дивитися самостійно",
"HCM_BADGE_SOLO": "Ви дивитеся самостійно",
"HCM_BADGE_RESYNC": "Синхронізувати",
"NO_PEERS_CONNECTED": "Немає підключених однорангових пристроїв",
"BTN_LEAVE_ROOM": "Вийти з кімнати",
"LABEL_SELECT_VIDEO": "Виберіть Відео",
"LABEL_SELECT_VIDEO_TOOLTIP": "Виберіть вкладку браузера, яка містить відео для синхронізації",
"OPTION_SELECT_TAB": "-- Виберіть вкладку --",
"LABEL_REMOTE_CONTROL": "Пульт дистанційного керування",
"BTN_COPY_INVITE": "📋 Посилання на запрошення",
"BTN_COPY_INVITE_TOOLTIP": "Копіювати посилання для запрошення",
"BTN_PLAY": "Відтворити",
"BTN_PLAY_TOOLTIP": "Надішліть усім команду Play",
"BTN_PAUSE": "Пауза",
"BTN_PAUSE_TOOLTIP": "Надішліть команду паузи всім",
"BTN_SYNC": "Синхр",
"BTN_SYNC_TOOLTIP": "Змусити всіх користувачів синхронізуватися",
"OPTION_JUMP_TO_OTHERS": "Перейти до інших",
"OPTION_JUMP_TO_ME": "Перейти до мене",
"OPTION_JUMP_MODE_TOOLTIP": "Виберіть ціль синхронізації",
"LABEL_LAST_ACTIVITY": "Статус останньої дії",
"LABEL_LAST_ACTIVITY_TOOLTIP": "Показує останню команду відтворення, паузи або перемотування",
"NO_RECENT_COMMANDS": "Немає останніх команд",
"LOBBY_HEADER": "ЕПІЗОДНЕ ЛОБІ",
"LOBBY_WAITING_FOR": "🎬 Чекаємо: \"{title}\"",
"LOBBY_WAITING_PEERS": "Очікування учасників...",
"BTN_SKIP_PLAY": "Все одно пропустити та відтворити",
"BTN_SKIP_PLAY_TOOLTIP": "Скасуйте лобі та все одно відтворюйте",
"LOBBY_CONNECT_FIRST": "Спочатку підключіться до кімнати",
"LOBBY_CONNECT_FIRST_DESC": "Вам потрібно приєднатися до кімнати за посиланням із запрошенням або створити нову, щоб синхронізувати відео.",
"BTN_CREATE_ROOM_ALT": "Створити нову кімнату",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "Створіть нову випадкову кімнату та приєднайтеся до неї",
"LABEL_USERNAME": "Ваше ім'я користувача",
"LABEL_USERNAME_TOOLTIP": "Ім'я користувача допомагає іншим ідентифікувати вас.",
"PLACEHOLDER_USERNAME": "Анонімна коала",
"LABEL_HIDE_CLUTTER": "Приховати безладні вкладки",
"LABEL_HIDE_CLUTTER_TOOLTIP": "Відфільтровує вкладки, не пов’язані з відео, і непов’язані домени, щоб зберегти список чистим",
"LABEL_AUTO_SYNC_NEXT": "Автоматична синхронізація наступного епізоду",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "Автоматично призупиняється та чекає на всіх однорангових пристроїв, коли епізод зміниться, а потім синхронізація починається разом.",
"LABEL_AUTO_COPY_INVITE": "Автоматичне копіювання посилання для запрошення",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "Автоматично копіює посилання запрошення в буфер обміну під час створення нової кімнати.",
"LABEL_NOTIFICATIONS": "Сповіщення браузера",
"LABEL_NOTIFICATIONS_TOOLTIP": "Показує рідні системні сповіщення, коли хтось приєднується/виходить або відтворює/призупиняє.",
"LABEL_LANGUAGE": "Мова програми",
"LABEL_LANGUAGE_TOOLTIP": "Виберіть бажану мову розширення",
"LABEL_TROUBLESHOOTING": "Усунення несправностей",
"LABEL_TROUBLESHOOTING_TOOLTIP": "Інструменти для вирішення проблем з підключенням",
"BTN_REGEN_ID": "Регенерувати Peer ID",
"BTN_REGEN_ID_TOOLTIP": "Повторно згенеруйте свій внутрішній ідентифікатор і повторно підключіться",
"REGEN_ID_DESC": "Використовуйте це, якщо ви бачите помилку \"Дублікати ідентифікатора\".",
"REGEN_ID_OTHER_ISSUE": "Інша проблема? Відкрийте випуск GitHub",
"TOAST_ID_REGENERATED": "Ідентичність перегенеровано — повторне підключення…",
"LABEL_CONN_STATUS": "Статус підключення",
"LABEL_CONN_STATUS_TOOLTIP": "Поточний стан підключення WebSocket",
"CONN_STATUS_DISCONNECTED": "Відключено",
"BTN_RETRY": "ПОВТОРИТИ",
"BTN_RETRY_TOOLTIP": "Спробуйте повторно підключитися до сервера",
"BTN_COPY_LOGS": "Копіювати журнали",
"BTN_COPY_LOGS_TOOLTIP": "Скопіюйте журнали в буфер обміну для спільного використання",
"LABEL_VIDEO_DEBUG": "Інформація про налагодження відео",
"LABEL_VIDEO_DEBUG_TOOLTIP": "Технічні відомості про поточний вибраний елемент відео",
"VIDEO_DEBUG_EMPTY": "Вкладка не вибрана або відео не виявлено.",
"LABEL_HISTORY": "Повна історія дій",
"LABEL_HISTORY_TOOLTIP": "Хронологічний журнал усіх команд синхронізації в кімнаті",
"HISTORY_EMPTY": "Активності ще немає",
"LABEL_LOGS": "Журнали (останні 50)",
"LABEL_LOGS_TOOLTIP": "Журнали технічного підключення для налагодження",
"BTN_CLEAR": "Очистити",
"BTN_CLEAR_TOOLTIP": "Очистити журнал",
"LABEL_GITHUB": "Репозиторій GitHub",
"BTN_ONBOARDING_SKIP": "Пропустити",
"BTN_ONBOARDING_SKIP_TOOLTIP": "Пропустити підручник",
"BTN_ONBOARDING_NEXT": "Далі",
"BTN_ONBOARDING_NEXT_TOOLTIP": "Перейти до наступного кроку",
"ONBOARDING_1_TITLE": "Ласкаво просимо до KoalaSync!",
"ONBOARDING_1_TEXT": "Дивіться відео разом, де б ви не були. Ось короткий огляд.",
"ONBOARDING_2_TITLE": "1. Створіть кімнату",
"ONBOARDING_2_TEXT": "Почніть тут. Створіть кімнату та поділіться посиланням запрошення з друзями.",
"ONBOARDING_3_TITLE": "2. Виберіть Відео",
"ONBOARDING_3_TEXT": "Перейдіть сюди, щоб вибрати відео, яке потрібно синхронізувати. Відтворення, пауза та пошук — усі залишаються синхронізованими.",
"ONBOARDING_4_TITLE": "3. Персоналізація",
"ONBOARDING_4_TEXT": "Виберіть цікаве ім’я користувача, щоб ваші друзі знали, хто ви.",
"ONBOARDING_5_TITLE": "Усе готово!",
"ONBOARDING_5_TEXT": "Час взяти попкорн. Приємного перегляду разом!",
"ERR_CONN_TIMEOUT": "Час очікування підключення минув. Спробуйте ще раз.",
"ERR_INVALID_SERVER_URL": "Недійсний формат URL-адреси сервера.",
"ERR_IDENTITY_NOT_LOADED": "Посвідчення особи ще не завантажено. Зачекайте хвилинку та повторіть спробу.",
"ERR_NO_PEERS_TIME": "Немає інших аналогів із відомим часом. Переключіться на «Перейти до мене».",
"ERR_NO_VIDEO_TAB": "Не вдалося підключитися до вкладки відео.",
"SITE_ACCESS_REQUIRED": "Браузер блокує KoalaSync на {host}. Натисніть «Дозволити доступ» нижче. Якщо діалог не з’явиться, дозвольте KoalaSync для цього сайту в налаштуваннях розширення або дозволах сайту.",
"BTN_RETRY_ACCESS": "Дозволити доступ",
"ERR_SELECT_VIDEO": "Спочатку виберіть відео!",
"TOAST_INVITE_COPIED": "Посилання на запрошення скопійовано!",
"TOAST_COPY_FAILED": "Не вдалося скопіювати в буфер обміну",
"TOAST_LOBBY_SKIPPED": "Лобі епізоду пропущено.",
"TOAST_LOBBY_SKIP_FAILED": "Не вдалося пропустити лобі.",
"TOAST_LOGS_COPIED": "Скопійовано!",
"TOAST_PEER_JOINED": "{name} приєднався до кімнати",
"TOAST_PEER_LEFT": "{name} вийшов з кімнати",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "Підключено",
"STATUS_RECONNECTING": "Повторне підключення...",
"STATUS_CONNECTING": "Підключення...",
"STATUS_FAILED": "Не вдалося",
"STATUS_DISCONNECTED": "Відключено",
"STATUS_IDLE": "Готовий до підключення",
"STATUS_IDLE_TOOLTIP": "KoalaSync готовий. Приєднайтеся до кімнати або створіть її, щоб підключитися та почати синхронізацію.",
"BTN_STATE_JOINING": "🚀 Приєднуюсь...",
"BTN_STATE_RECONNECTING": "🔄 Повторне підключення...",
"BTN_STATE_PLAYING": "▶ Грає...",
"BTN_STATE_PAUSING": "⏸ Призупинення...",
"BTN_STATE_SYNCING_GROUP": "Синхронізація з групою ({time})...",
"BTN_STATE_SYNCING": "Синхронізація...",
"BTN_STATE_SYNCED": "✅ Синхронізовано!",
"NOTIF_PLAY": "почав відтворення",
"NOTIF_PAUSE": "призупинено відтворення",
"NOTIF_SEEK": "перемотав відео",
"NOTIF_FORCE_PREPARE": "почав примусову синхронізацію",
"NOTIF_FORCE_EXECUTE": "синхронізував усіх",
"DEBUG_NO_TAB": "Цільова вкладка не вибрана.",
"DEBUG_COMM_FAIL": "Не вдалося зв’язатися з відео вкладки.",
"EMPTY_PEERS_TITLE": "Учасників ще немає",
"EMPTY_PEERS_HINT": "Поділіться своїм запрошенням, щоб почати",
"EMPTY_HISTORY_TITLE": "Активності ще немає",
"EMPTY_HISTORY_HINT": "Відтворення, призупинення або перемотування, щоб побачити історію",
"EMPTY_LOGS_TITLE": "Без журналів",
"EMPTY_LOGS_HINT": "Тут відображатимуться події підключення",
"EMPTY_ROOMS_TITLE": "Немає активних кімнат",
"EMPTY_ROOMS_HINT": "Створіть кімнату або оновіть, щоб знайти загальнодоступні",
"LABEL_YOU": "Ви",
"ONBOARDING_DONE": "Готово!",
"LABEL_LOBBY_PEER_READY": "Готовий",
"LABEL_LOBBY_PEER_LOADING": "Завантаження...",
"LABEL_PASSWORD_PROTECTED": "Захищено паролем",
"LABEL_PEERS_COUNT": "{count} учасників",
"LABEL_CUSTOM_SERVER": "Спеціальний сервер",
"BTN_STATE_CREATING": "🚀 Створення кімнати...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — Помилка синхронізації епізоду",
"NOTIF_LOBBY_CANCEL_MSG": "Автоматичну синхронізацію скасовано: {reason}. Можливо, вам знадобиться виконати синхронізацію вручну.",
"LOBBY_CANCEL_TIMEOUT": "Час очікування",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "Тайм-аут (відновлено)",
"LOBBY_CANCEL_PEERS_LEFT": "Усі інші учасники вийшли",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Тайм-аут — не всі вузли завантажили епізод",
"LOBBY_CANCEL_USER": "Скасовано користувачем",
"NOTIF_ERROR_TITLE": "Помилка KoalaSync",
"FOOTER_SUPPORT": "Підтримати KoalaSync",
"FOOTER_REVIEW": "★ Оцініть нас",
"FOOTER_SUPPORT_PROMPT": "Подобається KoalaSync? Залиште відгук!",
"LABEL_AUDIO_PROCESSING": "Обробка аудіо",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Застосуйте звукові ефекти, такі як стиснення, до відтворення відео",
"AUDIO_OPEN_SETTINGS": "Відкрити",
"NEW_FEATURE_AUDIO": "Новинка: обробка звуку — спробуйте компресор!",
"NEW_FEATURE_THEME": "Нове: Теми — перемкніться на класичний вигляд або мінімальний графіт.",
"AUDIO_BACK": "← Назад",
"AUDIO_PAGE_TITLE": "Параметри звуку",
"AUDIO_MASTER_TOGGLE": "Обробка аудіо",
"AUDIO_COMPRESSOR": "Компресор",
"AUDIO_COMPRESSOR_ENABLE": "Увімкнено",
"AUDIO_PRESET": "Попереднє налаштування",
"AUDIO_PRESET_RECOMMENDED": "Рекомендовано",
"AUDIO_PRESET_DYNAMIC_RANGE": "Динамічний діапазон",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Покращення голосу",
"AUDIO_PRESET_SMOOTH": "Плавний",
"AUDIO_PRESET_CUSTOM": "Власний",
"AUDIO_PARAM_THRESHOLD": "Поріг",
"AUDIO_PARAM_KNEE": "Коліно",
"AUDIO_PARAM_RATIO": "Співвідношення",
"AUDIO_PARAM_ATTACK": "Атака",
"AUDIO_PARAM_RELEASE": "Спад",
"AUDIO_EQUALIZER": "Еквалайзер",
"AUDIO_COMING_SOON": "Скоро буде",
"BTN_RESTART_TOUR": "Перезапустіть підручник",
"BTN_RESTART_TOUR_TOOLTIP": "Перезапустіть навчальний посібник із адаптації",
"HINT_SELECT_VIDEO": "Виберіть своє відео тут!",
"OPTION_TITLE_PRIVACY_FULL": "Надсилати повну назву",
"OPTION_TITLE_PRIVACY_EPISODE": "Надсилати ID епізоду",
"OPTION_TITLE_PRIVACY_HIDDEN": "Не надсилати",
"LABEL_PRIVACY_SETTINGS": "Налаштування приватності",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Виберіть, які назви вкладки й медіа надсилати до кімнати.",
"LABEL_SEND_TAB_TITLE": "Надсилати назву вкладки",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Надсилає до кімнати назву вибраної вкладки браузера.",
"LABEL_MEDIA_TITLE_PRIVACY": "Назва медіа",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Виберіть, як назви медіа надсилати до кімнати.",
"LABEL_SETTINGS_GROUP_PROFILE": "Профіль та Вигляд",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "Змініть ім'я користувача, тему та мовні налаштування.",
"LABEL_SETTINGS_GROUP_SYNC": "Відтворення та Синхронізація",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "Налаштуйте відтворення, сповіщення та параметри звуку.",
"LABEL_CHAT_ENABLED": "Увімкнути чат кімнати",
"LABEL_CHAT_ENABLED_TOOLTIP": "Показувати зашифрований чат кімнати на вибраній вкладці з відео.",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "Налаштуйте вигляд і розташування зашифрованого чату кімнати.",
"CHAT_SETTINGS_NOTE": "Необов'язковий чат із наскрізним шифруванням. Повідомлення доступні лише наживо й не зберігаються ретранслятором.",
"LABEL_CHAT_POSITION": "Розташування",
"LABEL_CHAT_POSITION_TOOLTIP": "Виберіть, де чат буде закріплено на вибраному програвачі.",
"LABEL_CHAT_SIZE": "Розмір вікна",
"LABEL_CHAT_SIZE_TOOLTIP": "Виберіть стандартний розмір вікна чату.",
"OPTION_CHAT_SIZE_COMPACT": "Компактний",
"OPTION_CHAT_SIZE_STANDARD": "Стандартний",
"OPTION_CHAT_SIZE_LARGE": "Великий",
"OPTION_CHAT_SIZE_CUSTOM": "Останній власний розмір",
"LABEL_CHAT_START_MODE": "Початковий вигляд",
"LABEL_CHAT_START_MODE_TOOLTIP": "Виберіть, запускати чат як плаваючу кнопку чи відкрите вікно.",
"OPTION_CHAT_START_BUBBLE": "Плаваюча кнопка",
"OPTION_CHAT_START_OPEN": "Відкрите вікно",
"CHAT_TITLE": "Чат кімнати",
"CHAT_LIVE_ONLY": "Лише наживо. Без історії.",
"CHAT_OPEN": "Відкрити чат кімнати",
"CHAT_CLOSE": "Закрити чат",
"CHAT_DOCK_LEFT": "Закріпити чат ліворуч",
"CHAT_DOCK_RIGHT": "Закріпити чат праворуч",
"CHAT_DETACHED": "Від'єднати чат",
"CHAT_PLACEHOLDER": "Напишіть повідомлення...",
"CHAT_SEND": "Надіслати",
"CHAT_MISSING_KEY": "Чат доступний лише за актуальним посиланням-запрошенням.",
"CHAT_TOO_LONG": "Не більше 500 символів.",
"CHAT_SEND_FAILED": "Не вдалося надіслати повідомлення.",
"CHAT_EMPTY": "Повідомлень ще немає. Чат працює лише наживо.",
"LABEL_CHAT_NOTIFICATIONS": "Сповіщення чату",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "Показує системне сповіщення про нові повідомлення, коли вкладка з відео неактивна.",
"LABEL_EVENT_NOTIFICATIONS": "Сповіщення про відтворення та кімнату",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "Показує сповіщення, коли хтось приєднується, виходить, запускає або призупиняє відео.",
"DEBUG_SECTION_VIDEO_DETECTION": "Виявлення відео",
"DEBUG_VIDEO_NOT_FOUND": "ВІДЕОЕЛЕМЕНТ НЕ ЗНАЙДЕНО",
"DEBUG_SECTION_MEDIA_SESSION": "API Media Session",
"DEBUG_VIDEO_HINT": "Відео зараз відтворюється? KoalaSync виявляє відеоелементи після завантаження медіа.",
"DEBUG_SECTION_PLAYBACK": "Відтворення",
"DEBUG_STATE_PAUSED": "ПРИЗУПИНЕНО",
"DEBUG_STATE_PLAYING": "ВІДТВОРЮЄТЬСЯ",
"DEBUG_SECTION_PROPERTIES": "Властивості",
"DEBUG_SECTION_DIMENSIONS": "Розміри",
"DEBUG_DIMENSIONS_HINT": "0×0 = відеоелемент прихований або ще не завантажений",
"DEBUG_SECTION_MEDIA_ERROR": "Помилка медіа",
"DEBUG_SECTION_DETECTION": "Виявлення",
"DEBUG_SECTION_IDENTIFICATION": "Ідентифікація",
"DEBUG_SECTION_MEDIA_SOURCE": "Джерело медіа",
"DEBUG_SECTION_DATA_ATTRIBUTES": "Атрибути даних",
"DEBUG_YES": "ТАК",
"DEBUG_NO": "НІ",
"DEBUG_YES_CHECKED": "ТАК (перевірено)",
"DEBUG_NA": "н/д"
}
+301
View File
@@ -0,0 +1,301 @@
{
"LANG_CODE": "zh",
"HTML_CLASS": "lang-zh",
"APP_TITLE": "KoalaSync",
"TAB_ROOM": "房间",
"TAB_ROOM_TOOLTIP": "房间设置和连接",
"TAB_SYNC": "同步",
"TAB_SYNC_TOOLTIP": "视频同步控制和远程操作",
"TAB_SETTINGS": "设置",
"TAB_SETTINGS_TOOLTIP": "扩展首选项",
"TAB_STATUS": "状态",
"TAB_STATUS_TOOLTIP": "高级诊断和日志",
"BTN_CREATE_ROOM": "+ 创建新房间",
"MANUAL_CONNECT_HEADER": "手动连接/高级",
"LABEL_SERVER": "服务器",
"BTN_SERVER_OFFICIAL": "官方的",
"BTN_SERVER_OFFICIAL_TOOLTIP": "使用官方可靠服务器",
"BTN_SERVER_CUSTOM": "自定义",
"BTN_SERVER_CUSTOM_TOOLTIP": "连接到您自己的自托管服务器",
"PLACEHOLDER_SERVER_URL": "wss://your-server:3000",
"LABEL_ROOM_ID": "房间 ID",
"LABEL_ROOM_ID_TOOLTIP": "您的同步室的唯一标识符",
"PLACEHOLDER_ROOM_ID": "输入Room ID",
"PLACEHOLDER_ROOM_ID_TOOLTIP": "您要加入的房间的唯一ID",
"LABEL_PASSWORD": "密码(可选)",
"LABEL_PASSWORD_TOOLTIP": "可选密码以限制房间访问",
"PLACEHOLDER_PASSWORD": "房间密码(可选)",
"PLACEHOLDER_PASSWORD_TOOLTIP": "房间密码(无则留空)",
"BTN_JOIN_ROOM": "加入/创建房间",
"BTN_JOIN_ROOM_TOOLTIP": "连接到房间",
"LABEL_PUBLIC_ROOMS": "公共房间",
"LABEL_PUBLIC_ROOMS_TOOLTIP": "该服务器上公开可用的房间列表",
"BTN_REFRESH": "刷新",
"BTN_REFRESH_TOOLTIP": "刷新公共房间列表",
"PUBLIC_ROOMS_REFRESHING": "刷新中...",
"BTN_REFRESH_COOLDOWN": "等等{seconds}",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "房间列表刷新正在冷却。在 {seconds} 中重试。",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "公共房间列表刷新冷却中。下次刷新将在 {seconds} 后可用。",
"LABEL_ACTIVE_ROOM": "活动室",
"LABEL_ACTIVE_ROOM_TOOLTIP": "您当前连接的房间",
"ACTIVE_ROOM_NONE": "没有任何",
"ACTIVE_SERVER_OFFICIAL": "官方服务器",
"LABEL_INVITE_LINK": "邀请链接",
"LABEL_INVITE_LINK_TOOLTIP": "与朋友分享此链接,以便他们可以加入",
"LABEL_PEERS_IN_ROOM": "房间里的同伴",
"LABEL_PEERS_IN_ROOM_TOOLTIP": "当前连接到该房间的其他用户",
"LABEL_HOST_CONTROL": "主持人控制",
"LABEL_HOST_CONTROL_TOOLTIP": "限制谁可以在此房间中控制播放",
"LABEL_HOST_ONLY_TOGGLE": "仅我可以控制播放",
"NOTICE_HOST_CONTROLS": "由主持人为所有人控制播放。",
"NOTICE_COHOST_HINT": "提示:在下方的参与者列表中将控制权授予单个参与者。",
"BADGE_HOST": "主持人",
"BADGE_GUEST": "访客",
"BADGE_CONTROLLER": "控制者",
"BTN_GIVE_CONTROL": "授予控制权",
"BTN_REVOKE_CONTROL": "撤销",
"BADGE_DESYNCED": "单独",
"TOOLTIP_PEER_DESYNCED": "单独观看中 — 忽略主持人的命令",
"HCM_DIALOG_TITLE": "KoalaSync · 主持人控制本房间",
"HCM_DIALOG_BODY": "本房间只有主持人可以控制播放。继续一起观看,还是自己单独观看?",
"HCM_DIALOG_STAY": "保持同步",
"HCM_DIALOG_SOLO": "自己单独观看",
"HCM_BADGE_SOLO": "正在单独观看",
"HCM_BADGE_RESYNC": "重新同步",
"NO_PEERS_CONNECTED": "没有对等点连接",
"BTN_LEAVE_ROOM": "离开房间",
"LABEL_SELECT_VIDEO": "选择视频",
"LABEL_SELECT_VIDEO_TOOLTIP": "选择包含要同步的视频的浏览器选项卡",
"OPTION_SELECT_TAB": "-- 选择一个选项卡 --",
"LABEL_REMOTE_CONTROL": "遥控",
"BTN_COPY_INVITE": "📋 邀请链接",
"BTN_COPY_INVITE_TOOLTIP": "复制邀请链接",
"BTN_PLAY": "播放",
"BTN_PLAY_TOOLTIP": "向所有人发送播放命令",
"BTN_PAUSE": "暂停",
"BTN_PAUSE_TOOLTIP": "向所有人发送暂停命令",
"BTN_SYNC": "同步",
"BTN_SYNC_TOOLTIP": "强制所有用户同步",
"OPTION_JUMP_TO_OTHERS": "跳转到其他",
"OPTION_JUMP_TO_ME": "跳到我这里",
"OPTION_JUMP_MODE_TOOLTIP": "选择同步目标",
"LABEL_LAST_ACTIVITY": "上次活动状态",
"LABEL_LAST_ACTIVITY_TOOLTIP": "显示最近的播放、暂停或跳转命令",
"NO_RECENT_COMMANDS": "没有最近的命令",
"LOBBY_HEADER": "剧集大厅",
"LOBBY_WAITING_FOR": "🎬 等待:“{title}”",
"LOBBY_WAITING_PEERS": "等待同行...",
"BTN_SKIP_PLAY": "仍然跳过并播放",
"BTN_SKIP_PLAY_TOOLTIP": "取消大厅并继续播放",
"LOBBY_CONNECT_FIRST": "首先连接到房间",
"LOBBY_CONNECT_FIRST_DESC": "您需要通过邀请链接加入房间或创建一个新房间来同步视频。",
"BTN_CREATE_ROOM_ALT": "创建新房间",
"BTN_CREATE_ROOM_ALT_TOOLTIP": "创建一个新的随机房间并加入它",
"LABEL_USERNAME": "您的用户名",
"LABEL_USERNAME_TOOLTIP": "用户名可以帮助其他人识别您。",
"PLACEHOLDER_USERNAME": "无名考拉",
"LABEL_HIDE_CLUTTER": "隐藏杂乱标签",
"LABEL_HIDE_CLUTTER_TOOLTIP": "过滤掉非视频选项卡和不相关的域以保持列表干净",
"LABEL_AUTO_SYNC_NEXT": "自动同步下一集",
"LABEL_AUTO_SYNC_NEXT_TOOLTIP": "当情节发生变化时,自动暂停并等待所有对等点,然后一起开始同步。",
"LABEL_AUTO_COPY_INVITE": "自动复制邀请链接",
"LABEL_AUTO_COPY_INVITE_TOOLTIP": "创建新房间时,自动将邀请链接复制到剪贴板。",
"LABEL_NOTIFICATIONS": "浏览器通知",
"LABEL_NOTIFICATIONS_TOOLTIP": "当有人加入/离开或播放/暂停时显示本机系统通知。",
"LABEL_LANGUAGE": "应用程序语言",
"LABEL_LANGUAGE_TOOLTIP": "选择您喜欢的扩展语言",
"LABEL_TROUBLESHOOTING": "故障排除",
"LABEL_TROUBLESHOOTING_TOOLTIP": "用于修复连接问题的工具",
"BTN_REGEN_ID": "再生Peer ID",
"BTN_REGEN_ID_TOOLTIP": "重新生成您的内部 ID 并重新连接",
"REGEN_ID_DESC": "如果您看到“重复身份”错误,请使用此选项。",
"REGEN_ID_OTHER_ISSUE": "其他问题?打开 GitHub 问题",
"TOAST_ID_REGENERATED": "身份已重新生成 — 正在重新连接…",
"LABEL_CONN_STATUS": "连接状态",
"LABEL_CONN_STATUS_TOOLTIP": "当前WebSocket连接状态",
"CONN_STATUS_DISCONNECTED": "已断开连接",
"BTN_RETRY": "重试",
"BTN_RETRY_TOOLTIP": "尝试重新连接服务器",
"BTN_COPY_LOGS": "复制日志",
"BTN_COPY_LOGS_TOOLTIP": "将日志复制到剪贴板以便共享",
"LABEL_VIDEO_DEBUG": "视频调试信息",
"LABEL_VIDEO_DEBUG_TOOLTIP": "有关当前所选视频元素的技术详细信息",
"VIDEO_DEBUG_EMPTY": "未选择任何选项卡或检测到视频。",
"LABEL_HISTORY": "完整的行动历史",
"LABEL_HISTORY_TOOLTIP": "房间内所有同步命令的时间日志",
"HISTORY_EMPTY": "还没有活动",
"LABEL_LOGS": "日志(最后 50 条)",
"LABEL_LOGS_TOOLTIP": "用于调试的技术连接日志",
"BTN_CLEAR": "清除",
"BTN_CLEAR_TOOLTIP": "清除日志输出",
"LABEL_GITHUB": "GitHub 存储库",
"BTN_ONBOARDING_SKIP": "跳过",
"BTN_ONBOARDING_SKIP_TOOLTIP": "跳过教程",
"BTN_ONBOARDING_NEXT": "下一个",
"BTN_ONBOARDING_NEXT_TOOLTIP": "进入下一步",
"ONBOARDING_1_TITLE": "欢迎来到KoalaSync",
"ONBOARDING_1_TEXT": "无论身在何处,都可以一起看视频。下面是快速介绍。",
"ONBOARDING_2_TITLE": "1. 创建房间",
"ONBOARDING_2_TEXT": "从这里开始。创建一个房间并与您的朋友分享邀请链接。",
"ONBOARDING_3_TITLE": "2. 选择视频",
"ONBOARDING_3_TEXT": "导航此处选择您要同步的视频。播放、暂停和搜索——每个人都保持同步。",
"ONBOARDING_4_TITLE": "3. 个性化",
"ONBOARDING_4_TEXT": "选择一个有趣的用户名,以便您的朋友知道您是谁。",
"ONBOARDING_5_TITLE": "你都准备好了!",
"ONBOARDING_5_TEXT": "是时候去买点爆米花了。一起观赏吧!",
"ERR_CONN_TIMEOUT": "连接超时。请再试一次。",
"ERR_INVALID_SERVER_URL": "服务器 URL 格式无效。",
"ERR_IDENTITY_NOT_LOADED": "身份尚未加载。稍等片刻,然后重试。",
"ERR_NO_PEERS_TIME": "没有其他已知时间的同行。切换到“跳到我这里”。",
"ERR_NO_VIDEO_TAB": "无法连接到视频选项卡。",
"SITE_ACCESS_REQUIRED": "浏览器正在阻止 KoalaSync 访问 {host}。请点击下方的“允许访问”。如果未显示对话框,请在扩展程序或网站权限中允许 KoalaSync 访问此网站。",
"BTN_RETRY_ACCESS": "允许访问",
"ERR_SELECT_VIDEO": "请先选择视频!",
"TOAST_INVITE_COPIED": "邀请链接已复制!",
"TOAST_COPY_FAILED": "无法复制到剪贴板",
"TOAST_LOBBY_SKIPPED": "剧集大厅已跳过。",
"TOAST_LOBBY_SKIP_FAILED": "无法跳过大厅。",
"TOAST_LOGS_COPIED": "复制了!",
"TOAST_PEER_JOINED": "{name} 加入房间",
"TOAST_PEER_LEFT": "{name} 离开房间",
"TOAST_PEER_ACTION": "{name} {action}",
"STATUS_CONNECTED": "已连接",
"STATUS_RECONNECTING": "正在重新连接...",
"STATUS_CONNECTING": "正在连接...",
"STATUS_FAILED": "失败的",
"STATUS_DISCONNECTED": "已断开连接",
"STATUS_IDLE": "已准备好连接",
"STATUS_IDLE_TOOLTIP": "KoalaSync 已准备就绪。加入或创建房间以连接并开始同步。",
"BTN_STATE_JOINING": "🚀 正在加入...",
"BTN_STATE_RECONNECTING": "🔄 正在重新连接...",
"BTN_STATE_PLAYING": "▶ 播放中...",
"BTN_STATE_PAUSING": "⏸ 暂停...",
"BTN_STATE_SYNCING_GROUP": "正在同步到群组 ({time})...",
"BTN_STATE_SYNCING": "正在同步...",
"BTN_STATE_SYNCED": "✅ 已同步!",
"NOTIF_PLAY": "开始播放",
"NOTIF_PAUSE": "暂停播放",
"NOTIF_SEEK": "跳转视频",
"NOTIF_FORCE_PREPARE": "开始强制同步",
"NOTIF_FORCE_EXECUTE": "同步所有人",
"DEBUG_NO_TAB": "未选择目标选项卡。",
"DEBUG_COMM_FAIL": "无法与标签视频通信。",
"EMPTY_PEERS_TITLE": "还没有同行",
"EMPTY_PEERS_HINT": "分享您的邀请链接以开始使用",
"EMPTY_HISTORY_TITLE": "还没有活动",
"EMPTY_HISTORY_HINT": "播放、暂停或跳转以查看历史",
"EMPTY_LOGS_TITLE": "无日志",
"EMPTY_LOGS_HINT": "连接事件将出现在这里",
"EMPTY_ROOMS_TITLE": "没有活动房间",
"EMPTY_ROOMS_HINT": "创建房间或刷新以查找公共房间",
"LABEL_YOU": "你",
"ONBOARDING_DONE": "完毕!",
"LABEL_LOBBY_PEER_READY": "准备好",
"LABEL_LOBBY_PEER_LOADING": "加载中...",
"LABEL_PASSWORD_PROTECTED": "密码保护",
"LABEL_PEERS_COUNT": "{count} 同行",
"LABEL_CUSTOM_SERVER": "定制服务器",
"BTN_STATE_CREATING": "🚀 正在创建房间...",
"NOTIF_LOBBY_CANCEL_TITLE": "KoalaSync — 剧集同步失败",
"NOTIF_LOBBY_CANCEL_MSG": "自动同步取消:{reason}。您可能需要手动同步。",
"LOBBY_CANCEL_TIMEOUT": "暂停",
"LOBBY_CANCEL_TIMEOUT_RECOVERED": "超时(已恢复)",
"LOBBY_CANCEL_PEERS_LEFT": "所有其他同伴都离开了",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "超时——并非所有同行都加载了该剧集",
"LOBBY_CANCEL_USER": "已被用户取消",
"NOTIF_ERROR_TITLE": "KoalaSync 错误",
"FOOTER_SUPPORT": "支持 KoalaSync",
"FOOTER_REVIEW": "★ 评价我们",
"FOOTER_SUPPORT_PROMPT": "喜欢 KoalaSync 吗?留下评论!",
"LABEL_AUDIO_PROCESSING": "音频处理",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "将压缩等音频效果应用于视频播放",
"AUDIO_OPEN_SETTINGS": "打开",
"NEW_FEATURE_AUDIO": "新功能:音频处理 - 尝试压缩器!",
"NEW_FEATURE_THEME": "新功能:主题 — 切换到经典外观或极简石墨风格。",
"AUDIO_BACK": "← 返回",
"AUDIO_PAGE_TITLE": "音频设置",
"AUDIO_MASTER_TOGGLE": "音频处理",
"AUDIO_COMPRESSOR": "压缩器",
"AUDIO_COMPRESSOR_ENABLE": "启用",
"AUDIO_PRESET": "预设",
"AUDIO_PRESET_RECOMMENDED": "推荐",
"AUDIO_PRESET_DYNAMIC_RANGE": "动态范围",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "声音增强",
"AUDIO_PRESET_SMOOTH": "平滑",
"AUDIO_PRESET_CUSTOM": "自定义",
"AUDIO_PARAM_THRESHOLD": "阈值",
"AUDIO_PARAM_KNEE": "拐点",
"AUDIO_PARAM_RATIO": "比率",
"AUDIO_PARAM_ATTACK": "起音",
"AUDIO_PARAM_RELEASE": "释放",
"AUDIO_EQUALIZER": "均衡器",
"AUDIO_COMING_SOON": "即将推出",
"BTN_RESTART_TOUR": "重启教程",
"BTN_RESTART_TOUR_TOOLTIP": "重新启动入门教程",
"HINT_SELECT_VIDEO": "在这里选择您的视频!",
"OPTION_TITLE_PRIVACY_FULL": "发送完整标题",
"OPTION_TITLE_PRIVACY_EPISODE": "发送剧集 ID",
"OPTION_TITLE_PRIVACY_HIDDEN": "不发送",
"LABEL_PRIVACY_SETTINGS": "隐私设置",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "选择要发送到房间的标签页标题和媒体标题。",
"LABEL_SEND_TAB_TITLE": "发送标签页标题",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "将所选浏览器标签页的标题发送到房间。",
"LABEL_MEDIA_TITLE_PRIVACY": "媒体标题",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "选择媒体标题发送到房间的方式。",
"LABEL_SETTINGS_GROUP_PROFILE": "个人资料与外观",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP": "更改您的用户名、主题和语言首选项。",
"LABEL_SETTINGS_GROUP_SYNC": "播放与同步",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP": "自定义播放、通知和音频首选项。",
"LABEL_CHAT_ENABLED": "启用房间聊天",
"LABEL_CHAT_ENABLED_TOOLTIP": "在选定的视频标签页中显示加密的房间聊天。",
"LABEL_SETTINGS_GROUP_CHAT_TOOLTIP": "配置加密房间聊天的外观和位置。",
"CHAT_SETTINGS_NOTE": "可选并采用端到端加密。消息仅实时显示,且不会由中继服务器存储。",
"LABEL_CHAT_POSITION": "位置",
"LABEL_CHAT_POSITION_TOOLTIP": "选择聊天在当前播放器上的固定位置。",
"LABEL_CHAT_SIZE": "浮层大小",
"LABEL_CHAT_SIZE_TOOLTIP": "选择聊天浮层的默认大小。",
"OPTION_CHAT_SIZE_COMPACT": "紧凑",
"OPTION_CHAT_SIZE_STANDARD": "标准",
"OPTION_CHAT_SIZE_LARGE": "大",
"OPTION_CHAT_SIZE_CUSTOM": "上次自定义大小",
"LABEL_CHAT_START_MODE": "启动方式",
"LABEL_CHAT_START_MODE_TOOLTIP": "选择以悬浮气泡或打开的浮层启动聊天。",
"OPTION_CHAT_START_BUBBLE": "悬浮聊天气泡",
"OPTION_CHAT_START_OPEN": "打开浮层",
"CHAT_TITLE": "房间聊天",
"CHAT_LIVE_ONLY": "仅实时显示,无历史记录。",
"CHAT_OPEN": "打开房间聊天",
"CHAT_CLOSE": "关闭聊天",
"CHAT_DOCK_LEFT": "将聊天固定在左侧",
"CHAT_DOCK_RIGHT": "将聊天固定在右侧",
"CHAT_DETACHED": "浮动聊天",
"CHAT_PLACEHOLDER": "输入消息...",
"CHAT_SEND": "发送",
"CHAT_MISSING_KEY": "聊天仅可通过当前邀请链接使用。",
"CHAT_TOO_LONG": "最多500个字符。",
"CHAT_SEND_FAILED": "无法发送消息。",
"CHAT_EMPTY": "暂无消息。聊天仅实时显示。",
"LABEL_CHAT_NOTIFICATIONS": "聊天通知",
"LABEL_CHAT_NOTIFICATIONS_TOOLTIP": "视频标签页未激活时,以系统通知显示新的房间聊天消息。",
"LABEL_EVENT_NOTIFICATIONS": "播放与房间通知",
"LABEL_EVENT_NOTIFICATIONS_TOOLTIP": "有人加入、离开、播放或暂停时显示系统通知。",
"DEBUG_SECTION_VIDEO_DETECTION": "视频检测",
"DEBUG_VIDEO_NOT_FOUND": "未找到视频元素",
"DEBUG_SECTION_MEDIA_SESSION": "Media Session API",
"DEBUG_VIDEO_HINT": "当前是否正在播放视频?媒体加载后,KoalaSync 才能检测视频元素。",
"DEBUG_SECTION_PLAYBACK": "播放",
"DEBUG_STATE_PAUSED": "已暂停",
"DEBUG_STATE_PLAYING": "播放中",
"DEBUG_SECTION_PROPERTIES": "属性",
"DEBUG_SECTION_DIMENSIONS": "尺寸",
"DEBUG_DIMENSIONS_HINT": "0×0 = 视频元素已隐藏或尚未加载",
"DEBUG_SECTION_MEDIA_ERROR": "媒体错误",
"DEBUG_SECTION_DETECTION": "检测",
"DEBUG_SECTION_IDENTIFICATION": "标识",
"DEBUG_SECTION_MEDIA_SOURCE": "媒体来源",
"DEBUG_SECTION_DATA_ATTRIBUTES": "数据属性",
"DEBUG_YES": "是",
"DEBUG_NO": "否",
"DEBUG_YES_CHECKED": "是(已检查)",
"DEBUG_NA": "无"
}
+5 -3
View File
@@ -1,8 +1,10 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "2.2.0",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"default_locale": "en",
"name": "__MSG_appName__",
"short_name": "KoalaSync",
"version": "3.0.1",
"description": "__MSG_appDesc__",
"permissions": [
"storage",
"tabs",
+28
View File
@@ -0,0 +1,28 @@
export function initTabManager({
getCurrentTabId,
reactivateCurrentTarget,
ensureState
}) {
chrome.storage.onChanged.addListener(async (changes, area) => {
if (area !== 'local' || !changes.audioSettings) return;
await ensureState();
const tabId = getCurrentTabId();
if (!tabId) return;
chrome.tabs.sendMessage(tabId, {
action: 'APPLY_AUDIO_SETTINGS',
settings: changes.audioSettings.newValue
}).catch(() => {});
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
await ensureState();
const currentTabId = Number(getCurrentTabId());
if (Number.isInteger(currentTabId)
&& currentTabId > 0
&& tabId === currentTabId
&& changeInfo.status === 'complete') {
reactivateCurrentTarget(tabId).catch(() => {});
}
});
}
+36
View File
@@ -0,0 +1,36 @@
(function(root) {
const PAGE_API_SEEK_FIXES = [
{
name: 'netflix-page-api-seek',
urls: ['netflix.com'],
provider: 'netflix'
},
{
name: 'disney-page-api-seek',
urls: ['disneyplus.com'],
provider: 'disney'
}
];
function normalizeHost(input) {
try {
return new URL(input).hostname.toLowerCase();
} catch (_e) {
return String(input || '').toLowerCase();
}
}
function matchesDomain(host, domain) {
const normalizedDomain = normalizeHost(domain);
return normalizedDomain && (host === normalizedDomain || host.endsWith(`.${normalizedDomain}`));
}
root.KOALA_PAGE_API_SEEK_FIXES = PAGE_API_SEEK_FIXES;
root.KOALA_PAGE_API_SEEK_PROVIDERS = PAGE_API_SEEK_FIXES;
root.koalaFindPageApiSeekProvider = (input) => {
const host = normalizeHost(input);
return PAGE_API_SEEK_FIXES.find(entry =>
Array.isArray(entry.urls) && entry.urls.some(url => matchesDomain(host, url))
) || null;
};
})(globalThis);
+1345 -186
View File
File diff suppressed because it is too large Load Diff
+1290 -224
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
(() => {
const systemTheme = window.matchMedia('(prefers-color-scheme: light)');
let mode = 'system';
let palette = 'eucalyptus';
const normalize = value => ['system', 'dark', 'light'].includes(value) ? value : 'system';
const normalizePalette = value => ['eucalyptus', 'cyber', 'graphite'].includes(value) ? value : 'eucalyptus';
const apply = value => {
mode = normalize(value);
const light = mode === 'light' || (mode === 'system' && systemTheme.matches);
document.documentElement.classList.toggle('theme-light', light);
document.documentElement.dataset.theme = light ? 'light' : 'dark';
document.documentElement.style.colorScheme = light ? 'light' : 'dark';
};
const applyPalette = value => {
palette = normalizePalette(value);
document.documentElement.dataset.palette = palette;
};
apply('system');
applyPalette('eucalyptus');
window.koalaTheme = {
getMode: () => mode,
setMode: apply,
getPalette: () => palette,
setPalette: applyPalette
};
systemTheme.addEventListener('change', () => {
if (mode === 'system') apply('system');
});
if (!globalThis.chrome?.storage?.local) return;
chrome.storage.local.get(['themeMode', 'themePalette'], data => {
apply(data.themeMode);
applyPalette(data.themePalette);
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'local') return;
if (changes.themeMode) apply(changes.themeMode.newValue);
if (changes.themePalette) applyPalette(changes.themePalette.newValue);
});
})();
+51
View File
@@ -0,0 +1,51 @@
import { extractEpisodeId } from './episode-utils.js';
export const TITLE_PRIVACY_MODES = Object.freeze({
FULL: 'full',
EPISODE: 'episode',
HIDDEN: 'hidden'
});
export function normalizeTitlePrivacyMode(mode) {
return Object.values(TITLE_PRIVACY_MODES).includes(mode)
? mode
: TITLE_PRIVACY_MODES.FULL;
}
export function normalizeSendTabTitle(sendTabTitle, legacyMode = TITLE_PRIVACY_MODES.FULL) {
if (typeof sendTabTitle === 'boolean') return sendTabTitle;
return normalizeTitlePrivacyMode(legacyMode) === TITLE_PRIVACY_MODES.FULL;
}
export function normalizeTabTitle(title) {
if (typeof title !== 'string') return null;
const normalized = title.replace(/^\s*(?:\(\d+\+?\)|\[\d+\+?\])\s+/, '').trim();
return normalized.length > 0 ? normalized : null;
}
export function sanitizeTabTitle(title, sendTabTitle) {
if (!sendTabTitle) return null;
return normalizeTabTitle(title);
}
export function sanitizeSharedTitle(title, mode) {
const normalizedMode = normalizeTitlePrivacyMode(mode);
if (normalizedMode === TITLE_PRIVACY_MODES.HIDDEN) return null;
if (typeof title !== 'string' || title.length === 0) return null;
if (normalizedMode === TITLE_PRIVACY_MODES.EPISODE) {
return extractEpisodeId(title) || null;
}
return title;
}
export function applyTitlePrivacyToPayload(payload, mode, keys = ['mediaTitle', 'expectedTitle', 'title']) {
const source = payload && typeof payload === 'object' ? payload : {};
const next = { ...source };
keys.forEach(key => {
if (Object.prototype.hasOwnProperty.call(next, key)) {
next[key] = sanitizeSharedTitle(next[key], mode);
}
});
return next;
}
+3648 -715
View File
File diff suppressed because it is too large Load Diff
+27 -8
View File
@@ -1,20 +1,39 @@
{
"name": "koalasync",
"version": "2.2.0",
"version": "3.0.1",
"description": "KoalaSync Build Scripts",
"private": true,
"type": "module",
"engines": {
"node": ">=20.9.0"
},
"scripts": {
"build:extension": "node scripts/build-extension.js",
"build:extension": "node scripts/build-extension.cjs",
"indexnow": "node website/submit-indexnow.cjs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"subset-flags": "node website/tools/subset-flag-font.mjs",
"test": "npm run verify",
"test:unit": "vitest run",
"verify": "node scripts/verify-release.mjs"
},
"devDependencies": {
"archiver": "^7.0.1",
"esbuild": "^0.28.0",
"eslint": "^10.4.0",
"fs-extra": "^11.2.0",
"sharp": "^0.34.5",
"svgo": "^4.0.1"
"@playwright/test": "^1.62.0",
"@vitest/coverage-v8": "^4.1.10",
"addons-linter": "^10.9.0",
"archiver": "^8.0.0",
"esbuild": "^0.28.1",
"eslint": "^10.8.0",
"eslint-plugin-no-unsanitized": "^4.1.5",
"fs-extra": "^11.4.0",
"htmlnano": "^3.4.0",
"sharp": "^0.35.3",
"svgo": "^4.0.2",
"vitest": "^4.1.10"
},
"overrides": {
"brace-expansion": "5.0.8",
"fast-uri": "3.1.4",
"postcss": "8.5.18"
}
}
+82 -22
View File
@@ -1,33 +1,93 @@
# Development Scripts
This directory contains utility scripts for the KoalaSync development workflow.
This directory contains build, synchronization, and verification scripts for the KoalaSync workspace. Run all commands from the repository root unless a script says otherwise.
## build-extension.js
The primary build tool for KoalaSync. This Node.js script automates two critical tasks:
1. **Protocol Synchronization**: Copies the "Single Source of Truth" constants (`shared/constants.js`) and the domain blacklist (`shared/blacklist.js`) from the root `/shared` directory into the `extension/shared/` directory.
2. **Content Script Injection**: Injects protocol constants directly into `content.js` using marker-based replacement. This is necessary because `content.js` executes synchronously and cannot use ES module imports.
3. **Artifact Generation**: Compiles the extension into browser-specific bundles for Chrome and Firefox, located in the `dist/` directory.
### Usage
From the **repository root**, run:
## Main Commands
```bash
node scripts/build-extension.js
npm run build:extension
npm run verify
npm run lint
npm run test:unit
```
### Why this script exists
KoalaSync uses **Vanilla JS** in the extension to maintain zero runtime dependencies and maximum privacy. Since we don't use a bundler (like Webpack or Vite) inside the extension, this script serves as our lightweight "pre-build" step to ensure that the protocol constants remain synchronized between the extension and the relay server.
- `npm run build:extension` runs `scripts/build-extension.cjs`.
- `npm run verify` runs the full release-safety suite in `scripts/verify-release.mjs`.
- `npm run lint` runs ESLint across the repository.
- `npm run test:unit` runs Vitest tests.
### Content Injection Markers
## build-extension.cjs
The build script uses marker comments in `content.js` to locate and replace constant blocks:
The primary extension build tool performs these steps:
| Marker Pair | Injected Value | Source |
|:---|:---|:---|
| `SHARED_EVENTS_INJECT_START` / `END` | The full `EVENTS` object | `shared/constants.js` |
| `SHARED_HEARTBEAT_INJECT_START` / `END` | `HEARTBEAT_INTERVAL` value | `shared/constants.js` |
1. Recreates `dist/`.
2. Copies `shared/constants.js`, `shared/blacklist.js`, `shared/names.js`, and `shared/README.md` into `extension/shared/`.
3. Injects synchronous shared values into `content.js`.
4. Injects browser-specific uninstall URL constants into `background.js`.
5. Injects the build timestamp into `popup.html`.
6. Generates browser-specific manifests for Chrome and Firefox.
7. Creates `dist/koalasync-chrome.zip` and `dist/koalasync-firefox.zip`.
> **⚠️ Do NOT remove or modify these marker comments in `content.js`.** They are required for the build script to function. If the markers are missing, the build will fail with a clear error message.
Usage:
```bash
node scripts/build-extension.cjs
# or
npm run build:extension
```
## Injection Markers
The build script uses marker comments/placeholders. Missing markers are a hard build failure so release artifacts cannot silently contain stale protocol data.
| Target | Marker / Placeholder | Injected Value | Source |
|:---|:---|:---|:---|
| `content.js` | `SHARED_EVENTS_INJECT_START` / `END` | Full `EVENTS` object | `shared/constants.js` |
| `content.js` | `SHARED_HEARTBEAT_INJECT_START` / `END` | `HEARTBEAT_INTERVAL` | `shared/constants.js` |
| `content.js` | `SHARED_EPISODE_UTILS_INJECT_START` / `END` | `extractEpisodeId()` and `sameEpisode()` | `extension/episode-utils.js` |
| `background.js` | `UNINSTALL_URL_INJECT_START` / `END` | Uninstall URL and browser type | `scripts/build-extension.cjs` |
| `popup.html` | `__BUILD_TIMESTAMP__` | UTC build timestamp | Build time |
Do not remove or rename these markers without updating the build script and tests.
## Verification Suite
`scripts/verify-release.mjs` is the best single command before release, PR review, or handoff:
```bash
npm run verify
```
It currently runs:
- Vitest unit tests.
- Server ops, route, WebSocket, and rate-limiter checks.
- Episode parser, title privacy, audio settings, popup cooldown, names, and content-video-finder checks.
- JavaScript syntax checks for server and extension entry points.
- Extension and website locale coverage checks.
- ESLint.
- Production `npm audit` checks for root and server dependencies.
- Extension build and website build.
## Focused Scripts
| Script | Purpose |
|:---|:---|
| `test-server-ops.mjs` | Health payload and admin metrics helpers |
| `test-server-routes.mjs` | HTTP health routes, caching, and admin metrics access |
| `test-server-ws.mjs` | Socket.IO relay integration, including host-control behavior |
| `test-rate-limiter.mjs` | Rate-limiter map and cooldown behavior |
| `test-episode-utils.mjs` | Episode-title extraction and comparison |
| `test-title-privacy.mjs` | Tab/media title privacy sanitization |
| `test-audio-settings.mjs` | Audio settings defaults and normalization |
| `test-popup-refresh-cooldown.mjs` | Popup refresh throttling behavior |
| `test-names.mjs` | Generated username format and coverage |
| `test-content-video-finder.cjs` | Content-script video selection helpers |
| `test-locales.cjs` | Extension runtime and browser-store locale coverage |
| `test-website-locales.mjs` | Website locale coverage |
## Do Not Break
- Keep scripts runnable from the repository root.
- Keep build output under `dist/` and generated website output under `website/www/`.
- Keep shared protocol sync automated; do not add manual copy steps.
- Treat warnings in verification scripts as release blockers unless the script explicitly documents them as informational.
+274
View File
@@ -0,0 +1,274 @@
const fs = require('fs');
const path = require('path');
const rootDir = path.join(__dirname, '..');
const extDir = path.join(rootDir, 'extension');
const distDir = path.join(rootDir, 'dist');
const baseManifestPath = path.join(extDir, 'manifest.base.json');
const localesDir = path.join(extDir, '_locales');
const requiredAppName = 'Watch Party - KoalaSync';
const maxAppNameLength = 75;
const maxAppDescLength = 132;
function countUnicodeCharacters(value) {
return Array.from(value).length;
}
function validateExtensionMetadata() {
const localeDirectories = fs.readdirSync(localesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
for (const locale of localeDirectories) {
const messagesPath = path.join(localesDir, locale, 'messages.json');
let messages;
try {
messages = JSON.parse(fs.readFileSync(messagesPath, 'utf8'));
} catch (error) {
throw new Error(`CRITICAL: Invalid locale JSON at _locales/${locale}/messages.json: ${error.message}`);
}
if (!messages || typeof messages !== 'object' || Array.isArray(messages)) {
throw new Error(`CRITICAL: _locales/${locale}/messages.json must contain a JSON object`);
}
for (const key of ['appName', 'appDesc']) {
if (!messages[key] || typeof messages[key].message !== 'string' || messages[key].message.trim().length === 0) {
throw new Error(`CRITICAL: _locales/${locale}/messages.json is missing a valid ${key}.message`);
}
}
const appNameLength = countUnicodeCharacters(messages.appName.message);
const appDescLength = countUnicodeCharacters(messages.appDesc.message);
if (appNameLength > maxAppNameLength) {
throw new Error(`CRITICAL: _locales/${locale}/messages.json appName.message is ${appNameLength} characters; maximum is ${maxAppNameLength}`);
}
if (messages.appName.message !== requiredAppName) {
throw new Error(`CRITICAL: _locales/${locale}/messages.json appName.message must exactly equal "${requiredAppName}"`);
}
if (appDescLength > maxAppDescLength) {
throw new Error(`CRITICAL: _locales/${locale}/messages.json appDesc.message is ${appDescLength} characters; maximum is ${maxAppDescLength}`);
}
}
console.log(`✓ Validated localized extension metadata for ${localeDirectories.length} locales`);
}
validateExtensionMetadata();
// Ensure dist directory exists
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true });
}
fs.mkdirSync(distDir, { recursive: true });
// Sync shared constants from root /shared to /extension/shared
console.log('Syncing protocol constants...');
const masterSharedDir = path.join(rootDir, 'shared');
const extSharedDir = path.join(extDir, 'shared');
if (!fs.existsSync(extSharedDir)) {
fs.mkdirSync(extSharedDir, { recursive: true });
}
const sharedFiles = ['constants.js', 'blacklist.js', 'names.js', 'invite-links.js', 'README.md'];
for (const file of sharedFiles) {
const src = path.join(masterSharedDir, file);
const dest = path.join(extSharedDir, file);
if (!fs.existsSync(src)) {
throw new Error(`CRITICAL: Source shared file missing: ${src}. Aborting build to prevent broken artifacts.`);
}
fs.copyFileSync(src, dest);
}
console.log('✓ shared runtime files synced to extension/shared/');
// Read the base manifest
const baseManifest = JSON.parse(fs.readFileSync(baseManifestPath, 'utf8'));
function replaceRequiredBlock(content, pattern, replacement, description) {
if (!pattern.test(content)) {
throw new Error(`CRITICAL: ${description} markers not found. Aborting build to prevent stale artifacts.`);
}
return content.replace(pattern, replacement);
}
// Helper to copy files, ignoring manifest.json and manifest.base.json
// Also injects shared constants into content.js
function copyExtensionFiles(targetDir, browserName) {
fs.mkdirSync(targetDir, { recursive: true });
// Read master constants for injection
const masterConstantsPath = path.join(rootDir, 'shared', 'constants.js');
const constantsContent = fs.readFileSync(masterConstantsPath, 'utf8');
// Robust Extraction using flexible regex
const eventsMatch = constantsContent.match(/export const EVENTS\s*=\s*({[\s\S]+?});/);
const heartbeatMatch = constantsContent.match(/export const HEARTBEAT_INTERVAL\s*=\s*(\d+);/);
if (!eventsMatch) {
throw new Error('CRITICAL: Could not find EVENTS object in shared/constants.js');
}
if (!heartbeatMatch) {
throw new Error('CRITICAL: Could not find HEARTBEAT_INTERVAL in shared/constants.js');
}
const eventsObject = eventsMatch[1];
const heartbeatVal = heartbeatMatch[1];
const items = fs.readdirSync(extDir);
for (const item of items) {
if (item === 'manifest.json' || item === 'manifest.base.json') continue;
const srcPath = path.join(extDir, item);
const destPath = path.join(targetDir, item);
if (fs.lstatSync(srcPath).isDirectory()) {
fs.cpSync(srcPath, destPath, { recursive: true });
} else {
if (item === 'content.js') {
// Perform injection
let content = fs.readFileSync(srcPath, 'utf8');
// 1. Inject Events
const eStart = '// --- SHARED_EVENTS_INJECT_START ---';
const eEnd = '// --- SHARED_EVENTS_INJECT_END ---';
const ePattern = new RegExp(`${eStart}[\\s\\S]+?${eEnd}`);
const eRep = `${eStart}\n // This block is automatically updated by /scripts/build-extension.cjs\n const EVENTS = ${eventsObject};\n ${eEnd}`;
content = replaceRequiredBlock(content, ePattern, eRep, 'Event injection');
// 2. Inject Heartbeat
const hStart = '// --- SHARED_HEARTBEAT_INJECT_START ---';
const hEnd = '// --- SHARED_HEARTBEAT_INJECT_END ---';
const hPattern = new RegExp(`${hStart}[\\s\\S]+?${hEnd}`);
const hRep = `${hStart}\n const HEARTBEAT_INTERVAL_VAL = ${heartbeatVal};\n ${hEnd}`;
content = replaceRequiredBlock(content, hPattern, hRep, 'Heartbeat injection');
// 3. Inject Episode Utils
const euStart = '// --- SHARED_EPISODE_UTILS_INJECT_START ---';
const euEnd = '// --- SHARED_EPISODE_UTILS_INJECT_END ---';
const euPattern = new RegExp(euStart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\s\\S]+?' + euEnd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const euPath = path.join(rootDir, 'extension', 'episode-utils.js');
if (!fs.existsSync(euPath)) {
throw new Error(`CRITICAL: Episode utils source missing: ${euPath}. Aborting build.`);
}
const euContent = fs.readFileSync(euPath, 'utf8');
const stripped = euContent
.replace(/^\/\*\*[\s\S]*?\*\/\s*/m, '')
.replace(/export function /g, 'function ')
.trim();
const euRep = `${euStart}\n // This block is automatically updated by /scripts/build-extension.cjs\n${stripped.split('\n').map(l => ' ' + l).join('\n')}\n ${euEnd}`;
content = replaceRequiredBlock(content, euPattern, euRep, 'Episode utils injection');
fs.writeFileSync(destPath, content);
console.log('✓ Injected shared constants into content.js');
} else if (item === 'background.js') {
let content = fs.readFileSync(srcPath, 'utf8');
// 3. Inject Uninstall URL Constants
const uStart = '// --- UNINSTALL_URL_INJECT_START ---';
const uEnd = '// --- UNINSTALL_URL_INJECT_END ---';
const uPattern = new RegExp(`${uStart}[\\s\\S]+?${uEnd}`);
const placeholderUrl = "https://bye.koalastuff.net/c/camp_99ztjRVbK1BNN2RU";
let uRep = `${uStart}\n // This block is automatically updated by /scripts/build-extension.cjs\n`;
uRep += ` const UNINSTALL_URL = "${placeholderUrl}";\n`;
uRep += ` const BROWSER_TYPE = "${browserName}";\n`;
uRep += ` ${uEnd}`;
content = replaceRequiredBlock(content, uPattern, uRep, 'Uninstall URL injection');
fs.writeFileSync(destPath, content);
console.log(`✓ Injected uninstall URL constants for ${browserName} into background.js`);
} else if (item === 'popup.html') {
let content = fs.readFileSync(srcPath, 'utf8');
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
if (!content.includes('__BUILD_TIMESTAMP__')) {
throw new Error('CRITICAL: Build timestamp placeholder not found in popup.html. Aborting build.');
}
content = content.replace(/__BUILD_TIMESTAMP__/g, timestamp);
fs.writeFileSync(destPath, content);
console.log(`✓ Injected build timestamp into popup.html: ${timestamp}`);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
}
// Helper to zip a directory
async function zipDirectory(sourceDir, outPath) {
const { ZipArchive } = await import('archiver');
return new Promise((resolve, reject) => {
const archive = new ZipArchive({ zlib: { level: 9 } });
const stream = fs.createWriteStream(outPath);
archive.on('error', reject);
stream.on('close', () => resolve());
stream.on('error', reject);
archive.pipe(stream);
archive.directory(sourceDir, false);
void archive.finalize().catch(reject);
});
}
async function buildBrowser(browserName, manifestModifier) {
console.log(`Building for ${browserName}...`);
const browserDistDir = path.join(distDir, browserName);
// 1. Copy files
copyExtensionFiles(browserDistDir, browserName);
// 2. Modify and write manifest
const browserManifest = manifestModifier(JSON.parse(JSON.stringify(baseManifest)));
fs.writeFileSync(
path.join(browserDistDir, 'manifest.json'),
JSON.stringify(browserManifest, null, 2)
);
// 3. Zip it
const zipPath = path.join(distDir, `koalasync-${browserName}.zip`);
await zipDirectory(browserDistDir, zipPath);
console.log(`Successfully built and zipped ${browserName} -> ${zipPath}`);
}
async function run() {
try {
// Build Chrome
await buildBrowser('chrome', (manifest) => {
manifest.background = {
service_worker: "background.js",
type: "module"
};
return manifest;
});
// Build Firefox
await buildBrowser('firefox', (manifest) => {
manifest.background = {
scripts: ["background.js"],
type: "module"
};
manifest.browser_specific_settings = {
gecko: {
id: "koalasync@koalastuff.net",
data_collection_permissions: {
required: ["none"]
}
}
};
return manifest;
});
console.log('Build complete!');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
run();
-180
View File
@@ -1,180 +0,0 @@
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const rootDir = path.join(__dirname, '..');
const extDir = path.join(rootDir, 'extension');
const distDir = path.join(rootDir, 'dist');
const baseManifestPath = path.join(extDir, 'manifest.base.json');
// Ensure dist directory exists
if (fs.existsSync(distDir)) {
fs.rmSync(distDir, { recursive: true, force: true });
}
fs.mkdirSync(distDir, { recursive: true });
// Sync shared constants from root /shared to /extension/shared
console.log('Syncing protocol constants...');
const masterSharedDir = path.join(rootDir, 'shared');
const extSharedDir = path.join(extDir, 'shared');
if (!fs.existsSync(extSharedDir)) {
fs.mkdirSync(extSharedDir, { recursive: true });
}
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);
if (!fs.existsSync(src)) {
throw new Error(`CRITICAL: Source shared file missing: ${src}. Aborting build to prevent broken artifacts.`);
}
fs.copyFileSync(src, dest);
}
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'));
// Helper to copy files, ignoring manifest.json and manifest.base.json
// Also injects shared constants into content.js
function copyExtensionFiles(targetDir) {
fs.mkdirSync(targetDir, { recursive: true });
// Read master constants for injection
const masterConstantsPath = path.join(rootDir, 'shared', 'constants.js');
const constantsContent = fs.readFileSync(masterConstantsPath, 'utf8');
// Robust Extraction using flexible regex
const eventsMatch = constantsContent.match(/export const EVENTS\s*=\s*({[\s\S]+?});/);
const heartbeatMatch = constantsContent.match(/export const HEARTBEAT_INTERVAL\s*=\s*(\d+);/);
if (!eventsMatch) {
throw new Error('CRITICAL: Could not find EVENTS object in shared/constants.js');
}
if (!heartbeatMatch) {
throw new Error('CRITICAL: Could not find HEARTBEAT_INTERVAL in shared/constants.js');
}
const eventsObject = eventsMatch[1];
const heartbeatVal = heartbeatMatch[1];
const items = fs.readdirSync(extDir);
for (const item of items) {
if (item === 'manifest.json' || item === 'manifest.base.json') continue;
const srcPath = path.join(extDir, item);
const destPath = path.join(targetDir, item);
if (fs.lstatSync(srcPath).isDirectory()) {
fs.cpSync(srcPath, destPath, { recursive: true });
} else {
if (item === 'content.js') {
// Perform injection
let content = fs.readFileSync(srcPath, 'utf8');
// 1. Inject Events
const eStart = '// --- SHARED_EVENTS_INJECT_START ---';
const eEnd = '// --- SHARED_EVENTS_INJECT_END ---';
const ePattern = new RegExp(`${eStart}[\\s\\S]+?${eEnd}`);
const eRep = `${eStart}\n // This block is automatically updated by /scripts/build-extension.js\n const EVENTS = ${eventsObject};\n ${eEnd}`;
if (ePattern.test(content)) {
content = content.replace(ePattern, eRep);
} else {
console.warn('⚠️ WARNING: Event markers not found in content.js');
}
// 2. Inject Heartbeat
const hStart = '// --- SHARED_HEARTBEAT_INJECT_START ---';
const hEnd = '// --- SHARED_HEARTBEAT_INJECT_END ---';
const hPattern = new RegExp(`${hStart}[\\s\\S]+?${hEnd}`);
const hRep = `${hStart}\n const HEARTBEAT_INTERVAL_VAL = ${heartbeatVal};\n ${hEnd}`;
if (hPattern.test(content)) {
content = content.replace(hPattern, hRep);
} else {
console.warn('⚠️ WARNING: Heartbeat markers not found in content.js');
}
fs.writeFileSync(destPath, content);
console.log('✓ Injected shared constants into content.js');
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
}
// Helper to zip a directory
function zipDirectory(sourceDir, outPath) {
return new Promise((resolve, reject) => {
const archive = archiver('zip', { zlib: { level: 9 } });
const stream = fs.createWriteStream(outPath);
archive
.directory(sourceDir, false)
.on('error', err => reject(err))
.pipe(stream);
stream.on('close', () => resolve());
archive.finalize();
});
}
async function buildBrowser(browserName, manifestModifier) {
console.log(`Building for ${browserName}...`);
const browserDistDir = path.join(distDir, browserName);
// 1. Copy files
copyExtensionFiles(browserDistDir);
// 2. Modify and write manifest
const browserManifest = manifestModifier(JSON.parse(JSON.stringify(baseManifest)));
fs.writeFileSync(
path.join(browserDistDir, 'manifest.json'),
JSON.stringify(browserManifest, null, 2)
);
// 3. Zip it
const zipPath = path.join(distDir, `koalasync-${browserName}.zip`);
await zipDirectory(browserDistDir, zipPath);
console.log(`Successfully built and zipped ${browserName} -> ${zipPath}`);
}
async function run() {
try {
// Build Chrome
await buildBrowser('chrome', (manifest) => {
manifest.background = {
service_worker: "background.js",
type: "module"
};
return manifest;
});
// Build Firefox
await buildBrowser('firefox', (manifest) => {
manifest.background = {
scripts: ["background.js"],
type: "module"
};
manifest.browser_specific_settings = {
gecko: {
id: "koalasync@koalastuff.net",
data_collection_permissions: {
required: ["none"]
}
}
};
return manifest;
});
console.log('Build complete!');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
run();
+65
View File
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const [popupHtml, popupJs, overlayJs, backgroundJs] = await Promise.all([
readFile(path.join(repoRoot, 'extension', 'popup.html'), 'utf8'),
readFile(path.join(repoRoot, 'extension', 'popup.js'), 'utf8'),
readFile(path.join(repoRoot, 'extension', 'chat-overlay.js'), 'utf8'),
readFile(path.join(repoRoot, 'extension', 'background.js'), 'utf8')
]);
function detailsSection(marker) {
const markerIndex = popupHtml.indexOf(marker);
assert.notEqual(markerIndex, -1, `missing settings marker: ${marker}`);
const start = popupHtml.lastIndexOf('<details', markerIndex);
const end = popupHtml.indexOf('</details>', markerIndex);
assert.ok(start >= 0 && end > markerIndex, `invalid details section: ${marker}`);
return popupHtml.slice(start, end + '</details>'.length);
}
const chatSection = detailsSection('data-i18n="CHAT_TITLE"');
const syncSection = detailsSection('data-i18n="LABEL_SETTINGS_GROUP_SYNC"');
for (const id of ['chatEnabled', 'chatNotifications', 'chatPosition', 'chatSize', 'chatStartMode']) {
assert.match(chatSection, new RegExp(`id="${id}"`), `${id} belongs in the dedicated chat settings section`);
}
assert.doesNotMatch(syncSection, /id="chatEnabled"/, 'chat enablement must not remain under Playback & Sync');
for (const value of ['right', 'left', 'detached']) {
assert.match(chatSection, new RegExp(`<option value="${value}"`), `chat position supports ${value}`);
}
for (const value of ['compact', 'standard', 'large', 'custom']) {
assert.match(chatSection, new RegExp(`<option value="${value}"`), `chat size supports ${value}`);
}
for (const value of ['bubble', 'open']) {
assert.match(chatSection, new RegExp(`<option value="${value}"`), `chat start mode supports ${value}`);
}
for (const key of ['chatEnabled', 'chatNotifications', 'chatPosition', 'chatSize', 'chatStartMode']) {
assert.match(popupJs, new RegExp(`chrome\\.storage\\.local\\.set\\(\\{ ${key}`), `popup persists ${key}`);
assert.ok(backgroundJs.includes(`'${key}'`), `legacy sync purge includes ${key}`);
}
for (const key of ['chatPosition', 'chatSize', 'chatStartMode']) {
assert.ok(overlayJs.includes(`'${key}'`), `overlay reads ${key}`);
}
assert.match(backgroundJs, /chatEnabled:\s*data\.chatEnabled === true/, 'background reads chatEnabled with an off-by-default contract');
assert.match(backgroundJs, /chatNotifications:\s*data\.chatNotifications !== false/, 'chat message notifications default on');
assert.match(popupJs, /elements\.chatNotifications\.checked = localData\.chatNotifications !== false/, 'popup shows chat notifications on by default');
assert.match(popupJs, /elements\.browserNotifications\.checked = localData\.browserNotifications === true/, 'activity notifications remain off by default');
assert.match(overlayJs, /context\?\.enabled/, 'overlay consumes chat enablement from the validated background context');
assert.match(overlayJs, /SIZE_PRESETS[\s\S]*compact[\s\S]*standard[\s\S]*large/, 'overlay defines all size presets');
assert.match(
overlayJs,
/} else {\s*layout\.width = Number\(layout\.customWidth\) \|\| DEFAULT_WIDTH;\s*layout\.height = Number\(layout\.customHeight\) \|\| DEFAULT_HEIGHT;/,
'custom size restores its dedicated dimensions'
);
assert.match(overlayJs, /layout\.customWidth = rect\.width[\s\S]*layout\.customHeight = rect\.height/, 'detached resize updates the custom-size snapshot');
assert.match(overlayJs, /changes\.chatPosition[\s\S]*setMode/, 'position changes apply live');
assert.match(overlayJs, /changes\.chatSize[\s\S]*setSize/, 'size changes apply live');
assert.match(overlayJs, /changes\.chatStartMode[\s\S]*setOpened/, 'startup-mode changes apply live');
console.log('chat settings tests passed');

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