feat: implement PWA installability for desktop and mobile (#247)

* feat: implement PWA installability for desktop and mobile

- Add @vite-pwa/sveltekit plugin with service worker generation
- Create manifest.webmanifest with required PWA fields
- Generate PWA icons (192x192, 512x512, maskable) from logo
- Configure workbox with network-first caching for API requests
- Add manifest link and theme-color meta tag to layout
- Enable auto-update service worker registration

Closes #165

* fix: use import.meta.env for PUBLIC_API_URL to fix type check

Replace /static/public import with import.meta.env to handle
optional environment variable without TypeScript errors.

* feat: replace placeholder icons with vinyl record logo

- replaced PWA icons with vinyl record design
- updated main logo.png
- added favicon.ico
- uses app colors (#0a0a0a bg, #3a7dff accent)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 16d5a534 27c54536

frontend/bun.lockb

This is a binary file and will not be displayed.

+1
frontend/package.json
··· 20 20 "@sveltejs/vite-plugin-svelte": "^6.2.0", 21 21 "@typescript-eslint/eslint-plugin": "^8.46.4", 22 22 "@typescript-eslint/parser": "^8.46.4", 23 + "@vite-pwa/sveltekit": "^1.0.1", 23 24 "eslint": "^9.39.1", 24 25 "eslint-plugin-svelte": "^3.13.0", 25 26 "svelte": "^5.39.5",
frontend/src/lib/assets/logo.png

This is a binary file and will not be displayed.

+1 -3
frontend/src/lib/config.ts
··· 1 - import { PUBLIC_API_URL } from '$env/static/public'; 2 - 3 - export const API_URL = PUBLIC_API_URL || 'http://localhost:8001'; 1 + export const API_URL = import.meta.env.PUBLIC_API_URL || 'http://localhost:8001'; 4 2 5 3 interface ServerConfig { 6 4 max_upload_size_mb: number;
+2
frontend/src/routes/+layout.svelte
··· 92 92 93 93 <svelte:head> 94 94 <link rel="icon" href={logo} /> 95 + <link rel="manifest" href="/manifest.webmanifest" /> 96 + <meta name="theme-color" content="#0a0a0a" /> 95 97 96 98 {#if !hasPageMetadata} 97 99 <!-- default meta tags for pages without specific metadata -->
frontend/static/favicon.ico

This is a binary file and will not be displayed.

frontend/static/icons/icon-192.png

This is a binary file and will not be displayed.

frontend/static/icons/icon-512.png

This is a binary file and will not be displayed.

frontend/static/icons/icon-maskable.png

This is a binary file and will not be displayed.

+30
frontend/static/manifest.webmanifest
··· 1 + { 2 + "name": "plyr.fm", 3 + "short_name": "Plyr", 4 + "start_url": "/", 5 + "display": "standalone", 6 + "background_color": "#0a0a0a", 7 + "theme_color": "#0a0a0a", 8 + "scope": "/", 9 + "icons": [ 10 + { 11 + "src": "/icons/icon-192.png", 12 + "sizes": "192x192", 13 + "type": "image/png", 14 + "purpose": "any" 15 + }, 16 + { 17 + "src": "/icons/icon-512.png", 18 + "sizes": "512x512", 19 + "type": "image/png", 20 + "purpose": "any" 21 + }, 22 + { 23 + "src": "/icons/icon-maskable.png", 24 + "sizes": "512x512", 25 + "type": "image/png", 26 + "purpose": "maskable" 27 + } 28 + ] 29 + } 30 +
+25 -1
frontend/vite.config.ts
··· 1 1 import { sveltekit } from '@sveltejs/kit/vite'; 2 2 import { defineConfig } from 'vite'; 3 + import { SvelteKitPWA } from '@vite-pwa/sveltekit'; 3 4 4 5 export default defineConfig({ 5 - plugins: [sveltekit()] 6 + plugins: [ 7 + sveltekit(), 8 + SvelteKitPWA({ 9 + strategies: 'generateSW', 10 + registerType: 'autoUpdate', 11 + manifest: false, 12 + workbox: { 13 + globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], 14 + runtimeCaching: [ 15 + { 16 + urlPattern: /^https:\/\/api\.plyr\.fm\/.*/i, 17 + handler: 'NetworkFirst', 18 + options: { 19 + cacheName: 'api-cache', 20 + expiration: { 21 + maxEntries: 50, 22 + maxAgeSeconds: 60 * 60 23 + } 24 + } 25 + } 26 + ] 27 + } 28 + }) 29 + ] 6 30 });