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 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;
+4
View File
@@ -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' });
}
}
+16 -16
View File
@@ -277,26 +277,19 @@
</div>
<label>Remote Control</label>
<div style="display: flex; gap: 8px; margin-bottom: 20px;">
<button id="playBtn" class="primary" style="flex: 1; background: var(--success);">Play</button>
<button id="pauseBtn" class="primary" style="flex: 1; background: var(--error);">Pause</button>
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
<button id="playBtn" class="primary" style="flex:1;">Play</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>
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); margin-bottom: 20px;">
⚡ Force Sync Everyone
</button>
<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>
<!-- NEW: Last Action Status Card -->
<label>Last Activity Status</label>
<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>
<label>Recent Activity</label>
<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 id="peerListSync" class="info-card" style="display:none;"></div>
</div>
<!-- Settings Tab -->
@@ -338,6 +331,13 @@
No tab selected or video detected.
</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;">
<label>Logs (Last 50)</label>
<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'),
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 = '<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) {
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';
+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', () => {
eventCounts.delete(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_ACK: "force_sync_ack",
FORCE_SYNC_EXECUTE: "force_sync_execute",
EVENT_ACK: "event_ack",
GET_ROOMS: "get_rooms",
ROOM_LIST: "room_list"
};