Compare commits

...

5 Commits

Author SHA1 Message Date
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
26 changed files with 1152 additions and 696 deletions
+16
View File
@@ -4,6 +4,22 @@ All notable changes to the KoalaSync browser extension and relay server.
---
## [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
+2 -1
View File
@@ -452,7 +452,8 @@ function emitEpisodeLobbyForCurrentPrivacy() {
// removes legacy keys that older versions wrote to sync (and that would
// otherwise be redistributed across devices and resurrected on reinstall).
const LEGACY_SYNC_KEYS = [
'serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey', 'username',
'serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey',
'chatEnabled', 'chatPosition', 'chatSize', 'chatStartMode', 'username',
'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode',
'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings',
'titlePrivacyMode', 'sendTabTitle', 'mediaTitlePrivacyMode'
+106 -13
View File
@@ -9,6 +9,11 @@
const MIN_HEIGHT = 320;
const DEFAULT_WIDTH = 360;
const DEFAULT_HEIGHT = 520;
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;
@@ -16,10 +21,24 @@
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 layout = { mode: 'right', x: 24, y: 72, width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, detachedInitialized: false };
let layout = {
mode: 'right',
x: 24,
y: 72,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
customWidth: DEFAULT_WIDTH,
customHeight: DEFAULT_HEIGHT,
detachedInitialized: false
};
let chatPosition = 'right';
let chatSize = 'standard';
let chatStartMode = 'bubble';
let themeMode = 'system';
let themePalette = 'eucalyptus';
@@ -243,6 +262,21 @@
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 clampDetached() {
const viewport = viewportBounds();
const maxWidth = Math.max(1, Math.min(600, viewport.width - 16));
@@ -265,6 +299,7 @@
function applyLayout() {
applyingLayout = true;
const size = preferredSize();
panel.style.right = 'auto';
panel.style.left = 'auto';
panel.style.bottom = 'auto';
@@ -288,8 +323,8 @@
const gutter = Math.min(16, Math.max(0, Math.floor((viewport.width - 1) / 2)));
const panelTop = viewport.height < MIN_HEIGHT + 80 ? Math.min(8, Math.max(0, viewport.height - 1)) : 64;
panel.style.top = `${panelTop}px`;
panel.style.width = `${Math.max(1, Math.min(DEFAULT_WIDTH, viewport.width - gutter * 2))}px`;
panel.style.height = `${Math.max(1, Math.min(560, viewport.height - panelTop - Math.min(16, Math.max(0, viewport.height - panelTop - 1))))}px`;
panel.style.width = `${Math.max(1, Math.min(size.width, viewport.width - gutter * 2))}px`;
panel.style.height = `${Math.max(1, Math.min(size.height, viewport.height - panelTop - Math.min(16, Math.max(0, viewport.height - panelTop - 1))))}px`;
panel.style[layout.mode] = `${gutter}px`;
launcher.style.top = `${Math.max(0, Math.min(viewport.height - 48, viewport.height / 2 - 24))}px`;
launcher.style.left = layout.mode === 'left' ? `${gutter}px` : 'auto';
@@ -298,16 +333,35 @@
globalThis.queueMicrotask(() => { applyingLayout = false; });
}
function setMode(mode) {
if (!['left', 'right', 'detached'].includes(mode)) return;
function setMode(mode, persistPreference = true) {
mode = normalizePosition(mode);
if (mode === 'detached' && !layout.detachedInitialized) {
layout.x = Math.max(8, Math.round((window.innerWidth - DEFAULT_WIDTH) / 2));
layout.y = Math.max(8, Math.round((window.innerHeight - DEFAULT_HEIGHT) / 2));
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) {
@@ -328,10 +382,19 @@
const hasKey = !!context?.hasKey;
const connected = !!context?.connected;
context = context ? { ...context, enabled: supported && optedIn && hasKey && connected } : null;
if (previousRoomId && previousRoomId !== context?.roomId) clearMessages();
if (previousRoomId && previousRoomId !== context?.roomId) {
clearMessages();
startStateApplied = false;
}
host.style.display = supported && optedIn ? '' : 'none';
launcher.setAttribute('aria-disabled', String(!context?.enabled));
if (!context?.enabled) setOpened(false);
if (!optedIn) startStateApplied = false;
if (!context?.enabled) {
setOpened(false);
} else if (preferencesLoaded && !startStateApplied) {
startStateApplied = true;
setOpened(chatStartMode === 'open');
}
applyStrings();
applyLayout();
}
@@ -451,8 +514,15 @@
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();
});
@@ -481,6 +551,15 @@
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();
}
@@ -516,16 +595,30 @@
systemTheme.addEventListener('change', handleSystemTheme);
chrome.storage.onChanged.addListener(handleStorage);
chrome.runtime.onMessage.addListener(handleRuntime);
chrome.storage.local.get([storageKey, 'themeMode', 'themePalette'], data => {
if (data[storageKey] && typeof data[storageKey] === 'object') layout = { ...layout, ...data[storageKey] };
if (!['left', 'right', 'detached'].includes(layout.mode)) layout.mode = 'right';
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 };
refresh();
})();
+14
View File
@@ -248,6 +248,20 @@
"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",
+14
View File
@@ -248,6 +248,20 @@
"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",
+14
View File
@@ -248,6 +248,20 @@
"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",
+14
View File
@@ -248,6 +248,20 @@
"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",
+14
View File
@@ -248,6 +248,20 @@
"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 mobile",
"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",
+14
View File
@@ -248,6 +248,20 @@
"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": "ルームチャットを開く",
+14
View File
@@ -248,6 +248,20 @@
"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": "방 채팅 열기",
+14
View File
@@ -248,6 +248,20 @@
"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",
+14
View File
@@ -248,6 +248,20 @@
"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",
+14
View File
@@ -248,6 +248,20 @@
"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",
+14
View File
@@ -248,6 +248,20 @@
"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 ponto a ponto. 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",
+14
View File
@@ -248,6 +248,20 @@
"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": "Открыть чат комнаты",
+14
View File
@@ -248,6 +248,20 @@
"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ç",
+14
View File
@@ -248,6 +248,20 @@
"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": "Відкрити чат кімнати",
+14
View File
@@ -248,6 +248,20 @@
"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": "打开房间聊天",
+50 -7
View File
@@ -1215,6 +1215,12 @@
line-height: 1.45;
text-align: left;
}
[data-chat-setting] {
transition: opacity 0.2s;
}
[data-chat-setting].is-disabled {
opacity: 0.5;
}
.settings-issue-link {
margin: 8px 0 2px;
padding-top: 9px;
@@ -1615,6 +1621,50 @@
</div>
</details>
<details class="form-group" name="settings-accordion">
<summary title="Configure encrypted room chat appearance and placement." data-i18n="CHAT_TITLE" data-i18n-title="LABEL_SETTINGS_GROUP_CHAT_TOOLTIP">Room Chat</summary>
<div class="details-content">
<p class="settings-note" data-i18n="CHAT_SETTINGS_NOTE">Optional and end-to-end encrypted. Messages are live only and are not stored by the relay.</p>
<div class="settings-row">
<label for="chatEnabled" title="Show encrypted room chat on the selected video tab." data-i18n="LABEL_CHAT_ENABLED" data-i18n-title="LABEL_CHAT_ENABLED_TOOLTIP">Enable Room Chat</label>
<label class="toggle-switch">
<input type="checkbox" id="chatEnabled">
<span class="slider"></span>
</label>
</div>
<div class="settings-row" data-chat-setting>
<label for="chatPosition" title="Choose where chat is attached to the selected player." data-i18n="LABEL_CHAT_POSITION" data-i18n-title="LABEL_CHAT_POSITION_TOOLTIP">Position</label>
<span class="settings-select">
<select id="chatPosition" class="settings-control">
<option value="right" data-i18n="CHAT_DOCK_RIGHT">Dock chat right</option>
<option value="left" data-i18n="CHAT_DOCK_LEFT">Dock chat left</option>
<option value="detached" data-i18n="CHAT_DETACHED">Detach chat</option>
</select>
</span>
</div>
<div class="settings-row" data-chat-setting>
<label for="chatSize" title="Choose the default size of the chat overlay." data-i18n="LABEL_CHAT_SIZE" data-i18n-title="LABEL_CHAT_SIZE_TOOLTIP">Overlay size</label>
<span class="settings-select">
<select id="chatSize" class="settings-control">
<option value="compact" data-i18n="OPTION_CHAT_SIZE_COMPACT">Compact</option>
<option value="standard" data-i18n="OPTION_CHAT_SIZE_STANDARD">Standard</option>
<option value="large" data-i18n="OPTION_CHAT_SIZE_LARGE">Large</option>
<option value="custom" data-i18n="OPTION_CHAT_SIZE_CUSTOM">Last custom size</option>
</select>
</span>
</div>
<div class="settings-row" data-chat-setting>
<label for="chatStartMode" title="Choose whether chat starts as a floating bubble or an open overlay." data-i18n="LABEL_CHAT_START_MODE" data-i18n-title="LABEL_CHAT_START_MODE_TOOLTIP">Start as</label>
<span class="settings-select">
<select id="chatStartMode" class="settings-control">
<option value="bubble" data-i18n="OPTION_CHAT_START_BUBBLE">Floating bubble</option>
<option value="open" data-i18n="OPTION_CHAT_START_OPEN">Open overlay</option>
</select>
</span>
</div>
</div>
</details>
<details class="form-group" name="settings-accordion">
<summary title="Customize playback, notifications, and audio preferences." data-i18n="LABEL_SETTINGS_GROUP_SYNC" data-i18n-title="LABEL_SETTINGS_GROUP_SYNC_TOOLTIP">Playback &amp; Sync</summary>
<div class="details-content">
@@ -1625,13 +1675,6 @@
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label for="chatEnabled" title="Show encrypted room chat on the selected video tab." data-i18n="LABEL_CHAT_ENABLED" data-i18n-title="LABEL_CHAT_ENABLED_TOOLTIP">Enable Room Chat</label>
<label class="toggle-switch">
<input type="checkbox" id="chatEnabled">
<span class="slider"></span>
</label>
</div>
<div class="settings-row">
<label for="filterNoise" title="Filters out non-video tabs and unrelated domains to keep the list clean" data-i18n="LABEL_HIDE_CLUTTER" data-i18n-title="LABEL_HIDE_CLUTTER_TOOLTIP">Hide Clutter Tabs</label>
<label class="toggle-switch">
+37 -1
View File
@@ -69,6 +69,9 @@ const elements = {
pauseBtn: document.getElementById('pauseBtn'),
autoSyncNextEpisode: document.getElementById('autoSyncNextEpisode'),
chatEnabled: document.getElementById('chatEnabled'),
chatPosition: document.getElementById('chatPosition'),
chatSize: document.getElementById('chatSize'),
chatStartMode: document.getElementById('chatStartMode'),
sendTabTitle: document.getElementById('sendTabTitle'),
mediaTitlePrivacyMode: document.getElementById('mediaTitlePrivacyMode'),
episodeLobbyCard: document.getElementById('episodeLobbyCard'),
@@ -338,7 +341,7 @@ function setRoomRefreshCooldown() {
async function init() {
// Local-only by design — settings and room credentials never come from
// storage.sync (only onboardingComplete + dismissedHints live there).
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey', 'chatEnabled', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab', 'themeMode', 'themePalette']);
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey', 'chatEnabled', 'chatPosition', 'chatSize', 'chatStartMode', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab', 'themeMode', 'themePalette']);
let activeLang = localData.locale;
if (!activeLang) {
@@ -370,6 +373,10 @@ async function init() {
if (elements.filterNoise) elements.filterNoise.checked = localData.filterNoise !== false;
if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = localData.autoSyncNextEpisode !== false;
if (elements.chatEnabled) elements.chatEnabled.checked = localData.chatEnabled === true;
if (elements.chatPosition) elements.chatPosition.value = ['left', 'detached'].includes(localData.chatPosition) ? localData.chatPosition : 'right';
if (elements.chatSize) elements.chatSize.value = ['compact', 'large', 'custom'].includes(localData.chatSize) ? localData.chatSize : 'standard';
if (elements.chatStartMode) elements.chatStartMode.value = localData.chatStartMode === 'open' ? 'open' : 'bubble';
syncChatSettingsState();
const legacyTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.titlePrivacyMode) ? localData.titlePrivacyMode : TITLE_PRIVACY_MODES.FULL;
const mediaTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.mediaTitlePrivacyMode) ? localData.mediaTitlePrivacyMode : legacyTitlePrivacyMode;
if (elements.sendTabTitle) elements.sendTabTitle.checked = normalizeSendTabTitle(localData.sendTabTitle, legacyTitlePrivacyMode);
@@ -1458,11 +1465,40 @@ elements.autoSyncNextEpisode.addEventListener('change', () => {
chrome.storage.local.set({ autoSyncNextEpisode: elements.autoSyncNextEpisode.checked });
});
function syncChatSettingsState() {
const enabled = elements.chatEnabled?.checked === true;
for (const control of [elements.chatPosition, elements.chatSize, elements.chatStartMode]) {
if (control) control.disabled = !enabled;
}
document.querySelectorAll('[data-chat-setting]').forEach(row => {
row.classList.toggle('is-disabled', !enabled);
row.setAttribute('aria-disabled', String(!enabled));
});
}
if (elements.chatEnabled) {
elements.chatEnabled.addEventListener('change', () => {
syncChatSettingsState();
chrome.storage.local.set({ chatEnabled: elements.chatEnabled.checked });
});
}
if (elements.chatPosition) {
elements.chatPosition.addEventListener('change', () => {
const chatPosition = ['left', 'detached'].includes(elements.chatPosition.value) ? elements.chatPosition.value : 'right';
chrome.storage.local.set({ chatPosition });
});
}
if (elements.chatSize) {
elements.chatSize.addEventListener('change', () => {
const chatSize = ['compact', 'large', 'custom'].includes(elements.chatSize.value) ? elements.chatSize.value : 'standard';
chrome.storage.local.set({ chatSize });
});
}
if (elements.chatStartMode) {
elements.chatStartMode.addEventListener('change', () => {
chrome.storage.local.set({ chatStartMode: elements.chatStartMode.value === 'open' ? 'open' : 'bubble' });
});
}
if (elements.sendTabTitle) {
elements.sendTabTitle.addEventListener('change', () => {
+611 -627
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -18,18 +18,18 @@
"verify": "node scripts/verify-release.mjs"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@vitest/coverage-v8": "^4.1.9",
"@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.4.0",
"eslint": "^10.8.0",
"eslint-plugin-no-unsanitized": "^4.1.5",
"fs-extra": "^11.2.0",
"fs-extra": "^11.4.0",
"htmlnano": "^3.4.0",
"sharp": "^0.35.3",
"svgo": "^4.0.2",
"vitest": "^4.1.9"
"vitest": "^4.1.10"
},
"overrides": {
"brace-expansion": "5.0.8",
+62
View File
@@ -0,0 +1,62 @@
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', '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', '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(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');
+1
View File
@@ -20,6 +20,7 @@ const checks = [
['content video finder', 'node', ['scripts/test-content-video-finder.cjs']],
['audio settings', 'node', ['scripts/test-audio-settings.mjs']],
['popup refresh cooldown', 'node', ['scripts/test-popup-refresh-cooldown.mjs']],
['chat settings', 'node', ['scripts/test-chat-settings.mjs']],
['host access recovery', 'node', ['scripts/test-host-access.mjs']],
['server syntax index', 'node', ['-c', 'server/index.js']],
['server syntax ops', 'node', ['-c', 'server/ops.js']],
+6 -5
View File
@@ -1,15 +1,16 @@
FROM node:20-alpine
FROM node:24-alpine
WORKDIR /app
# Copy shared protocol first
COPY shared/ ./shared/
COPY --chown=node:node shared/ ./shared/
# Copy server
COPY server/package*.json ./server/
RUN cd server && npm install --production
COPY server/ ./server/
COPY --chown=node:node server/package*.json ./server/
RUN cd server && npm ci --omit=dev
COPY --chown=node:node server/ ./server/
WORKDIR /app/server
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
+46 -37
View File
@@ -30,12 +30,12 @@
}
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
"undici-types": "~8.3.0"
}
},
"node_modules/@types/ws": {
@@ -360,9 +360,9 @@
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -539,9 +539,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -571,9 +571,9 @@
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -617,12 +617,16 @@
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/merge-descriptors": {
@@ -752,12 +756,13 @@
}
},
"node_modules/qs": {
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
@@ -767,12 +772,16 @@
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/raw-body": {
@@ -864,14 +873,14 @@
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"object-inspect": "^1.13.4",
"side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@@ -964,9 +973,9 @@
}
},
"node_modules/socket.io-parser": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
"integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
@@ -1069,9 +1078,9 @@
}
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"license": "MIT"
},
"node_modules/unpipe": {
@@ -1099,9 +1108,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"