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 = '';