From 221961820a01eb8b4e1055a174f752475dcc6617 Mon Sep 17 00:00:00 2001 From: Timo <6156589+Shik3i@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:20:32 +0200 Subject: [PATCH] feat: implement visual confirmation system and redesigned Sync tab --- extension/background.js | 45 ++++++++++++++++++++++++++++-- extension/content.js | 4 +++ extension/popup.html | 32 ++++++++++----------- extension/popup.js | 62 +++++++++++++++++++++++++++++++++++++++++ server/index.js | 11 ++++++++ shared/constants.js | 1 + 6 files changed, 136 insertions(+), 19 deletions(-) diff --git a/extension/background.js b/extension/background.js index f891a70..4130bb3 100644 --- a/extension/background.js +++ b/extension/background.js @@ -18,12 +18,15 @@ let pendingLogs = []; let pendingHistory = []; let eventQueue = []; let isNamespaceJoined = false; +let lastActionState = { action: null, senderId: null, timestamp: 0, acks: [] }; +let currentCommandSenderId = null; // Track who sent the last command we are executing // Restore state from session storage -chrome.storage.session.get(['logs', 'history', 'currentRoom'], (data) => { +chrome.storage.session.get(['logs', 'history', 'currentRoom', 'lastActionState'], (data) => { if (data.logs) logs = data.logs; if (data.history) history = data.history; if (data.currentRoom) currentRoom = data.currentRoom; + if (data.lastActionState) lastActionState = data.lastActionState; storageInitialized = true; if (pendingLogs.length > 0) { @@ -383,7 +386,8 @@ function handleServerEvent(event, data) { case EVENTS.FORCE_SYNC_PREPARE: if (data.senderId) { addToHistory(event, data.senderId); - showNotification(data.senderId, event); + showNotification(event, data.senderId); + updateLastAction(event, data.senderId); } routeToContent(event, data); break; @@ -405,6 +409,15 @@ function handleServerEvent(event, data) { } routeToContent(event, data); break; + case EVENTS.EVENT_ACK: + if (lastActionState && lastActionState.action && data.senderId) { + if (!lastActionState.acks.includes(data.senderId)) { + lastActionState.acks.push(data.senderId); + if (storageInitialized) chrome.storage.session.set({ lastActionState }); + chrome.runtime.sendMessage({ type: 'ACTION_UPDATE', state: lastActionState }).catch(() => {}); + } + } + break; case EVENTS.PEER_STATUS: if (currentRoom) { if (data.status === 'joined') { @@ -449,6 +462,17 @@ function executeForceSync() { addLog('Force Sync Executed', 'success'); } +function updateLastAction(action, senderId) { + lastActionState = { + action, + senderId, + timestamp: Date.now(), + acks: [] + }; + if (storageInitialized) chrome.storage.session.set({ lastActionState }); + chrome.runtime.sendMessage({ type: 'ACTION_UPDATE', state: lastActionState }).catch(() => {}); +} + async function routeToContent(action, payload) { if (!currentTabId) { const settings = await getSettings(); @@ -459,6 +483,8 @@ async function routeToContent(action, payload) { const tabId = parseInt(currentTabId); if (isNaN(tabId)) return; + currentCommandSenderId = payload.senderId || null; + chrome.tabs.sendMessage(tabId, { type: 'SERVER_COMMAND', action, @@ -530,7 +556,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined; let status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : 'disconnected'); if (reconnectFailed) status = 'reconnect_failed'; - sendResponse({ status, peerId, peers: currentRoom ? currentRoom.peers : [] }); + sendResponse({ + status, + peerId, + peers: currentRoom ? currentRoom.peers : [], + lastActionState + }); // Global return true at the end handles this } else if (message.type === 'LEAVE_ROOM') { emit(EVENTS.LEAVE_ROOM, { peerId }); @@ -610,6 +641,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { routeToContent(message.action, message.payload); } + // Update local state as initiator + updateLastAction(message.action, 'You'); + // Events coming from content script or popup if (message.action === EVENTS.FORCE_SYNC_PREPARE) { isForceSyncInitiator = true; @@ -640,6 +674,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } else { emit(EVENTS.FORCE_SYNC_ACK, { peerId }); } + } else if (message.type === 'CMD_ACK') { + // Content script successfully ran a command. Send ACK back to the initiator. + if (currentCommandSenderId && currentCommandSenderId !== peerId) { + emit(EVENTS.EVENT_ACK, { senderId: peerId, targetId: currentCommandSenderId }); + } } else if (message.type === 'HEARTBEAT') { if (sender.tab) { currentTabId = sender.tab.id; diff --git a/extension/content.js b/extension/content.js index 904395e..71f2e7d 100644 --- a/extension/content.js +++ b/extension/content.js @@ -127,10 +127,13 @@ if (action === EVENTS.PLAY) { tryMediaAction(EVENTS.PLAY); + chrome.runtime.sendMessage({ type: 'CMD_ACK' }); } else if (action === EVENTS.PAUSE) { tryMediaAction(EVENTS.PAUSE); + chrome.runtime.sendMessage({ type: 'CMD_ACK' }); } else if (action === EVENTS.SEEK) { tryMediaAction(EVENTS.SEEK, payload); + chrome.runtime.sendMessage({ type: 'CMD_ACK' }); } else if (action === EVENTS.FORCE_SYNC_PREPARE) { if (!payload || payload.targetTime === undefined) return; const video = findVideo(); @@ -144,6 +147,7 @@ } } else if (action === EVENTS.FORCE_SYNC_EXECUTE) { tryMediaAction(EVENTS.PLAY); + chrome.runtime.sendMessage({ type: 'CMD_ACK' }); } } diff --git a/extension/popup.html b/extension/popup.html index d110c89..293b075 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -277,26 +277,19 @@ -
- - +
+ + +
- - -
- -
-
No peers connected
-
+ + +
+
No recent commands
- -
- -
+
@@ -338,6 +331,13 @@ No tab selected or video detected.
+
+ +
+
+
No activity yet
+
+
diff --git a/extension/popup.js b/extension/popup.js index 78067d4..a11f550 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -32,6 +32,7 @@ const elements = { inviteLink: document.getElementById('inviteLink'), filterNoise: document.getElementById('filterNoise'), regenId: document.getElementById('regenId'), + lastActionCard: document.getElementById('lastActionCard'), historyList: document.getElementById('historyList'), copyLogs: document.getElementById('copyLogs'), createRoomBtn: document.getElementById('createRoomBtn'), @@ -82,6 +83,7 @@ async function init() { localPeerId = res.peerId; applyConnectionStatus(res.status); updatePeerList(res.peers); + if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); } }); @@ -119,6 +121,56 @@ function updateUI(roomId, password, useCustomServer = false, serverUrl = '') { } } +function updateLastActionUI(state, peers) { + if (!state || !state.action) { + elements.lastActionCard.innerHTML = '
No recent commands
'; + return; + } + + const actionNames = { + 'play': 'PLAY', + 'pause': 'PAUSE', + 'seek': 'SEEK', + 'force_sync_prepare': 'SYNCING...', + 'force_sync_execute': 'FORCE PLAY' + }; + + let senderName = state.senderId === 'You' ? 'You' : state.senderId; + const senderPeer = peers.find(p => (p.peerId || p) === state.senderId); + if (senderPeer && senderPeer.username) senderName = senderPeer.username; + + const timeStr = new Date(state.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + + let html = ` +
+ ${actionNames[state.action] || state.action.toUpperCase()} + ${senderName} @ ${timeStr} +
+
+ `; + + // Filter out "You" if we are the sender, but show status of other peers + peers.forEach(peer => { + const pId = typeof peer === 'object' ? peer.peerId : peer; + const pName = (typeof peer === 'object' && peer.username) ? peer.username : pId.substring(0, 4); + const isAcked = state.acks.includes(pId) || pId === state.senderId; + const color = isAcked ? 'var(--success)' : '#475569'; + const icon = isAcked ? '✓' : '...'; + + html += ` +
+
+ ${icon} +
+ ${pName} +
+ `; + }); + + html += `
`; + elements.lastActionCard.innerHTML = html; +} + function updatePeerList(peers) { if (!peers) return; @@ -542,10 +594,20 @@ async function refreshLogs() { chrome.runtime.onMessage.addListener((msg) => { if (msg.type === 'LOG_UPDATE') { refreshLogs(); + } else if (msg.type === 'ACTION_UPDATE') { + chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => { + if (res && res.peers) updateLastActionUI(msg.state, res.peers); + }); } else if (msg.type === 'PEER_UPDATE') { updatePeerList(msg.peers); } else if (msg.type === 'CONNECTION_STATUS') { applyConnectionStatus(msg.status); + if (msg.status === 'connected') { + chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => { + if (res && res.peers) updatePeerList(res.peers); + if (res && res.lastActionState) updateLastActionUI(res.lastActionState, res.peers); + }); + } if (msg.status === 'disconnected' || msg.status === 'reconnect_failed') { elements.joinBtn.disabled = false; elements.joinBtn.textContent = 'Join / Create Room'; diff --git a/server/index.js b/server/index.js index 03a862e..c3ec47b 100644 --- a/server/index.js +++ b/server/index.js @@ -326,6 +326,17 @@ io.on('connection', (socket) => { } }); + socket.on(EVENTS.EVENT_ACK, (data) => { + if (!data.targetId) return; + const targetSocket = Array.from(io.sockets.sockets.values()).find(s => { + const roomData = socketToRoom.get(s.id); + return roomData && roomData.peerId === data.targetId; + }); + if (targetSocket) { + targetSocket.emit(EVENTS.EVENT_ACK, { senderId: data.senderId }); + } + }); + socket.on('disconnect', () => { eventCounts.delete(socket.id); const mapping = socketToRoom.get(socket.id); diff --git a/shared/constants.js b/shared/constants.js index 2e3da01..790ed3a 100644 --- a/shared/constants.js +++ b/shared/constants.js @@ -26,6 +26,7 @@ export const EVENTS = { FORCE_SYNC_PREPARE: "force_sync_prepare", FORCE_SYNC_ACK: "force_sync_ack", FORCE_SYNC_EXECUTE: "force_sync_execute", + EVENT_ACK: "event_ack", GET_ROOMS: "get_rooms", ROOM_LIST: "room_list" };