Compare commits

...

12 Commits

Author SHA1 Message Date
Timo 003741a63f docs: add devops.md detailing release workflow 2026-07-01 19:47:31 +02:00
KoalaDev e644ab33cd Merge pull request #15 from Shik3i/codex/issue-13-title-privacy
feat(extension): add title privacy controls and settings
2026-07-01 19:43:46 +02:00
Timo 9006ebbc67 docs: remove temporary branch planning document 2026-07-01 19:40:57 +02:00
Timo e40017f053 Move privacy settings under username 2026-07-01 19:26:25 +02:00
Timo fa7eea54d8 Split tab and media title privacy controls 2026-07-01 19:21:38 +02:00
KoalaDev 04d2360814 Remove plan for splitting large JavaScript files
Removed the plan to split large JavaScript files into smaller modules due to prioritization changes.
2026-07-01 19:16:42 +02:00
Timo 29aba936ce Clarify tab and media title privacy setting 2026-07-01 18:58:39 +02:00
Timo 4bae88e107 Fix title privacy popup import 2026-07-01 18:50:09 +02:00
Skrockle 3e4b97b11a Fix: Remove redundant TITLE_PRIVACY_MODES array in popup.js
- Remove duplicate local TITLE_PRIVACY_MODES array definition
- Use Object.values(TITLE_PRIVACY_MODES).includes() for validation instead
- Keeps single source of truth in title-privacy.js

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 18:37:25 +02:00
Timo e555906553 fix(extension): harden title privacy edge cases 2026-06-29 13:43:10 +02:00
Timo 4f1335242c feat(extension): add shared title privacy modes 2026-06-29 13:35:34 +02:00
GitHub Action e683466bf0 chore(release): update versions to v2.5.0 [skip ci] 2026-06-29 10:58:06 +00:00
33 changed files with 555 additions and 124 deletions
+2 -2
View File
@@ -6,7 +6,7 @@
<p align="center">
<a href="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml"><img src="https://github.com/Shik3i/KoalaSync/actions/workflows/release.yml/badge.svg" alt="Release Status"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v2.4.6-blue?logo=github" alt="GitHub release"></a>
<a href="https://github.com/Shik3i/KoalaSync/releases"><img src="https://img.shields.io/badge/Release-v2.5.0-blue?logo=github" alt="GitHub release"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue" alt="License"></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" alt="Firefox Add-on"></a>
<a href="https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc"><img src="https://img.shields.io/badge/Chrome-Download-blue?logo=googlechrome&logoColor=white" alt="Chrome Extension"></a>
@@ -14,7 +14,7 @@
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.4.6 Release!</b> — See what's changed</a></p>
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.5.0 Release!</b> — See what's changed</a></p>
### 🌟 Why KoalaSync?
+7
View File
@@ -4,6 +4,13 @@ All notable changes to the KoalaSync browser extension and relay server.
---
## Unreleased
### Added
- **Extension: Privacy title controls** - Advanced users can now disable sending browser tab titles separately from media titles. Media titles can still be sent in full, reduced to detected episode identifiers such as `S01E04`, or hidden entirely. Defaults remain full titles for backwards compatibility.
---
## [v2.5.0] — 2026-06-29
### Added
+2 -2
View File
@@ -156,7 +156,7 @@ While in a room, two heartbeats keep the session alive:
| Heartbeat | Interval | Source | Purpose |
|:----------|:---------|:-------|:--------|
| **Background** | 30 seconds | `background.js` | While connected, signals "I'm still connected" and triggers automatic reconnect (500ms base, max 5s). No heartbeats fire when idle (lazy connect). |
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, `mediaTitle`, `playbackState`, `volume`, `muted` |
| **Content** | 15 seconds | `content.js` | Sends video metadata: `currentTime`, privacy-filtered `mediaTitle`, `playbackState`, `volume`, `muted` |
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
- **Room Cleanup**: Empty rooms are deleted immediately. Inactive rooms are pruned after 2 hours.
@@ -184,7 +184,7 @@ When a user clicks **"Leave"** or closes their browser:
When watching a series and an episode ends:
1. `content.js` monitors the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) for title changes.
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected new title.
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected title after applying the local media title privacy setting. In episode-only mode this is an identifier such as `S01E04`; when media titles are not sent, the client does not create a new episode lobby.
3. All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
4. Each peer's `content.js` polls for the new title to appear in the `<video>` element's metadata.
5. Once a peer detects the matching title, they send `EPISODE_READY`.
+1 -1
View File
@@ -8,7 +8,7 @@ KoalaSync is designed with a **Security-First & Volatile** architecture. This me
## 1. Data Processing (In-Memory Only)
KoalaSync does not use a database. All active session data exists only in the server's RAM and is purged immediately when no longer needed.
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId`, `username`, and the `title` of the video you are watching. Additionally, playback metadata (`mediaTitle`, `playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId` and `username`. By default, KoalaSync also shares the selected tab title and media title with the room so peers can identify matching videos and coordinate episode transitions. Privacy Settings let you disable sending the tab title separately, and choose whether media titles are sent in full, reduced to a detected episode identifier (for example `S01E04`), or not sent. Playback metadata (`playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
- **Room Passwords**: If you set a room password, it is stored only as an in-memory **keyed SHA-256 HMAC hash**. The server receives the plaintext password only during join validation, never stores it, and keeps only the hash for the short room lifetime.
- **Routing Maps**: The server maintains ephemeral lookup tables (`socketToRoom`, `peerToSocket`) to route messages between peers. These contain only transport identifiers and are purged on disconnect.
-9
View File
@@ -30,15 +30,6 @@
*Prioritized for upcoming phases.*
### 1. Split large JavaScript files into smaller modules
- **Priority:** P1
- **Category:** Maintainability / AI Context Optimization
- **Background:** Core files like `background.js` and `popup.js` have grown large and exceed 800 lines. This makes manual debugging harder and wastes context window space for AI models.
- **Planned solution:**
- Structurally split logic into separate focused modules (e.g., UI Renderer, Message Router, Storage Manager, Socket Client).
- Use ES modules for clean separation and better reusability.
### 2. Invite link with target URL for auto-redirect
- **Priority:** P2
+51
View File
@@ -0,0 +1,51 @@
# DevOps Release Workflow
This document describes the deployment and release process for KoalaSync.
## Tag-Based Releases
KoalaSync uses a fully automated release pipeline triggered by Git tags.
> [!IMPORTANT]
> **DO NOT** manually bump the version numbers in any files (such as `package.json`, `manifest.base.json`, `shared/constants.js`, etc.) before creating a release.
> Bumping versions manually is redundant, leads to conflicts, and is completely handled by the CI/CD pipeline.
### How it Works
When you push a Git tag matching `v*` (e.g., `v2.5.1`), the GitHub Actions release workflow (`.github/workflows/release.yml`) is triggered. The workflow performs the following actions:
1. **Extracts the version** from the tag (e.g., `2.5.1` from `v2.5.1`).
2. **Injects the version** automatically into the following files:
- `extension/manifest.base.json`
- `shared/constants.js` (updates `APP_VERSION`)
- `package.json`
- `website/version.json`
- `website/template.html` (updates `softwareVersion` schema)
- `README.md` (updates badge and announcement banner)
- `website/sitemap.xml` (updates `lastmod` dates)
3. **Commits and pushes** these version updates back to the `main` branch automatically with the commit message `chore(release): update versions to vX.X.X [skip ci]`.
4. **Builds the extension** for both Chrome and Firefox and publishes the zipped archives.
5. **Builds the website** and uploads website artifacts.
6. **Builds and publishes** the Docker image for the relay server to the GitHub Container Registry (`ghcr.io`).
---
## Steps to Deploy a New Release
To release a new version (e.g., `v2.5.1`), follow these steps:
1. Make sure your local repository is synced on `main`:
```bash
git checkout main
git pull origin main
```
2. Create a local Git tag:
```bash
git tag v2.5.1
```
3. Push the tag to GitHub:
```bash
git push origin v2.5.1
```
The release pipeline will take care of the rest! You can monitor the progress under the **Actions** tab of the GitHub repository.
+109 -26
View File
@@ -2,6 +2,7 @@ import { EVENTS, CONTROL_MODES, CAPABILITIES, PROTOCOL_VERSION, OFFICIAL_SERVER_
import { generateUsername } from './shared/names.js';
import { loadLocale, getMessage, getSystemLanguage } from './i18n.js';
import { sameEpisode } from './episode-utils.js';
import { applyTitlePrivacyToPayload, sanitizeSharedTitle, sanitizeTabTitle, normalizeSendTabTitle, normalizeTitlePrivacyMode } from './title-privacy.js';
import { initTabManager } from './modules/tab-manager.js';
// --- Uninstall URL Initialization ---
@@ -362,21 +363,49 @@ async function getSettings() {
// (username) must NEVER come from storage.sync — syncing them across devices
// both leaks them and resurrects dead rooms on reinstall (a fresh install
// has empty local storage but sync survives in the user's Google account).
const data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']);
const data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode']);
let username = data.username;
if (!username) {
username = generateUsername();
await chrome.storage.local.set({ username });
}
const legacyTitlePrivacyMode = normalizeTitlePrivacyMode(data.titlePrivacyMode);
const mediaTitlePrivacyMode = normalizeTitlePrivacyMode(data.mediaTitlePrivacyMode || legacyTitlePrivacyMode);
return {
serverUrl: data.serverUrl || '',
useCustomServer: data.useCustomServer || false,
roomId: data.roomId || '',
password: data.password || '',
username
username,
sendTabTitle: normalizeSendTabTitle(data.sendTabTitle, legacyTitlePrivacyMode),
mediaTitlePrivacyMode
};
}
function getSharedTitleFields(settings, mediaTitle = null) {
return {
tabTitle: sanitizeTabTitle(currentTabTitle, settings?.sendTabTitle),
mediaTitle: sanitizeSharedTitle(mediaTitle, settings?.mediaTitlePrivacyMode)
};
}
function withTitlePrivacy(payload, settings, keys) {
return applyTitlePrivacyToPayload(payload, settings?.mediaTitlePrivacyMode, keys);
}
function emitEpisodeLobbyForCurrentPrivacy() {
if (!episodeLobby || episodeLobby.initiatorPeerId !== peerId) return;
getSettings().then(settings => {
if (!episodeLobby || episodeLobby.initiatorPeerId !== peerId) return;
const expectedTitle = sanitizeSharedTitle(episodeLobby.expectedTitle, settings.mediaTitlePrivacyMode);
if (expectedTitle) {
emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle });
}
}).catch(err => {
addLog('Episode lobby privacy error: ' + err.message, 'error');
});
}
// Privacy + correctness: only onboardingComplete and dismissedHints belong in
// storage.sync. Everything else is per-device local storage. This actively
// removes legacy keys that older versions wrote to sync (and that would
@@ -384,7 +413,8 @@ async function getSettings() {
const LEGACY_SYNC_KEYS = [
'serverUrl', 'useCustomServer', 'roomId', 'password', 'username',
'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode',
'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings'
'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings',
'titlePrivacyMode', 'sendTabTitle', 'mediaTitlePrivacyMode'
];
function purgeLegacySyncKeys() {
chrome.storage.sync.remove(LEGACY_SYNC_KEYS).catch(() => {});
@@ -640,12 +670,13 @@ async function connect() {
addLog('Joined Namespace /', 'success');
const settings = await getSettings();
if (settings.roomId) {
const sharedTitles = getSharedTitleFields(settings);
emit(EVENTS.JOIN_ROOM, {
roomId: settings.roomId,
password: settings.password,
peerId,
username: settings.username,
tabTitle: currentTabTitle,
tabTitle: sharedTitles.tabTitle,
protocolVersion: PROTOCOL_VERSION
});
}
@@ -1240,7 +1271,7 @@ function handleServerEvent(event, data) {
}
if (episodeLobby && episodeLobby.initiatorPeerId === peerId) {
emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: episodeLobby.expectedTitle });
emitEpisodeLobbyForCurrentPrivacy();
}
}
} else if (data.status === 'left') {
@@ -1615,11 +1646,12 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
if (otherCount > 0) {
const settings = await getSettings();
const sharedTitles = getSharedTitleFields(settings);
emit(EVENTS.PEER_STATUS, {
peerId,
status: 'heartbeat',
username: settings.username,
tabTitle: currentTabTitle,
tabTitle: sharedTitles.tabTitle,
desynced: hcmDesynced
});
}
@@ -1712,12 +1744,13 @@ async function handleAsyncMessage(message, sender, sendResponse) {
if (desiredUrl !== currentServerUrl) forceDisconnect();
if (settings.roomId) connect();
} else if (settings.roomId) {
const sharedTitles = getSharedTitleFields(settings);
emit(EVENTS.JOIN_ROOM, {
roomId: settings.roomId,
password: settings.password,
peerId,
username: settings.username,
tabTitle: currentTabTitle,
tabTitle: sharedTitles.tabTitle,
protocolVersion: PROTOCOL_VERSION
});
}
@@ -1929,12 +1962,13 @@ async function handleAsyncMessage(message, sender, sendResponse) {
if (desiredUrl !== currentServerUrl) forceDisconnect();
connect();
} else if (roomId) {
const sharedTitles = getSharedTitleFields(settings);
emit(EVENTS.JOIN_ROOM, {
roomId,
password,
peerId,
username: settings.username,
tabTitle: currentTabTitle,
tabTitle: sharedTitles.tabTitle,
protocolVersion: PROTOCOL_VERSION
});
}
@@ -1964,7 +1998,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}
});
} else if (message.type === 'CONTENT_EVENT') {
const processEvent = () => {
const processEvent = async () => {
// Host Control Mode (sender-side): a non-controller in host-only mode must
// not drive the room. Don't broadcast; hand the action back to content.js so
// it can snap the local player back / offer desync.
@@ -2047,7 +2081,9 @@ async function handleAsyncMessage(message, sender, sendResponse) {
return;
}
emit(message.action, { ...message.payload, peerId });
const settings = await getSettings();
const outboundPayload = withTitlePrivacy(message.payload, settings, ['mediaTitle']);
emit(message.action, { ...outboundPayload, peerId });
sendResponse({ status: 'ok' });
};
@@ -2062,10 +2098,16 @@ async function handleAsyncMessage(message, sender, sendResponse) {
currentTabTitle = sender.tab.title ? sender.tab.title.substring(0, 50) : null;
chrome.storage.session.set({ currentTabTitle });
updateBadgeStatus();
processEvent();
processEvent().catch(err => {
addLog('Content event privacy error: ' + err.message, 'error');
sendResponse({ status: 'error' });
});
} else {
routeToContent(message.action, message.payload);
processEvent();
processEvent().catch(err => {
addLog('Content event privacy error: ' + err.message, 'error');
sendResponse({ status: 'error' });
});
}
} else if (message.type === 'FORCE_SYNC_ACK') {
if (isForceSyncInitiator) {
@@ -2122,16 +2164,24 @@ async function handleAsyncMessage(message, sender, sendResponse) {
markRoomUseful();
getSettings().then(settings => {
const statusPayload = { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle, desynced: hcmDesynced };
const sharedTitles = getSharedTitleFields(settings, message.payload?.mediaTitle);
const statusPayload = {
...message.payload,
peerId,
username: settings.username,
tabTitle: sharedTitles.tabTitle,
mediaTitle: sharedTitles.mediaTitle,
desynced: hcmDesynced
};
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
if (otherCount > 0) emit(EVENTS.PEER_STATUS, statusPayload);
if (currentRoom && Array.isArray(currentRoom.peers)) {
const me = currentRoom.peers.find(p => (p.peerId || p) === peerId);
if (me && typeof me === 'object') {
me.tabTitle = currentTabTitle;
me.tabTitle = sharedTitles.tabTitle;
me.username = settings.username;
me.mediaTitle = message.payload?.mediaTitle;
me.mediaTitle = sharedTitles.mediaTitle;
me.playbackState = message.payload?.playbackState;
me.currentTime = message.payload?.currentTime;
me.volume = message.payload?.volume;
@@ -2193,10 +2243,18 @@ async function handleAsyncMessage(message, sender, sendResponse) {
return;
}
const settings = await getSettings();
const lobbyTitle = sanitizeSharedTitle(newTitle, settings.mediaTitlePrivacyMode);
if (!lobbyTitle) {
addLog(`Episode change detected but media title sharing is ${settings.mediaTitlePrivacyMode}; not creating a lobby.`, 'info');
sendResponse({ status: 'title_privacy_no_lobby' });
return;
}
// Check setting
const epSettings = await chrome.storage.local.get(['autoSyncNextEpisode']);
if (epSettings.autoSyncNextEpisode === false) {
addLog(`Episode change detected ("${newTitle}") but Auto-Sync is disabled.`, 'info');
addLog(`Episode change detected ("${lobbyTitle}") but Auto-Sync is disabled.`, 'info');
sendResponse({ status: 'disabled' });
return;
}
@@ -2208,7 +2266,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
// guest just follows / snaps back. Use amController() for parity with the
// CONTENT_EVENT gate and the server's controllers-based check.
if (controlMode === CONTROL_MODES.HOST_ONLY && !amController()) {
addLog(`Episode change ("${newTitle}") — host-only guest, not creating a lobby (controller drives).`, 'info');
addLog(`Episode change ("${lobbyTitle}") — host-only guest, not creating a lobby (controller drives).`, 'info');
sendResponse({ status: 'host_only_guest_skip' });
return;
}
@@ -2218,18 +2276,18 @@ async function handleAsyncMessage(message, sender, sendResponse) {
// Live peer check, so the moment someone joins the next transition syncs.
const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
if (otherCount === 0) {
addLog(`Episode change ("${newTitle}") — alone in room, playing through without a lobby.`, 'info');
addLog(`Episode change ("${lobbyTitle}") — alone in room, playing through without a lobby.`, 'info');
sendResponse({ status: 'solo_no_lobby' });
return;
}
// If lobby already exists for this title, just mark self ready
if (episodeLobby && sameEpisode(episodeLobby.expectedTitle, newTitle)) {
if (episodeLobby && sameEpisode(episodeLobby.expectedTitle, lobbyTitle)) {
if (!episodeLobby.readyPeers.includes(peerId)) {
episodeLobby.readyPeers.push(peerId);
persistEpisodeLobby();
broadcastLobbyUpdate();
emit(EVENTS.EPISODE_READY, { peerId, title: newTitle });
emit(EVENTS.EPISODE_READY, { peerId, title: lobbyTitle });
checkEpisodeLobbyCompletion();
}
sendResponse({ status: 'ready_sent' });
@@ -2241,26 +2299,26 @@ async function handleAsyncMessage(message, sender, sendResponse) {
// Create new lobby
episodeLobby = {
expectedTitle: newTitle,
expectedTitle: lobbyTitle,
initiatorPeerId: peerId,
readyPeers: [peerId], // We are already ready
createdAt: Date.now()
};
persistEpisodeLobby();
broadcastLobbyUpdate();
addLog(`Episode lobby created: "${newTitle}"`, 'info');
addLog(`Episode lobby created: "${lobbyTitle}"`, 'info');
// Tell content script to pause the video and start polling
// (This is the only place we pause — after confirming the feature is enabled)
if (sender.tab && sender.tab.id) {
chrome.tabs.sendMessage(sender.tab.id, {
type: 'PAUSE_FOR_LOBBY',
expectedTitle: newTitle
expectedTitle: lobbyTitle
}).catch(() => {});
}
// Broadcast to room
emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: newTitle });
emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: lobbyTitle });
// Start timeout (Q1: Option B — cancel on timeout)
episodeLobbyTimeout = setTimeout(() => cancelEpisodeLobby('Timeout — not all peers loaded the episode'), EPISODE_LOBBY_TIMEOUT);
@@ -2273,15 +2331,40 @@ async function handleAsyncMessage(message, sender, sendResponse) {
// Content script confirmed it loaded the lobby episode
if (episodeLobby && message.payload && sameEpisode(message.payload.title, episodeLobby.expectedTitle)) {
if (!episodeLobby.readyPeers.includes(peerId)) {
const settings = await getSettings();
const readyTitle = sanitizeSharedTitle(message.payload.title, settings.mediaTitlePrivacyMode);
episodeLobby.readyPeers.push(peerId);
persistEpisodeLobby();
broadcastLobbyUpdate();
emit(EVENTS.EPISODE_READY, { peerId, title: message.payload.title });
addLog(`Local episode ready: "${message.payload.title}"`, 'success');
emit(EVENTS.EPISODE_READY, { peerId, title: readyTitle });
addLog(`Local episode ready: "${readyTitle || episodeLobby.expectedTitle}"`, 'success');
checkEpisodeLobbyCompletion();
}
}
sendResponse({ status: 'ok' });
} else if (message.type === 'TITLE_PRIVACY_CHANGED') {
const settings = await getSettings();
if (episodeLobby && episodeLobby.initiatorPeerId === peerId) {
const nextLobbyTitle = sanitizeSharedTitle(episodeLobby.expectedTitle, settings.mediaTitlePrivacyMode);
if (!nextLobbyTitle || nextLobbyTitle !== episodeLobby.expectedTitle) {
cancelEpisodeLobby('Title privacy changed');
}
}
if (currentRoom) {
const sharedTitles = getSharedTitleFields(settings);
emit(EVENTS.PEER_STATUS, {
peerId,
status: 'heartbeat',
username: settings.username,
tabTitle: sharedTitles.tabTitle,
mediaTitle: sharedTitles.mediaTitle,
desynced: hcmDesynced
});
}
if (currentRoom && currentTabId) {
chrome.tabs.sendMessage(currentTabId, { type: 'REQUEST_HEARTBEAT' }).catch(() => {});
}
sendResponse({ status: 'ok' });
} else if (message.type === 'CONTENT_BOOT') {
// Content script re-injected, check if there's an active lobby
if (episodeLobby) {
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Demnächst",
"BTN_RESTART_TOUR": "Einführung neu starten",
"BTN_RESTART_TOUR_TOOLTIP": "Startet die Einführung für neue Benutzer erneut",
"HINT_SELECT_VIDEO": "Wähle hier dein Video aus!"
"HINT_SELECT_VIDEO": "Wähle hier dein Video aus!",
"OPTION_TITLE_PRIVACY_FULL": "Voll senden",
"OPTION_TITLE_PRIVACY_EPISODE": "Nur Episoden-ID senden",
"OPTION_TITLE_PRIVACY_HIDDEN": "Nicht senden",
"LABEL_PRIVACY_SETTINGS": "Privatsphäre-Einstellungen",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Lege fest, welche Tab- und Medientitel an den Raum gesendet werden.",
"LABEL_SEND_TAB_TITLE": "Tab-Titel senden",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Sendet den Titel des ausgewählten Browser-Tabs an den Raum.",
"LABEL_MEDIA_TITLE_PRIVACY": "Medientitel",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Lege fest, wie Medientitel an den Raum gesendet werden."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Coming soon",
"BTN_RESTART_TOUR": "Restart Tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Restart the onboarding tutorial",
"HINT_SELECT_VIDEO": "Select your video here!"
"HINT_SELECT_VIDEO": "Select your video here!",
"OPTION_TITLE_PRIVACY_FULL": "Send full title",
"OPTION_TITLE_PRIVACY_EPISODE": "Send episode ID",
"OPTION_TITLE_PRIVACY_HIDDEN": "Do not send",
"LABEL_PRIVACY_SETTINGS": "Privacy Settings",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choose which tab and media titles are sent to the room.",
"LABEL_SEND_TAB_TITLE": "Send tab title",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Send the selected browser tab title to the room.",
"LABEL_MEDIA_TITLE_PRIVACY": "Media title",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choose how media titles are sent to the room."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Próximamente",
"BTN_RESTART_TOUR": "Reiniciar tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar el tutorial de inicio",
"HINT_SELECT_VIDEO": "¡Selecciona tu video aquí!"
"HINT_SELECT_VIDEO": "¡Selecciona tu video aquí!",
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID de episodio",
"OPTION_TITLE_PRIVACY_HIDDEN": "No enviar",
"LABEL_PRIVACY_SETTINGS": "Ajustes de privacidad",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Elige qué títulos de pestaña y multimedia se envían a la sala.",
"LABEL_SEND_TAB_TITLE": "Enviar título de pestaña",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envía a la sala el título de la pestaña del navegador seleccionada.",
"LABEL_MEDIA_TITLE_PRIVACY": "Título multimedia",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Elige cómo se envían los títulos multimedia a la sala."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Bientôt disponible",
"BTN_RESTART_TOUR": "Redémarrer le tutoriel",
"BTN_RESTART_TOUR_TOOLTIP": "Recommencer le tutoriel d'intégration",
"HINT_SELECT_VIDEO": "Sélectionnez votre vidéo ici !"
"HINT_SELECT_VIDEO": "Sélectionnez votre vidéo ici !",
"OPTION_TITLE_PRIVACY_FULL": "Envoyer le titre complet",
"OPTION_TITLE_PRIVACY_EPISODE": "Envoyer lidentifiant d’épisode",
"OPTION_TITLE_PRIVACY_HIDDEN": "Ne pas envoyer",
"LABEL_PRIVACY_SETTINGS": "Paramètres de confidentialité",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Choisissez quels titres donglet et de média sont envoyés au salon.",
"LABEL_SEND_TAB_TITLE": "Envoyer le titre de longlet",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envoie au salon le titre de longlet de navigateur sélectionné.",
"LABEL_MEDIA_TITLE_PRIVACY": "Titre du média",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Choisissez comment les titres des médias sont envoyés au salon."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Prossimamente",
"BTN_RESTART_TOUR": "Riavvia Tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Riavvia il tutorial introduttivo",
"HINT_SELECT_VIDEO": "Scegli il tuo video qui!"
"HINT_SELECT_VIDEO": "Scegli il tuo video qui!",
"OPTION_TITLE_PRIVACY_FULL": "Invia titolo completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Invia ID episodio",
"OPTION_TITLE_PRIVACY_HIDDEN": "Non inviare",
"LABEL_PRIVACY_SETTINGS": "Impostazioni privacy",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Scegli quali titoli di scheda e contenuto multimediale vengono inviati alla stanza.",
"LABEL_SEND_TAB_TITLE": "Invia titolo scheda",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Invia alla stanza il titolo della scheda del browser selezionata.",
"LABEL_MEDIA_TITLE_PRIVACY": "Titolo multimediale",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Scegli come inviare alla stanza i titoli multimediali."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "近日公開",
"BTN_RESTART_TOUR": "チュートリアルを再起動",
"BTN_RESTART_TOUR_TOOLTIP": "オンボーディングチュートリアルを再起動する",
"HINT_SELECT_VIDEO": "ここで動画を選択してください!"
"HINT_SELECT_VIDEO": "ここで動画を選択してください!",
"OPTION_TITLE_PRIVACY_FULL": "完全なタイトルを送信",
"OPTION_TITLE_PRIVACY_EPISODE": "エピソードIDを送信",
"OPTION_TITLE_PRIVACY_HIDDEN": "送信しない",
"LABEL_PRIVACY_SETTINGS": "プライバシー設定",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "ルームに送信するタブタイトルとメディアタイトルを選択します。",
"LABEL_SEND_TAB_TITLE": "タブタイトルを送信",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "選択中のブラウザータブのタイトルをルームに送信します。",
"LABEL_MEDIA_TITLE_PRIVACY": "メディアタイトル",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "メディアタイトルをルームに送信する方法を選択します。"
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "출시 예정",
"BTN_RESTART_TOUR": "튜토리얼 다시 시작",
"BTN_RESTART_TOUR_TOOLTIP": "온보딩 튜토리얼을 다시 시작합니다",
"HINT_SELECT_VIDEO": "여기에서 비디오를 선택하세요!"
"HINT_SELECT_VIDEO": "여기에서 비디오를 선택하세요!",
"OPTION_TITLE_PRIVACY_FULL": "전체 제목 보내기",
"OPTION_TITLE_PRIVACY_EPISODE": "에피소드 ID 보내기",
"OPTION_TITLE_PRIVACY_HIDDEN": "보내지 않음",
"LABEL_PRIVACY_SETTINGS": "개인정보 설정",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "방에 보낼 탭 제목과 미디어 제목을 선택합니다.",
"LABEL_SEND_TAB_TITLE": "탭 제목 보내기",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "선택한 브라우저 탭 제목을 방에 보냅니다.",
"LABEL_MEDIA_TITLE_PRIVACY": "미디어 제목",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "미디어 제목을 방에 보내는 방식을 선택합니다."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Binnenkort beschikbaar",
"BTN_RESTART_TOUR": "Tutorial opnieuw starten",
"BTN_RESTART_TOUR_TOOLTIP": "Start de introductie-tutorial opnieuw",
"HINT_SELECT_VIDEO": "Selecteer hier je video!"
"HINT_SELECT_VIDEO": "Selecteer hier je video!",
"OPTION_TITLE_PRIVACY_FULL": "Volledige titel verzenden",
"OPTION_TITLE_PRIVACY_EPISODE": "Afleverings-ID verzenden",
"OPTION_TITLE_PRIVACY_HIDDEN": "Niet verzenden",
"LABEL_PRIVACY_SETTINGS": "Privacy-instellingen",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Kies welke tab- en mediatitels naar de kamer worden gestuurd.",
"LABEL_SEND_TAB_TITLE": "Tabtitel verzenden",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Stuurt de titel van het geselecteerde browsertabblad naar de kamer.",
"LABEL_MEDIA_TITLE_PRIVACY": "Mediatitel",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Kies hoe mediatitels naar de kamer worden gestuurd."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Wkrótce",
"BTN_RESTART_TOUR": "Uruchom samouczek ponownie",
"BTN_RESTART_TOUR_TOOLTIP": "Uruchom ponownie samouczek powitalny",
"HINT_SELECT_VIDEO": "Wybierz swoje wideo tutaj!"
"HINT_SELECT_VIDEO": "Wybierz swoje wideo tutaj!",
"OPTION_TITLE_PRIVACY_FULL": "Wysyłaj pełny tytuł",
"OPTION_TITLE_PRIVACY_EPISODE": "Wysyłaj ID odcinka",
"OPTION_TITLE_PRIVACY_HIDDEN": "Nie wysyłaj",
"LABEL_PRIVACY_SETTINGS": "Ustawienia prywatności",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Wybierz, które tytuły kart i multimediów są wysyłane do pokoju.",
"LABEL_SEND_TAB_TITLE": "Wysyłaj tytuł karty",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Wysyła do pokoju tytuł wybranej karty przeglądarki.",
"LABEL_MEDIA_TITLE_PRIVACY": "Tytuł multimediów",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Wybierz, jak tytuły multimediów są wysyłane do pokoju."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Em breve",
"BTN_RESTART_TOUR": "Reiniciar tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar o tutorial de introdução",
"HINT_SELECT_VIDEO": "Selecione seu vídeo aqui!"
"HINT_SELECT_VIDEO": "Selecione seu vídeo aqui!",
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID do episódio",
"OPTION_TITLE_PRIVACY_HIDDEN": "Não enviar",
"LABEL_PRIVACY_SETTINGS": "Configurações de privacidade",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Escolha quais títulos de aba e mídia são enviados para a sala.",
"LABEL_SEND_TAB_TITLE": "Enviar título da aba",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envia para a sala o título da aba selecionada do navegador.",
"LABEL_MEDIA_TITLE_PRIVACY": "Título da mídia",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Escolha como os títulos de mídia são enviados para a sala."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Em breve",
"BTN_RESTART_TOUR": "Reiniciar Tutorial",
"BTN_RESTART_TOUR_TOOLTIP": "Reiniciar o tutorial introdutório",
"HINT_SELECT_VIDEO": "Selecione o seu vídeo aqui!"
"HINT_SELECT_VIDEO": "Selecione o seu vídeo aqui!",
"OPTION_TITLE_PRIVACY_FULL": "Enviar título completo",
"OPTION_TITLE_PRIVACY_EPISODE": "Enviar ID do episódio",
"OPTION_TITLE_PRIVACY_HIDDEN": "Não enviar",
"LABEL_PRIVACY_SETTINGS": "Definições de privacidade",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Escolhe que títulos do separador e do conteúdo multimédia são enviados para a sala.",
"LABEL_SEND_TAB_TITLE": "Enviar título do separador",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Envia para a sala o título do separador do navegador selecionado.",
"LABEL_MEDIA_TITLE_PRIVACY": "Título multimédia",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Escolhe como os títulos multimédia são enviados para a sala."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Скоро",
"BTN_RESTART_TOUR": "Перезапустить обучение",
"BTN_RESTART_TOUR_TOOLTIP": "Запустить приветственное руководство заново",
"HINT_SELECT_VIDEO": "Выберите ваше видео здесь!"
"HINT_SELECT_VIDEO": "Выберите ваше видео здесь!",
"OPTION_TITLE_PRIVACY_FULL": "Отправлять полное название",
"OPTION_TITLE_PRIVACY_EPISODE": "Отправлять ID эпизода",
"OPTION_TITLE_PRIVACY_HIDDEN": "Не отправлять",
"LABEL_PRIVACY_SETTINGS": "Настройки приватности",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Выберите, какие названия вкладки и медиа отправляются в комнату.",
"LABEL_SEND_TAB_TITLE": "Отправлять название вкладки",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Отправляет в комнату название выбранной вкладки браузера.",
"LABEL_MEDIA_TITLE_PRIVACY": "Название медиа",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Выберите, как названия медиа отправляются в комнату."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Çok yakında",
"BTN_RESTART_TOUR": "Eğitimi Yeniden Başlat",
"BTN_RESTART_TOUR_TOOLTIP": "Tanıtım eğitimini yeniden başlat",
"HINT_SELECT_VIDEO": "Videonuzu buradan seçin!"
"HINT_SELECT_VIDEO": "Videonuzu buradan seçin!",
"OPTION_TITLE_PRIVACY_FULL": "Tam başlığı gönder",
"OPTION_TITLE_PRIVACY_EPISODE": "Bölüm IDsini gönder",
"OPTION_TITLE_PRIVACY_HIDDEN": "Gönderme",
"LABEL_PRIVACY_SETTINGS": "Gizlilik ayarları",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Odaya hangi sekme ve medya başlıklarının gönderileceğini seçin.",
"LABEL_SEND_TAB_TITLE": "Sekme başlığını gönder",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Seçili tarayıcı sekmesinin başlığını odaya gönderir.",
"LABEL_MEDIA_TITLE_PRIVACY": "Medya başlığı",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Medya başlıklarının odaya nasıl gönderileceğini seçin."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "Скоро буде",
"BTN_RESTART_TOUR": "Перезапустіть підручник",
"BTN_RESTART_TOUR_TOOLTIP": "Перезапустіть навчальний посібник із адаптації",
"HINT_SELECT_VIDEO": "Виберіть своє відео тут!"
"HINT_SELECT_VIDEO": "Виберіть своє відео тут!",
"OPTION_TITLE_PRIVACY_FULL": "Надсилати повну назву",
"OPTION_TITLE_PRIVACY_EPISODE": "Надсилати ID епізоду",
"OPTION_TITLE_PRIVACY_HIDDEN": "Не надсилати",
"LABEL_PRIVACY_SETTINGS": "Налаштування приватності",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "Виберіть, які назви вкладки й медіа надсилати до кімнати.",
"LABEL_SEND_TAB_TITLE": "Надсилати назву вкладки",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "Надсилає до кімнати назву вибраної вкладки браузера.",
"LABEL_MEDIA_TITLE_PRIVACY": "Назва медіа",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "Виберіть, як назви медіа надсилати до кімнати."
}
+10 -1
View File
@@ -229,5 +229,14 @@
"AUDIO_COMING_SOON": "即将推出",
"BTN_RESTART_TOUR": "重启教程",
"BTN_RESTART_TOUR_TOOLTIP": "重新启动入门教程",
"HINT_SELECT_VIDEO": "在这里选择您的视频!"
"HINT_SELECT_VIDEO": "在这里选择您的视频!",
"OPTION_TITLE_PRIVACY_FULL": "发送完整标题",
"OPTION_TITLE_PRIVACY_EPISODE": "发送剧集 ID",
"OPTION_TITLE_PRIVACY_HIDDEN": "不发送",
"LABEL_PRIVACY_SETTINGS": "隐私设置",
"LABEL_PRIVACY_SETTINGS_TOOLTIP": "选择要发送到房间的标签页标题和媒体标题。",
"LABEL_SEND_TAB_TITLE": "发送标签页标题",
"LABEL_SEND_TAB_TITLE_TOOLTIP": "将所选浏览器标签页的标题发送到房间。",
"LABEL_MEDIA_TITLE_PRIVACY": "媒体标题",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP": "选择媒体标题发送到房间的方式。"
}
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"default_locale": "en",
"name": "KoalaSync",
"version": "2.4.6",
"version": "2.5.0",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
+19
View File
@@ -550,6 +550,25 @@
<input type="text" id="username" data-i18n-placeholder="PLACEHOLDER_USERNAME" placeholder="Anonymous Koala" maxlength="20" style="width: 150px;">
</div>
<details class="form-group" style="background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<summary style="cursor: pointer; font-size: 12px; font-weight: 700; color: var(--text);" title="Choose which titles are sent to the room." data-i18n="LABEL_PRIVACY_SETTINGS" data-i18n-title="LABEL_PRIVACY_SETTINGS_TOOLTIP">Privacy Settings</summary>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px;">
<label style="margin-bottom: 0; cursor: help;" title="Send the selected browser tab title to the room." data-i18n="LABEL_SEND_TAB_TITLE" data-i18n-title="LABEL_SEND_TAB_TITLE_TOOLTIP">Send tab title</label>
<label class="toggle-switch">
<input type="checkbox" id="sendTabTitle" checked>
<span class="slider"></span>
</label>
</div>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px;">
<label style="margin-bottom: 0; cursor: help;" title="Choose how media titles are sent to the room." data-i18n="LABEL_MEDIA_TITLE_PRIVACY" data-i18n-title="LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP">Media title</label>
<select id="mediaTitlePrivacyMode" style="width: 165px; background: var(--bg); border: 1px solid #334155; color: white; padding: 6px 10px; border-radius: 8px; font-size: 12px; cursor: pointer; outline: none; font-family: inherit;">
<option value="full" data-i18n="OPTION_TITLE_PRIVACY_FULL">Send full title</option>
<option value="episode" data-i18n="OPTION_TITLE_PRIVACY_EPISODE">Send episode ID</option>
<option value="hidden" data-i18n="OPTION_TITLE_PRIVACY_HIDDEN">Do not send</option>
</select>
</div>
</details>
<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="Filters out non-video tabs and unrelated domains to keep the list clean" data-i18n="LABEL_HIDE_CLUTTER" data-i18n-title="LABEL_HIDE_CLUTTER_TOOLTIP">Hide Clutter Tabs</label>
<label class="toggle-switch">
+24 -1
View File
@@ -2,6 +2,7 @@ import { EVENTS, OFFICIAL_LANDING_PAGE_URL, SUPPORT_URL, getReviewUrl } from './
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
import { getAvatarForName, generateUsername, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './shared/names.js';
import { loadLocale, translateDOM, getMessage, getSystemLanguage } from './i18n.js';
import { TITLE_PRIVACY_MODES, normalizeSendTabTitle } from './title-privacy.js';
const elements = {
@@ -54,6 +55,8 @@ const elements = {
playBtn: document.getElementById('playBtn'),
pauseBtn: document.getElementById('pauseBtn'),
autoSyncNextEpisode: document.getElementById('autoSyncNextEpisode'),
sendTabTitle: document.getElementById('sendTabTitle'),
mediaTitlePrivacyMode: document.getElementById('mediaTitlePrivacyMode'),
episodeLobbyCard: document.getElementById('episodeLobbyCard'),
lobbyTitle: document.getElementById('lobbyTitle'),
lobbyPeerStatus: document.getElementById('lobbyPeerStatus'),
@@ -184,7 +187,7 @@ function setRoomRefreshCooldown() {
async function init() {
// Local-only by design — settings and room credentials never come from
// storage.sync (only onboardingComplete + dismissedHints live there).
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab']);
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab']);
let activeLang = localData.locale;
if (!activeLang) {
@@ -209,6 +212,10 @@ async function init() {
elements.username.value = username;
if (elements.filterNoise) elements.filterNoise.checked = localData.filterNoise !== false;
if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = localData.autoSyncNextEpisode !== false;
const legacyTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.titlePrivacyMode) ? localData.titlePrivacyMode : TITLE_PRIVACY_MODES.FULL;
const mediaTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.mediaTitlePrivacyMode) ? localData.mediaTitlePrivacyMode : legacyTitlePrivacyMode;
if (elements.sendTabTitle) elements.sendTabTitle.checked = normalizeSendTabTitle(localData.sendTabTitle, legacyTitlePrivacyMode);
if (elements.mediaTitlePrivacyMode) elements.mediaTitlePrivacyMode.value = mediaTitlePrivacyMode;
if (elements.forceSyncMode) elements.forceSyncMode.value = localData.forceSyncMode || 'jump-to-others';
if (elements.browserNotifications) elements.browserNotifications.checked = localData.browserNotifications === true;
if (elements.autoCopyInvite) elements.autoCopyInvite.checked = localData.autoCopyInvite !== false;
@@ -1222,6 +1229,22 @@ elements.autoSyncNextEpisode.addEventListener('change', () => {
chrome.storage.local.set({ autoSyncNextEpisode: elements.autoSyncNextEpisode.checked });
});
if (elements.sendTabTitle) {
elements.sendTabTitle.addEventListener('change', () => {
chrome.storage.local.set({ sendTabTitle: elements.sendTabTitle.checked }, () => {
chrome.runtime.sendMessage({ type: 'TITLE_PRIVACY_CHANGED' }).catch(() => {});
});
});
}
if (elements.mediaTitlePrivacyMode) {
elements.mediaTitlePrivacyMode.addEventListener('change', () => {
chrome.storage.local.set({ mediaTitlePrivacyMode: elements.mediaTitlePrivacyMode.value }, () => {
chrome.runtime.sendMessage({ type: 'TITLE_PRIVACY_CHANGED' }).catch(() => {});
});
});
}
elements.browserNotifications.addEventListener('change', () => {
chrome.storage.local.set({ browserNotifications: elements.browserNotifications.checked });
});
+45
View File
@@ -0,0 +1,45 @@
import { extractEpisodeId } from './episode-utils.js';
export const TITLE_PRIVACY_MODES = Object.freeze({
FULL: 'full',
EPISODE: 'episode',
HIDDEN: 'hidden'
});
export function normalizeTitlePrivacyMode(mode) {
return Object.values(TITLE_PRIVACY_MODES).includes(mode)
? mode
: TITLE_PRIVACY_MODES.FULL;
}
export function normalizeSendTabTitle(sendTabTitle, legacyMode = TITLE_PRIVACY_MODES.FULL) {
if (typeof sendTabTitle === 'boolean') return sendTabTitle;
return normalizeTitlePrivacyMode(legacyMode) === TITLE_PRIVACY_MODES.FULL;
}
export function sanitizeTabTitle(title, sendTabTitle) {
if (!sendTabTitle) return null;
return typeof title === 'string' && title.length > 0 ? title : null;
}
export function sanitizeSharedTitle(title, mode) {
const normalizedMode = normalizeTitlePrivacyMode(mode);
if (normalizedMode === TITLE_PRIVACY_MODES.HIDDEN) return null;
if (typeof title !== 'string' || title.length === 0) return null;
if (normalizedMode === TITLE_PRIVACY_MODES.EPISODE) {
return extractEpisodeId(title) || null;
}
return title;
}
export function applyTitlePrivacyToPayload(payload, mode, keys = ['mediaTitle', 'expectedTitle', 'title']) {
const source = payload && typeof payload === 'object' ? payload : {};
const next = { ...source };
keys.forEach(key => {
if (Object.prototype.hasOwnProperty.call(next, key)) {
next[key] = sanitizeSharedTitle(next[key], mode);
}
});
return next;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "koalasync",
"version": "2.4.6",
"version": "2.5.0",
"description": "KoalaSync Build Scripts",
"private": true,
"type": "module",
+76
View File
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict';
import {
TITLE_PRIVACY_MODES,
applyTitlePrivacyToPayload,
normalizeSendTabTitle,
normalizeTitlePrivacyMode,
sanitizeSharedTitle,
sanitizeTabTitle
} from '../extension/title-privacy.js';
assert.equal(normalizeTitlePrivacyMode(undefined), TITLE_PRIVACY_MODES.FULL);
assert.equal(normalizeTitlePrivacyMode('unknown'), TITLE_PRIVACY_MODES.FULL);
assert.equal(normalizeTitlePrivacyMode(TITLE_PRIVACY_MODES.HIDDEN), TITLE_PRIVACY_MODES.HIDDEN);
assert.equal(normalizeSendTabTitle(undefined, TITLE_PRIVACY_MODES.FULL), true);
assert.equal(normalizeSendTabTitle(undefined, TITLE_PRIVACY_MODES.EPISODE), false);
assert.equal(normalizeSendTabTitle(true, TITLE_PRIVACY_MODES.HIDDEN), true);
assert.equal(normalizeSendTabTitle(false, TITLE_PRIVACY_MODES.FULL), false);
assert.equal(sanitizeTabTitle('Private Tab', true), 'Private Tab');
assert.equal(sanitizeTabTitle('Private Tab', false), null);
assert.equal(sanitizeTabTitle('', true), null);
assert.equal(sanitizeSharedTitle('Example Movie', 'full'), 'Example Movie');
assert.equal(sanitizeSharedTitle('', 'full'), null);
assert.equal(sanitizeSharedTitle(null, 'full'), null);
assert.equal(sanitizeSharedTitle('Show Name - S01/E04 - Title', 'episode'), 'S01E04');
assert.equal(sanitizeSharedTitle('Folge 7 - Private Server', 'episode'), 'EP007');
assert.equal(sanitizeSharedTitle('Example Movie', 'episode'), null);
assert.equal(sanitizeSharedTitle('Show Name - S01E04', 'hidden'), null);
assert.equal(sanitizeSharedTitle('Private Tab Title', 'hidden'), null);
assert.deepEqual(
applyTitlePrivacyToPayload({
tabTitle: 'Private Jellyfin - S01E04',
mediaTitle: 'Show Name - S01E04',
currentTime: 42
}, 'episode'),
{
tabTitle: 'Private Jellyfin - S01E04',
mediaTitle: 'S01E04',
currentTime: 42
},
'media privacy must not rewrite tabTitle'
);
assert.deepEqual(
applyTitlePrivacyToPayload({
tabTitle: 'Private Jellyfin - S01E04',
status: 'heartbeat'
}, 'episode'),
{
tabTitle: 'Private Jellyfin - S01E04',
status: 'heartbeat'
},
'media privacy must not rewrite tabTitle or add absent media keys'
);
assert.deepEqual(
applyTitlePrivacyToPayload({
tabTitle: 'Private Tab',
mediaTitle: 'Private Media',
expectedTitle: 'S01E04',
title: 'S01E04'
}, 'hidden'),
{
tabTitle: 'Private Tab',
mediaTitle: null,
expectedTitle: null,
title: null
},
'hidden media privacy must not clear tabTitle'
);
console.log('title-privacy tests passed');
+1
View File
@@ -13,6 +13,7 @@ const checks = [
}],
['rate-limiter unit tests', 'node', ['scripts/test-rate-limiter.mjs']],
['episode-utils unit tests', 'node', ['scripts/test-episode-utils.mjs']],
['title privacy unit tests', 'node', ['scripts/test-title-privacy.mjs']],
['server WebSocket integration', 'node', ['scripts/test-server-ws.mjs']],
['names generator', 'node', ['scripts/test-names.mjs']],
['content video finder', 'node', ['scripts/test-content-video-finder.cjs']],
+1 -1
View File
@@ -7,7 +7,7 @@
*/
export const PROTOCOL_VERSION = "1.0.0";
export const APP_VERSION = "2.4.6";
export const APP_VERSION = "2.5.0";
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
+62 -62
View File
@@ -3,31 +3,31 @@
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://sync.koalastuff.net/imprint</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/privacy</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/de/impressum</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/de/datenschutz</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://sync.koalastuff.net/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -49,7 +49,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/de/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -71,7 +71,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -93,7 +93,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/de/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -115,7 +115,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -137,7 +137,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/de/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -159,7 +159,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/fr/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -181,7 +181,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/fr/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -203,7 +203,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/fr/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -225,7 +225,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/es/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -247,7 +247,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/es/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -269,7 +269,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/es/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -291,7 +291,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/it/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -313,7 +313,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/it/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -335,7 +335,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/it/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -357,7 +357,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/nl/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -379,7 +379,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/nl/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -401,7 +401,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/nl/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -423,7 +423,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pl/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -445,7 +445,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pl/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -467,7 +467,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pl/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -489,7 +489,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -511,7 +511,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -533,7 +533,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -555,7 +555,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt-BR/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -577,7 +577,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt-BR/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -599,7 +599,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt-BR/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -621,7 +621,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ru/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -643,7 +643,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ru/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -665,7 +665,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ru/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -687,7 +687,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/tr/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -709,7 +709,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/tr/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -731,7 +731,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/tr/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -753,7 +753,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ja/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -775,7 +775,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ja/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -797,7 +797,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ja/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -819,7 +819,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ko/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -841,7 +841,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ko/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -863,7 +863,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ko/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -885,7 +885,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/zh/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -907,7 +907,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/zh/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -929,7 +929,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/zh/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -951,7 +951,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/uk/alternatives</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives"/>
@@ -973,7 +973,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/uk/alternatives/teleparty</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/teleparty"/>
@@ -995,7 +995,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/uk/alternatives/screen-sharing</loc>
<lastmod>2026-06-24</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/alternatives/screen-sharing"/>
@@ -1017,7 +1017,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1037,7 +1037,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/de/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1057,7 +1057,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/fr/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1077,7 +1077,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/es/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1097,7 +1097,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt-BR/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1117,7 +1117,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ru/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1137,7 +1137,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/it/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1157,7 +1157,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pl/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1177,7 +1177,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/tr/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1197,7 +1197,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/nl/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1217,7 +1217,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ja/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1237,7 +1237,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/ko/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
@@ -1257,7 +1257,7 @@
</url>
<url>
<loc>https://sync.koalastuff.net/pt/</loc>
<lastmod>2026-06-23</lastmod>
<lastmod>2026-06-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://sync.koalastuff.net/"/>
+1 -1
View File
@@ -106,7 +106,7 @@
"priceCurrency": "EUR"
},
"description": "{{SCHEMA_APP_DESC}}",
"softwareVersion": "2.4.6",
"softwareVersion": "2.5.0",
"license": "https://opensource.org/licenses/MIT",
"sameAs": "https://github.com/Shik3i/KoalaSync",
"image": "https://sync.koalastuff.net/assets/NewLogoIcon.webp",
+2 -2
View File
@@ -1,4 +1,4 @@
{
"version": "2.4.6",
"date": "2026-06-23T16:36:23Z"
"version": "2.5.0",
"date": "2026-06-29T10:58:06Z"
}