From 8a21fbe07fa5a8bdfd00e7eeb434af0e33922d85 Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:43:11 +0200 Subject: [PATCH 01/22] Fix Netflix seek handling --- extension/background.js | 86 +++++++++++++++++++++++++++++++++++++---- extension/content.js | 28 ++++++++++---- extension/popup.js | 17 ++++---- 3 files changed, 106 insertions(+), 25 deletions(-) diff --git a/extension/background.js b/extension/background.js index 2b5f601..dac9678 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1592,6 +1592,68 @@ async function routeToContent(action, payload) { _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, 0); } +function isNetflixUrl(url) { + try { + const host = new URL(url).hostname.toLowerCase(); + return host === 'netflix.com' || host.endsWith('.netflix.com'); + } catch (_e) { + return false; + } +} + +function installNetflixSeekBridge() { + if (window.__koalaNetflixSeekBridgeInstalled) return; + window.__koalaNetflixSeekBridgeInstalled = true; + + function getNetflixPlayer() { + try { + const videoPlayer = window.netflix?.appContext?.state?.playerApp?.getAPI?.().videoPlayer; + const sessionId = videoPlayer?.getAllPlayerSessionIds?.()[0]; + return sessionId ? videoPlayer.getVideoPlayerBySessionId(sessionId) : null; + } catch (_e) { + return null; + } + } + + window.addEventListener('message', (event) => { + if (event.source !== window) return; + const data = event.data; + if (!data || data.__koalaNetflixSeek !== 1 || data.kind !== 'seek' || typeof data.time !== 'number') return; + try { + getNetflixPlayer()?.seek(Math.round(data.time * 1000)); + } catch (_e) { + // Netflix player not ready or private API changed; the next sync tick can retry. + } + }); +} + +async function injectContentScript(tabId) { + let isNetflix = false; + try { + const tab = await chrome.tabs.get(tabId); + isNetflix = isNetflixUrl(tab?.url || ''); + } catch (_e) { + // Fall through to the generic content script injection. + } + + if (isNetflix) { + try { + await chrome.scripting.executeScript({ + target: { tabId }, + world: 'MAIN', + func: installNetflixSeekBridge + }); + } catch (err) { + addLog(`Netflix seek bridge injection failed: ${err.message}`, 'warn'); + } + } + + return chrome.scripting.executeScript({ + target: { tabId }, + files: ['content.js'] + }); +} + function _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries) { chrome.tabs.sendMessage(tabId, { type: 'SERVER_COMMAND', @@ -1606,10 +1668,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman return; } if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) { - chrome.scripting.executeScript({ - target: { tabId }, - files: ['content.js'] - }).then(() => { + injectContentScript(tabId).then(() => { setTimeout(() => _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries + 1), 500); }).catch(_err => { addLog(`Auto-reinject failed for tab ${tabId}`, 'warn'); @@ -2196,6 +2255,20 @@ async function handleAsyncMessage(message, sender, sendResponse) { addLog('Heartbeat settings error: ' + err.message, 'error'); sendResponse({ status: 'ok' }); }); + } else if (message.type === 'INJECT_CONTENT_SCRIPT') { + const tabId = Number(message.tabId); + if (!Number.isInteger(tabId)) { + sendResponse({ status: 'invalid_tab' }); + return true; + } + + injectContentScript(tabId).then(() => { + sendResponse({ status: 'ok' }); + }).catch(err => { + addLog(`Failed to inject into tab: ${err.message}`, 'warn'); + sendResponse({ status: 'error', message: err.message }); + }); + return true; } else if (message.type === 'SET_TARGET_TAB') { const previousTabId = currentTabId; currentTabId = message.tabId; @@ -2213,10 +2286,7 @@ async function handleAsyncMessage(message, sender, sendResponse) { if (currentTabId) { const selectedTabId = currentTabId; - chrome.scripting.executeScript({ - target: { tabId: selectedTabId }, - files: ['content.js'] - }) + injectContentScript(selectedTabId) .then(() => applyAudioSettingsToTab(selectedTabId)) .catch(err => { addLog(`Failed to inject into tab: ${err.message}`, 'warn'); diff --git a/extension/content.js b/extension/content.js index a7ade14..01a491e 100644 --- a/extension/content.js +++ b/extension/content.js @@ -57,6 +57,22 @@ let seekDebounceTimer = null; // debounce timer for rapid seek events let expectedSeekTime = null; // strictly track programmatic seeks + const NETFLIX_SEEK_BRIDGE = 1; + + function isNetflixHost() { + const host = window.location.hostname.toLowerCase(); + return host === 'netflix.com' || host.endsWith('.netflix.com'); + } + + function seekVideo(video, targetTime) { + expectedSeekTime = targetTime; + if (isNetflixHost()) { + window.postMessage({ __koalaNetflixSeek: NETFLIX_SEEK_BRIDGE, kind: 'seek', time: targetTime }, '*'); + return; + } + video.currentTime = targetTime; + } + // --- Play/Pause Coalescing (leading + trailing) --- // Media players (HLS/DASH, ad insertion, ABR/quality switches, source swaps, // page teardown) fire bursts of native play/pause events within a few hundred @@ -748,8 +764,7 @@ ytButton.click(); } if (action === EVENTS.SEEK) { - expectedSeekTime = data.targetTime; - video.currentTime = data.targetTime; + seekVideo(video, data.targetTime); } return; } @@ -764,8 +779,7 @@ twitchButton.click(); } if (action === EVENTS.SEEK) { - expectedSeekTime = data.targetTime; - video.currentTime = data.targetTime; + seekVideo(video, data.targetTime); } return; } @@ -782,8 +796,7 @@ _setSuppress('paused'); video.pause(); } else if (action === EVENTS.SEEK) { - expectedSeekTime = data.targetTime; - video.currentTime = data.targetTime; + seekVideo(video, data.targetTime); } } catch (e) { reportLog(`Media Action Error: ${e.message}`, 'error'); @@ -931,10 +944,9 @@ return; } _setSuppress('paused'); - expectedSeekTime = payload.targetTime; video.pause(); try { - video.currentTime = payload.targetTime; + seekVideo(video, payload.targetTime); } catch (e) { reportLog(`Force Sync Seek Error: ${e.message}`, 'error'); } diff --git a/extension/popup.js b/extension/popup.js index a9e2952..7ecead6 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -1609,10 +1609,14 @@ elements.forceSyncBtn.addEventListener('click', async () => { if (mode === 'jump-to-me') { chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (response) => { if (chrome.runtime.lastError || !response || response.currentTime === undefined) { - chrome.scripting.executeScript({ - target: { tabId }, - files: ['content.js'] - }).then(() => { + chrome.runtime.sendMessage({ type: 'INJECT_CONTENT_SCRIPT', tabId }, (injectResponse) => { + if (chrome.runtime.lastError || !injectResponse || injectResponse.status !== 'ok') { + showError(getMessage('ERR_NO_VIDEO_TAB')); + forceSyncDone = true; + elements.forceSyncBtn.disabled = false; + elements.forceSyncBtn.textContent = originalText; + return; + } setTimeout(() => { chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (retryResponse) => { if (chrome.runtime.lastError) return; @@ -1621,11 +1625,6 @@ elements.forceSyncBtn.addEventListener('click', async () => { } }); }, 500); - }).catch(() => { - showError(getMessage('ERR_NO_VIDEO_TAB')); - forceSyncDone = true; - elements.forceSyncBtn.disabled = false; - elements.forceSyncBtn.textContent = originalText; }); return; } From 57cd071d58cce500daaae15eb738a3684b0fb50b Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:50:39 +0200 Subject: [PATCH 02/22] Add hidden remote seek test controls --- extension/background.js | 66 +++++++++++++++++++++++++++++++++++++++++ extension/popup.html | 9 ++++++ extension/popup.js | 43 +++++++++++++++++++++++++-- 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/extension/background.js b/extension/background.js index dac9678..d46b479 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1592,6 +1592,62 @@ async function routeToContent(action, payload) { _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, 0); } +function getTabVideoState(tabId) { + return new Promise((resolve) => { + chrome.tabs.sendMessage(tabId, { type: 'GET_VIDEO_STATE' }, (res) => { + if (chrome.runtime.lastError) { + resolve({ error: chrome.runtime.lastError.message }); + return; + } + resolve(res); + }); + }); +} + +async function getReadyTabVideoState(tabId) { + let state = await getTabVideoState(tabId); + if (!state || state.error) { + await injectContentScript(tabId); + await new Promise(resolve => setTimeout(resolve, 250)); + state = await getTabVideoState(tabId); + } + return state; +} + +async function simulateRemoteSeek(delta) { + if (!currentTabId) return { status: 'no_tab' }; + const tabId = parseInt(currentTabId); + if (isNaN(tabId)) return { status: 'no_tab' }; + + const state = await getReadyTabVideoState(tabId); + if (!state || state.error) return { status: 'error', message: state?.error || 'No video state' }; + if (!state.found || !Number.isFinite(state.currentTime)) return { status: 'no_video' }; + + let targetTime = Math.max(0, state.currentTime + delta); + if (Number.isFinite(state.duration) && state.duration > 0) { + targetTime = Math.min(targetTime, Math.max(0, state.duration - 0.1)); + } + + const senderId = 'KoalaDev'; + const timestamp = Date.now(); + const payload = { + senderId, + actionTimestamp: timestamp, + currentTime: targetTime, + targetTime + }; + + addToHistory(EVENTS.SEEK, senderId); + showNotification(senderId, EVENTS.SEEK); + updateLastAction(EVENTS.SEEK, senderId, timestamp); + lastActionState.targetTime = targetTime; + if (storageInitialized) chrome.storage.session.set({ lastActionState }); + updateLocalPeerState(senderId, { currentTime: targetTime }); + routeToContent(EVENTS.SEEK, payload); + + return { status: 'ok', targetTime }; +} + function isNetflixUrl(url) { try { const host = new URL(url).hostname.toLowerCase(); @@ -2056,6 +2112,16 @@ async function handleAsyncMessage(message, sender, sendResponse) { sendResponse(res); } }); + } else if (message.type === 'DEV_SIMULATE_REMOTE_SEEK') { + const delta = Number(message.delta); + if (!Number.isFinite(delta)) { + sendResponse({ status: 'invalid_delta' }); + return; + } + simulateRemoteSeek(delta).then(sendResponse).catch(err => { + addLog(`Remote seek simulation failed: ${err.message}`, 'warn'); + sendResponse({ status: 'error', message: err.message }); + }); } else if (message.type === 'CONTENT_EVENT') { const processEvent = async () => { // Host Control Mode (sender-side): a non-controller in host-only mode must diff --git a/extension/popup.html b/extension/popup.html index 23a2080..5df9ef9 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -391,6 +391,7 @@ + @@ -692,6 +693,14 @@ +
+ +
+ + +
+
+ diff --git a/extension/popup.js b/extension/popup.js index 7ecead6..7ea0322 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -71,7 +71,10 @@ const elements = { settingsVersion: document.getElementById('settingsVersion'), devSupportLink: document.getElementById('devSupportLink'), devReviewLink: document.getElementById('devReviewLink'), - syncTabCopyInvite: document.getElementById('syncTabCopyInvite') + syncTabCopyInvite: document.getElementById('syncTabCopyInvite'), + devToolsTabBtn: document.getElementById('devToolsTabBtn'), + remoteSeekBack: document.getElementById('remoteSeekBack'), + remoteSeekForward: document.getElementById('remoteSeekForward') }; let localPeerId = null; @@ -88,6 +91,19 @@ let errorToken = 0; let forceSyncDone = false; let connectionErrorTimer = null; let pendingConnectionErrorMsg = null; + +function devToolsEnabled() { + return elements.username && elements.username.value.trim() === 'KoalaDev'; +} + +function syncDevToolsVisibility() { + if (!elements.devToolsTabBtn) return; + const enabled = devToolsEnabled(); + elements.devToolsTabBtn.style.display = enabled ? '' : 'none'; + if (!enabled && document.getElementById('tab-devtools')?.classList.contains('active')) { + document.querySelector('.tab-btn[data-tab="tab-settings"]')?.click(); + } +} let roomListRefreshTimer = null; let roomListRefreshInterval = null; const ROOM_LIST_REFRESH_COOLDOWN_MS = 11000; @@ -210,6 +226,7 @@ async function init() { elements.roomId.value = localData.roomId || ''; elements.password.value = localData.password || ''; elements.username.value = username; + syncDevToolsVisibility(); 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; @@ -277,13 +294,13 @@ async function init() { const syncTabBtn = document.querySelector('.tab-btn[data-tab="tab-sync"]'); if (syncTabBtn) syncTabBtn.click(); showSelectVideoHint(); - } else if (localData.activeTab) { + } else if (localData.activeTab && (localData.activeTab !== 'tab-devtools' || devToolsEnabled())) { const btn = document.querySelector(`.tab-btn[data-tab="${localData.activeTab}"]`); if (btn) btn.click(); } } else { await populateTabs(); - if (localData.activeTab) { + if (localData.activeTab && (localData.activeTab !== 'tab-devtools' || devToolsEnabled())) { const btn = document.querySelector(`.tab-btn[data-tab="${localData.activeTab}"]`); if (btn) btn.click(); } @@ -1275,8 +1292,10 @@ elements.serverUrl.addEventListener('input', () => { chrome.storage.local.set({ serverUrl: elements.serverUrl.value }); }); +elements.username.addEventListener('input', syncDevToolsVisibility); elements.username.addEventListener('change', () => { chrome.storage.local.set({ username: elements.username.value }); + syncDevToolsVisibility(); }); if (elements.langSelector) { @@ -1689,6 +1708,24 @@ elements.pauseBtn.addEventListener('click', () => { }, 2500); }); +function simulateRemoteSeek(delta) { + chrome.runtime.sendMessage({ type: 'DEV_SIMULATE_REMOTE_SEEK', delta }, (res) => { + if (chrome.runtime.lastError || !res || res.status !== 'ok') { + showToast(res?.message || 'Remote seek failed', 'error'); + return; + } + showToast(`Remote seek -> ${formatTime(res.targetTime)}`, 'success', 1200); + refreshDebugInfo(); + }); +} + +if (elements.remoteSeekBack) { + elements.remoteSeekBack.addEventListener('click', () => simulateRemoteSeek(-30)); +} +if (elements.remoteSeekForward) { + elements.remoteSeekForward.addEventListener('click', () => simulateRemoteSeek(30)); +} + elements.clearLogs.addEventListener('click', () => { chrome.runtime.sendMessage({ type: 'CLEAR_LOGS' }, () => { elements.logList.innerHTML = ''; From 52265e84eb9d1f55513f6d64d57ba6c67ff902f7 Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:57:57 +0200 Subject: [PATCH 03/22] Generalize page API seek overrides --- extension/background.js | 82 +++++++++++++++++++--------- extension/content.js | 13 +++-- extension/page-api-seek-overrides.js | 23 ++++++++ 3 files changed, 86 insertions(+), 32 deletions(-) create mode 100644 extension/page-api-seek-overrides.js diff --git a/extension/background.js b/extension/background.js index d46b479..b346e71 100644 --- a/extension/background.js +++ b/extension/background.js @@ -4,6 +4,7 @@ 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'; +import './page-api-seek-overrides.js'; // --- Uninstall URL Initialization --- let uninstallURLInitPromise = null; @@ -1648,62 +1649,87 @@ async function simulateRemoteSeek(delta) { return { status: 'ok', targetTime }; } -function isNetflixUrl(url) { - try { - const host = new URL(url).hostname.toLowerCase(); - return host === 'netflix.com' || host.endsWith('.netflix.com'); - } catch (_e) { - return false; - } +async function devRemoteToolsAllowed() { + const data = await chrome.storage.local.get(['username']); + return data.username === 'KoalaDev'; } -function installNetflixSeekBridge() { - if (window.__koalaNetflixSeekBridgeInstalled) return; - window.__koalaNetflixSeekBridgeInstalled = true; +function shouldUsePageApiSeek(url) { + return typeof globalThis.koalaFindPageApiSeekProvider === 'function' && + !!globalThis.koalaFindPageApiSeekProvider(url); +} + +function installPageApiSeekBridge() { + if (window.__koalaPageApiSeekBridgeInstalled) return; + window.__koalaPageApiSeekBridgeInstalled = true; + + function seekWithPageApi(time) { + const match = typeof window.koalaFindPageApiSeekProvider === 'function' + ? window.koalaFindPageApiSeekProvider(window.location.hostname) + : null; + if (!match) return; - function getNetflixPlayer() { try { - const videoPlayer = window.netflix?.appContext?.state?.playerApp?.getAPI?.().videoPlayer; - const sessionId = videoPlayer?.getAllPlayerSessionIds?.()[0]; - return sessionId ? videoPlayer.getVideoPlayerBySessionId(sessionId) : null; + if (match.provider === 'netflix') { + const videoPlayer = window.netflix?.appContext?.state?.playerApp?.getAPI?.().videoPlayer; + const sessionId = videoPlayer?.getAllPlayerSessionIds?.()[0]; + const player = sessionId ? videoPlayer.getVideoPlayerBySessionId(sessionId) : null; + player?.seek(Math.round(time * 1000)); + } } catch (_e) { - return null; + // Player not ready or private API changed; the next sync tick can retry. } } window.addEventListener('message', (event) => { if (event.source !== window) return; const data = event.data; - if (!data || data.__koalaNetflixSeek !== 1 || data.kind !== 'seek' || typeof data.time !== 'number') return; - try { - getNetflixPlayer()?.seek(Math.round(data.time * 1000)); - } catch (_e) { - // Netflix player not ready or private API changed; the next sync tick can retry. - } + if (!data || data.__koalaPageApiSeek !== 1 || data.kind !== 'seek' || typeof data.time !== 'number') return; + seekWithPageApi(data.time); }); } +function setPageApiSeekEnabled(enabled) { + window.KOALA_PAGE_API_SEEK_ENABLED = enabled === true; +} + async function injectContentScript(tabId) { - let isNetflix = false; + let needsPageApiSeek = false; + let pageApiSeekReady = false; try { const tab = await chrome.tabs.get(tabId); - isNetflix = isNetflixUrl(tab?.url || ''); + needsPageApiSeek = shouldUsePageApiSeek(tab?.url || ''); } catch (_e) { // Fall through to the generic content script injection. } - if (isNetflix) { + if (needsPageApiSeek) { try { await chrome.scripting.executeScript({ target: { tabId }, world: 'MAIN', - func: installNetflixSeekBridge + files: ['page-api-seek-overrides.js'] }); + await chrome.scripting.executeScript({ + target: { tabId }, + world: 'MAIN', + func: installPageApiSeekBridge + }); + pageApiSeekReady = true; } catch (err) { - addLog(`Netflix seek bridge injection failed: ${err.message}`, 'warn'); + addLog(`Page API seek bridge injection failed: ${err.message}`, 'warn'); } } + await chrome.scripting.executeScript({ + target: { tabId }, + files: ['page-api-seek-overrides.js'] + }); + await chrome.scripting.executeScript({ + target: { tabId }, + func: setPageApiSeekEnabled, + args: [pageApiSeekReady] + }); return chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] @@ -2113,6 +2139,10 @@ async function handleAsyncMessage(message, sender, sendResponse) { } }); } else if (message.type === 'DEV_SIMULATE_REMOTE_SEEK') { + if (!(await devRemoteToolsAllowed())) { + sendResponse({ status: 'forbidden' }); + return; + } const delta = Number(message.delta); if (!Number.isFinite(delta)) { sendResponse({ status: 'invalid_delta' }); diff --git a/extension/content.js b/extension/content.js index 01a491e..ef59983 100644 --- a/extension/content.js +++ b/extension/content.js @@ -57,17 +57,18 @@ let seekDebounceTimer = null; // debounce timer for rapid seek events let expectedSeekTime = null; // strictly track programmatic seeks - const NETFLIX_SEEK_BRIDGE = 1; + const PAGE_API_SEEK_BRIDGE = 1; - function isNetflixHost() { - const host = window.location.hostname.toLowerCase(); - return host === 'netflix.com' || host.endsWith('.netflix.com'); + function shouldUsePageApiSeek() { + return window.KOALA_PAGE_API_SEEK_ENABLED === true && + typeof window.koalaFindPageApiSeekProvider === 'function' && + !!window.koalaFindPageApiSeekProvider(window.location.hostname); } function seekVideo(video, targetTime) { expectedSeekTime = targetTime; - if (isNetflixHost()) { - window.postMessage({ __koalaNetflixSeek: NETFLIX_SEEK_BRIDGE, kind: 'seek', time: targetTime }, '*'); + if (shouldUsePageApiSeek()) { + window.postMessage({ __koalaPageApiSeek: PAGE_API_SEEK_BRIDGE, kind: 'seek', time: targetTime }, '*'); return; } video.currentTime = targetTime; diff --git a/extension/page-api-seek-overrides.js b/extension/page-api-seek-overrides.js new file mode 100644 index 0000000..58532ed --- /dev/null +++ b/extension/page-api-seek-overrides.js @@ -0,0 +1,23 @@ +(function(root) { + const PAGE_API_SEEK_PROVIDERS = [ + { domain: 'netflix.com', provider: 'netflix' } // Avoids M7375 when seeking via video.currentTime. + ]; + + function normalizeHost(input) { + try { + return new URL(input).hostname.toLowerCase(); + } catch (_e) { + return String(input || '').toLowerCase(); + } + } + + function matchesDomain(host, domain) { + return host === domain || host.endsWith(`.${domain}`); + } + + root.KOALA_PAGE_API_SEEK_PROVIDERS = PAGE_API_SEEK_PROVIDERS; + root.koalaFindPageApiSeekProvider = (input) => { + const host = normalizeHost(input); + return PAGE_API_SEEK_PROVIDERS.find(entry => matchesDomain(host, entry.domain)) || null; + }; +})(globalThis); From 0af22998c8890e6d36028255d752f6a14e108d27 Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:02:31 +0200 Subject: [PATCH 04/22] Handle target tab navigation reinjection --- extension/background.js | 1 + extension/modules/tab-manager.js | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/extension/background.js b/extension/background.js index b346e71..0c22ffa 100644 --- a/extension/background.js +++ b/extension/background.js @@ -2565,6 +2565,7 @@ initTabManager({ getSettings, emit, applyAudioSettingsToTab, + injectContentScript, ensureState, EVENTS }); diff --git a/extension/modules/tab-manager.js b/extension/modules/tab-manager.js index 063a7be..8fcbc22 100644 --- a/extension/modules/tab-manager.js +++ b/extension/modules/tab-manager.js @@ -12,6 +12,7 @@ export function initTabManager({ getSettings, emit, applyAudioSettingsToTab, + injectContentScript, ensureState, EVENTS }) { @@ -83,10 +84,7 @@ export function initTabManager({ await ensureState(); const curTabId = getCurrentTabId(); if (curTabId && tabId === parseInt(curTabId) && changeInfo.status === 'complete') { - chrome.scripting.executeScript({ - target: { tabId }, - files: ['content.js'] - }) + injectContentScript(tabId) .then(() => applyAudioSettingsToTab(tabId)) .catch(() => {}); } From 01762b0c2fff1f2702930cc548871751417cdfc8 Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:20:25 +0200 Subject: [PATCH 05/22] Normalize shared tab titles --- docs/ROADMAP.md | 17 ++++++++++++++++- extension/popup.js | 4 ++-- extension/title-privacy.js | 8 +++++++- scripts/test-title-privacy.mjs | 6 ++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d667bcf..d55cba6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -60,6 +60,22 @@ - **Legal/moderation:** Unclear what moderation requirements would apply if users can exchange chat messages. Could be relevant depending on jurisdiction. - **Status:** Under evaluation, may come later. +### Cross-frame video detection and control + +- **Priority:** P3 +- **Category:** Compatibility / Embedded Players +- **Background:** KoalaSync currently injects on demand into the selected tab's top frame. This works for normal top-frame players, including current Emby/Jellyfin usage, but does not cover cases where the real `