From c1bd74c2b99d0da0a11b9bfdb6b986883696d413 Mon Sep 17 00:00:00 2001 From: Ruslan Tryasko <681362+tryasko@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:48:42 +0100 Subject: [PATCH 1/2] Bump extension to support manifest v3 --- background/index.html | 12 ----- background/offscreen.html | 10 ++++ background/offscreen.js | 22 ++++++++ background/player.js | 28 ---------- background/service-worker.js | 89 +++++++++++++++++++++++++++++++ background/updater.js | 22 -------- manifest.json | 27 +++++----- popup/application.js | 100 ++++++++++++++++++++++------------- shared/stations.js | 9 +++- 9 files changed, 205 insertions(+), 114 deletions(-) delete mode 100644 background/index.html create mode 100644 background/offscreen.html create mode 100644 background/offscreen.js delete mode 100644 background/player.js create mode 100644 background/service-worker.js delete mode 100644 background/updater.js diff --git a/background/index.html b/background/index.html deleted file mode 100644 index f724de8..0000000 --- a/background/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - RadioWave player - - - - - - - diff --git a/background/offscreen.html b/background/offscreen.html new file mode 100644 index 0000000..fcd4c58 --- /dev/null +++ b/background/offscreen.html @@ -0,0 +1,10 @@ + + + + + RadioWave Audio Player + + + + + diff --git a/background/offscreen.js b/background/offscreen.js new file mode 100644 index 0000000..f198e36 --- /dev/null +++ b/background/offscreen.js @@ -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)); + break; + + case 'AUDIO_STOP': + audio.pause(); + audio.src = ''; + break; + + case 'AUDIO_VOLUME': + audio.volume = message.volume; + break; + } +}); diff --git a/background/player.js b/background/player.js deleted file mode 100644 index a049e21..0000000 --- a/background/player.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; - -const STREAM_API_URL = "https://europe-southwest1-radio--wave.cloudfunctions.net/getstream-v2"; -const VERSION = `version=${localStorage.version}`; -const CLIENT = "client=chrome-extension"; - -window.backgroundPlayer = new class BackgroundPlayer { - audio = new Audio(); - - play() { - this.volume(); - this.audio.src = `${STREAM_API_URL}?station=${localStorage.station}&${CLIENT}&${VERSION}`; - this.audio.play(); - - localStorage.setItem("state", "played"); - } - - stop() { - this.audio.pause(); - this.audio.src = ""; - - localStorage.setItem("state", "paused"); - } - - volume() { - this.audio.volume = localStorage.volume / 100; - } -}(); diff --git a/background/service-worker.js b/background/service-worker.js new file mode 100644 index 0000000..15efdde --- /dev/null +++ b/background/service-worker.js @@ -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) { + 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 + }); + + 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 }); + 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; + } + })(); + + return true; // Keep message channel open for async response +}); diff --git a/background/updater.js b/background/updater.js deleted file mode 100644 index 4caf804..0000000 --- a/background/updater.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; - -const currentVersion = "2.2.9"; - -(() => { - const { volume, station, version } = localStorage; - - if (!version || version !== currentVersion) { - const isCurrentStationExist = window.stationList.some(item => station === `${item.group}.${item.station}`); - - const state = { - version: currentVersion, - volume: volume || 30, - state: "paused", - station: isCurrentStationExist ? station : "TVR.KissFM" - }; - - localStorage.clear(); - - Object.keys(state).forEach(key => localStorage.setItem(key, state[key])); - } -})(); diff --git a/manifest.json b/manifest.json index 3962578..2a165a8 100644 --- a/manifest.json +++ b/manifest.json @@ -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/*"] } diff --git a/popup/application.js b/popup/application.js index 8177b38..2cea9ed 100644 --- a/popup/application.js +++ b/popup/application.js @@ -2,44 +2,79 @@ 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() { + const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); + currentState = state; + + controlPlay.setAttribute("class", currentState.state); + controlVolume.value = currentState.volume; + + playList.innerHTML = window.stationList + .map(({ name, group, station }) => { + const stationId = getStationId(group, station); + + return `
  • + ${group} + ${name} +
  • `; + }) + .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); }); // 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) + }); }); -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) + }); }); // 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", ""); } @@ -47,28 +82,17 @@ playList.addEventListener("click", event => { 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"; - 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 `
  • - ${group} - ${name} -
  • `; - }) - .join(""); + await chrome.runtime.sendMessage({ type: 'PLAY' }); +}); - if (document.querySelector(".selected")) { - document.querySelector(".selected").scrollIntoView(); - } -})(); +// Initialize on load +initializePopup(); diff --git a/shared/stations.js b/shared/stations.js index 08c54a7..9e5f2ba 100644 --- a/shared/stations.js +++ b/shared/stations.js @@ -1,4 +1,4 @@ -window.stationList = [ +const stationList = [ { name: "MFM", group: "UA", @@ -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; +} From 043165f746c72155aa07b6514d5827e606a2e14f Mon Sep 17 00:00:00 2001 From: Ruslan Tryasko <681362+tryasko@users.noreply.github.com> Date: Fri, 28 Nov 2025 20:50:26 +0100 Subject: [PATCH 2/2] Remove redundant local variable --- popup/application.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/popup/application.js b/popup/application.js index 2cea9ed..e438c46 100644 --- a/popup/application.js +++ b/popup/application.js @@ -10,8 +10,7 @@ let currentState = { state: "paused", volume: 30, station: "TVR.KissFM" }; // Initialize popup with current state async function initializePopup() { - const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); - currentState = state; + currentState = await chrome.runtime.sendMessage({ type: 'GET_STATE' }); controlPlay.setAttribute("class", currentState.state); controlVolume.value = currentState.volume;