Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions background/index.html

This file was deleted.

10 changes: 10 additions & 0 deletions background/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>RadioWave Audio Player</title>
</head>
<body>
<script src="offscreen.js"></script>
</body>
</html>
22 changes: 22 additions & 0 deletions background/offscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use strict";

const audio = new Audio();

chrome.runtime.onMessage.addListener((message) => {
switch (message.type) {
case 'AUDIO_PLAY':
audio.volume = message.volume || 0.3;
audio.src = message.url;
audio.play().catch(err => console.error('Audio play error:', err));
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete error handling: audio.play() returns a Promise that can be rejected, but the error is only logged to console. Consider notifying the service worker of playback failures so it can update the state to "paused" and inform the user, rather than leaving the UI in an inconsistent "played" state when audio actually failed.

Suggested change
audio.play().catch(err => console.error('Audio play error:', err));
audio.play().catch(err => {
console.error('Audio play error:', err);
chrome.runtime.sendMessage({
type: 'AUDIO_PLAY_FAILED',
error: err && err.message ? err.message : String(err),
url: message.url
});
});

Copilot uses AI. Check for mistakes.
break;

case 'AUDIO_STOP':
audio.pause();
audio.src = '';
break;

case 'AUDIO_VOLUME':
audio.volume = message.volume;
break;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Missing default case: The switch statement doesn't handle unknown message types. Consider adding a default case to log unexpected messages for debugging purposes.

Suggested change
break;
break;
default:
console.warn('Unknown message type received:', message.type, message);

Copilot uses AI. Check for mistakes.
}
});
28 changes: 0 additions & 28 deletions background/player.js

This file was deleted.

89 changes: 89 additions & 0 deletions background/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use strict";

// Import stations list
importScripts('../shared/stations.js');

const STREAM_API_URL = "https://europe-southwest1-radio--wave.cloudfunctions.net/getstream-v2";
const CLIENT = "client=chrome-extension";
const currentVersion = "3.0.0";

// Initialize storage on install
chrome.runtime.onInstalled.addListener(async () => {
const data = await chrome.storage.local.get(['version', 'volume', 'station', 'state']);

if (!data.version || data.version !== currentVersion) {
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Logic issue: The condition checks if data.version exists before comparing it to currentVersion, but if this is a fresh install, data.version will be undefined and the check will proceed to verify if a station exists using data.station, which will also be undefined. The some() comparison will always fail for undefined, but it would be clearer to explicitly handle the fresh install case separately.

Suggested change
if (!data.version || data.version !== currentVersion) {
if (typeof data.version === "undefined") {
// Fresh install: set default values
await chrome.storage.local.set({
version: currentVersion,
volume: 30,
state: "paused",
station: "TVR.KissFM"
});
} else if (data.version !== currentVersion) {
// Upgrade: preserve station if it exists in the new station list

Copilot uses AI. Check for mistakes.
const isCurrentStationExist = self.stationList && self.stationList.some(
item => data.station === `${item.group}.${item.station}`
);

await chrome.storage.local.set({
version: currentVersion,
volume: data.volume || 30,
state: "paused",
station: isCurrentStationExist ? data.station : "TVR.KissFM"
});
}
});

// Create offscreen document for audio playback
async function setupOffscreenDocument() {
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
});

if (existingContexts.length > 0) {
return;
}

await chrome.offscreen.createDocument({
url: 'background/offscreen.html',
reasons: ['AUDIO_PLAYBACK'],
justification: 'Play radio audio stream',
});
}

