mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 20:18:14 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42029f86bf | |||
| 4c4a2638d7 | |||
| 5b57970c4c | |||
| 61492f953b | |||
| da6a1cc643 | |||
| 62fdffa5ee | |||
| fb13978c9d | |||
| 82c09a5328 | |||
| 2fbeafeb3f | |||
| 80f8c821cb | |||
| 7d3965a9fd |
@@ -108,6 +108,7 @@ Before starting any task, committing, or pushing, you **MUST** run `git pull --r
|
||||
2. Commit all verified code changes and push to `main`.
|
||||
3. Create and push a new tag. **MANDATORY**: Tags MUST start with a `v` (e.g., `v1.4.0`). The GitHub Actions release workflow is strictly configured to ignore any tags without the `v` prefix.
|
||||
- **🚫 TAG IMMUTABILITY**: Once a tag is pushed to `origin`, it is **PERMANENT**. You MUST **NEVER** reuse, move, or force-push an existing tag — not even to "fix" a mistake. If a release is missing a fix, increment the version and create a **new** tag (e.g., `v1.7.0` → `v1.7.1`). Tags are immutable identifiers; moving them breaks CI pipelines, corrupts the release history, and causes unreproducible builds.
|
||||
- **🚫 WHEN NOT TO TAG**: Do NOT create a release tag for changes that do NOT affect the shipped extension or server artifacts. Website text changes, documentation updates (`.md` files), and landing page content do NOT require a version tag. Tags trigger the full CI pipeline (Docker build, extension packaging, GitHub Release) — running this for a typo fix wastes CI resources and creates meaningless releases. Only tag when extension code (`extension/`), server code (`server/`), or shared protocol constants (`shared/`) have changed.
|
||||
4. The CI will extract the version from the tag (e.g., `v1.4.0` → `1.4.0`), inject it into all source files, build the extension artifacts, publish the Docker image, and create a GitHub Release.
|
||||
5. Verify the release builds on GitHub Actions.
|
||||
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "KoalaSync",
|
||||
"version": "1.7.3",
|
||||
"version": "1.8.0",
|
||||
"description": "Watch party extension to synchronize video playback on YouTube, Twitch, Netflix, and HTML5 sites in real-time with friends.",
|
||||
"permissions": [
|
||||
"storage",
|
||||
|
||||
+33
-16
@@ -247,7 +247,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 +255,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 +277,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 +285,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,8 +320,10 @@
|
||||
|
||||
<!-- 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>
|
||||
@@ -329,7 +336,7 @@
|
||||
</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>
|
||||
<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">
|
||||
<option value="jump-to-others">Jump to Others</option>
|
||||
<option value="jump-to-me">Jump to Me</option>
|
||||
@@ -353,35 +360,45 @@
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
<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;">
|
||||
</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>
|
||||
</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;">
|
||||
</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 +444,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>
|
||||
|
||||
+116
-22
@@ -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;
|
||||
@@ -63,7 +64,7 @@ let forceSyncDone = false;
|
||||
// --- 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 +77,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,6 +144,11 @@ 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 = '') {
|
||||
@@ -152,6 +159,14 @@ function updateUI(roomId, password, useCustomServer = false, serverUrl = '') {
|
||||
const encodedUrl = encodeURIComponent(serverUrl || '');
|
||||
const invite = `${OFFICIAL_LANDING_PAGE_URL}/join.html#join:${roomId}:${password}:${serverFlag}:${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';
|
||||
@@ -531,7 +546,15 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
|
||||
if (!tab.url || tab.url.startsWith('chrome://')) return false;
|
||||
if (isFilterActive && tab.id !== parseInt(currentTargetTabId)) {
|
||||
const urlStr = tab.url.toLowerCase();
|
||||
if (BLACKLIST_DOMAINS.some(d => urlStr.includes(d.toLowerCase()))) return false;
|
||||
if (BLACKLIST_DOMAINS.some(d => {
|
||||
const domain = d.toLowerCase();
|
||||
try {
|
||||
const hostname = new URL(tab.url).hostname.toLowerCase();
|
||||
if (domain.endsWith('.')) return hostname.startsWith(domain) || hostname.includes('.' + domain);
|
||||
if (domain.includes('.')) return hostname === domain || hostname.endsWith('.' + domain);
|
||||
} catch {}
|
||||
return urlStr.includes(domain);
|
||||
})) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -589,6 +612,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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -807,6 +837,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 });
|
||||
});
|
||||
@@ -923,17 +959,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', () => {
|
||||
@@ -1037,6 +1079,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,
|
||||
@@ -1045,6 +1093,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,
|
||||
@@ -1114,6 +1168,23 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||
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) {
|
||||
@@ -1122,7 +1193,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 All Viewers';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
|
||||
@@ -1331,11 +1405,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;
|
||||
@@ -1360,6 +1434,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');
|
||||
@@ -1374,6 +1465,9 @@ function completeOnboarding() {
|
||||
const overlay = document.getElementById('onboarding-overlay');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
chrome.storage.sync.set({ onboardingComplete: true });
|
||||
|
||||
const inRoom = elements.sectionActive && elements.sectionActive.style.display === 'block';
|
||||
toggleUIState(inRoom);
|
||||
}
|
||||
|
||||
document.getElementById('onboarding-next')?.addEventListener('click', () => {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "1.6.1",
|
||||
"version": "1.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "koalasync",
|
||||
"version": "1.6.1",
|
||||
"version": "1.8.0",
|
||||
"devDependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"eslint": "^10.4.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "1.7.3",
|
||||
"version": "1.8.0",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+22
-4
@@ -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
@@ -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
@@ -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 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 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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "1.7.3",
|
||||
"date": "2026-05-25T10:52:55Z"
|
||||
"version": "1.7.5",
|
||||
"date": "2026-05-25T11:26:16Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user