chore(release): release v2.0.5

This commit is contained in:
Koala
2026-06-03 11:33:24 +02:00
parent a948780745
commit 595ea297f5
26 changed files with 461 additions and 74 deletions
+96 -8
View File
@@ -56,7 +56,7 @@ function ensureState() {
'logs', 'history', 'currentRoom', 'lastActionState',
'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks',
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle',
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount'
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt'
], (data) => {
clearTimeout(storageTimeout);
if (data.expectedAcksCount !== undefined) expectedAcksCount = data.expectedAcksCount;
@@ -80,6 +80,8 @@ function ensureState() {
if (data.reconnectFailed !== undefined) reconnectFailed = data.reconnectFailed;
if (data.reconnectStartTime) reconnectStartTime = data.reconnectStartTime;
if (data.reconnectAttempts !== undefined) reconnectAttempts = data.reconnectAttempts;
if (data.roomIdleSince !== undefined) roomIdleSince = data.roomIdleSince;
if (data.lastContentHeartbeatAt !== undefined) lastContentHeartbeatAt = data.lastContentHeartbeatAt;
// Recover Force Sync Timeout
if (data.forceSyncDeadline) {
@@ -139,9 +141,12 @@ let reconnectStartTime = null;
let reconnectFailed = false;
let reconnectAttempts = 0;
let currentServerUrl = null;
let roomIdleSince = null;
let lastContentHeartbeatAt = null;
const MAX_RECONNECT_ATTEMPTS = 20;
const _RECONNECT_BASE_DELAY = 500;
const _RECONNECT_MAX_DELAY = 5000;
const ROOM_IDLE_AUTO_LEAVE_MS = 2 * 60 * 60 * 1000;
// Force Sync Coordination
let isForceSyncInitiator = false;
@@ -304,6 +309,8 @@ function forceDisconnect() {
isNamespaceJoined = false;
isForceSyncInitiator = false;
expectedAcksCount = 0;
roomIdleSince = null;
lastContentHeartbeatAt = null;
forceSyncAcks.clear();
eventQueue = [];
chrome.storage.session.set({
@@ -312,7 +319,9 @@ function forceDisconnect() {
forceSyncDeadline: null,
expectedAcksCount: 0,
eventQueue: [],
episodeLobby: null
episodeLobby: null,
roomIdleSince: null,
lastContentHeartbeatAt: null
}).catch(() => {});
if (currentRoom) {
currentRoom.peers = [];
@@ -322,6 +331,63 @@ function forceDisconnect() {
broadcastConnectionStatus('disconnected');
}
function persistRoomIdleState() {
chrome.storage.session.set({ roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
}
function markRoomUseful() {
roomIdleSince = null;
lastContentHeartbeatAt = Date.now();
persistRoomIdleState();
}
function markRoomPotentiallyIdle() {
if (!currentRoom) {
roomIdleSince = null;
lastContentHeartbeatAt = null;
persistRoomIdleState();
return;
}
if (!roomIdleSince) {
roomIdleSince = Date.now();
persistRoomIdleState();
}
}
function clearTargetTabForIdle() {
currentTabId = null;
currentTabTitle = null;
lastContentHeartbeatAt = null;
if (currentRoom) {
roomIdleSince = Date.now();
}
chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
updateBadgeStatus();
}
async function leaveRoomAfterIdleGrace(reason) {
if (!currentRoom) return;
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
currentTabId = null;
currentTabTitle = null;
roomIdleSince = null;
lastContentHeartbeatAt = null;
clearEpisodeLobbyState();
await chrome.storage.session.set({
currentRoom: null,
currentTabId: null,
currentTabTitle: null,
roomIdleSince: null,
lastContentHeartbeatAt: null,
episodeLobby: null
}).catch(() => {});
await chrome.storage.sync.set({ roomId: '', password: '' }).catch(() => {});
addLog(reason, 'info');
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
updateBadgeStatus();
}
async function connect() {
if (isConnecting) return;
isConnecting = true;
@@ -631,6 +697,7 @@ function handleServerEvent(event, data) {
switch (event) {
case EVENTS.ROOM_DATA:
currentRoom = data;
markRoomPotentiallyIdle();
if (currentRoom && Array.isArray(currentRoom.peers)) {
currentRoom.peers = currentRoom.peers.map(p => typeof p === 'object' ? createPeerData(p) : { peerId: p, username: null, tabTitle: null, mediaTitle: null, playbackState: null, currentTime: null, volume: null, muted: null, lastHeartbeat: Date.now() });
@@ -1152,8 +1219,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
}).catch(err => {
if (retries >= 3) {
addLog(`Content Script not responding in tab ${tabId} after ${retries} retries`, 'warn');
currentTabId = null;
updateBadgeStatus();
clearTargetTabForIdle();
return;
}
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
@@ -1167,8 +1233,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
});
} else {
addLog(`Content Script not responding in tab ${tabId}`, 'warn');
currentTabId = null;
updateBadgeStatus();
clearTargetTabForIdle();
}
});
}
@@ -1184,6 +1249,15 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
connect();
}
} else if (currentRoom) {
const now = Date.now();
const heartbeatAge = lastContentHeartbeatAt ? (now - lastContentHeartbeatAt) : Infinity;
if (!currentTabId || heartbeatAge > 45000) {
markRoomPotentiallyIdle();
}
if (roomIdleSince && Date.now() - roomIdleSince >= ROOM_IDLE_AUTO_LEAVE_MS) {
await leaveRoomAfterIdleGrace('Left room after 2 hours without a selected video heartbeat.');
return;
}
// Heartbeat Logic: Always include identity metadata
const settings = await getSettings();
emit(EVENTS.PEER_STATUS, {
@@ -1287,6 +1361,9 @@ async function handleAsyncMessage(message, sender, sendResponse) {
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
currentTabId = null;
currentTabTitle = null;
roomIdleSince = null;
lastContentHeartbeatAt = null;
updateBadgeStatus();
@@ -1300,6 +1377,10 @@ async function handleAsyncMessage(message, sender, sendResponse) {
chrome.storage.session.set({
currentRoom: null,
currentTabId: null,
currentTabTitle: null,
roomIdleSince: null,
lastContentHeartbeatAt: null,
isForceSyncInitiator: false,
forceSyncAcks: [],
forceSyncDeadline: null,
@@ -1492,6 +1573,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
updateBadgeStatus();
}
markRoomUseful();
getSettings().then(settings => {
const statusPayload = { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle };
emit(EVENTS.PEER_STATUS, statusPayload);
@@ -1519,7 +1601,11 @@ async function handleAsyncMessage(message, sender, sendResponse) {
} else if (message.type === 'SET_TARGET_TAB') {
currentTabId = message.tabId;
currentTabTitle = message.tabTitle;
chrome.storage.session.set({ currentTabId, currentTabTitle });
lastContentHeartbeatAt = null;
if (currentRoom) {
roomIdleSince = Date.now();
}
chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
if (currentTabId) {
@@ -1645,7 +1731,9 @@ chrome.tabs.onRemoved.addListener(async (tabId) => {
const wasInRoom = !!currentRoom;
currentTabId = null;
currentTabTitle = null;
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null });
lastContentHeartbeatAt = null;
roomIdleSince = Date.now();
chrome.storage.session.set({ currentTabId: null, currentTabTitle: null, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
addLog('Target tab closed.', 'warn');
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "AKTUALISIEREN",
"BTN_REFRESH_TOOLTIP": "Die Liste der öffentlichen Räume aktualisieren",
"PUBLIC_ROOMS_REFRESHING": "Aktualisiere...",
"BTN_REFRESH_COOLDOWN": "WARTE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Die Raumliste kühlt ab. Versuche es in {seconds}s erneut.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Aktualisiere öffentliche Räume. Nächste Aktualisierung in {seconds}s verfügbar.",
"LABEL_ACTIVE_ROOM": "Aktiver Raum",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Der Raum, mit dem du gerade verbunden bist",
"ACTIVE_ROOM_NONE": "KEINER",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "REFRESH",
"BTN_REFRESH_TOOLTIP": "Refresh the list of public rooms",
"PUBLIC_ROOMS_REFRESHING": "Refreshing...",
"BTN_REFRESH_COOLDOWN": "WAIT {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Room list refresh is cooling down. Try again in {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Refreshing public rooms. Next refresh available in {seconds}s.",
"LABEL_ACTIVE_ROOM": "Active Room",
"LABEL_ACTIVE_ROOM_TOOLTIP": "The room you are currently connected to",
"ACTIVE_ROOM_NONE": "NONE",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ACTUALIZAR",
"BTN_REFRESH_TOOLTIP": "Actualizar la lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Actualizando...",
"BTN_REFRESH_COOLDOWN": "ESPERA {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "La lista de salas está en espera. Inténtalo de nuevo en {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Actualizando salas públicas. Próxima actualización en {seconds}s.",
"LABEL_ACTIVE_ROOM": "Sala activa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "La sala a la que estás conectado actualmente",
"ACTIVE_ROOM_NONE": "NINGUNA",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ACTUALISER",
"BTN_REFRESH_TOOLTIP": "Actualiser la liste des salons publics",
"PUBLIC_ROOMS_REFRESHING": "Actualisation...",
"BTN_REFRESH_COOLDOWN": "ATTENDRE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "La liste des salons est en pause. Réessayez dans {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Actualisation des salons publics. Prochaine actualisation dans {seconds}s.",
"LABEL_ACTIVE_ROOM": "Salon actif",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Le salon auquel vous êtes actuellement connecté",
"ACTIVE_ROOM_NONE": "AUCUN",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ATUALIZAR",
"BTN_REFRESH_TOOLTIP": "Atualizar a lista de salas públicas",
"PUBLIC_ROOMS_REFRESHING": "Atualizando...",
"BTN_REFRESH_COOLDOWN": "AGUARDE {seconds}s",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "A lista de salas está em espera. Tente novamente em {seconds}s.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Atualizando salas públicas. Próxima atualização em {seconds}s.",
"LABEL_ACTIVE_ROOM": "Sala ativa",
"LABEL_ACTIVE_ROOM_TOOLTIP": "A sala à qual você está conectado no momento",
"ACTIVE_ROOM_NONE": "NENHUMA",
+3
View File
@@ -33,6 +33,9 @@
"BTN_REFRESH": "ОБНОВИТЬ",
"BTN_REFRESH_TOOLTIP": "Обновить список публичных комнат",
"PUBLIC_ROOMS_REFRESHING": "Обновление...",
"BTN_REFRESH_COOLDOWN": "ЖДИТЕ {seconds}с",
"BTN_REFRESH_COOLDOWN_TOOLTIP": "Список комнат временно ограничен. Повторите через {seconds}с.",
"PUBLIC_ROOMS_REFRESHING_COOLDOWN": "Обновление публичных комнат. Следующее обновление через {seconds}с.",
"LABEL_ACTIVE_ROOM": "Активная комната",
"LABEL_ACTIVE_ROOM_TOOLTIP": "Комната, к которой вы сейчас подключены",
"ACTIVE_ROOM_NONE": "НЕТ",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "KoalaSync",
"version": "2.0.4",
"version": "2.0.5",
"description": "Synchronize video playback on YouTube, Netflix, Emby, Jellyfin, and any HTML5 site in real-time with friends.",
"permissions": [
"storage",
+32 -6
View File
@@ -69,6 +69,7 @@ let forceSyncDone = false;
let connectionErrorTimer = null;
let pendingConnectionErrorMsg = null;
let roomListRefreshTimer = null;
let roomListRefreshInterval = null;
const ROOM_LIST_REFRESH_COOLDOWN_MS = 11000;
// --- Helpers ---
@@ -80,6 +81,31 @@ function clearConnectionErrorTimer() {
pendingConnectionErrorMsg = null;
}
function setRoomRefreshCooldown() {
if (roomListRefreshTimer) clearTimeout(roomListRefreshTimer);
if (roomListRefreshInterval) clearInterval(roomListRefreshInterval);
const originalLabel = getMessage('BTN_REFRESH');
const updateLabel = () => {
const secondsLeft = Math.max(1, Math.ceil((cooldownEndsAt - Date.now()) / 1000));
elements.refreshRooms.textContent = getMessage('BTN_REFRESH_COOLDOWN', { seconds: secondsLeft });
elements.refreshRooms.title = getMessage('BTN_REFRESH_COOLDOWN_TOOLTIP', { seconds: secondsLeft });
};
const cooldownEndsAt = Date.now() + ROOM_LIST_REFRESH_COOLDOWN_MS;
elements.refreshRooms.disabled = true;
updateLabel();
roomListRefreshInterval = setInterval(updateLabel, 250);
roomListRefreshTimer = setTimeout(() => {
elements.refreshRooms.disabled = false;
elements.refreshRooms.textContent = originalLabel;
elements.refreshRooms.title = getMessage('BTN_REFRESH_TOOLTIP');
clearInterval(roomListRefreshInterval);
roomListRefreshInterval = null;
roomListRefreshTimer = null;
}, ROOM_LIST_REFRESH_COOLDOWN_MS);
}
// --- Initialization ---
async function init() {
// Load Settings
@@ -1099,16 +1125,12 @@ if (syncTabCreateRoomBtn) syncTabCreateRoomBtn.addEventListener('click', () => {
elements.refreshRooms.addEventListener('click', () => {
if (elements.refreshRooms.disabled) return;
elements.refreshRooms.disabled = true;
roomListRefreshTimer = setTimeout(() => {
elements.refreshRooms.disabled = false;
roomListRefreshTimer = null;
}, ROOM_LIST_REFRESH_COOLDOWN_MS);
setRoomRefreshCooldown();
elements.publicRooms.replaceChildren();
const el = document.createElement('div');
el.style.cssText = 'text-align:center; color: var(--text-muted); font-size: 11px; padding: 10px;';
el.textContent = getMessage('PUBLIC_ROOMS_REFRESHING');
el.textContent = getMessage('PUBLIC_ROOMS_REFRESHING_COOLDOWN', { seconds: Math.ceil(ROOM_LIST_REFRESH_COOLDOWN_MS / 1000) });
elements.publicRooms.appendChild(el);
chrome.runtime.sendMessage({ type: 'GET_ROOM_LIST' });
});
@@ -1832,6 +1854,10 @@ window.addEventListener('unload', () => {
clearTimeout(roomListRefreshTimer);
roomListRefreshTimer = null;
}
if (roomListRefreshInterval) {
clearInterval(roomListRefreshInterval);
roomListRefreshInterval = null;
}
});
// --- Episode Lobby UI ---