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>
This commit is contained in:
KoalaDev
2026-06-26 04:14:55 +02:00
parent f9dc9d4017
commit 9006570bd1
17 changed files with 140 additions and 0 deletions
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Gast",
"NO_PEERS_CONNECTED": "Keine Teilnehmer verbunden",
"BTN_LEAVE_ROOM": "Raum verlassen",
"LABEL_SELECT_VIDEO": "Video auswählen",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Guest",
"NO_PEERS_CONNECTED": "No peers connected",
"BTN_LEAVE_ROOM": "Leave Room",
"LABEL_SELECT_VIDEO": "Select Video",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Anfitrión",
"BADGE_GUEST": "Invitado",
"NO_PEERS_CONNECTED": "Sin participantes conectados",
"BTN_LEAVE_ROOM": "Salir de la sala",
"LABEL_SELECT_VIDEO": "Seleccionar video",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Hôte",
"BADGE_GUEST": "Invité",
"NO_PEERS_CONNECTED": "Aucun membre connecté",
"BTN_LEAVE_ROOM": "Quitter le salon",
"LABEL_SELECT_VIDEO": "Choisir une vidéo",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Ospite",
"NO_PEERS_CONNECTED": "Nessun partecipante connesso",
"BTN_LEAVE_ROOM": "Lascia Stanza",
"LABEL_SELECT_VIDEO": "Seleziona Video",
+6
View File
@@ -44,6 +44,12 @@
"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": "ホストが全員の再生を操作します。",
"BADGE_HOST": "ホスト",
"BADGE_GUEST": "ゲスト",
"NO_PEERS_CONNECTED": "接続しているメンバーはいません",
"BTN_LEAVE_ROOM": "ルームを退室",
"LABEL_SELECT_VIDEO": "ビデオを選択",
+6
View File
@@ -44,6 +44,12 @@
"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": "호스트가 모두의 재생을 제어합니다.",
"BADGE_HOST": "호스트",
"BADGE_GUEST": "게스트",
"NO_PEERS_CONNECTED": "연결된 참여자가 없습니다",
"BTN_LEAVE_ROOM": "방 나가기",
"LABEL_SELECT_VIDEO": "비디오 선택",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Gast",
"NO_PEERS_CONNECTED": "Geen deelnemers verbonden",
"BTN_LEAVE_ROOM": "Kamer verlaten",
"LABEL_SELECT_VIDEO": "Selecteer video",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Host",
"BADGE_GUEST": "Gość",
"NO_PEERS_CONNECTED": "Brak połączonych uczestników",
"BTN_LEAVE_ROOM": "Opuść pokój",
"LABEL_SELECT_VIDEO": "Wybierz wideo",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Anfitrião",
"BADGE_GUEST": "Convidado",
"NO_PEERS_CONNECTED": "Sem participantes conectados",
"BTN_LEAVE_ROOM": "Sair da sala",
"LABEL_SELECT_VIDEO": "Selecionar vídeo",
+6
View File
@@ -44,6 +44,12 @@
"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": "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.",
"BADGE_HOST": "Anfitrião",
"BADGE_GUEST": "Convidado",
"NO_PEERS_CONNECTED": "Nenhum participante ligado",
"BTN_LEAVE_ROOM": "Sair da Sala",
"LABEL_SELECT_VIDEO": "Selecionar Vídeo",
+6
View File
@@ -44,6 +44,12 @@
"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": "Ведущий управляет воспроизведением для всех.",
"BADGE_HOST": "Ведущий",
"BADGE_GUEST": "Гость",
"NO_PEERS_CONNECTED": "Нет подключенных участников",
"BTN_LEAVE_ROOM": "Выйти из комнаты",
"LABEL_SELECT_VIDEO": "Выбрать видео",
+6
View File
@@ -44,6 +44,12 @@
"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.",
"BADGE_HOST": "Sunucu",
"BADGE_GUEST": "Misafir",
"NO_PEERS_CONNECTED": "Bağlı kimse yok",
"BTN_LEAVE_ROOM": "Odadan Ayrıl",
"LABEL_SELECT_VIDEO": "Video Seç",
+6
View File
@@ -44,6 +44,12 @@
"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": "Ведучий керує відтворенням для всіх.",
"BADGE_HOST": "Ведучий",
"BADGE_GUEST": "Гість",
"NO_PEERS_CONNECTED": "Немає підключених однорангових пристроїв",
"BTN_LEAVE_ROOM": "Вийти з кімнати",
"LABEL_SELECT_VIDEO": "Виберіть Відео",
+6
View File
@@ -44,6 +44,12 @@
"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": "由主持人为所有人控制播放。",
"BADGE_HOST": "主持人",
"BADGE_GUEST": "访客",
"NO_PEERS_CONNECTED": "没有对等点连接",
"BTN_LEAVE_ROOM": "离开房间",
"LABEL_SELECT_VIDEO": "选择视频",
+12
View File
@@ -450,6 +450,18 @@
</div>
</div>
<div id="hostControlCard" class="info-card" style="margin-bottom: 20px; display:none;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label style="margin:0;" title="Restrict who can control playback in this room" data-i18n="LABEL_HOST_CONTROL" data-i18n-title="LABEL_HOST_CONTROL_TOOLTIP">Host Control</label>
<span id="hostRoleBadge" style="font-size:10px; font-weight:700; text-transform:uppercase; padding:2px 8px; border-radius:8px; background:var(--accent); color:#fff;"></span>
</div>
<label id="hostControlToggleRow" style="display:none; align-items:center; gap:8px; margin-top:10px; cursor:pointer; font-size:12px; color:var(--text-muted);">
<input type="checkbox" id="hostControlToggle">
<span data-i18n="LABEL_HOST_ONLY_TOGGLE">Only I can control playback</span>
</label>
<div id="hostControlGuestNote" style="display:none; margin-top:10px; font-size:12px; color:var(--text-muted);" data-i18n="NOTICE_HOST_CONTROLS">The host controls playback for everyone.</div>
</div>
<div style="margin-bottom: 20px;">
<label title="Other users currently connected to this room" data-i18n="LABEL_PEERS_IN_ROOM" data-i18n-title="LABEL_PEERS_IN_ROOM_TOOLTIP">Peers in Room</label>
<div id="peerList" class="info-card">
+38
View File
@@ -41,6 +41,11 @@ const elements = {
retryBtn: document.getElementById('retryBtn'),
sectionJoin: document.getElementById('section-join'),
sectionActive: document.getElementById('section-active'),
hostControlCard: document.getElementById('hostControlCard'),
hostRoleBadge: document.getElementById('hostRoleBadge'),
hostControlToggleRow: document.getElementById('hostControlToggleRow'),
hostControlToggle: document.getElementById('hostControlToggle'),
hostControlGuestNote: document.getElementById('hostControlGuestNote'),
activeRoomId: document.getElementById('activeRoomId'),
activeServer: document.getElementById('activeServer'),
peerListSync: document.getElementById('peerListSync'),
@@ -244,6 +249,7 @@ async function init() {
updatePingDisplay(res.ping);
updatePeerList(res.peers);
lastKnownPeers = res.peers || [];
updateHostControlUI(res.controlMode, res.amHost, 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),
@@ -303,6 +309,34 @@ function toggleUIState(inRoom) {
if (syncInactive) syncInactive.style.display = inRoom ? 'none' : 'block';
}
// --- Host Control Mode UI ---
function updateHostControlUI(controlMode, amHost, inRoom) {
const card = elements.hostControlCard;
if (!card) return;
if (!inRoom) { card.style.display = 'none'; return; }
card.style.display = 'block';
const hostOnly = controlMode === 'host-only';
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)';
}
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.hostControlToggle) {
elements.hostControlToggle.addEventListener('change', () => {
const mode = elements.hostControlToggle.checked ? 'host-only' : 'everyone';
chrome.runtime.sendMessage({ type: 'SET_CONTROL_MODE', controlMode: mode }, (res) => {
// Server broadcasts CONTROL_MODE back to refresh the UI; revert on failure.
if (chrome.runtime.lastError || !res || res.status !== 'ok') {
elements.hostControlToggle.checked = (mode === 'everyone');
}
});
});
}
function updateUI(roomId, password, useCustomServer = false, serverUrl = '') {
const inRoom = !!roomId;
toggleUIState(inRoom);
@@ -1656,6 +1690,9 @@ chrome.runtime.onMessage.addListener((msg) => {
} else if (msg.type === 'PEER_UPDATE') {
updatePeerList(msg.peers);
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, inRoom);
} else if (msg.type === 'CONNECTION_STATUS') {
if (msg.status === 'connected' || msg.status === 'disconnected') {
if (joinBtnTimeout) { clearTimeout(joinBtnTimeout); joinBtnTimeout = null; }
@@ -1673,6 +1710,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, true);
});
}
if (msg.status === 'disconnected') {