feat: implement visual confirmation system and redesigned Sync tab

This commit is contained in:
Timo
2026-04-22 12:20:32 +02:00
parent 3317994354
commit 221961820a
6 changed files with 136 additions and 19 deletions
+42 -3
View File
@@ -18,12 +18,15 @@ let pendingLogs = [];
let pendingHistory = []; let pendingHistory = [];
let eventQueue = []; let eventQueue = [];
let isNamespaceJoined = false; 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 // 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.logs) logs = data.logs;
if (data.history) history = data.history; if (data.history) history = data.history;
if (data.currentRoom) currentRoom = data.currentRoom; if (data.currentRoom) currentRoom = data.currentRoom;
if (data.lastActionState) lastActionState = data.lastActionState;
storageInitialized = true; storageInitialized = true;
if (pendingLogs.length > 0) { if (pendingLogs.length > 0) {
@@ -383,7 +386,8 @@ function handleServerEvent(event, data) {
case EVENTS.FORCE_SYNC_PREPARE: case EVENTS.FORCE_SYNC_PREPARE:
if (data.senderId) { if (data.senderId) {
addToHistory(event, data.senderId); addToHistory(event, data.senderId);
showNotification(data.senderId, event); showNotification(event, data.senderId);
updateLastAction(event, data.senderId);
} }
routeToContent(event, data); routeToContent(event, data);
break; break;
@@ -405,6 +409,15 @@ function handleServerEvent(event, data) {
} }
routeToContent(event, data); routeToContent(event, data);
break; 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: case EVENTS.PEER_STATUS:
if (currentRoom) { if (currentRoom) {
if (data.status === 'joined') { if (data.status === 'joined') {
@@ -449,6 +462,17 @@ function executeForceSync() {
addLog('Force Sync Executed', 'success'); 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) { async function routeToContent(action, payload) {
if (!currentTabId) { if (!currentTabId) {
const settings = await getSettings(); const settings = await getSettings();
@@ -459,6 +483,8 @@ async function routeToContent(action, payload) {
const tabId = parseInt(currentTabId); const tabId = parseInt(currentTabId);
if (isNaN(tabId)) return; if (isNaN(tabId)) return;
currentCommandSenderId = payload.senderId || null;
chrome.tabs.sendMessage(tabId, { chrome.tabs.sendMessage(tabId, {
type: 'SERVER_COMMAND', type: 'SERVER_COMMAND',
action, action,
@@ -530,7 +556,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined; const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined;
let status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : 'disconnected'); let status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : 'disconnected');
if (reconnectFailed) status = 'reconnect_failed'; 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 // Global return true at the end handles this
} else if (message.type === 'LEAVE_ROOM') { } else if (message.type === 'LEAVE_ROOM') {
emit(EVENTS.LEAVE_ROOM, { peerId }); emit(EVENTS.LEAVE_ROOM, { peerId });
@@ -610,6 +641,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
routeToContent(message.action, message.payload); routeToContent(message.action, message.payload);
} }
// Update local state as initiator
updateLastAction(message.action, 'You');
// Events coming from content script or popup // Events coming from content script or popup
if (message.action === EVENTS.FORCE_SYNC_PREPARE) { if (message.action === EVENTS.FORCE_SYNC_PREPARE) {
isForceSyncInitiator = true; isForceSyncInitiator = true;
@@ -640,6 +674,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
} else { } else {
emit(EVENTS.FORCE_SYNC_ACK, { peerId }); 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') { } else if (message.type === 'HEARTBEAT') {
if (sender.tab) { if (sender.tab) {
currentTabId = sender.tab.id; currentTabId = sender.tab.id;
+4
View File
@@ -127,10 +127,13 @@
if (action === EVENTS.PLAY) { if (action === EVENTS.PLAY) {
tryMediaAction(EVENTS.PLAY); tryMediaAction(EVENTS.PLAY);
chrome.runtime.sendMessage({ type: 'CMD_ACK' });
} else if (action === EVENTS.PAUSE) { } else if (action === EVENTS.PAUSE) {
tryMediaAction(EVENTS.PAUSE); tryMediaAction(EVENTS.PAUSE);
chrome.runtime.sendMessage({ type: 'CMD_ACK' });
} else if (action === EVENTS.SEEK) { } else if (action === EVENTS.SEEK) {
tryMediaAction(EVENTS.SEEK, payload); tryMediaAction(EVENTS.SEEK, payload);
chrome.runtime.sendMessage({ type: 'CMD_ACK' });
} else if (action === EVENTS.FORCE_SYNC_PREPARE) { } else if (action === EVENTS.FORCE_SYNC_PREPARE) {
if (!payload || payload.targetTime === undefined) return; if (!payload || payload.targetTime === undefined) return;
const video = findVideo(); const video = findVideo();
@@ -144,6 +147,7 @@
} }
} else if (action === EVENTS.FORCE_SYNC_EXECUTE) { } else if (action === EVENTS.FORCE_SYNC_EXECUTE) {
tryMediaAction(EVENTS.PLAY); tryMediaAction(EVENTS.PLAY);
chrome.runtime.sendMessage({ type: 'CMD_ACK' });
} }
} }
+16 -16
View File
@@ -277,26 +277,19 @@
</div> </div>
<label>Remote Control</label> <label>Remote Control</label>
<div style="display: flex; gap: 8px; margin-bottom: 20px;"> <div style="display: flex; gap: 8px; margin-bottom: 12px;">
<button id="playBtn" class="primary" style="flex: 1; background: var(--success);">Play</button> <button id="playBtn" class="primary" style="flex:1;">Play</button>
<button id="pauseBtn" class="primary" style="flex: 1; background: var(--error);">Pause</button> <button id="pauseBtn" class="primary" style="flex:1;">Pause</button>
<button id="forceSyncBtn" class="secondary" style="flex:1; border-color:var(--accent); color:var(--accent);">Force Sync</button>
</div> </div>
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); margin-bottom: 20px;"> <!-- NEW: Last Action Status Card -->
⚡ Force Sync Everyone <label>Last Activity Status</label>
</button> <div id="lastActionCard" class="info-card" style="margin-bottom: 15px; min-height: 70px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding-top: 20px;">No recent commands</div>
<div style="margin-bottom: 20px;">
<label>Peers in Room</label>
<div id="peerListSync" class="info-card">
<div style="text-align:center; color: var(--text-muted); font-size: 12px;">No peers connected</div>
</div>
</div> </div>
<label>Recent Activity</label> <div id="peerListSync" class="info-card" style="display:none;"></div>
<div id="historyList" class="info-card" style="max-height: 120px; overflow-y: auto; font-size: 11px; color: var(--text-muted);">
<!-- History will be injected here -->
</div>
</div> </div>
<!-- Settings Tab --> <!-- Settings Tab -->
@@ -338,6 +331,13 @@
No tab selected or video detected. No tab selected or video detected.
</div> </div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label>Full Action History</label>
</div>
<div id="historyList" class="info-card" style="max-height: 120px; overflow-y: auto; font-size: 10px; margin-bottom: 15px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px;">No activity yet</div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
<label>Logs (Last 50)</label> <label>Logs (Last 50)</label>
<button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;">CLEAR</button> <button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;">CLEAR</button>
+62
View File
@@ -32,6 +32,7 @@ const elements = {
inviteLink: document.getElementById('inviteLink'), inviteLink: document.getElementById('inviteLink'),
filterNoise: document.getElementById('filterNoise'), filterNoise: document.getElementById('filterNoise'),
regenId: document.getElementById('regenId'), regenId: document.getElementById('regenId'),
lastActionCard: document.getElementById('lastActionCard'),
historyList: document.getElementById('historyList'), historyList: document.getElementById('historyList'),
copyLogs: document.getElementById('copyLogs'), copyLogs: document.getElementById('copyLogs'),
createRoomBtn: document.getElementById('createRoomBtn'), createRoomBtn: document.getElementById('createRoomBtn'),
@@ -82,6 +83,7 @@ async function init() {
localPeerId = res.peerId; localPeerId = res.peerId;
applyConnectionStatus(res.status); applyConnectionStatus(res.status);
updatePeerList(res.peers); 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 = '<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding-top: 20px;">No recent commands</div>';
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 = `
<div style="display:flex; justify-content:space-between; margin-bottom:10px; align-items:baseline;">
<span style="font-weight:700; color:var(--accent); font-size:13px;">${actionNames[state.action] || state.action.toUpperCase()}</span>
<span style="font-size:10px; color:var(--text-muted);">${senderName} @ ${timeStr}</span>
</div>
<div style="display:grid; grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); gap: 6px;">
`;
// 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 += `
<div title="${pName}" style="display:flex; flex-direction:column; align-items:center; opacity: ${isAcked ? 1 : 0.6};">
<div style="width:20px; height:20px; border-radius:50%; background:${color}; color:white; display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:bold; margin-bottom:2px;">
${icon}
</div>
<span style="font-size:8px; color:var(--text-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:40px;">${pName}</span>
</div>
`;
});
html += `</div>`;
elements.lastActionCard.innerHTML = html;
}
function updatePeerList(peers) { function updatePeerList(peers) {
if (!peers) return; if (!peers) return;
@@ -542,10 +594,20 @@ async function refreshLogs() {
chrome.runtime.onMessage.addListener((msg) => { chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'LOG_UPDATE') { if (msg.type === 'LOG_UPDATE') {
refreshLogs(); 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') { } else if (msg.type === 'PEER_UPDATE') {
updatePeerList(msg.peers); updatePeerList(msg.peers);
} else if (msg.type === 'CONNECTION_STATUS') { } else if (msg.type === 'CONNECTION_STATUS') {
applyConnectionStatus(msg.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') { if (msg.status === 'disconnected' || msg.status === 'reconnect_failed') {
elements.joinBtn.disabled = false; elements.joinBtn.disabled = false;
elements.joinBtn.textContent = 'Join / Create Room'; elements.joinBtn.textContent = 'Join / Create Room';
+11
View File
@@ -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', () => { socket.on('disconnect', () => {
eventCounts.delete(socket.id); eventCounts.delete(socket.id);
const mapping = socketToRoom.get(socket.id); const mapping = socketToRoom.get(socket.id);
+1
View File
@@ -26,6 +26,7 @@ export const EVENTS = {
FORCE_SYNC_PREPARE: "force_sync_prepare", FORCE_SYNC_PREPARE: "force_sync_prepare",
FORCE_SYNC_ACK: "force_sync_ack", FORCE_SYNC_ACK: "force_sync_ack",
FORCE_SYNC_EXECUTE: "force_sync_execute", FORCE_SYNC_EXECUTE: "force_sync_execute",
EVENT_ACK: "event_ack",
GET_ROOMS: "get_rooms", GET_ROOMS: "get_rooms",
ROOM_LIST: "room_list" ROOM_LIST: "room_list"
}; };