Compare commits

...

16 Commits

Author SHA1 Message Date
Koala db11812bd6 fix: suppress seek events when solo, add moz-extension CORS, add server logging 2026-05-26 01:06:04 +02:00
GitHub Action 6db8fdbf75 chore(release): update versions to v1.8.5 [skip ci] 2026-05-25 21:56:58 +00:00
Koala d23c37f87f chore(release): v1.8.5 2026-05-25 23:56:50 +02:00
Koala acd428d4f7 fix: audit fixes — auto-match title corruption, duplicate HTML attrs, stale ℹ️ icons, missing tooltip, remove fix_ui.js 2026-05-25 23:56:14 +02:00
Koala c0a6f0adc2 ui: clipboard emoji for invite button, movie emoji for audible tab indicator 2026-05-25 23:56:14 +02:00
GitHub Action 42b73bb97f chore(release): update versions to v1.8.3 [skip ci] 2026-05-25 21:43:51 +00:00
Koala dc36bfdded chore(release): v1.8.3 2026-05-25 23:43:41 +02:00
Koala 06db850387 fix(ui): add comprehensive tooltips to all labels, buttons and inputs 2026-05-25 23:43:09 +02:00
Koala 0de92b5b61 fix(ui): tooltips, full emoji map, and onboarding layout 2026-05-25 23:37:21 +02:00
GitHub Action 8ff8e7beb6 chore(release): update versions to v1.8.1 [skip ci] 2026-05-25 21:32:16 +00:00
Koala 8317099072 chore(release): prep for v1.8.1 2026-05-25 23:32:07 +02:00
Koala 04c63dcf68 fix(ui): add missing avatar emoji in episode lobby UI 2026-05-25 23:30:26 +02:00
Koala fd23ccc23a feat(ui): v1.8.1 visual upgrades (emojis, animations, audio indicator) 2026-05-25 23:29:06 +02:00
Koala 4d5caeda9e style(ui): redesign sync button layout and rename to SYNC 2026-05-25 23:20:37 +02:00
Koala f1f41e5cac fix(ui): apply tooltip to full label text for settings 2026-05-25 23:19:23 +02:00
GitHub Action 473eacda22 chore(release): update versions to v1.8.0 [skip ci] 2026-05-25 21:09:07 +00:00
8 changed files with 218 additions and 71 deletions
+9
View File
@@ -1167,6 +1167,15 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}, FORCE_SYNC_TIMEOUT);
}
addToHistory(message.action, 'You');
const isNonEssentialEvent = message.action === EVENTS.PLAY || message.action === EVENTS.PAUSE || message.action === EVENTS.SEEK;
const hasOtherPeers = currentRoom && Array.isArray(currentRoom.peers) && currentRoom.peers.length > 0;
if (isNonEssentialEvent && !hasOtherPeers) {
sendResponse({ status: 'ok_solo' });
return;
}
emit(message.action, { ...message.payload, peerId });
sendResponse({ status: 'ok' });
};
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "1.8.0",
"version": "1.8.5",
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, Netflix, and HTML5 sites in real-time with friends.",
"permissions": [
"storage",
+108 -46
View File
@@ -237,6 +237,53 @@
from { opacity: 1; }
to { opacity: 0; transform: translateY(-10px); }
}
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateX(5px); }
to { opacity: 1; transform: translateX(0); }
}
.tab-active-animate {
animation: fadeSlideIn 0.15s ease-out forwards;
}
/* Toggle Switch CSS */
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #334155;
transition: .3s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: #94a3b8;
transition: .3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(16px);
background-color: white;
}
</style>
</head>
<body>
@@ -244,9 +291,9 @@
<h1><img src="icons/icon128.png" alt="KoalaSync Logo">KoalaSync</h1>
<div class="tabs">
<button class="tab-btn active" data-tab="tab-room">Room</button>
<button class="tab-btn" data-tab="tab-sync">Sync</button>
<button class="tab-btn" data-tab="tab-settings">Settings</button>
<button class="tab-btn active" data-tab="tab-room" title="Room settings and connection">Room</button>
<button class="tab-btn" data-tab="tab-sync" title="Video sync controls and remote actions">Sync</button>
<button class="tab-btn" data-tab="tab-settings" title="Extension preferences">Settings</button>
<button class="tab-btn" data-tab="tab-dev" title="Advanced Diagnostics & Logs">Status</button>
</div>
@@ -260,27 +307,27 @@
<summary style="font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; cursor: pointer; outline: none;">Manual Connect / Advanced</summary>
<div style="margin-top: 12px;">
<div class="form-group">
<label>Server</label>
<label title="Select which KoalaSync server to use">Server</label>
<div style="display:flex; gap:4px; margin-bottom:8px;">
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;">Official</button>
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;">Custom</button>
<button id="serverOfficial" class="tab-btn active" style="flex:1; padding:6px; font-size:11px;" title="Use the official reliable server">Official</button>
<button id="serverCustom" class="tab-btn" style="flex:1; padding:6px; font-size:11px;" title="Connect to your own self-hosted server">Custom</button>
</div>
<input type="text" id="serverUrl" placeholder="wss://your-server:3000" style="display:none;">
</div>
<div class="form-group">
<label>Room ID</label>
<input type="text" id="roomId" placeholder="Leave empty to create">
<label title="The unique identifier for your sync room">Room ID</label>
<input type="text" id="roomId" placeholder="Enter Room ID" title="The unique ID of the room you want to join">
</div>
<div class="form-group">
<label>Password (Optional)</label>
<input type="password" id="password" placeholder="Room password">
<label title="Optional password to restrict room access">Password (Optional)</label>
<input type="password" id="password" placeholder="Room Password (optional)" title="Password for the room (leave empty if none)">
</div>
<div id="roomError" style="display:none; color:var(--error); font-size:11px; margin-bottom:8px; text-align:center;"></div>
<button id="joinBtn" class="primary">Join Room</button>
<button id="joinBtn" class="primary" title="Connect to the room">Join Room</button>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1.5rem; margin-bottom: 8px;">
<label style="margin:0;">Public Rooms</label>
<button id="refreshRooms" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;">REFRESH</button>
<label style="margin:0;" title="List of publicly available rooms on this server">Public Rooms</label>
<button id="refreshRooms" class="secondary" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" title="Refresh the list of public rooms">REFRESH</button>
</div>
<div id="publicRooms" class="info-card" style="max-height: 120px; overflow-y: auto; padding: 4px;">
<div style="text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;">Refreshing...</div>
@@ -293,14 +340,14 @@
<div id="section-active" style="display:none;">
<div class="info-card" style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; border-left: 4px solid var(--accent);">
<div>
<label style="margin-bottom: 0;">Active Room</label>
<label style="margin-bottom: 0;" title="The room you are currently connected to">Active Room</label>
<div id="activeRoomId" style="font-weight: 700; color: var(--accent); font-size: 16px; letter-spacing: 1px;">NONE</div>
</div>
<div id="activeServer" style="font-size: 10px; color: var(--text-muted); text-align: right; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Official Server</div>
</div>
<div class="info-card" style="margin-bottom: 20px;">
<label>Invite Link</label>
<label title="Share this link with friends so they can join">Invite Link</label>
<div class="invite-box">
<input type="text" id="inviteLink" readonly>
<button id="copyInvite" class="secondary">📋</button>
@@ -308,7 +355,7 @@
</div>
<div style="margin-bottom: 20px;">
<label>Peers in Room</label>
<label title="Other users currently connected to this room">Peers in Room</label>
<div id="peerList" class="info-card">
<div style="text-align:center; color: var(--text-muted); font-size: 12px;">No peers connected</div>
</div>
@@ -323,28 +370,31 @@
<!-- SYNC ACTIVE: Visible when in a room -->
<div id="sync-active">
<div class="form-group">
<label>Select Video</label>
<label title="Choose the browser tab containing the video to sync">Select Video</label>
<select id="targetTab">
<option value="">-- Select a Tab --</option>
</select>
</div>
<label>Remote Control</label>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="margin: 0;">Remote Control</label>
<button id="syncTabCopyInvite" title="Copy Invite Link" style="background:transparent; border: 1px solid #334155; border-radius: 6px; padding: 4px 8px; font-size: 11px; cursor:pointer; opacity:0.8; transition: opacity 0.2s; color: var(--text-muted); display: flex; align-items: center; gap: 4px; white-space: nowrap;">📋 Invite Link</button>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
<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>
<button id="playBtn" class="primary" style="flex:1; background: var(--success);" title="Send a Play command to everyone">▶ Play</button>
<button id="pauseBtn" class="primary" style="flex:1; background: var(--error);" title="Send a Pause command to everyone">⏸ Pause</button>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 15px; align-items: stretch;">
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); flex: 1;">⚡ Sync All Viewers</button>
<select id="forceSyncMode" style="width: auto; min-width: 130px; background: var(--card); border: 1px solid #334155; color: white; padding: 10px 8px; border-radius: 8px; font-size: 11px; font-family: inherit; cursor: pointer; align-self: stretch;" title="Choose sync target">
<button id="forceSyncBtn" class="primary" style="background: linear-gradient(135deg, #6366f1, #a855f7); flex: 1;" title="Force all users to sync up">⚡ SYNC</button>
<select id="forceSyncMode" style="flex: 1; background: var(--card); border: 1px solid #334155; color: white; padding: 10px 8px; border-radius: 8px; font-size: 11px; font-family: inherit; cursor: pointer; align-self: stretch;" title="Choose sync target">
<option value="jump-to-others">Jump to Others</option>
<option value="jump-to-me">Jump to Me</option>
</select>
</div>
<!-- NEW: Last Action Status Card -->
<label>Last Activity Status</label>
<label title="Shows the most recent play, pause, or seek command">Last Activity Status</label>
<div id="lastActionCard" class="info-card" style="margin-bottom: 15px; max-height: 120px; overflow-y: auto;">
<div style="text-align:center; color: var(--text-muted); font-size: 10px;">No recent commands</div>
</div>
@@ -367,71 +417,83 @@
<div style="font-size: 32px; margin-bottom: 12px;">🔒</div>
<h3 style="margin: 0 0 8px 0; color: var(--accent); font-size: 15px;">Connect to a room first</h3>
<p style="color: var(--text-muted); font-size: 12px; margin-bottom: 20px;">You need to join a room via an invite link or create a new one to sync videos.</p>
<button id="syncTabCreateRoomBtn" class="primary" style="padding: 12px; font-size: 14px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);">Create New Room</button>
<button id="syncTabCreateRoomBtn" class="primary" style="padding: 12px; font-size: 14px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);" title="Create a new random room and join it">Create New Room</button>
</div>
</div>
<!-- Settings Tab -->
<div id="tab-settings" class="tab-content">
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" title="Username helps others identify you.">Your Username </label>
<label style="margin-bottom: 0;" title="Username helps others identify you.">Your Username</label>
<input type="text" id="username" placeholder="Anonymous Koala" maxlength="20" style="width: 150px;">
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" title="Hides non-video sites like search engines or social media.">Hide Clutter Tabs </label>
<input type="checkbox" id="filterNoise" style="width: auto;" checked>
<label style="margin-bottom: 0; cursor: help;" title="Filters out non-video tabs and unrelated domains to keep the list clean">Hide Clutter Tabs</label>
<label class="toggle-switch">
<input type="checkbox" id="filterNoise" checked>
<span class="slider"></span>
</label>
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" title="Pauses automatically and waits for all peers when an episode changes, then sync-starts together.">Auto-Sync Next Episode </label>
<input type="checkbox" id="autoSyncNextEpisode" style="width: auto;">
<label style="margin-bottom: 0; cursor: help;" title="Pauses automatically and waits for all peers when an episode changes, then sync-starts together.">Auto-Sync Next Episode</label>
<label class="toggle-switch">
<input type="checkbox" id="autoSyncNextEpisode">
<span class="slider"></span>
</label>
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;" title="Automatically copies the invite link to your clipboard when creating a new room.">Auto-Copy Invite Link </label>
<input type="checkbox" id="autoCopyInvite" style="width: auto;" checked>
<label style="margin-bottom: 0; cursor: help;" title="Automatically copies the invite link to your clipboard when creating a new room.">Auto-Copy Invite Link</label>
<label class="toggle-switch">
<input type="checkbox" id="autoCopyInvite" checked>
<span class="slider"></span>
</label>
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<label style="margin-bottom: 0;">Browser Notifications</label>
<input type="checkbox" id="browserNotifications" style="width: auto;">
<label style="margin-bottom: 0; cursor: help;" title="Shows native system notifications when someone joins/leaves or plays/pauses.">Browser Notifications</label>
<label class="toggle-switch">
<input type="checkbox" id="browserNotifications">
<span class="slider"></span>
</label>
</div>
<div style="margin-top: 15px; padding: 8px; border-top: 1px solid var(--card);">
<label>Troubleshooting</label>
<button id="regenId" class="secondary" style="width: 100%; font-size: 11px;">Regenerate Peer ID</button>
<label title="Tools for fixing connection issues">Troubleshooting</label>
<button id="regenId" class="secondary" style="width: 100%; font-size: 11px;" title="Regenerate your internal ID and reconnect">Regenerate Peer ID</button>
<p style="font-size: 9px; color: var(--text-muted); margin-top: 5px; text-align: center;">Use this if you see "Duplicate Identity" errors.</p>
</div>
</div>
<!-- Dev Tab -->
<div id="tab-dev" class="tab-content">
<label>Connection Status</label>
<label title="Current WebSocket connection state">Connection Status</label>
<div id="connStatus" class="info-card" style="display:flex; align-items:center; gap: 10px;">
<span id="connDot" class="status-dot status-offline"></span>
<span id="connText" style="flex:1;">Disconnected</span>
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px;">Copy Logs</button>
<button id="retryBtn" class="secondary" style="display:none; width: auto; padding: 4px 8px; font-size: 10px; margin: 0;" title="Attempt to reconnect to the server">RETRY</button>
<button id="copyLogs" class="btn secondary" style="width: auto; padding: 4px 10px; font-size: 11px;" title="Copy logs to clipboard for sharing">Copy Logs</button>
</div>
<label>Video Debug Info</label>
<label title="Technical details about the currently selected video element">Video Debug Info</label>
<div id="videoDebug" class="info-card" style="font-size: 10px; font-family: monospace; color: var(--text-muted); max-height: 250px; overflow-y: auto; line-height: 1.4;">
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>
<label title="Chronological log of all sync commands in the room">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>
<label title="Technical connection logs for debugging">Logs (Last 50)</label>
<button id="clearLogs" style="background:transparent; border:none; color:var(--accent); font-size:10px; cursor:pointer;" title="Clear log output">CLEAR</button>
</div>
<div id="logList"></div>
@@ -444,14 +506,14 @@
<script src="popup.js" type="module"></script>
<!-- Onboarding Overlay -->
<div id="onboarding-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); backdrop-filter: blur(2px); z-index:1000; align-items:center; justify-content:center;">
<div id="onboarding-card" style="background:var(--card); padding:24px; border-radius:16px; max-width:280px; width:90%; text-align:center; box-shadow:0 8px 32px rgba(0,0,0,0.5); margin-top: 50px;">
<div id="onboarding-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); backdrop-filter: blur(2px); z-index:1000; align-items:flex-end; justify-content:center; padding-bottom: 20px;">
<div id="onboarding-card" style="background:var(--card); padding:24px; border-radius:16px; max-width:280px; width:90%; text-align:center; box-shadow:0 8px 32px rgba(0,0,0,0.5); ">
<div id="onboarding-icon" style="font-size:48px; margin-bottom:12px;">\u{1F44B}</div>
<h2 id="onboarding-title" style="color:var(--accent); margin:0 0 8px; font-size:16px;">Welcome to KoalaSync!</h2>
<p id="onboarding-text" style="color:var(--text-muted); font-size:13px; margin:0 0 16px; line-height:1.4;">Let's get you started.</p>
<div style="display:flex; gap:8px; justify-content:center;">
<button id="onboarding-skip" class="secondary" style="width:auto; padding:8px 16px;">Skip</button>
<button id="onboarding-next" class="primary" style="width:auto; padding:8px 16px;">Next</button>
<button id="onboarding-skip" class="secondary" style="width:auto; padding:8px 16px;" title="Skip the tutorial">Skip</button>
<button id="onboarding-next" class="primary" style="width:auto; padding:8px 16px;" title="Go to next step">Next</button>
</div>
<div id="onboarding-dots" style="margin-top:12px; display:flex; gap:6px; justify-content:center;"></div>
</div>
+89 -18
View File
@@ -47,7 +47,8 @@ const elements = {
lobbyTitle: document.getElementById('lobbyTitle'),
lobbyPeerStatus: document.getElementById('lobbyPeerStatus'),
browserNotifications: document.getElementById('browserNotifications'),
autoCopyInvite: document.getElementById('autoCopyInvite')
autoCopyInvite: document.getElementById('autoCopyInvite'),
syncTabCopyInvite: document.getElementById('syncTabCopyInvite')
};
let localPeerId = null;
@@ -61,6 +62,26 @@ let populateTabsToken = null;
let errorToken = 0;
let forceSyncDone = false;
// --- Helpers ---
function getAvatarForName(username) {
if (!username) return '👤';
const lower = username.toLowerCase();
const map = {
'koala': '🐨', 'panda': '🐼', 'tiger': '🐯', 'eagle': '🦅',
'fox': '🦊', 'bear': '🐻', 'wolf': '🐺', 'lion': '🦁',
'hawk': '🦅', 'seal': '🦭', 'owl': '🦉', 'shark': '🦈',
'dragon': '🐉', 'phoenix': '🐦', 'falcon': '🦅', 'panther': '🐆',
'raven': '🐦‍⬛', 'cobra': '🐍', 'lynx': '🐈', 'jaguar': '🐆',
'orca': '🐋', 'mantis': '🦗', 'viper': '🐍', 'condor': '🦅',
'badger': '🦡', 'otter': '🦦', 'rhino': '🦏', 'crane': '🦩',
'mongoose': '🦦', 'specter': '👻'
};
for (const [key, emoji] of Object.entries(map)) {
if (lower.includes(key)) return emoji;
}
return '👤';
}
// --- Initialization ---
async function init() {
// Load Settings
@@ -155,9 +176,11 @@ function updateUI(roomId, password, useCustomServer = false, serverUrl = '') {
const inRoom = !!roomId;
toggleUIState(inRoom);
if (inRoom) {
const serverFlag = useCustomServer ? '1' : '0';
const encodedUrl = encodeURIComponent(serverUrl || '');
const invite = `${OFFICIAL_LANDING_PAGE_URL}/join.html#join:${roomId}:${password}:${serverFlag}:${encodedUrl}`;
let invite = `${OFFICIAL_LANDING_PAGE_URL}/join.html#join:${roomId}:${password}`;
if (useCustomServer) {
const encodedUrl = encodeURIComponent(serverUrl || '');
invite += `:1:${encodedUrl}`;
}
elements.inviteLink.value = invite;
if (window.justCreatedRoom) {
@@ -253,6 +276,7 @@ function updateLastActionUI(state, peers) {
const isAcked = safeAcks.includes(pId) || pId === state.senderId;
const color = isAcked ? 'var(--success)' : '#475569';
const icon = isAcked ? '✓' : '...';
const avatar = getAvatarForName(pName);
const peerItem = document.createElement('div');
peerItem.title = pName;
@@ -264,7 +288,7 @@ function updateLastActionUI(state, peers) {
const nameSpan = document.createElement('span');
nameSpan.style.cssText = 'font-size:7px; color:var(--text-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:36px;';
nameSpan.textContent = pName;
nameSpan.textContent = `${avatar} ${pName}`;
peerItem.appendChild(dot);
peerItem.appendChild(nameSpan);
@@ -383,10 +407,11 @@ function updatePeerList(peers) {
header.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding-right: 24px;';
const nameSpan = document.createElement('span');
const avatar = getAvatarForName(pUsername || pId);
if (pUsername) {
const u = document.createElement('span');
u.style.cssText = 'font-weight:600; color:white;';
u.textContent = pUsername;
u.textContent = `${avatar} ${pUsername}`;
const i = document.createElement('span');
i.style.cssText = 'font-size:10px; opacity:0.6; font-style:italic;';
i.textContent = ` (${pId})`;
@@ -394,7 +419,7 @@ function updatePeerList(peers) {
nameSpan.appendChild(i);
} else {
nameSpan.style.fontWeight = '600';
nameSpan.textContent = `👤 ${pId}`;
nameSpan.textContent = `${avatar} ${pId}`;
}
header.appendChild(nameSpan);
@@ -583,7 +608,12 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
option.style.color = 'var(--star)';
}
if (tab.audible) {
label = `[🎬] ${label}`;
}
option.textContent = label;
option.dataset.originalTitle = tab.title;
elements.targetTab.appendChild(option);
});
@@ -616,7 +646,7 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
const matchOpt = options.find(o => o.textContent.includes('⭐ MATCH:'));
if (matchOpt && elements.targetTab.options.length > 1) {
elements.targetTab.value = matchOpt.value;
const tabTitle = matchOpt.text.replace('⭐ MATCH: ', '') || null;
const tabTitle = matchOpt.dataset.originalTitle || null;
chrome.runtime.sendMessage({ type: 'SET_TARGET_TAB', tabId: parseInt(matchOpt.value), tabTitle });
}
}
@@ -665,7 +695,7 @@ function applyConnectionStatus(status) {
if (elements.playBtn) elements.playBtn.textContent = '▶ Play';
if (elements.pauseBtn) elements.pauseBtn.textContent = '⏸ Pause';
if (elements.forceSyncBtn) elements.forceSyncBtn.textContent = '⚡ Force Sync';
if (elements.forceSyncBtn) elements.forceSyncBtn.textContent = '⚡ SYNC';
}
function updateHistory(history) {
@@ -869,7 +899,14 @@ elements.tabs.forEach(btn => {
elements.tabs.forEach(b => b.classList.remove('active'));
elements.contents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(btn.dataset.tab).classList.add('active');
const targetContent = document.getElementById(btn.dataset.tab);
targetContent.classList.add('active');
targetContent.classList.remove('tab-active-animate');
void targetContent.offsetWidth; // Force reflow to restart animation
targetContent.classList.add('tab-active-animate');
isDevTabVisible = btn.dataset.tab === 'tab-dev';
if (isDevTabVisible) refreshLogs();
if (btn.dataset.tab === 'tab-sync') refreshHistory();
@@ -994,7 +1031,7 @@ elements.retryBtn.addEventListener('click', () => {
elements.targetTab.addEventListener('change', () => {
const val = elements.targetTab.value;
const tabId = val ? parseInt(val) : null;
const tabTitle = elements.targetTab.options[elements.targetTab.selectedIndex]?.text.replace('⭐ MATCH: ', '') || null;
const tabTitle = elements.targetTab.options[elements.targetTab.selectedIndex]?.dataset.originalTitle || null;
chrome.runtime.sendMessage({ type: 'SET_TARGET_TAB', tabId, tabTitle });
});
@@ -1031,13 +1068,15 @@ elements.forceSyncBtn.addEventListener('click', async () => {
elements.forceSyncBtn.disabled = true;
elements.forceSyncBtn.textContent = mode === 'jump-to-others' ? `Syncing to group (${formatTime(targetTime)})...` : 'Syncing...';
forceSyncDone = false;
const peerCount = (status.peers || []).filter(p => (typeof p === 'object' ? p.peerId : p) !== localPeerId).length;
const syncTimeoutMs = peerCount === 0 ? 3000 : 12000;
const forceSyncReset = () => {
if (!forceSyncDone) {
elements.forceSyncBtn.disabled = false;
elements.forceSyncBtn.textContent = originalText;
}
};
forceSyncResetTimer = setTimeout(forceSyncReset, 12000);
forceSyncResetTimer = setTimeout(forceSyncReset, syncTimeoutMs);
const tabId = parseInt(status.targetTabId);
const sendForceSync = (time) => {
@@ -1083,13 +1122,20 @@ elements.playBtn.addEventListener('click', () => {
showToast('Please select a video first!', 'warning');
return;
}
elements.playBtn.textContent = 'Playing...';
elements.playBtn.textContent = 'Playing...';
elements.playBtn.disabled = true;
chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action: EVENTS.PLAY,
payload: {}
});
// Safety reset: restore button after 2.5s in case no peers respond
setTimeout(() => {
if (elements.playBtn.disabled) {
elements.playBtn.textContent = '▶ Play';
elements.playBtn.disabled = false;
}
}, 2500);
});
elements.pauseBtn.addEventListener('click', () => {
@@ -1097,13 +1143,20 @@ elements.pauseBtn.addEventListener('click', () => {
showToast('Please select a video first!', 'warning');
return;
}
elements.pauseBtn.textContent = 'Pausing...';
elements.pauseBtn.textContent = 'Pausing...';
elements.pauseBtn.disabled = true;
chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action: EVENTS.PAUSE,
payload: {}
});
// Safety reset: restore button after 2.5s in case no peers respond
setTimeout(() => {
if (elements.pauseBtn.disabled) {
elements.pauseBtn.textContent = '⏸ Pause';
elements.pauseBtn.disabled = false;
}
}, 2500);
});
elements.clearLogs.addEventListener('click', () => {
@@ -1129,6 +1182,21 @@ elements.copyInvite.addEventListener('click', () => {
});
});
if (elements.syncTabCopyInvite) {
elements.syncTabCopyInvite.addEventListener('click', () => {
navigator.clipboard.writeText(elements.inviteLink.value).then(() => {
const original = elements.syncTabCopyInvite.textContent;
elements.syncTabCopyInvite.textContent = '✓';
elements.syncTabCopyInvite.style.color = 'var(--success)';
showToast('Invite link copied!', 'success', 2000);
setTimeout(() => {
elements.syncTabCopyInvite.textContent = original;
elements.syncTabCopyInvite.style.color = '';
}, 2000);
});
});
}
// --- Logs & Status ---
async function refreshLogs() {
chrome.runtime.sendMessage({ type: 'GET_LOGS' }, (logs) => {
@@ -1162,8 +1230,8 @@ chrome.runtime.onMessage.addListener((msg) => {
'play': '▶ Play',
'pause': '⏸ Pause',
'seek': '⏩ Seek',
'force_sync_prepare': '⚡ Force Sync',
'force_sync_execute': '⚡ Force Play'
'force_sync_prepare': '⚡ Sync',
'force_sync_execute': '⚡ Sync Play'
};
const action = actionNames[state.action] || state.action;
showToast(`${state.senderId} ${action}`, 'info', 2000);
@@ -1195,7 +1263,7 @@ chrome.runtime.onMessage.addListener((msg) => {
elements.forceSyncBtn.disabled = false;
elements.forceSyncBtn.textContent = '✅ Synced!';
setTimeout(() => {
elements.forceSyncBtn.textContent = '⚡ Sync All Viewers';
elements.forceSyncBtn.textContent = '⚡ SYNC';
}, 2000);
}
}
@@ -1383,10 +1451,11 @@ function updateLobbyUI(lobby, peers) {
peers.forEach(p => {
const pId = typeof p === 'object' ? p.peerId : p;
const pName = (typeof p === 'object' && p.username) ? p.username : pId;
const avatar = getAvatarForName(pName);
const isReady = readySet.has(pId);
const icon = isReady ? '\u2705' : '\u23f3';
const label = isReady ? 'Ready' : 'Loading...';
peerLines.push(`${icon} ${pName} \u2014 ${label}`);
peerLines.push(`${icon} ${avatar} ${pName} \u2014 ${label}`);
});
}
@@ -1417,6 +1486,7 @@ let onboardingStep = 0;
function showOnboarding() {
const overlay = document.getElementById('onboarding-overlay');
if (!overlay) return;
document.body.style.minHeight = '400px';
overlay.style.display = 'flex';
renderOnboardingStep();
}
@@ -1464,6 +1534,7 @@ function renderOnboardingStep() {
function completeOnboarding() {
const overlay = document.getElementById('onboarding-overlay');
if (overlay) overlay.style.display = 'none';
document.body.style.minHeight = '';
chrome.storage.sync.set({ onboardingComplete: true });
const inRoom = elements.sectionActive && elements.sectionActive.style.display === 'block';
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "koalasync",
"version": "1.8.0",
"version": "1.8.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "koalasync",
"version": "1.8.0",
"version": "1.8.5",
"devDependencies": {
"archiver": "^7.0.1",
"eslint": "^10.4.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "koalasync",
"version": "1.8.0",
"version": "1.8.6",
"description": "KoalaSync Build Scripts",
"private": true,
"scripts": {
+6 -1
View File
@@ -44,9 +44,10 @@ const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: (origin, callback) => {
if (!origin || origin === 'https://sync.koalastuff.net' || origin.startsWith('chrome-extension://')) {
if (!origin || origin === 'https://sync.koalastuff.net' || origin.startsWith('chrome-extension://') || origin.startsWith('moz-extension://')) {
callback(null, true);
} else {
log('CORS', `Rejected origin: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
@@ -296,6 +297,7 @@ io.on('connection', (socket) => {
const ip = socket._clientIp || socket.handshake.address;
if (!checkAuthRate(ip, roomId)) {
log('AUTH', `Auth rate limit blocked ${ip} from room ${roomId.substring(0, 3)}***`);
socket.emit(EVENTS.ERROR, { message: "Too many failed attempts. Try again later." });
return;
}
@@ -317,6 +319,7 @@ io.on('connection', (socket) => {
roomCreationLocks.set(roomId, lockPromise);
try {
if (rooms.size >= MAX_ROOMS) {
log('ROOM', `Server at capacity: ${rooms.size}/${MAX_ROOMS} rooms — rejecting join`);
socket.emit(EVENTS.ERROR, { message: "Server capacity reached" });
return;
}
@@ -343,11 +346,13 @@ io.on('connection', (socket) => {
if (room.passwordHash) {
if (!password || !(await bcrypt.compare(password, room.passwordHash))) {
recordAuthFailure(ip, roomId);
log('AUTH', `Invalid password from ${ip} for room ${roomId.substring(0, 3)}***`);
socket.emit(EVENTS.ERROR, { message: "Invalid password" });
return;
}
}
if (room.peers.size >= MAX_PEERS_PER_ROOM) {
log('ROOM', `Room full (${room.peers.size}/${MAX_PEERS_PER_ROOM}): ${roomId.substring(0, 3)}***`);
socket.emit(EVENTS.ERROR, { message: "Room full" });
return;
}
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "1.7.5",
"date": "2026-05-25T11:26:16Z"
"version": "1.8.5",
"date": "2026-05-25T21:56:58Z"
}