fix(extension): harden target frame recovery and switching

This commit is contained in:
Timo
2026-08-17 23:06:46 +02:00
parent 3c99efe4e4
commit e1daeecab1
25 changed files with 537 additions and 179 deletions
+37 -24
View File
@@ -468,12 +468,12 @@ async function init() {
// Keep a denied selection visible while Chrome waits for the user
// to grant access; it becomes active automatically after approval.
await populateTabs(res.peers, getSelectedTargetTabId(res));
await populateTabs(res.peers, res.targetTabId);
// Render lobby status if active
if (res.episodeLobby) updateLobbyUI(res.episodeLobby, res.peers);
if (res.status === 'connected' && !getSelectedTargetTabId(res) && localData.roomId) {
if (res.status === 'connected' && normalizeTabId(res.targetTabId) === null && localData.roomId) {
const syncTabBtn = document.querySelector('.tab-btn[data-tab="tab-sync"]');
if (syncTabBtn) syncTabBtn.click();
showSelectVideoHint();
@@ -647,15 +647,6 @@ function handleTargetTabResponse(response) {
return false;
}
function getSelectedTargetTabId(status) {
return status?.selectedTargetTabId
?? status?.requestedTargetTabId
?? status?.targetTabId
?? status?.pendingTargetTabId
?? status?.activatingTargetTabId
?? null;
}
function selectTargetTab(tabId, tabTitle) {
return new Promise(resolve => {
chrome.runtime.sendMessage({ type: 'SET_TARGET_TAB', tabId, tabTitle }, response => {
@@ -691,7 +682,7 @@ async function refreshTargetAccessState() {
}
await populateTabs(
status.peers,
getSelectedTargetTabId(status)
status.targetTabId
);
}
@@ -1231,14 +1222,14 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
const blacklistDomains = getEffectiveBlacklistDomains(await readBlacklistOverrides());
const isFilterActive = data.filterNoise !== false;
let currentTargetTabId = providedTargetTabId;
let currentTargetTabId = normalizeTabId(providedTargetTabId);
if (currentTargetTabId === null) {
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
if (chrome.runtime.lastError) {
if (populateTabsToken !== token) return;
currentTargetTabId = null;
} else {
currentTargetTabId = getSelectedTargetTabId(status);
currentTargetTabId = status?.targetTabId ?? null;
}
}
@@ -1269,7 +1260,7 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
const filteredTabs = tabs.filter(tab => {
if (!tab.url || tab.url.startsWith('chrome://')) return false;
if (isFilterActive && tab.id !== parseInt(currentTargetTabId)) {
if (isFilterActive && currentTargetTabId !== null && tab.id !== currentTargetTabId) {
if (isUrlBlacklisted(tab.url, blacklistDomains)) return false;
}
return true;
@@ -1320,7 +1311,7 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
// Sort: 1. Current tab first, 2. Matches, 3. Rest alphabetically
const options = Array.from(elements.targetTab.options);
const placeholder = options.shift();
const currentTabId = currentTargetTabId ? parseInt(currentTargetTabId) : null;
const currentTabId = currentTargetTabId;
options.sort((a, b) => {
const aId = parseInt(a.value);
@@ -1340,7 +1331,7 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
elements.targetTab.appendChild(placeholder);
options.forEach(opt => elements.targetTab.appendChild(opt));
if (currentTargetTabId) {
if (currentTargetTabId !== null) {
elements.targetTab.value = currentTargetTabId;
} else {
const matchOpt = options.find(o => o.textContent.includes('⭐ MATCH:'));
@@ -1754,7 +1745,7 @@ if (elements.langSelector) {
} else {
hideSiteAccessNotice();
}
await populateTabs(res.peers, getSelectedTargetTabId(res));
await populateTabs(res.peers, res.targetTabId);
if (res.episodeLobby) updateLobbyUI(res.episodeLobby, res.peers);
} else {
applyConnectionStatus('disconnected');
@@ -2103,7 +2094,7 @@ elements.forceSyncBtn.addEventListener('click', async () => {
elements.forceSyncBtn.disabled = true;
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
if (chrome.runtime.lastError || !status || !status.targetTabId) {
if (chrome.runtime.lastError || !status || status.targetReady !== true || normalizeTabId(status.targetTabId) === null) {
elements.forceSyncBtn.disabled = false;
return;
}
@@ -2169,7 +2160,8 @@ elements.forceSyncBtn.addEventListener('click', async () => {
resolve(false);
return;
}
resolve(normalizeTabId(currentStatus?.targetTabId) === tabId);
resolve(currentStatus?.targetReady === true
&& normalizeTabId(currentStatus?.targetTabId) === tabId);
});
});
@@ -2588,8 +2580,9 @@ elements.copyLogs.addEventListener('click', () => {
logs = logs || [];
history = history || [];
const videoPromise = (status && status.targetTabId)
? new Promise(resolve => chrome.runtime.sendMessage({ type: 'GET_VIDEO_STATE', tabId: status.targetTabId }, resolve))
const targetTabId = normalizeTabId(status.targetTabId);
const videoPromise = (targetTabId !== null && status.targetReady === true)
? new Promise(resolve => chrome.runtime.sendMessage({ type: 'GET_VIDEO_STATE', tabId: targetTabId }, resolve))
: Promise.resolve(null);
videoPromise.then(rawVideo => {
@@ -2609,6 +2602,11 @@ elements.copyLogs.addEventListener('click', () => {
lines.push(`- **User Agent:** ${userAgent}`);
lines.push('');
lines.push('## Target');
lines.push(`- **Target Tab ID:** ${targetTabId ?? 'none'}`);
lines.push(`- **Activation:** ${safe(status.targetActivationState, 'unknown')}`);
lines.push('');
// ── Tab ──
if (rawVideo) {
lines.push('## Tab');
@@ -2661,7 +2659,9 @@ elements.copyLogs.addEventListener('click', () => {
// ── Video ──
lines.push('## Video');
if (!rawVideo) {
lines.push('- *No tab selected / communication failed*');
lines.push(targetTabId !== null
? '- *Target tab selected; video communication is not ready yet*'
: '- *No tab selected / communication failed*');
} else if (!vs.found) {
lines.push('- **Found:** \u274C NO VIDEO ELEMENT');
if (vs.videoCount != null) lines.push(`- **Video Tags:** ${vs.videoCount}`);
@@ -2801,11 +2801,24 @@ function refreshDebugInfo() {
if (!devTab || devTab.style.display === 'none') return;
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
if (!res || !res.targetTabId) {
if (!res || normalizeTabId(res.targetTabId) === null) {
if (res?.targetActivationState === 'activating' || res?.targetActivationState === 'access_required') {
if (elements.videoDebug) {
elements.videoDebug.textContent = getMessage('DEBUG_TARGET_ACTIVATING');
}
return;
}
if (elements.videoDebug) elements.videoDebug.textContent = getMessage('DEBUG_NO_TAB');
return;
}
if (res.targetReady !== true) {
if (elements.videoDebug) {
elements.videoDebug.textContent = getMessage('DEBUG_TARGET_ACTIVATING');
}
return;
}
// Request direct state from the content script via background
chrome.runtime.sendMessage({ type: 'GET_VIDEO_STATE', tabId: res.targetTabId }, (state) => {
if (!state || (!state.found && state.error)) {