// Handle messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
switch (message.type) {
case 'PLAY':
await setupOffscreenDocument();
const playData = await chrome.storage.local.get(['station', 'volume', 'version']);
const playUrl = `${STREAM_API_URL}?station=${playData.station}&${CLIENT}&version=${playData.version}`;

chrome.runtime.sendMessage({
type: 'AUDIO_PLAY',
url: playUrl,
volume: playData.volume / 100
});
Comment on lines +54 to +58
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition: chrome.runtime.sendMessage is called without ensuring the offscreen document is fully ready to receive messages. The offscreen document creation is async, but there's no guarantee it has loaded and registered its message listener before this message is sent. Consider adding a small delay or a handshake mechanism to ensure the offscreen document is ready.

Copilot uses AI. Check for mistakes.

await chrome.storage.local.set({ state: 'played' });
sendResponse({ success: true });
break;

case 'STOP':
chrome.runtime.sendMessage({ type: 'AUDIO_STOP' });
await chrome.storage.local.set({ state: 'paused' });
sendResponse({ success: true });
break;

case 'SET_VOLUME':
chrome.runtime.sendMessage({ type: 'AUDIO_VOLUME', volume: message.volume / 100 });
Comment on lines +65 to +71
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: chrome.runtime.sendMessage can fail if the offscreen document doesn't exist or isn't ready. These calls should include error handling (e.g., .catch()) to gracefully handle message delivery failures, especially since the STOP and SET_VOLUME cases don't call setupOffscreenDocument() first.

Suggested change
chrome.runtime.sendMessage({ type: 'AUDIO_STOP' });
await chrome.storage.local.set({ state: 'paused' });
sendResponse({ success: true });
break;
case 'SET_VOLUME':
chrome.runtime.sendMessage({ type: 'AUDIO_VOLUME', volume: message.volume / 100 });
await chrome.runtime.sendMessage({ type: 'AUDIO_STOP' }).catch((err) => {
console.error('Failed to send AUDIO_STOP message:', err);
});
await chrome.storage.local.set({ state: 'paused' });
sendResponse({ success: true });
break;
case 'SET_VOLUME':
await chrome.runtime.sendMessage({ type: 'AUDIO_VOLUME', volume: message.volume / 100 }).catch((err) => {
console.error('Failed to send AUDIO_VOLUME message:', err);
});

Copilot uses AI. Check for mistakes.
await chrome.storage.local.set({ volume: message.volume });
sendResponse({ success: true });
break;

case 'SET_STATION':
await chrome.storage.local.set({ station: message.station });
sendResponse({ success: true });
break;

case 'GET_STATE':
const state = await chrome.storage.local.get(['state', 'volume', 'station']);
sendResponse(state);
break;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Missing default case: The switch statement doesn't handle unknown message types. Add a default case to log or handle unexpected message types, which can help with debugging and prevent silent failures.

Suggested change
break;
break;
default:
console.warn("Unknown message type received:", message.type, message);
sendResponse({ success: false, error: "Unknown message type" });
break;

Copilot uses AI. Check for mistakes.
}
})();

return true; // Keep message channel open for async response
});
22 changes: 0 additions & 22 deletions background/updater.js

This file was deleted.

27 changes: 14 additions & 13 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
{
"background": {
"page": "background/index.html"
},
"browser_action": {
"default_popup": "popup/index.html"
},
"manifest_version": 3,
"name": "Radio Wave",
"version": "3.0.0",
"description": "A simple application for listening to online radio.",
"icons": {
"128": "icons/128.png",
"16": "icons/16.png",
"24": "icons/24.png",
"256": "icons/256.png",
"32": "icons/32.png",
"64": "icons/64.png"
"64": "icons/64.png",
"128": "icons/128.png",
"256": "icons/256.png"
},
"permissions": ["https://europe-southwest1-radio--wave.cloudfunctions.net/*"],
"manifest_version": 2,
"name": "Radio Wave",
"version": "2.2.9"
"action": {
"default_popup": "popup/index.html"
},
"background": {
"service_worker": "background/service-worker.js"
},
"permissions": ["storage", "offscreen"],
"host_permissions": ["https://europe-southwest1-radio--wave.cloudfunctions.net/*"]
}
99 changes: 61 additions & 38 deletions popup/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,96 @@

const getStationId = (group, station) => `${group}.${station}`;

const backgroundPlayer = chrome.extension.getBackgroundPage().backgroundPlayer;

const controlPlay = document.getElementById("cnt_play");
const controlVolume = document.getElementById("cnt_volume");
const playList = document.getElementById("play_list");

let currentState = { state: "paused", volume: 30, station: "TVR.KissFM" };

// Initialize popup with current state
async function initializePopup() {
currentState = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: chrome.runtime.sendMessage can fail if the service worker is not running or if there are other messaging issues. Add .catch() error handling or wrap in try-catch to handle potential failures gracefully and provide user feedback.

Suggested change
currentState = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
try {
currentState = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
} catch (error) {
console.error("Failed to get state from background:", error);
// Optionally, show user feedback in the UI
// For example, display an error message or set a default state
currentState = { state: "paused", volume: 30, station: "TVR.KissFM" };
// Optionally, display an error message in the popup
if (playList) {
playList.innerHTML = '<li class="error">Failed to load state. Please reload the extension.</li>';
}
}

Copilot uses AI. Check for mistakes.

controlPlay.setAttribute("class", currentState.state);
controlVolume.value = currentState.volume;

playList.innerHTML = window.stationList
.map(({ name, group, station }) => {
const stationId = getStationId(group, station);

return `<li class="${currentState.station === stationId ? "selected" : ""}" data-id="${stationId}">
<span class="group">${group}</span>
<span class="name">${name}</span>
</li>`;
})
.join("");

if (document.querySelector(".selected")) {
document.querySelector(".selected").scrollIntoView();
}
}

