From 1ec2396c418dce8991d915c40318aa49c31779cf Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Sun, 28 Jun 2026 03:45:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(co-host):=20popup=20UI=20=E2=80=94=20promo?= =?UTF-8?q?te/demote,=20controller=20badges,=20controller-aware=20lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- extension/locales/de.json | 3 ++ extension/locales/en.json | 3 ++ extension/locales/es.json | 3 ++ extension/locales/fr.json | 3 ++ extension/locales/it.json | 3 ++ extension/locales/ja.json | 3 ++ extension/locales/ko.json | 3 ++ extension/locales/nl.json | 3 ++ extension/locales/pl.json | 3 ++ extension/locales/pt-BR.json | 3 ++ extension/locales/pt.json | 3 ++ extension/locales/ru.json | 3 ++ extension/locales/tr.json | 3 ++ extension/locales/uk.json | 3 ++ extension/locales/zh.json | 3 ++ extension/popup.js | 85 +++++++++++++++++++++++++++--------- 16 files changed, 110 insertions(+), 20 deletions(-) diff --git a/extension/locales/de.json b/extension/locales/de.json index 1313c47..cefe399 100644 --- a/extension/locales/de.json +++ b/extension/locales/de.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "Der Host steuert die Wiedergabe für alle.", "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", diff --git a/extension/locales/en.json b/extension/locales/en.json index 3bc6356..786dd15 100644 --- a/extension/locales/en.json +++ b/extension/locales/en.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "The host controls playback for everyone.", "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", diff --git a/extension/locales/es.json b/extension/locales/es.json index 2c1f2d6..8fe004e 100644 --- a/extension/locales/es.json +++ b/extension/locales/es.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "El anfitrión controla la reproducción para todos.", "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", diff --git a/extension/locales/fr.json b/extension/locales/fr.json index 0ff7b82..b51dca7 100644 --- a/extension/locales/fr.json +++ b/extension/locales/fr.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "L'hôte contrôle la lecture pour tout le monde.", "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", diff --git a/extension/locales/it.json b/extension/locales/it.json index b5780b7..3f8160d 100644 --- a/extension/locales/it.json +++ b/extension/locales/it.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "L'host controlla la riproduzione per tutti.", "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", diff --git a/extension/locales/ja.json b/extension/locales/ja.json index f4f6e66..3b15360 100644 --- a/extension/locales/ja.json +++ b/extension/locales/ja.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "ホストが全員の再生を操作します。", "BADGE_HOST": "ホスト", "BADGE_GUEST": "ゲスト", + "BADGE_CONTROLLER": "操作権あり", + "BTN_GIVE_CONTROL": "操作権を付与", + "BTN_REVOKE_CONTROL": "取り消す", "BADGE_DESYNCED": "ソロ", "TOOLTIP_PEER_DESYNCED": "単独で視聴中 — ホストのコマンドを無視しています", "HCM_DIALOG_TITLE": "KoalaSync · ホストがこのルームを操作中", diff --git a/extension/locales/ko.json b/extension/locales/ko.json index f9bbe80..7063238 100644 --- a/extension/locales/ko.json +++ b/extension/locales/ko.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "호스트가 모두의 재생을 제어합니다.", "BADGE_HOST": "호스트", "BADGE_GUEST": "게스트", + "BADGE_CONTROLLER": "제어자", + "BTN_GIVE_CONTROL": "제어 권한 부여", + "BTN_REVOKE_CONTROL": "취소", "BADGE_DESYNCED": "단독", "TOOLTIP_PEER_DESYNCED": "혼자 시청 중 — 호스트 명령을 무시합니다", "HCM_DIALOG_TITLE": "KoalaSync · 호스트가 이 방을 제어합니다", diff --git a/extension/locales/nl.json b/extension/locales/nl.json index fe6ede8..edfc97c 100644 --- a/extension/locales/nl.json +++ b/extension/locales/nl.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "De host bedient de weergave voor iedereen.", "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", diff --git a/extension/locales/pl.json b/extension/locales/pl.json index e28a7ce..0196a94 100644 --- a/extension/locales/pl.json +++ b/extension/locales/pl.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "Host steruje odtwarzaniem dla wszystkich.", "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", diff --git a/extension/locales/pt-BR.json b/extension/locales/pt-BR.json index d353142..4c64d6e 100644 --- a/extension/locales/pt-BR.json +++ b/extension/locales/pt-BR.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "O anfitrião controla a reprodução para todos.", "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", diff --git a/extension/locales/pt.json b/extension/locales/pt.json index ab5544d..949bf54 100644 --- a/extension/locales/pt.json +++ b/extension/locales/pt.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "O anfitrião controla a reprodução para todos.", "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", diff --git a/extension/locales/ru.json b/extension/locales/ru.json index 66a222c..0ad6f57 100644 --- a/extension/locales/ru.json +++ b/extension/locales/ru.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "Ведущий управляет воспроизведением для всех.", "BADGE_HOST": "Ведущий", "BADGE_GUEST": "Гость", + "BADGE_CONTROLLER": "Управляющий", + "BTN_GIVE_CONTROL": "Дать управление", + "BTN_REVOKE_CONTROL": "Отозвать", "BADGE_DESYNCED": "Соло", "TOOLTIP_PEER_DESYNCED": "Смотрит отдельно — игнорирует команды ведущего", "HCM_DIALOG_TITLE": "KoalaSync · Комнатой управляет ведущий", diff --git a/extension/locales/tr.json b/extension/locales/tr.json index ff4918b..c3a2616 100644 --- a/extension/locales/tr.json +++ b/extension/locales/tr.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "Oynatmayı herkes için sunucu kontrol eder.", "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", diff --git a/extension/locales/uk.json b/extension/locales/uk.json index a76366d..551e5a3 100644 --- a/extension/locales/uk.json +++ b/extension/locales/uk.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "Ведучий керує відтворенням для всіх.", "BADGE_HOST": "Ведучий", "BADGE_GUEST": "Гість", + "BADGE_CONTROLLER": "Керуючий", + "BTN_GIVE_CONTROL": "Дати керування", + "BTN_REVOKE_CONTROL": "Відкликати", "BADGE_DESYNCED": "Соло", "TOOLTIP_PEER_DESYNCED": "Дивиться окремо — ігнорує команди ведучого", "HCM_DIALOG_TITLE": "KoalaSync · Кімнатою керує ведучий", diff --git a/extension/locales/zh.json b/extension/locales/zh.json index 9169d7b..7e07428 100644 --- a/extension/locales/zh.json +++ b/extension/locales/zh.json @@ -50,6 +50,9 @@ "NOTICE_HOST_CONTROLS": "由主持人为所有人控制播放。", "BADGE_HOST": "主持人", "BADGE_GUEST": "访客", + "BADGE_CONTROLLER": "控制者", + "BTN_GIVE_CONTROL": "授予控制权", + "BTN_REVOKE_CONTROL": "撤销", "BADGE_DESYNCED": "单独", "TOOLTIP_PEER_DESYNCED": "单独观看中 — 忽略主持人的命令", "HCM_DIALOG_TITLE": "KoalaSync · 主持人控制本房间", diff --git a/extension/popup.js b/extension/popup.js index 5c29a92..f2785f7 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -249,7 +249,7 @@ async function init() { updatePingDisplay(res.ping); updatePeerList(res.peers); lastKnownPeers = res.peers || []; - updateHostControlUI(res.controlMode, res.amHost, res.hostControlSupported, res.status === 'connected'); + updateHostControlUI({ controlMode: res.controlMode, amHost: res.amHost, amController: res.amController, controllers: res.controllers, hostPeerId: res.hostPeerId, hostControlSupported: res.hostControlSupported, coHostSupported: res.coHostSupported, inRoom: res.status === 'connected' }); if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); // If user has a room configured but background is not connected (disconnected or idle), @@ -313,19 +313,37 @@ function toggleUIState(inRoom) { // True when we're a guest in a host-only room → remote-control buttons are locked. let hcmGuestLocked = false; -function updateHostControlUI(controlMode, amHost, hostControlSupported, inRoom) { +// Co-Host state mirrored for the peer-list renderer (promote/demote + role badges). +let hcmAmOwner = false; +let hcmControllers = []; +let hcmCoHostSupported = false; +let hcmOwnerPeerId = null; +let hcmRoomIsHostOnly = false; + +function updateHostControlUI(state) { + const s = state || {}; + const controlMode = s.controlMode; + const amHost = !!s.amHost; + const amController = !!s.amController; + const hostOnly = controlMode === 'host-only'; + + // Stash for the peer-list renderer, then force it to re-render with new roles. + hcmAmOwner = amHost; + hcmControllers = Array.isArray(s.controllers) ? s.controllers : []; + hcmCoHostSupported = !!s.coHostSupported; + hcmOwnerPeerId = s.hostPeerId || null; + hcmRoomIsHostOnly = hostOnly; + lastPeersJson = ''; + if (activePeers) updatePeerList(activePeers); + const card = elements.hostControlCard; if (!card) return; - const hostOnly = controlMode === 'host-only'; - // Explicit capability advertised by the relay in ROOM_DATA. Against an older - // relay it's false/absent → the feature is unavailable, so hide the card - // entirely instead of showing a misleading "Guest". - const serverSupportsHostControl = !!hostControlSupported; - // Only show the card when it's actually meaningful: - // - host: always (so they can enable/disable host-only) - // - guest: only while host-only is active (explains why they can't control) - // A guest in a normal "everyone" room sees nothing — no confusing noise. - const show = inRoom && serverSupportsHostControl && (amHost || hostOnly); + // Explicit capability advertised by the relay. Against an older relay it's + // false/absent → feature unavailable, so hide the card instead of misleading UI. + const serverSupportsHostControl = !!s.hostControlSupported; + // Show the card only when meaningful: owner always (to manage); a non-controller + // guest only while host-only is active. A guest in a normal room sees nothing. + const show = s.inRoom && serverSupportsHostControl && (amHost || hostOnly); if (!show) { card.style.display = 'none'; hcmGuestLocked = false; @@ -334,16 +352,18 @@ function updateHostControlUI(controlMode, amHost, hostControlSupported, inRoom) } card.style.display = 'block'; if (elements.hostRoleBadge) { - elements.hostRoleBadge.textContent = amHost ? (getMessage('BADGE_HOST') || 'Host') : (getMessage('BADGE_GUEST') || 'Guest'); - elements.hostRoleBadge.style.background = amHost ? 'var(--accent)' : 'var(--text-muted)'; + const role = amHost ? (getMessage('BADGE_HOST') || 'Host') + : amController ? (getMessage('BADGE_CONTROLLER') || 'Controller') + : (getMessage('BADGE_GUEST') || 'Guest'); + elements.hostRoleBadge.textContent = role; + elements.hostRoleBadge.style.background = (amHost || amController) ? 'var(--accent)' : 'var(--text-muted)'; } if (elements.hostControlToggleRow) elements.hostControlToggleRow.style.display = amHost ? 'flex' : 'none'; if (elements.hostControlToggle) elements.hostControlToggle.checked = hostOnly; - if (elements.hostControlGuestNote) elements.hostControlGuestNote.style.display = (!amHost && hostOnly) ? 'block' : 'none'; + if (elements.hostControlGuestNote) elements.hostControlGuestNote.style.display = (!amController && hostOnly) ? 'block' : 'none'; - // A guest in host-only mode can't drive the room → lock the remote controls so - // clicks don't silently get gated (and leave the button stuck). - hcmGuestLocked = (!amHost && hostOnly); + // Only a non-controller is locked out of the remote controls. + hcmGuestLocked = (!amController && hostOnly); setRemoteControlsLocked(hcmGuestLocked); } @@ -676,6 +696,31 @@ function updatePeerList(peers) { header.appendChild(solo); } + // Co-Host: role badges + the owner's promote/demote control. Shown when the + // feature is active and either the room is host-only or we're the owner + // (so a guest in a normal room sees no role noise). + const showRoles = !!hcmOwnerPeerId && (hcmRoomIsHostOnly || hcmAmOwner); + if (showRoles) { + const isOwner = pId === hcmOwnerPeerId; + const isController = !isOwner && hcmControllers.includes(pId); + if (isOwner || isController) { + const roleBadge = document.createElement('span'); + roleBadge.style.cssText = 'font-size:10px; color:#fff; background:var(--accent); padding:2px 6px; border-radius:6px; font-weight:600;'; + roleBadge.textContent = isOwner ? (getMessage('BADGE_HOST') || 'Host') : (getMessage('BADGE_CONTROLLER') || 'Controller'); + header.appendChild(roleBadge); + } + // Owner can promote/demote any non-owner peer. + if (hcmAmOwner && hcmCoHostSupported && !isOwner) { + const btn = document.createElement('button'); + btn.style.cssText = 'font-size:10px; padding:2px 8px; border-radius:6px; cursor:pointer; border:1px solid var(--accent); background:transparent; color:var(--accent); white-space:nowrap;'; + btn.textContent = isController ? (getMessage('BTN_REVOKE_CONTROL') || 'Revoke') : (getMessage('BTN_GIVE_CONTROL') || 'Give control'); + btn.addEventListener('click', () => { + chrome.runtime.sendMessage({ type: 'SET_PEER_ROLE', peerId: pId, controller: !isController }, () => {}); + }); + header.appendChild(btn); + } + } + peerItem.appendChild(header); // Media Info @@ -1743,7 +1788,7 @@ chrome.runtime.onMessage.addListener((msg) => { if (msg.peers) detectPeerChanges(msg.peers); } else if (msg.type === 'CONTROL_MODE') { const inRoom = elements.sectionActive && elements.sectionActive.style.display === 'block'; - updateHostControlUI(msg.controlMode, msg.amHost, msg.hostControlSupported, inRoom); + updateHostControlUI({ controlMode: msg.controlMode, amHost: msg.amHost, amController: msg.amController, controllers: msg.controllers, hostPeerId: msg.hostPeerId, hostControlSupported: msg.hostControlSupported, coHostSupported: msg.coHostSupported, inRoom }); } else if (msg.type === 'CONNECTION_STATUS') { if (msg.status === 'connected' || msg.status === 'disconnected') { if (joinBtnTimeout) { clearTimeout(joinBtnTimeout); joinBtnTimeout = null; } @@ -1761,7 +1806,7 @@ chrome.runtime.onMessage.addListener((msg) => { if (res.peers) updatePeerList(res.peers); if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); updatePingDisplay(res.ping); - updateHostControlUI(res.controlMode, res.amHost, res.hostControlSupported, true); + updateHostControlUI({ controlMode: res.controlMode, amHost: res.amHost, amController: res.amController, controllers: res.controllers, hostPeerId: res.hostPeerId, hostControlSupported: res.hostControlSupported, coHostSupported: res.coHostSupported, inRoom: true }); }); } if (msg.status === 'disconnected') {