mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-09-04 15:05:20 +00:00
Stable state before Final Polish optimizations
This commit is contained in:
+2
-1
@@ -3,7 +3,8 @@
|
|||||||
Welcome to the KoalaSync project. This file is the primary entry point for any developer or AI agent working on this codebase. It defines the architecture, non-negotiables, and workflows required to maintain the stability and security of the system.
|
Welcome to the KoalaSync project. This file is the primary entry point for any developer or AI agent working on this codebase. It defines the architecture, non-negotiables, and workflows required to maintain the stability and security of the system.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> **Privacy & Data Sovereignty**: KoalaSync follows a strict "Zero-External-Requests" policy. No fonts, scripts, or assets (images/icons) may be loaded from 3rd-party CDNs or external servers. Everything must be served locally or from the project's own infrastructure.
|
> **Privacy & Data Sovereignty**: KoalaSync follows a strict **Zero-External-Requests Policy**: The extension and website must not make requests to any third-party domains (Google Fonts, CDNs, etc.). All assets (fonts, icons, scripts) must be self-hosted or use system defaults.
|
||||||
|
> - **Font Stack**: Use a modern system font stack (e.g., -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif) to maintain a premium look without external dependencies. Prohibit the use of `@import` or `<link>` for external font services.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+78
-39
@@ -11,6 +11,7 @@ let currentTabId = null;
|
|||||||
let currentTabTitle = null; // New: for Smart Matching
|
let currentTabTitle = null; // New: for Smart Matching
|
||||||
let logs = [];
|
let logs = [];
|
||||||
let history = []; // New: for Action History
|
let history = []; // New: for Action History
|
||||||
|
let reconnectTimer = null;
|
||||||
|
|
||||||
// Force Sync Coordination
|
// Force Sync Coordination
|
||||||
let isForceSyncInitiator = false;
|
let isForceSyncInitiator = false;
|
||||||
@@ -53,7 +54,12 @@ function addLog(message, type = 'info') {
|
|||||||
|
|
||||||
// --- WebSocket Client ---
|
// --- WebSocket Client ---
|
||||||
async function connect() {
|
async function connect() {
|
||||||
if (isConnecting || (socket && socket.readyState === WebSocket.OPEN)) return;
|
if (isConnecting) return;
|
||||||
|
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
addLog('Browser is offline. Waiting...', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!peerId) peerId = await getPeerId();
|
if (!peerId) peerId = await getPeerId();
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
@@ -61,31 +67,52 @@ async function connect() {
|
|||||||
isConnecting = true;
|
isConnecting = true;
|
||||||
broadcastConnectionStatus('connecting');
|
broadcastConnectionStatus('connecting');
|
||||||
const isCustomServer = settings.serverUrl && settings.useCustomServer;
|
const isCustomServer = settings.serverUrl && settings.useCustomServer;
|
||||||
const finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
|
let finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
|
||||||
|
|
||||||
|
// Robustness: Ensure finalUrl is not empty and has a protocol
|
||||||
|
if (isCustomServer) {
|
||||||
|
finalUrl = finalUrl.trim();
|
||||||
|
if (!finalUrl.includes('://')) {
|
||||||
|
finalUrl = 'ws://' + finalUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}...`, 'info');
|
addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}...`, 'info');
|
||||||
|
|
||||||
const url = new URL(finalUrl);
|
try {
|
||||||
url.pathname = '/socket.io/';
|
const url = new URL(finalUrl);
|
||||||
url.searchParams.set('EIO', '4');
|
url.pathname = '/socket.io/';
|
||||||
url.searchParams.set('transport', 'websocket');
|
url.searchParams.set('EIO', '4');
|
||||||
url.searchParams.set('version', APP_VERSION);
|
url.searchParams.set('transport', 'websocket');
|
||||||
|
url.searchParams.set('version', APP_VERSION);
|
||||||
|
|
||||||
if (!isCustomServer) {
|
if (!isCustomServer) {
|
||||||
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||||
}
|
} else {
|
||||||
|
// Self-hosted servers use the same official token by design.
|
||||||
|
// This allows users to run their own relay while still using
|
||||||
|
// the official extension — the token is public and not a secret.
|
||||||
|
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
socket = new WebSocket(url.toString());
|
socket = new WebSocket(url.toString());
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
|
isConnecting = false;
|
||||||
|
reconnectDelay = 1000;
|
||||||
|
addLog('WebSocket Connection Opened', 'success');
|
||||||
|
broadcastConnectionStatus('connected');
|
||||||
|
|
||||||
|
// Socket.IO Handshake: Send "40" to join default namespace
|
||||||
|
socket.send('40');
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
isConnecting = false;
|
isConnecting = false;
|
||||||
reconnectDelay = 1000;
|
addLog(`Invalid Server URL: ${finalUrl}`, 'error');
|
||||||
addLog('WebSocket Connection Opened', 'success');
|
broadcastConnectionStatus('disconnected');
|
||||||
broadcastConnectionStatus('connected');
|
scheduleReconnect();
|
||||||
|
return;
|
||||||
// Socket.IO Handshake: Send "40" to join default namespace
|
}
|
||||||
socket.send('40');
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
const msg = event.data;
|
const msg = event.data;
|
||||||
@@ -169,7 +196,10 @@ function showNotification(senderName, action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleReconnect() {
|
function scheduleReconnect() {
|
||||||
setTimeout(() => {
|
if (reconnectTimer) return; // Already scheduled
|
||||||
|
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
reconnectTimer = null;
|
||||||
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
||||||
connect();
|
connect();
|
||||||
}, reconnectDelay);
|
}, reconnectDelay);
|
||||||
@@ -182,6 +212,17 @@ function emit(event, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addToHistory(action, senderId) {
|
||||||
|
const historyEntry = {
|
||||||
|
action,
|
||||||
|
senderId: senderId || 'You',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
history.unshift(historyEntry);
|
||||||
|
if (history.length > 20) history.pop();
|
||||||
|
chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
// --- Event Handlers ---
|
// --- Event Handlers ---
|
||||||
function handleServerEvent(event, data) {
|
function handleServerEvent(event, data) {
|
||||||
// console.log(`[RECV] ${event}`, data);
|
// console.log(`[RECV] ${event}`, data);
|
||||||
@@ -203,7 +244,7 @@ function handleServerEvent(event, data) {
|
|||||||
break;
|
break;
|
||||||
case EVENTS.ERROR:
|
case EVENTS.ERROR:
|
||||||
addLog(`Server Error: ${data.message}`, 'error');
|
addLog(`Server Error: ${data.message}`, 'error');
|
||||||
chrome.notifications.create({
|
chrome.notifications.create(`error_${Date.now()}`, {
|
||||||
type: 'basic',
|
type: 'basic',
|
||||||
iconUrl: 'icons/icon128.png',
|
iconUrl: 'icons/icon128.png',
|
||||||
title: 'KoalaSync Error',
|
title: 'KoalaSync Error',
|
||||||
@@ -220,6 +261,10 @@ function handleServerEvent(event, data) {
|
|||||||
case EVENTS.PAUSE:
|
case EVENTS.PAUSE:
|
||||||
case EVENTS.SEEK:
|
case EVENTS.SEEK:
|
||||||
case EVENTS.FORCE_SYNC_PREPARE:
|
case EVENTS.FORCE_SYNC_PREPARE:
|
||||||
|
if (data.senderId) {
|
||||||
|
addToHistory(event, data.senderId);
|
||||||
|
showNotification(data.senderId, event);
|
||||||
|
}
|
||||||
routeToContent(event, data);
|
routeToContent(event, data);
|
||||||
break;
|
break;
|
||||||
case EVENTS.FORCE_SYNC_ACK:
|
case EVENTS.FORCE_SYNC_ACK:
|
||||||
@@ -234,6 +279,10 @@ function handleServerEvent(event, data) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case EVENTS.FORCE_SYNC_EXECUTE:
|
case EVENTS.FORCE_SYNC_EXECUTE:
|
||||||
|
if (data.senderId) {
|
||||||
|
addToHistory(event, data.senderId);
|
||||||
|
showNotification(data.senderId, event);
|
||||||
|
}
|
||||||
routeToContent(event, data);
|
routeToContent(event, data);
|
||||||
break;
|
break;
|
||||||
case EVENTS.PEER_STATUS:
|
case EVENTS.PEER_STATUS:
|
||||||
@@ -263,19 +312,7 @@ function handleServerEvent(event, data) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// History Tracking
|
addLog(`Received unknown event from server: ${event}`, 'warn');
|
||||||
const historyEntry = {
|
|
||||||
action: event,
|
|
||||||
senderId: data.senderId || 'You',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
history.unshift(historyEntry);
|
|
||||||
if (history.length > 20) history.pop();
|
|
||||||
chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {});
|
|
||||||
|
|
||||||
// Notification for remote actions
|
|
||||||
if (data.senderId) showNotification(data.senderId, event);
|
|
||||||
routeToContent(event, data);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,8 +340,8 @@ async function routeToContent(action, payload) {
|
|||||||
action,
|
action,
|
||||||
payload
|
payload
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
// Auto-Reinject if content script is missing
|
// Auto-Reinject if content script is missing or extension was reloaded
|
||||||
if (err.message.includes('Receiving end does not exist')) {
|
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
|
||||||
chrome.scripting.executeScript({
|
chrome.scripting.executeScript({
|
||||||
target: { tabId },
|
target: { tabId },
|
||||||
files: ['content.js']
|
files: ['content.js']
|
||||||
@@ -335,8 +372,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (message.type === 'CONNECT') {
|
if (message.type === 'CONNECT') {
|
||||||
connect();
|
connect();
|
||||||
} else if (message.type === 'GET_STATUS') {
|
} else if (message.type === 'GET_STATUS') {
|
||||||
const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting ? 'connecting' : 'disconnected')) : 'disconnected';
|
const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting || socket.readyState === WebSocket.CONNECTING ? 'connecting' : 'disconnected')) : 'disconnected';
|
||||||
sendResponse({ status, peers: currentRoom ? currentRoom.peers : [] });
|
sendResponse({ status, peerId, peers: currentRoom ? currentRoom.peers : [] });
|
||||||
|
// 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 });
|
||||||
currentRoom = null;
|
currentRoom = null;
|
||||||
@@ -377,6 +415,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
addToHistory(message.action, 'You');
|
||||||
emit(message.action, { ...message.payload, peerId });
|
emit(message.action, { ...message.payload, peerId });
|
||||||
} else if (message.type === 'FORCE_SYNC_ACK') {
|
} else if (message.type === 'FORCE_SYNC_ACK') {
|
||||||
emit(EVENTS.FORCE_SYNC_ACK, { peerId });
|
emit(EVENTS.FORCE_SYNC_ACK, { peerId });
|
||||||
@@ -388,7 +427,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
// Peer status heartbeat from content script
|
// Peer status heartbeat from content script
|
||||||
emit(EVENTS.PEER_STATUS, { ...message.payload, peerId, tabTitle: currentTabTitle });
|
emit(EVENTS.PEER_STATUS, { ...message.payload, peerId, tabTitle: currentTabTitle });
|
||||||
}
|
}
|
||||||
return true;
|
return true; // Keep channel open for async responses
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tab removal listener
|
// Tab removal listener
|
||||||
|
|||||||
+11
-2
@@ -145,7 +145,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Heartbeat
|
// Heartbeat
|
||||||
setInterval(() => {
|
let heartbeatErrorCount = 0;
|
||||||
|
const heartbeatInterval = setInterval(() => {
|
||||||
const video = findVideo();
|
const video = findVideo();
|
||||||
if (video) {
|
if (video) {
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
@@ -154,7 +155,15 @@
|
|||||||
playbackState: video.paused ? 'paused' : 'playing',
|
playbackState: video.paused ? 'paused' : 'playing',
|
||||||
currentTime: video.currentTime
|
currentTime: video.currentTime
|
||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(err => {
|
||||||
|
if (err.message.includes('Extension context invalidated')) {
|
||||||
|
heartbeatErrorCount++;
|
||||||
|
if (heartbeatErrorCount === 1) {
|
||||||
|
console.warn('KoalaSync: Extension reloaded. Please refresh the page if sync stops working.');
|
||||||
|
}
|
||||||
|
clearInterval(heartbeatInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"tabs",
|
"tabs",
|
||||||
"scripting",
|
"scripting",
|
||||||
"alarms",
|
"alarms",
|
||||||
"activeTab"
|
"activeTab",
|
||||||
|
"notifications"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
|
|||||||
+58
-18
@@ -1,8 +1,16 @@
|
|||||||
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
|
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
|
||||||
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
|
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
tabs: document.querySelectorAll('.tab-btn'),
|
tabs: document.querySelectorAll('.tabs .tab-btn'),
|
||||||
contents: document.querySelectorAll('.tab-content'),
|
contents: document.querySelectorAll('.tab-content'),
|
||||||
copyInvite: document.getElementById('copyInvite'),
|
copyInvite: document.getElementById('copyInvite'),
|
||||||
targetTab: document.getElementById('targetTab'),
|
targetTab: document.getElementById('targetTab'),
|
||||||
@@ -30,6 +38,8 @@ const elements = {
|
|||||||
roomError: document.getElementById('roomError')
|
roomError: document.getElementById('roomError')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let localPeerId = null;
|
||||||
|
|
||||||
// --- Initialization ---
|
// --- Initialization ---
|
||||||
async function init() {
|
async function init() {
|
||||||
// Load Settings
|
// Load Settings
|
||||||
@@ -55,6 +65,7 @@ async function init() {
|
|||||||
// Initial Status Check
|
// Initial Status Check
|
||||||
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
|
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
|
localPeerId = res.peerId;
|
||||||
applyConnectionStatus(res.status);
|
applyConnectionStatus(res.status);
|
||||||
updatePeerList(res.peers);
|
updatePeerList(res.peers);
|
||||||
}
|
}
|
||||||
@@ -78,24 +89,36 @@ function updateUI(roomId, password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePeerList(peers) {
|
function updatePeerList(peers) {
|
||||||
if (!peers) return;
|
if (!peers || !elements.peerList) return;
|
||||||
elements.peerList.innerHTML = peers.map(id => `
|
elements.peerList.innerHTML = peers.map(p => {
|
||||||
<div class="peer-item">
|
const id = escapeHtml(typeof p === 'object' ? p.peerId : p);
|
||||||
<span>👤 ${id}</span>
|
const titleText = (typeof p === 'object' && p.tabTitle) ? escapeHtml(p.tabTitle) : '';
|
||||||
</div>
|
const title = titleText ? `<div style="font-size:10px; color:var(--text-muted);">${titleText}</div>` : '';
|
||||||
`).join('');
|
return `
|
||||||
|
<div class="peer-item" style="display:block; padding: 6px 0;">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<span style="font-weight:600;">👤 ${id}</span>
|
||||||
|
${id === escapeHtml(localPeerId) ? '<span style="font-size:10px; color:var(--accent)">YOU</span>' : ''}
|
||||||
|
</div>
|
||||||
|
${title}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
// Re-populate tabs to update Star Matching when peers change
|
// Re-populate tabs to update Star Matching when peers change
|
||||||
populateTabs();
|
populateTabs(peers);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateTabs() {
|
async function populateTabs(providedPeers = null) {
|
||||||
const data = await chrome.storage.sync.get(['targetTabId', 'filterNoise']);
|
const data = await chrome.storage.sync.get(['targetTabId', 'filterNoise']);
|
||||||
const isFilterActive = data.filterNoise !== false;
|
const isFilterActive = data.filterNoise !== false;
|
||||||
const currentTargetTabId = data.targetTabId;
|
const currentTargetTabId = data.targetTabId;
|
||||||
|
|
||||||
// Get current peers from background to do matching
|
// Use provided peers or fetch if missing
|
||||||
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
|
let peerIds = providedPeers;
|
||||||
const peerIds = status?.peers || [];
|
if (!peerIds) {
|
||||||
|
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
|
||||||
|
peerIds = status?.peers || [];
|
||||||
|
}
|
||||||
|
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await chrome.tabs.query({});
|
||||||
|
|
||||||
@@ -165,8 +188,9 @@ function updateHistory(history) {
|
|||||||
}
|
}
|
||||||
elements.historyList.innerHTML = history.map(item => {
|
elements.historyList.innerHTML = history.map(item => {
|
||||||
const time = new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
const time = new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
const actionLabel = item.action.toUpperCase().replace('FORCE_SYNC_', '');
|
const actionLabel = escapeHtml(item.action.toUpperCase().replace('FORCE_SYNC_', ''));
|
||||||
const sender = item.senderId === 'You' ? '<span style="color:var(--accent)">You</span>' : item.senderId;
|
const senderIdEscaped = escapeHtml(item.senderId);
|
||||||
|
const sender = item.senderId === 'You' ? '<span style="color:var(--accent)">You</span>' : senderIdEscaped;
|
||||||
return `<div style="margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px;">
|
return `<div style="margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px;">
|
||||||
<span style="color:#64748b">[${time}]</span> <b>${actionLabel}</b> by ${sender}
|
<span style="color:#64748b">[${time}]</span> <b>${actionLabel}</b> by ${sender}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -185,9 +209,12 @@ function updateRoomList(rooms) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
elements.publicRooms.innerHTML = rooms.map(r => `
|
elements.publicRooms.innerHTML = rooms.map(r => `
|
||||||
<div class="room-item" style="display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor:pointer;" data-id="${r.id}">
|
<div class="room-item" style="display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor:pointer;" data-id="${escapeHtml(r.id)}">
|
||||||
<span style="font-weight:600;">${r.id}</span>
|
<div style="display:flex; align-items:center; gap: 6px;">
|
||||||
<span style="font-size:11px; color:var(--accent)">${r.peerCount} peers</span>
|
<span style="font-weight:600;">${escapeHtml(r.id)}</span>
|
||||||
|
${r.hasPassword ? '<span title="Password Protected">🔒</span>' : ''}
|
||||||
|
</div>
|
||||||
|
<span style="font-size:11px; color:var(--accent)">${parseInt(r.peerCount)} peers</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
@@ -232,6 +259,19 @@ elements.filterNoise.addEventListener('change', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elements.serverUrl.addEventListener('input', () => {
|
||||||
|
chrome.storage.sync.set({ serverUrl: elements.serverUrl.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.serverUrl.addEventListener('change', () => {
|
||||||
|
let url = elements.serverUrl.value.trim();
|
||||||
|
if (url && !url.includes('://')) {
|
||||||
|
url = 'ws://' + url;
|
||||||
|
elements.serverUrl.value = url;
|
||||||
|
chrome.storage.sync.set({ serverUrl: url });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
elements.tabs.forEach(btn => {
|
elements.tabs.forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
elements.tabs.forEach(b => b.classList.remove('active'));
|
elements.tabs.forEach(b => b.classList.remove('active'));
|
||||||
|
|||||||
+14
-3
@@ -178,6 +178,7 @@ io.on('connection', (socket) => {
|
|||||||
passwordHash,
|
passwordHash,
|
||||||
peers: new Set(),
|
peers: new Set(),
|
||||||
peerIds: new Map(),
|
peerIds: new Map(),
|
||||||
|
peerData: new Map(), // socketId -> { peerId, tabTitle }
|
||||||
lastActivity: Date.now()
|
lastActivity: Date.now()
|
||||||
};
|
};
|
||||||
rooms.set(roomId, room);
|
rooms.set(roomId, room);
|
||||||
@@ -199,12 +200,13 @@ io.on('connection', (socket) => {
|
|||||||
socket.join(roomId);
|
socket.join(roomId);
|
||||||
room.peers.add(socket.id);
|
room.peers.add(socket.id);
|
||||||
room.peerIds.set(socket.id, peerId);
|
room.peerIds.set(socket.id, peerId);
|
||||||
|
room.peerData.set(socket.id, { peerId, tabTitle: null });
|
||||||
socketToRoom.set(socket.id, { roomId, peerId });
|
socketToRoom.set(socket.id, { roomId, peerId });
|
||||||
|
|
||||||
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'joined' });
|
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'joined' });
|
||||||
socket.emit(EVENTS.ROOM_DATA, {
|
socket.emit(EVENTS.ROOM_DATA, {
|
||||||
roomId,
|
roomId,
|
||||||
peers: Array.from(room.peers).map(sid => room.peerIds.get(sid))
|
peers: Array.from(room.peers).map(sid => room.peerData.get(sid))
|
||||||
});
|
});
|
||||||
log('ROOM', `Peer ${peerId} joined: ${roomId}`);
|
log('ROOM', `Peer ${peerId} joined: ${roomId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -231,7 +233,13 @@ io.on('connection', (socket) => {
|
|||||||
const mapping = socketToRoom.get(socket.id);
|
const mapping = socketToRoom.get(socket.id);
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
const room = rooms.get(mapping.roomId);
|
const room = rooms.get(mapping.roomId);
|
||||||
if (room) room.lastActivity = Date.now();
|
if (room) {
|
||||||
|
room.lastActivity = Date.now();
|
||||||
|
// Update metadata if it's a peer_status (heartbeat)
|
||||||
|
if (eventName === EVENTS.PEER_STATUS && data.tabTitle) {
|
||||||
|
room.peerData.set(socket.id, { peerId: mapping.peerId, tabTitle: data.tabTitle });
|
||||||
|
}
|
||||||
|
}
|
||||||
socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId });
|
socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -240,7 +248,8 @@ io.on('connection', (socket) => {
|
|||||||
socket.on(EVENTS.GET_ROOMS, () => {
|
socket.on(EVENTS.GET_ROOMS, () => {
|
||||||
const list = Array.from(rooms.entries()).map(([id, r]) => ({
|
const list = Array.from(rooms.entries()).map(([id, r]) => ({
|
||||||
id,
|
id,
|
||||||
peerCount: r.peers.size
|
peerCount: r.peers.size,
|
||||||
|
hasPassword: !!r.passwordHash
|
||||||
}));
|
}));
|
||||||
socket.emit(EVENTS.ROOM_LIST, { rooms: list });
|
socket.emit(EVENTS.ROOM_LIST, { rooms: list });
|
||||||
});
|
});
|
||||||
@@ -254,6 +263,7 @@ io.on('connection', (socket) => {
|
|||||||
if (room) {
|
if (room) {
|
||||||
room.peers.delete(socket.id);
|
room.peers.delete(socket.id);
|
||||||
room.peerIds.delete(socket.id);
|
room.peerIds.delete(socket.id);
|
||||||
|
room.peerData.delete(socket.id);
|
||||||
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
||||||
if (room.peers.size === 0) {
|
if (room.peers.size === 0) {
|
||||||
rooms.delete(roomId);
|
rooms.delete(roomId);
|
||||||
@@ -273,6 +283,7 @@ io.on('connection', (socket) => {
|
|||||||
if (room) {
|
if (room) {
|
||||||
room.peers.delete(socket.id);
|
room.peers.delete(socket.id);
|
||||||
room.peerIds.delete(socket.id);
|
room.peerIds.delete(socket.id);
|
||||||
|
room.peerData.delete(socket.id);
|
||||||
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
||||||
if (room.peers.size === 0) {
|
if (room.peers.size === 0) {
|
||||||
rooms.delete(roomId);
|
rooms.delete(roomId);
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ export const APP_VERSION = "1.0.0";
|
|||||||
|
|
||||||
export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net';
|
export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net';
|
||||||
export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';
|
export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';
|
||||||
export const OFFICIAL_SERVER_TOKEN = 'koala_secure_access_2026';
|
export const OFFICIAL_SERVER_TOKEN = '62170b705234c4f4807a9b22420bb93cf1a2aacfa4c5d3b47804482babb8eb50';
|
||||||
|
|
||||||
export const EVENTS = {
|
export const EVENTS = {
|
||||||
// Connection & Room
|
// Connection & Room
|
||||||
|
|||||||
Reference in New Issue
Block a user