// Play/Pause control
controlPlay.addEventListener("click", () => {
if (localStorage.state === "paused") {
backgroundPlayer.play();
controlPlay.addEventListener("click", async () => {
if (currentState.state === "paused") {
await chrome.runtime.sendMessage({ type: 'PLAY' });
currentState.state = "played";
} else {
backgroundPlayer.stop();
await chrome.runtime.sendMessage({ type: 'STOP' });
currentState.state = "paused";
}

controlPlay.setAttribute("class", localStorage.state);
controlPlay.setAttribute("class", currentState.state);
Comment on lines +35 to +44
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State synchronization issue: The popup updates currentState.state to "played" or "paused" immediately after sending messages, but if the service worker operation fails, the local state will be out of sync with the actual state. The response from sendMessage should be checked before updating local state, or the state should be re-fetched after the operation.

Copilot uses AI. Check for mistakes.
});

// Volume control
controlVolume.addEventListener("input", event => {
localStorage.volume = event.target.value;

backgroundPlayer.volume();
controlVolume.addEventListener("input", async (event) => {
const volume = event.target.value;
currentState.volume = volume;

await chrome.runtime.sendMessage({
type: 'SET_VOLUME',
volume: parseInt(volume)
});
Comment on lines +52 to +55
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: If the SET_VOLUME message fails, the volume slider and state will be inconsistent. Add error handling to revert the UI on failure.

Copilot uses AI. Check for mistakes.
});

controlVolume.addEventListener("mousewheel", e => {
const value = +localStorage.volume + e.wheelDelta / 24;
controlVolume.addEventListener("mousewheel", async (e) => {
const value = +currentState.volume + e.wheelDelta / 24;
const volume = value < 0 ? 0 : value > 100 ? 100 : value;

controlVolume.value = volume;
localStorage.volume = volume;
currentState.volume = volume;

backgroundPlayer.volume();
await chrome.runtime.sendMessage({
type: 'SET_VOLUME',
volume: parseInt(volume)
});
Comment on lines +65 to +68
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: If the SET_VOLUME message fails, the volume slider will show an incorrect value that doesn't match the actual audio volume. Add error handling to revert the UI state on failure.

Copilot uses AI. Check for mistakes.
});

// List control
playList.addEventListener("click", event => {
playList.addEventListener("click", async (event) => {
const element = event.target.closest("li");

if (!element) return;

if (document.querySelector(".selected")) {
document.querySelector(".selected").setAttribute("class", "");
}

element.setAttribute("class", "selected");
controlPlay.setAttribute("class", "played");

localStorage.station = element.getAttribute("data-id");
const stationId = element.getAttribute("data-id");
currentState.station = stationId;
currentState.state = "played";
Comment on lines +84 to +86
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State synchronization issue: Setting currentState.station and currentState.state before the message operations complete (lines 88-93) can lead to inconsistent state if the operations fail. The local state should only be updated after verifying the operations succeeded.

Copilot uses AI. Check for mistakes.

backgroundPlayer.play();
});
await chrome.runtime.sendMessage({
type: 'SET_STATION',
station: stationId
});

// Render station list
(() => {
controlPlay.setAttribute("class", localStorage.state);
controlVolume.value = localStorage.volume;

playList.innerHTML = window.stationList
.map(({ name, group, station }) => {
const stationId = getStationId(group, station);

return `<li class="${localStorage.station === stationId ? "selected" : ""}" data-id="${stationId}">
<span class="group">${group}</span>
<span class="name">${name}</span>
</li>`;
})
.join("");
await chrome.runtime.sendMessage({ type: 'PLAY' });
Comment on lines +88 to +93
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The message sending operations (lines 88-93) should include error handling. If either SET_STATION or PLAY fails, the UI will be in an inconsistent state showing the station as selected and playing when it may not be.

Copilot uses AI. Check for mistakes.
});

if (document.querySelector(".selected")) {
document.querySelector(".selected").scrollIntoView();
}
})();
// Initialize on load
initializePopup();
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: If initializePopup() fails (e.g., service worker not responding), the popup will not initialize properly and will remain in an unusable state. Wrap the call in a try-catch or add .catch() to provide fallback behavior or user feedback.

Suggested change
initializePopup();
initializePopup().catch((err) => {
// Provide fallback behavior or user feedback
// For example, display an error message in the popup
playList.innerHTML = '<li class="error">Failed to initialize popup. Please try again later.</li>';
console.error("Failed to initialize popup:", err);
});

Copilot uses AI. Check for mistakes.
9 changes: 8 additions & 1 deletion shared/stations.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
window.stationList = [
const stationList = [
{
name: "MFM",
group: "UA",
Expand Down Expand Up @@ -59,3 +59,10 @@ window.stationList = [
station: "RadioBayraktar"
}
];

// Support both window context (popup/offscreen) and service worker (self)
if (typeof window !== 'undefined') {
window.stationList = stationList;
} else if (typeof self !== 'undefined') {
self.stationList = stationList;
}