The player now takes any RSS feed URL as well as a podsteadr server, and plays podsteadr feeds hosted on the FIPS mesh. A browser can't speak FIPS, and an HTTPS page can't fetch or play http:// URLs, so .fips URLs are routed one of two ways (src/lib/net.ts): - served over the mesh (http://<npub>.fips:8097/), the page fetches .fips URLs directly over the device's own FIPS connection; - served over HTTPS (player.atobitcoin.io), .fips URLs are rewritten to the host's /fips/<host>/<port>/<path> gateway, which relays only podsteadr feeds (8095) and blossom blobs (8098) over the host's FIPS link (deploy/player-domain.conf). Also: - "Add" accepts a feed, an OPML catalog, or a server address and works out which; old saved servers migrate to the new subscription format - PWA: manifest, icons, service worker caching only the app shell (never feeds, audio or the gateway), install button, safe areas - lock-screen controls and artwork via the Media Session API - artwork, dates and durations in the episode list - no secure-context-only APIs on the mesh copy (randomUUID, clipboard) - buying episodes over FIPS is refused with a clear message: NIP-98 signs the server's public URL, which a .fips address never matches Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
1.9 KiB
JavaScript
62 lines
1.9 KiB
JavaScript
// Offline app shell for the installed PWA. Only the player's own files are cached:
|
|
// feeds must stay fresh, and audio (range requests, often hundreds of MB) and the
|
|
// /fips/ gateway always go straight to the network.
|
|
const CACHE = 'podsteadr-player-v1';
|
|
const SHELL = ['./', 'manifest.webmanifest', 'icons/icon.svg', 'icons/icon-192.png', 'icons/icon-512.png'];
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)).then(() => self.skipWaiting()));
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
const scope = new URL(self.registration.scope);
|
|
if (
|
|
request.method !== 'GET' ||
|
|
url.origin !== scope.origin ||
|
|
url.pathname.startsWith(`${scope.pathname}fips/`) ||
|
|
request.headers.has('range')
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Page loads: network first so a deploy shows up immediately; cached shell when offline.
|
|
if (request.mode === 'navigate') {
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((res) => {
|
|
const copy = res.clone();
|
|
caches.open(CACHE).then((cache) => cache.put('./', copy));
|
|
return res;
|
|
})
|
|
.catch(() => caches.match('./')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Hashed build assets and icons never change under the same URL: cache first.
|
|
event.respondWith(
|
|
caches.match(request).then(
|
|
(hit) =>
|
|
hit ||
|
|
fetch(request).then((res) => {
|
|
if (res.ok) {
|
|
const copy = res.clone();
|
|
caches.open(CACHE).then((cache) => cache.put(request, copy));
|
|
}
|
|
return res;
|
|
}),
|
|
),
|
|
);
|
|
});
|