mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-09-04 15:05:20 +00:00
feat: implement visual confirmation system and redesigned Sync tab
This commit is contained in:
+42
-3
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user