mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +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 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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user