Compare commits

...

13 Commits

Author SHA1 Message Date
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
Koala 42029f86bf chore(release): prep for v1.8.0 2026-05-25 23:08:55 +02:00
Koala 4c4a2638d7 docs: rearrange README to prioritize user quick start 2026-05-25 23:06:58 +02:00
Koala 5b57970c4c docs: use for-the-badge style for download links 2026-05-25 23:05:22 +02:00
Koala 61492f953b docs: Update README quick start for users 2026-05-25 23:04:14 +02:00
Koala da6a1cc643 Server updates 2026-05-25 22:59:54 +02:00
Koala 62fdffa5ee UX Improvements: Onboarding, UI Feedback, Auto-Copy Invite 2026-05-25 22:59:54 +02:00
GitHub Action fb13978c9d chore(release): update versions to v1.7.5 [skip ci] 2026-05-25 11:26:16 +00:00
10 changed files with 337 additions and 88 deletions
+23 -11
View File
@@ -30,7 +30,28 @@
---
### 📂 Repository Structure
### 🚀 Quick Start
#### For Users (Installation & Usage)
The easiest and safest way to install KoalaSync is directly through the official browser stores:
<p>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white&style=for-the-badge" alt="Chrome Extension"></a>
<a href="https://addons.mozilla.org/de/firefox/addon/koalasync/"><img src="https://img.shields.io/badge/Firefox-Download-orange?logo=firefoxbrowser&logoColor=white&style=for-the-badge" alt="Firefox Add-on"></a>
</p>
*(For manual offline installation: Download the latest `.zip` from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page and load it as an "Unpacked Extension" in Developer Mode).*
**How to use:**
1. **Create a Room:** Click the Koala icon in your browser and hit `+ Create New Room`.
2. **Invite Friends:** Share the auto-copied invite link. Once they click it, they automatically join.
3. **Pick a Video:** Navigate to the Sync Tab, select the tab playing your video, and grab some popcorn! 🍿
---
### 🛠️ For Developers & Self-Hosters
#### 📂 Repository Structure
- `extension/`: Browser Extension (Chrome & Firefox).
- `server/`: Node.js + Socket.IO Relay Server (Containerized).
@@ -39,16 +60,7 @@
- `scripts/`: Automated build and synchronization utilities.
- `docs/`: Technical deep-dives ([Architecture](docs/ARCHITECTURE.md), [Sync Guide](docs/SYNC_GUIDE.md)).
---
### 🚀 Quick Start
#### For Users (Installation)
The easiest way to install KoalaSync is to download the pre-compiled version from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page.
1. Download the latest `koalasync-chrome.zip` or `koalasync-firefox.zip`.
2. Extract the file and load it as an "Unpacked Extension" in your browser's Developer Mode.
#### For Developers (Building)
#### Building from Source
To build the extension from source and synchronize protocol constants:
```bash
npm install
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "1.7.5",
"version": "1.8.1",
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, Netflix, and HTML5 sites in real-time with friends.",
"permissions": [
"storage",
+101 -22
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>
@@ -247,7 +294,7 @@
<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" data-tab="tab-dev">Dev</button>
<button class="tab-btn" data-tab="tab-dev" title="Advanced Diagnostics & Logs">Status</button>
</div>
<!-- Room Tab -->
@@ -255,6 +302,10 @@
<!-- JOIN SECTION: Visible when not in a room -->
<div id="section-join">
<button id="createRoomBtn" class="primary" style="margin-bottom: 16px; padding: 14px; font-size: 15px; background: linear-gradient(135deg, #6366f1, #a855f7); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);">+ Create New Room</button>
<details style="margin-bottom: 12px; background: var(--card); border: 1px solid #334155; border-radius: 8px; padding: 8px;">
<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>
<div style="display:flex; gap:4px; margin-bottom:8px;">
@@ -273,7 +324,6 @@
</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="createRoomBtn" class="secondary" style="border: 1px solid var(--accent); color: var(--accent);">Create Random 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>
@@ -282,6 +332,8 @@
<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>
</div>
</div>
</details>
</div>
<!-- ACTIVE SECTION: Visible when in a room -->
@@ -315,22 +367,27 @@
<!-- Sync Tab -->
<div id="tab-sync" class="tab-content">
<!-- SYNC ACTIVE: Visible when in a room -->
<div id="sync-active">
<div class="form-group">
<label>Target Tab (Video Source)</label>
<label>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:none; padding:4px; font-size:14px; cursor:pointer; opacity:0.8; transition: opacity 0.2s;">🔗</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>
</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;">Force Sync</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;">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>
@@ -353,35 +410,57 @@
</div>
<div id="peerListSync" class="info-card" style="display:none;"></div>
</div>
<!-- SYNC INACTIVE: Visible when not in a room -->
<div id="sync-inactive" style="display:none; text-align: center; padding: 20px 10px;">
<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>
</div>
</div>
<!-- Settings Tab -->
<div id="tab-settings" class="tab-content">
<div class="form-group">
<label>Your Username</label>
<input type="text" id="username" placeholder="Anonymous Koala" maxlength="20">
<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>
<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;">Filter Noise Tabs</label>
<input type="checkbox" id="filterNoise" style="width: auto;" checked>
<label style="margin-bottom: 0; cursor: help;" title="Hides non-video sites like search engines or social media.">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;">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;">Browser Notifications</label>
<input type="checkbox" id="browserNotifications" style="width: auto;">
<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; 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="font-size: 11px; color: var(--text-muted); padding: 8px;">
<p>• Username helps others identify you.</p>
<p>• Noise filtering uses a blacklist to hide common non-video sites (e.g. Search, Social Media) from the Target Tab selector.</p>
<p>• Auto-Sync will pause and wait for all peers when an episode changes, then sync-start together.</p>
</div>
<div style="margin-top: 15px; padding: 8px; border-top: 1px solid var(--card);">
<label>Troubleshooting</label>
@@ -427,8 +506,8 @@
<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.7); 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);">
<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-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>
+173 -33
View File
@@ -46,7 +46,8 @@ const elements = {
episodeLobbyCard: document.getElementById('episodeLobbyCard'),
lobbyTitle: document.getElementById('lobbyTitle'),
lobbyPeerStatus: document.getElementById('lobbyPeerStatus'),
browserNotifications: document.getElementById('browserNotifications')
browserNotifications: document.getElementById('browserNotifications'),
autoCopyInvite: document.getElementById('autoCopyInvite')
};
let localPeerId = null;
@@ -60,10 +61,30 @@ let populateTabsToken = null;
let errorToken = 0;
let forceSyncDone = false;
// --- Helpers ---
function getAvatarForName(username) {
if (!username) return '👤';
const lower = username.toLowerCase();
if (lower.includes('koala')) return '🐨';
if (lower.includes('panda')) return '🐼';
if (lower.includes('tiger')) return '🐯';
if (lower.includes('penguin')) return '🐧';
if (lower.includes('fox')) return '🦊';
if (lower.includes('bear')) return '🐻';
if (lower.includes('rabbit')) return '🐰';
if (lower.includes('owl')) return '🦉';
if (lower.includes('eagle')) return '🦅';
if (lower.includes('wolf')) return '🐺';
if (lower.includes('lion')) return '🦁';
if (lower.includes('shark')) return '🦈';
if (lower.includes('dragon')) return '🐉';
return '👤';
}
// --- Initialization ---
async function init() {
// Load Settings
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications']);
const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite']);
let username = data.username;
if (!username) {
const adjs = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave', 'Calm', 'Sneaky', 'Lazy', 'Wild', 'Chill', 'Lucky', 'Epic', 'Swift', 'Bold', 'Mighty', 'Cosmic', 'Neon', 'Shadow', 'Crystal', 'Thunder', 'Silent', 'Golden', 'Fierce', 'Noble', 'Mystic', 'Frozen', 'Blazing', 'Sapphire', 'Iron', 'Crimson'];
@@ -76,10 +97,11 @@ async function init() {
elements.roomId.value = data.roomId || '';
elements.password.value = data.password || '';
elements.username.value = username;
elements.filterNoise.checked = data.filterNoise !== false;
elements.autoSyncNextEpisode.checked = data.autoSyncNextEpisode !== false;
elements.forceSyncMode.value = data.forceSyncMode || 'jump-to-others';
elements.browserNotifications.checked = data.browserNotifications === true;
if (elements.filterNoise) elements.filterNoise.checked = data.filterNoise !== false;
if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = data.autoSyncNextEpisode !== false;
if (elements.forceSyncMode) elements.forceSyncMode.value = data.forceSyncMode || 'jump-to-others';
if (elements.browserNotifications) elements.browserNotifications.checked = data.browserNotifications === true;
if (elements.autoCopyInvite) elements.autoCopyInvite.checked = data.autoCopyInvite !== false;
// Set Version Info
const versionEl = document.getElementById('appVersion');
@@ -142,16 +164,31 @@ function toggleUIState(inRoom) {
if (elements.sectionJoin) elements.sectionJoin.style.display = inRoom ? 'none' : 'block';
if (elements.sectionActive) elements.sectionActive.style.display = inRoom ? 'block' : 'none';
if (elements.peerListSync) elements.peerListSync.style.display = inRoom ? 'block' : 'none';
const syncActive = document.getElementById('sync-active');
const syncInactive = document.getElementById('sync-inactive');
if (syncActive) syncActive.style.display = inRoom ? 'block' : 'none';
if (syncInactive) syncInactive.style.display = inRoom ? 'none' : 'block';
}
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) {
window.justCreatedRoom = false;
if (elements.autoCopyInvite && elements.autoCopyInvite.checked && elements.copyInvite) {
elements.copyInvite.click();
}
}
if (elements.activeRoomId) elements.activeRoomId.textContent = roomId;
if (elements.activeServer) {
elements.activeServer.textContent = useCustomServer ? (serverUrl || 'Custom Server') : 'Official Server';
@@ -238,6 +275,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;
@@ -249,7 +287,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);
@@ -368,10 +406,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})`;
@@ -379,7 +418,7 @@ function updatePeerList(peers) {
nameSpan.appendChild(i);
} else {
nameSpan.style.fontWeight = '600';
nameSpan.textContent = `👤 ${pId}`;
nameSpan.textContent = `${avatar} ${pId}`;
}
header.appendChild(nameSpan);
@@ -568,7 +607,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);
});
@@ -597,6 +641,13 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
if (currentTargetTabId) {
elements.targetTab.value = currentTargetTabId;
} else {
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;
chrome.runtime.sendMessage({ type: 'SET_TARGET_TAB', tabId: parseInt(matchOpt.value), tabTitle });
}
}
}
@@ -643,7 +694,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) {
@@ -815,6 +866,12 @@ elements.browserNotifications.addEventListener('change', () => {
chrome.storage.sync.set({ browserNotifications: elements.browserNotifications.checked });
});
if (elements.autoCopyInvite) {
elements.autoCopyInvite.addEventListener('change', () => {
chrome.storage.sync.set({ autoCopyInvite: elements.autoCopyInvite.checked });
});
}
elements.forceSyncMode.addEventListener('change', () => {
chrome.storage.sync.set({ forceSyncMode: elements.forceSyncMode.value });
});
@@ -841,7 +898,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();
@@ -931,17 +995,23 @@ elements.leaveBtn.addEventListener('click', async () => {
updateUI(null, null);
});
elements.createRoomBtn.addEventListener('click', () => {
const animals = ['koala', 'panda', 'tiger', 'eagle', 'fox', 'bear'];
const adj = ['happy', 'cool', 'fast', 'smart', 'brave', 'calm'];
const id = `${adj[Math.floor(Math.random() * adj.length)]}-${animals[Math.floor(Math.random() * animals.length)]}-${Math.floor(Math.random() * 100)}`;
const array = new Uint32Array(1);
window.crypto.getRandomValues(array);
const pass = array[0].toString(36).substring(0, 6);
function handleCreateRoom() {
const generateId = () => Math.random().toString(36).substring(2, 8).toUpperCase();
const roomId = generateId();
const password = generateId();
elements.roomId.value = roomId;
elements.password.value = password;
window.justCreatedRoom = true;
elements.roomId.value = id;
elements.password.value = pass;
// Auto-connect
elements.joinBtn.click();
};
elements.createRoomBtn.addEventListener('click', handleCreateRoom);
const syncTabCreateRoomBtn = document.getElementById('syncTabCreateRoomBtn');
if (syncTabCreateRoomBtn) syncTabCreateRoomBtn.addEventListener('click', () => {
document.querySelector('.tab-btn[data-tab="tab-room"]').click();
handleCreateRoom();
});
elements.refreshRooms.addEventListener('click', () => {
@@ -960,7 +1030,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 });
});
@@ -1045,6 +1115,12 @@ elements.forceSyncBtn.addEventListener('click', async () => {
});
elements.playBtn.addEventListener('click', () => {
if (!elements.targetTab.value) {
showToast('Please select a video first!', 'warning');
return;
}
elements.playBtn.textContent = 'Playing...';
elements.playBtn.disabled = true;
chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action: EVENTS.PLAY,
@@ -1053,6 +1129,12 @@ elements.playBtn.addEventListener('click', () => {
});
elements.pauseBtn.addEventListener('click', () => {
if (!elements.targetTab.value) {
showToast('Please select a video first!', 'warning');
return;
}
elements.pauseBtn.textContent = 'Pausing...';
elements.pauseBtn.disabled = true;
chrome.runtime.sendMessage({
type: 'CONTENT_EVENT',
action: EVENTS.PAUSE,
@@ -1083,6 +1165,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) => {
@@ -1116,12 +1213,29 @@ 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);
}
if (state && (state.action === 'play' || state.action === 'pause')) {
const btn = state.action === 'play' ? elements.playBtn : elements.pauseBtn;
if (btn && btn.disabled) {
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
const peerCount = res && res.peers ? res.peers.length : 1;
if (state.acks && state.acks.length >= peerCount) {
btn.textContent = '✅ Synced!';
setTimeout(() => {
btn.disabled = false;
btn.textContent = state.action === 'play' ? '▶ Play' : '⏸ Pause';
}, 2000);
}
});
}
}
if (state && state.action === 'force_sync_execute') {
forceSyncDone = true;
if (forceSyncResetTimer) {
@@ -1130,7 +1244,10 @@ chrome.runtime.onMessage.addListener((msg) => {
}
if (elements.forceSyncBtn) {
elements.forceSyncBtn.disabled = false;
elements.forceSyncBtn.textContent = '⚡ Force Sync';
elements.forceSyncBtn.textContent = ' Synced!';
setTimeout(() => {
elements.forceSyncBtn.textContent = '⚡ SYNC';
}, 2000);
}
}
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
@@ -1317,10 +1434,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}`);
});
}
@@ -1339,11 +1457,11 @@ function updateLobbyUI(lobby, peers) {
// --- Onboarding Tour ---
const onboardingSteps = [
{ icon: '\u{1F44B}', title: 'Welcome to KoalaSync!', text: 'Watch videos together in perfect sync — no matter where you are. Let\'s take a quick tour!' },
{ icon: '\u{1F3E0}', title: 'Room Tab', text: 'Create a room and share the invite link with friends. Anyone with the link can join instantly.' },
{ icon: '\u{1F3AC}', title: 'Sync Tab', text: 'Pick the tab with your video. Play, pause, and seek — everyone stays in sync. Drift? Just hit Force Sync.' },
{ icon: '\u2699\uFE0F', title: 'Settings', text: 'Pick a fun username, hide distracting tabs, and enable notifications so you never miss a moment.' },
{ icon: '\u{1F389}', title: 'You\'re all set!', text: 'Open a video, create a room, and start watching together. Enjoy!' }
{ icon: '👋', title: 'Welcome to KoalaSync!', text: 'Watch videos together in perfect sync — no matter where you are. Let\'s take a quick tour!', targetTab: 'tab-room' },
{ icon: '🏠', title: '1. Create a Room', text: 'Start here. Create a room and share the invite link with your friends.', targetTab: 'tab-room' },
{ icon: '🎬', title: '2. Select Video', text: 'Navigate here to select the video you want to sync. Play, pause, and seek — everyone stays in sync.', targetTab: 'tab-sync' },
{ icon: '⚙️', title: '3. Personalize', text: 'Pick a fun username so your friends know who you are.', targetTab: 'tab-settings' },
{ icon: '🎉', title: 'You\'re all set!', text: 'Time to grab some popcorn. Enjoy watching together!', targetTab: 'tab-room' }
];
let onboardingStep = 0;
@@ -1351,6 +1469,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();
}
@@ -1368,6 +1487,23 @@ function renderOnboardingStep() {
title.textContent = step.title;
text.textContent = step.text;
if (step.targetTab) {
const tabBtn = document.querySelector(`.tab-btn[data-tab="${step.targetTab}"]`);
if (tabBtn) tabBtn.click();
const syncActive = document.getElementById('sync-active');
const syncInactive = document.getElementById('sync-inactive');
if (step.targetTab === 'tab-sync') {
if (syncActive) syncActive.style.display = 'block';
if (syncInactive) syncInactive.style.display = 'none';
} else {
// Restore actual lock state when on other tabs so we don't leave it unlocked
const inRoom = elements.sectionActive && elements.sectionActive.style.display === 'block';
if (syncActive) syncActive.style.display = inRoom ? 'block' : 'none';
if (syncInactive) syncInactive.style.display = inRoom ? 'none' : 'block';
}
}
dots.replaceChildren();
onboardingSteps.forEach((_, i) => {
const dot = document.createElement('div');
@@ -1381,7 +1517,11 @@ 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';
toggleUIState(inRoom);
}
document.getElementById('onboarding-next')?.addEventListener('click', () => {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "koalasync",
"version": "1.6.1",
"version": "1.8.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "koalasync",
"version": "1.6.1",
"version": "1.8.1",
"devDependencies": {
"archiver": "^7.0.1",
"eslint": "^10.4.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "koalasync",
"version": "1.7.5",
"version": "1.8.1",
"description": "KoalaSync Build Scripts",
"private": true,
"scripts": {
+22 -4
View File
@@ -93,6 +93,10 @@ function checkAuthRate(ip, roomId) {
}
function recordAuthFailure(ip, roomId) {
if (failedAuthAttempts.size > 50000) {
failedAuthAttempts.clear();
log('SECURITY', 'Cleared failedAuthAttempts map to prevent memory leak');
}
const key = `${ip}:${roomId}`;
const record = failedAuthAttempts.get(key) || { count: 0, lastAttempt: 0 };
record.count++;
@@ -227,6 +231,12 @@ io.on('connection', (socket) => {
}
if (clientVersion) {
if (typeof clientVersion !== 'string') {
log('AUTH', `Invalid version type from ${clientIp}`);
socket.emit(EVENTS.ERROR, { message: 'Invalid version format' });
socket.disconnect(true);
return;
}
const parts = clientVersion.split('.').map(Number);
const cMaj = parts[0], cMin = parts[1], cPatch = parts[2] || 0;
const [mMaj, mMin, mPatch] = MIN_VERSION.split('.').map(Number);
@@ -291,6 +301,7 @@ io.on('connection', (socket) => {
}
let room = rooms.get(roomId);
let createdByMe = false;
if (!room) {
// Acquire per-room creation lock to prevent race conditions
@@ -298,9 +309,6 @@ io.on('connection', (socket) => {
if (lockPromise) {
await lockPromise;
room = rooms.get(roomId);
if (room) {
// Another concurrent request created it, fall through to password check
}
}
if (!room) {
// Create and store lock before async boundary
@@ -322,13 +330,16 @@ io.on('connection', (socket) => {
lastActivity: Date.now()
};
rooms.set(roomId, room);
createdByMe = true;
log('ROOM', `Created room: ${roomId.substring(0, 3)}***`);
} finally {
roomCreationLocks.delete(roomId);
resolveLock();
}
}
} else {
}
if (!createdByMe) {
if (room.passwordHash) {
if (!password || !(await bcrypt.compare(password, room.passwordHash))) {
recordAuthFailure(ip, roomId);
@@ -464,6 +475,11 @@ io.on('connection', (socket) => {
});
socket.on(EVENTS.GET_ROOMS, () => {
if (!checkEventRate(socket.id)) {
log('SECURITY', `Event rate limit exceeded for socket (GET_ROOMS): ${socket.id}`);
socket.disconnect(true);
return;
}
const list = Array.from(rooms.entries()).map(([id, r]) => ({
id,
peerCount: r.peers.size,
@@ -581,8 +597,10 @@ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('uncaughtException', (err) => {
log('ERROR', `Uncaught exception: ${err.message}`, err.stack);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
log('ERROR', `Unhandled rejection: ${reason}`);
process.exit(1);
});
+3 -3
View File
@@ -195,7 +195,7 @@ document.addEventListener('DOMContentLoaded', () => {
const isDE = document.documentElement.classList.contains('lang-de');
title.textContent = isDE ? 'Erfolgreich!' : 'Success!';
let count = 3;
let count = 2;
const updateCountdown = () => {
const closingMsg = isDE
? `Du bist dem Raum beigetreten. <br><span style="color:var(--accent); font-weight:bold;">Dieser Tab schließt sich in ${count} Sekunden...</span>`
@@ -226,8 +226,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (banner) {
if (success) {
banner.style.background = 'var(--success)';
banner.innerHTML = '<div class="container">✅ Joined! This tab will close in 3s...</div>';
setTimeout(() => window.close(), 3000);
banner.innerHTML = '<div class="container">✅ Joined! This tab will close in 2s...</div>';
setTimeout(() => window.close(), 2000);
} else {
banner.style.background = 'var(--error)';
banner.innerHTML = '';
+9 -9
View File
@@ -54,8 +54,8 @@
<span lang="de">Gemeinsam schauen.<br>Perfekt synchron.</span>
</h1>
<h2 class="hero-subtitle" data-reveal>
<span lang="en">A free, open-source watch party extension for YouTube, Twitch, Netflix, and almost any website with a video player. Built for reliable synchronization and data sovereignty.</span>
<span lang="de">Eine kostenlose, quelloffene Watch-Party-Erweiterung für YouTube, Twitch, Netflix und fast jede Webseite mit einem Videoplayer. Entwickelt für zuverlässige Synchronisation und Datenhoheit.</span>
<span lang="en">Your remote movie night without lags. No registration, no data collection. Just share a link and watch together.</span>
<span lang="de">Dein Kino-Abend auf Distanz. Keine Lags, keine Anmeldung. Einfach Link teilen und zusammen schauen.</span>
</h2>
<div class="cta-group" data-reveal>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc" class="btn btn-primary">
@@ -131,7 +131,7 @@
<div id="mock-sync" class="mock-screen">
<div class="mock-form-group" style="margin-bottom: 12px;">
<label style="display: block; font-size: 11px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px; font-weight: 700;">
<span lang="en">Target Tab (Video Source)</span><span lang="de">Ziel-Tab (Video-Quelle)</span>
<span lang="en">Select Video</span><span lang="de">Video auswählen</span>
</label>
<select class="mock-input" style="width: 100%; box-sizing: border-box; background: var(--card); border: 1px solid #334155; color: white; padding: 8px; border-radius: 8px; font-family: inherit; font-size: 0.75rem; outline: none; cursor: default;">
<option>🎬 Germany's Next Flopmodel - S21E12</option>
@@ -146,7 +146,7 @@
<button class="mock-btn mock-btn-pause" style="flex: 1; background: #ef4444; color: white; padding: 8px; border-radius: 8px; font-weight: 700; border: none; cursor: pointer;"><span lang="en">⏸ Pause</span><span lang="de">⏸ Pause</span></button>
</div>
<button class="mock-btn mock-btn-force" style="background: linear-gradient(135deg, #6366f1, #a855f7); width: 100%; padding: 10px; color: white; border: none; font-weight: 700; border-radius: 8px; margin-bottom: 15px; cursor: pointer;">
<span lang="en">Force Sync Everyone</span><span lang="de">Synchronisation erzwingen</span>
<span lang="en">Sync All Viewers</span><span lang="de">Alle synchronisieren</span>
</button>
<label style="display: block; font-size: 11px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px; font-weight: 700;">
@@ -174,7 +174,7 @@
<div style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 8px; border: 1px solid #334155;">
<label style="margin: 0; font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; cursor: pointer;" for="chk-noise">
<span lang="en">Filter Noise Tabs</span><span lang="de">Noise-Tabs filtern</span>
<span lang="en">Show Video Tabs Only</span><span lang="de">Nur Video-Tabs anzeigen</span>
</label>
<input type="checkbox" id="chk-noise" checked disabled style="width: auto; cursor: not-allowed;">
</div>
@@ -277,8 +277,8 @@
<span lang="en">Zero Accounts / Pure Privacy</span>
<span lang="de">Keine Accounts / Datenschutz</span>
</h3>
<p lang="en">No registration, no tracking, and no database persistence. The server runs entirely in RAM, collects no telemetry or persistent logs, and purges your room when you leave.</p>
<p lang="de">Keine Registrierung, kein Tracking und keine Datenspeicherung. Der Server läuft flüchtig im RAM, speichert keine dauerhaften Logs und löscht deinen Raum nach dem Verlassen.</p>
<p lang="en">No registration, no tracking, no bullshit. Just share a link and watch. The server runs entirely in RAM and purges your room when you leave.</p>
<p lang="de">Kein Login. Keine Daten. Einfach Link teilen und schauen. Der Server läuft flüchtig im RAM und löscht deinen Raum komplett nach dem Verlassen.</p>
</div>
<div class="feature-card" data-reveal>
<div class="feature-icon">🌐</div>
@@ -295,8 +295,8 @@
<span lang="en">Self-Hostable & Docker-Ready</span>
<span lang="de">Self-Hostable & Docker-Ready</span>
</h3>
<p lang="en">Take full ownership of your sync relay. We offer ready-to-use Docker images and lightweight Caddy configurations, enabling you to launch your private server in seconds.</p>
<p lang="de">Übernimm die volle Kontrolle. Mit unseren fertigen Docker-Images und einfachen Caddy-Konfigurationen startest du deinen eigenen, komplett privaten Relay-Server in wenigen Sekunden.</p>
<p lang="en">Our official servers are free and fast, but you <strong>can</strong> take full ownership if you want to. Launch your own private relay server in seconds via Docker.</p>
<p lang="de">Unsere Server sind kostenlos und schnell, aber du <strong>kannst</strong> auch die volle Kontrolle übernehmen, wenn du willst. Starte dein eigenes privates Relay in Sekunden via Docker.</p>
</div>
<div class="feature-card" data-reveal>
<div class="feature-icon">🔗</div>
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "1.7.5",
"date": "2026-05-25T14:00:00Z"
"version": "1.8.0",
"date": "2026-05-25T21:09:07Z"
}