From 7417c2121741d8ba28a38a2036c31205fe665672 Mon Sep 17 00:00:00 2001 From: Timo <6156589+Shik3i@users.noreply.github.com> Date: Sat, 25 Apr 2026 03:47:57 +0200 Subject: [PATCH] feat: enhance peer list with volume icons, play/pause status, and time interpolation (v1.1.3) --- extension/background.js | 24 ++++++++- extension/content.js | 5 +- extension/manifest.json | 2 +- extension/popup.html | 3 -- extension/popup.js | 110 ++++++++++++++++++++++++++++++++++++---- server/index.js | 4 ++ shared/constants.js | 2 +- 7 files changed, 131 insertions(+), 19 deletions(-) diff --git a/extension/background.js b/extension/background.js index f8a7fad..e616b5b 100644 --- a/extension/background.js +++ b/extension/background.js @@ -523,7 +523,12 @@ function handleServerEvent(event, data) { peerId: data.peerId, username: data.username, tabTitle: data.tabTitle, - mediaTitle: data.mediaTitle || null + mediaTitle: data.mediaTitle || null, + playbackState: data.playbackState || null, + currentTime: data.currentTime != null ? data.currentTime : null, + volume: data.volume != null ? data.volume : null, + muted: data.muted != null ? data.muted : null, + lastHeartbeat: Date.now() }); if (storageInitialized) chrome.storage.session.set({ currentRoom }); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {}); @@ -540,6 +545,11 @@ function handleServerEvent(event, data) { peer.tabTitle = data.tabTitle; peer.username = data.username; peer.mediaTitle = data.mediaTitle !== undefined ? data.mediaTitle : peer.mediaTitle; + peer.playbackState = data.playbackState !== undefined ? data.playbackState : peer.playbackState; + peer.currentTime = data.currentTime !== undefined ? data.currentTime : peer.currentTime; + peer.volume = data.volume !== undefined ? data.volume : peer.volume; + peer.muted = data.muted !== undefined ? data.muted : peer.muted; + peer.lastHeartbeat = Date.now(); } else { // Migration: replace string with object const idx = currentRoom.peers.indexOf(peer); @@ -547,7 +557,12 @@ function handleServerEvent(event, data) { peerId: data.peerId, username: data.username, tabTitle: data.tabTitle, - mediaTitle: data.mediaTitle || null + mediaTitle: data.mediaTitle || null, + playbackState: data.playbackState || null, + currentTime: data.currentTime != null ? data.currentTime : null, + volume: data.volume != null ? data.volume : null, + muted: data.muted != null ? data.muted : null, + lastHeartbeat: Date.now() }; } if (storageInitialized) chrome.storage.session.set({ currentRoom }); @@ -878,6 +893,11 @@ async function handleAsyncMessage(message, sender, sendResponse) { me.tabTitle = currentTabTitle; me.username = settings.username; me.mediaTitle = message.payload.mediaTitle; + me.playbackState = message.payload.playbackState; + me.currentTime = message.payload.currentTime; + me.volume = message.payload.volume; + me.muted = message.payload.muted; + me.lastHeartbeat = Date.now(); chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {}); } } diff --git a/extension/content.js b/extension/content.js index 6c8b444..7a074bf 100644 --- a/extension/content.js +++ b/extension/content.js @@ -211,6 +211,7 @@ duration: video.duration || 0, readyState: video.readyState, muted: video.muted, + volume: video.volume, playbackRate: video.playbackRate, url: window.location.href, id: video.id || 'none', @@ -314,7 +315,9 @@ payload: { playbackState: video.paused ? 'paused' : 'playing', currentTime: video.currentTime, - mediaTitle: mediaTitle + mediaTitle: mediaTitle, + volume: video.volume, + muted: video.muted } }).catch(err => { if (err.message.includes('Extension context invalidated')) { diff --git a/extension/manifest.json b/extension/manifest.json index 8622618..2d84aed 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "KoalaSync", - "version": "1.1.2", + "version": "1.1.3", "description": "Synchronize video playback across different tabs and users.", "permissions": [ "storage", diff --git a/extension/popup.html b/extension/popup.html index 45a52ca..45da138 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -148,9 +148,6 @@ } .peer-item { - display: flex; - justify-content: space-between; - align-items: center; padding: 8px 0; border-bottom: 1px solid #334155; } diff --git a/extension/popup.js b/extension/popup.js index 259bc40..40065da 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -188,11 +188,56 @@ function updateLastActionUI(state, peers) { elements.lastActionCard.appendChild(grid); } +function formatTime(seconds) { + if (seconds === null || seconds === undefined || isNaN(seconds)) return '--:--'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + return `${m}:${String(s).padStart(2, '0')}`; +} + +function getVolumeIcon(volume, muted) { + if (muted || volume === 0) return '🔇'; + if (volume < 0.33) return '🔈'; + if (volume < 0.66) return '🔉'; + return '🔊'; +} + +let activePeers = []; +let interpolationInterval = null; + +function startInterpolation() { + if (interpolationInterval) return; + interpolationInterval = setInterval(() => { + const timeElements = document.querySelectorAll('.peer-time-display'); + timeElements.forEach(el => { + const peerId = el.dataset.peerId; + const peer = activePeers.find(p => p.peerId === peerId); + if (peer && peer.playbackState === 'playing' && peer.currentTime != null && peer.lastHeartbeat) { + const elapsed = (Date.now() - peer.lastHeartbeat) / 1000; + el.textContent = formatTime(peer.currentTime + elapsed); + } + }); + }, 1000); +} + function updatePeerList(peers) { if (!peers) return; + activePeers = peers; + if (!interpolationInterval) startInterpolation(); - // UI Throttle: Only re-render if the peer state actually changed - const currentPeersJson = JSON.stringify(peers); + // UI Throttle: Only re-render if the peer state actually changed (excluding time interpolation) + const stateToHash = peers.map(p => ({ + id: p.peerId, + user: p.username, + tab: p.tabTitle, + media: p.mediaTitle, + state: p.playbackState, + vol: p.volume, + muted: p.muted + })); + const currentPeersJson = JSON.stringify(stateToHash); if (currentPeersJson === lastPeersJson) return; lastPeersJson = currentPeersJson; @@ -213,10 +258,10 @@ function updatePeerList(peers) { const peerItem = document.createElement('div'); peerItem.className = 'peer-item'; - peerItem.style.cssText = 'display:block; padding: 6px 0;'; + peerItem.style.cssText = 'position:relative; display:block; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05);'; const header = document.createElement('div'); - header.style.cssText = 'display:flex; justify-content:space-between; align-items:center;'; + header.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding-right: 24px;'; const nameSpan = document.createElement('span'); if (pUsername) { @@ -235,29 +280,72 @@ function updatePeerList(peers) { header.appendChild(nameSpan); + // Volume Icon (Top Right) + if (p.volume !== undefined && p.volume !== null) { + const volIcon = document.createElement('div'); + volIcon.style.cssText = 'position:absolute; top:8px; right:0; cursor:help; font-size:14px;'; + volIcon.textContent = getVolumeIcon(p.volume, p.muted); + volIcon.title = p.muted ? 'Muted' : `Volume: ${Math.round(p.volume * 100)}%`; + peerItem.appendChild(volIcon); + } + if (pId === localPeerId) { const you = document.createElement('span'); - you.style.cssText = 'font-size:10px; color:var(--accent)'; + you.style.cssText = 'font-size:10px; color:var(--accent); font-weight:bold;'; you.textContent = 'YOU'; header.appendChild(you); } peerItem.appendChild(header); + // Media Info if (p.mediaTitle) { const mediaDiv = document.createElement('div'); - mediaDiv.style.cssText = 'font-size:11px; color:var(--star); font-weight: 600; margin-top: 2px;'; + mediaDiv.style.cssText = 'font-size:11px; color:var(--star); font-weight: 600; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 280px;'; mediaDiv.textContent = `🎬 ${p.mediaTitle}`; peerItem.appendChild(mediaDiv); } - if (pTabTitle) { - const titleDiv = document.createElement('div'); - titleDiv.style.cssText = 'font-size:10px; color:var(--text-muted); opacity: 0.8;'; - titleDiv.textContent = p.mediaTitle ? `via ${pTabTitle}` : pTabTitle; - peerItem.appendChild(titleDiv); + // Status Line (Play/Pause + Time) + const statusLine = document.createElement('div'); + statusLine.style.cssText = 'display:flex; align-items:center; gap:8px; margin-top:4px;'; + + if (p.playbackState) { + const stateIcon = document.createElement('span'); + stateIcon.style.fontSize = '10px'; + if (p.playbackState === 'playing') { + stateIcon.textContent = '▶'; + stateIcon.style.color = 'var(--success)'; + } else { + stateIcon.textContent = '⏸'; + stateIcon.style.color = 'var(--error)'; + } + statusLine.appendChild(stateIcon); } + if (p.currentTime !== undefined && p.currentTime !== null) { + const timeSpan = document.createElement('span'); + timeSpan.className = 'peer-time-display'; + timeSpan.dataset.peerId = pId; + timeSpan.style.cssText = 'font-size:11px; font-family:monospace; color:var(--text-muted);'; + + let displayTime = p.currentTime; + if (p.playbackState === 'playing' && p.lastHeartbeat && p.currentTime != null) { + const elapsed = (Date.now() - p.lastHeartbeat) / 1000; + displayTime += elapsed; + } + timeSpan.textContent = formatTime(displayTime); + statusLine.appendChild(timeSpan); + } + + if (pTabTitle) { + const titleDiv = document.createElement('span'); + titleDiv.style.cssText = 'font-size:10px; color:var(--text-muted); opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; text-align: right;'; + titleDiv.textContent = pTabTitle; + statusLine.appendChild(titleDiv); + } + + peerItem.appendChild(statusLine); container.appendChild(peerItem); }); }; diff --git a/server/index.js b/server/index.js index fe1dee8..9cef0fd 100644 --- a/server/index.js +++ b/server/index.js @@ -292,6 +292,10 @@ io.on('connection', (socket) => { username: data.username !== undefined ? data.username : existing.username, tabTitle: data.tabTitle !== undefined ? data.tabTitle : existing.tabTitle, mediaTitle: data.mediaTitle !== undefined ? data.mediaTitle : existing.mediaTitle, + playbackState: data.playbackState !== undefined ? data.playbackState : existing.playbackState, + currentTime: data.currentTime !== undefined ? data.currentTime : existing.currentTime, + volume: data.volume !== undefined ? data.volume : existing.volume, + muted: data.muted !== undefined ? data.muted : existing.muted, lastSeen: Date.now() }); diff --git a/shared/constants.js b/shared/constants.js index 3d43593..2b09c9f 100644 --- a/shared/constants.js +++ b/shared/constants.js @@ -7,7 +7,7 @@ */ export const PROTOCOL_VERSION = "1.0.0"; -export const APP_VERSION = "1.1.2"; +export const APP_VERSION = "1.1.3"; export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net'; export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';