feat: initial commit — RangerHQ Tuner v0.1.0 (Tier 1 MVP)

Chrome MV3 extension, browser-resident sibling to rangerhq-radio
(WP plugin). Plays SomaFM via the chrome.offscreen API + a source-
adapter pattern at src/sources/.

Architecture highlights:
- Audio runs in offscreen document — SW would get killed.
- Source-adapter pattern locks Tier 1 contract (RadioSource interface
  in src/sources/base-source.js). Adding a network = drop a file +
  register one line in src/sources/index.js.
- Vanilla JS, no build step. Pure ES modules.
- No telemetry, no third-party JS. Outbound only to somafm.com.
- Narrow permissions: offscreen + storage + somafm.com host_perms.
  No tabs, no <all_urls>, no webRequest.

22 files, ~30 min build following the saved plan at
~/.ranger-memory/projects/rangerhq-tuner-plan.md.

Tier 2 + Tier 3 (Web Store submission) not started.
This commit is contained in:
2026-06-08 23:31:29 +01:00
commit 38b6b8d3f7
20 changed files with 1001 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
// RangerHQ Tuner — Service Worker.
// Job: open the offscreen audio document on demand and route messages
// between the popup (UI) and the offscreen document (audio engine).
// We hold no state here — Chrome will kill this worker at any time.
const OFFSCREEN_PATH = 'src/offscreen/offscreen.html';
async function ensureOffscreen() {
// hasDocument is the official MV3 way to check. Optional-chained
// for older Chrome safety (we still require 116+ via the manifest).
const exists = await chrome.offscreen.hasDocument?.();
if (exists) return;
await chrome.offscreen.createDocument({
url: OFFSCREEN_PATH,
reasons: ['AUDIO_PLAYBACK'],
justification: 'Persistent audio playback for internet radio streams.',
});
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Only react to messages targeted at the service worker.
if (msg.target !== 'sw') return;
(async () => {
try {
await ensureOffscreen();
// Re-route the message to the offscreen document.
const response = await chrome.runtime.sendMessage({
...msg,
target: 'offscreen',
});
sendResponse(response);
} catch (err) {
sendResponse({ ok: false, error: err.message });
}
})();
return true; // keep the channel open for async sendResponse
});