soundwave/docs/PWA_DEVELOPER_GUIDE.md
Iulian 51679d1943 Initial commit - SoundWave v1.0
- Full PWA support with offline capabilities
- Comprehensive search across songs, playlists, and channels
- Offline playlist manager with download tracking
- Pre-built frontend for zero-build deployment
- Docker-based deployment with docker compose
- Material-UI dark theme interface
- YouTube audio download and management
- Multi-user authentication support
2025-12-16 23:43:07 +00:00

11 KiB

PWA Developer Quick Reference

Using PWA Features in Components

1. Access PWA State

import { usePWA } from '../context/PWAContext';

function MyComponent() {
  const {
    isOnline,        // boolean - network status
    canInstall,      // boolean - can show install prompt
    isInstalled,     // boolean - app is installed
    isUpdateAvailable, // boolean - update available
    cacheSize,       // { usage: number, quota: number } | null
    showInstallPrompt, // () => Promise<boolean>
    updateApp,       // () => Promise<void>
    clearCache,      // () => Promise<boolean>
    cacheAudio,      // (url: string) => Promise<boolean>
    requestNotifications, // () => Promise<NotificationPermission>
  } = usePWA();

  // Use PWA state
  return (
    <div>
      {!isOnline && <OfflineWarning />}
      {canInstall && <InstallButton onClick={showInstallPrompt} />}
    </div>
  );
}

2. Show Install Prompt

import { usePWA } from '../context/PWAContext';

function InstallButton() {
  const { canInstall, showInstallPrompt } = usePWA();

  const handleInstall = async () => {
    const installed = await showInstallPrompt();
    if (installed) {
      console.log('App installed!');
    }
  };

  if (!canInstall) return null;

  return <Button onClick={handleInstall}>Install App</Button>;
}

3. Handle Offline State

import { usePWA } from '../context/PWAContext';

function MyComponent() {
  const { isOnline } = usePWA();

  return (
    <div>
      {!isOnline && (
        <Alert severity="warning">
          You're offline. Some features may be limited.
        </Alert>
      )}
      {/* Component content */}
    </div>
  );
}

4. Cache Audio for Offline

import { usePWA } from '../context/PWAContext';

function AudioItem({ audio }) {
  const { cacheAudio } = usePWA();
  const [cached, setCached] = useState(false);

  const handleCache = async () => {
    const success = await cacheAudio(audio.file_url);
    if (success) {
      setCached(true);
      showNotification('Audio cached for offline playback');
    }
  };

  return (
    <div>
      <span>{audio.title}</span>
      <Button onClick={handleCache} disabled={cached}>
        {cached ? 'Cached' : 'Download for Offline'}
      </Button>
    </div>
  );
}

5. Request Notifications

import { usePWA } from '../context/PWAContext';

function NotificationSettings() {
  const { requestNotifications } = usePWA();

  const handleEnable = async () => {
    const permission = await requestNotifications();
    if (permission === 'granted') {
      console.log('Notifications enabled');
    }
  };

  return <Button onClick={handleEnable}>Enable Notifications</Button>;
}

6. Show App Update

import { usePWA } from '../context/PWAContext';

function UpdateBanner() {
  const { isUpdateAvailable, updateApp } = usePWA();

  if (!isUpdateAvailable) return null;

  return (
    <Alert
      severity="info"
      action={
        <Button onClick={updateApp} color="inherit">
          Update Now
        </Button>
      }
    >
      New version available!
    </Alert>
  );
}

Media Session API Usage

Set Media Metadata

import { setMediaMetadata } from '../utils/mediaSession';

setMediaMetadata({
  title: 'Song Title',
  artist: 'Artist Name',
  album: 'Album Name',
  artwork: [
    { src: '/cover-96.jpg', sizes: '96x96', type: 'image/jpeg' },
    { src: '/cover-512.jpg', sizes: '512x512', type: 'image/jpeg' },
  ],
});

Set Action Handlers

import { setMediaActionHandlers } from '../utils/mediaSession';

setMediaActionHandlers({
  play: () => audioElement.play(),
  pause: () => audioElement.pause(),
  previoustrack: () => playPrevious(),
  nexttrack: () => playNext(),
  seekbackward: () => seek(-10),
  seekforward: () => seek(10),
  seekto: (details) => audioElement.currentTime = details.seekTime,
});

Update Playback State

import { setPlaybackState, setPositionState } from '../utils/mediaSession';

// When playing/paused
setPlaybackState('playing'); // or 'paused' or 'none'

// Update position (for seek bar)
setPositionState({
  duration: audio.duration,
  playbackRate: 1.0,
  position: audioElement.currentTime,
});

Offline Storage Usage

Save/Get Data

import { offlineStorage } from '../utils/offlineStorage';

// Save audio queue
await offlineStorage.saveAudioQueue(queue);

// Get audio queue
const queue = await offlineStorage.getAudioQueue();

// Add favorite
await offlineStorage.addFavorite({
  id: audio.id,
  title: audio.title,
  // ... other data
});

// Get favorites
const favorites = await offlineStorage.getFavorites();

// Save setting
await offlineStorage.saveSetting('theme', 'dark');

// Get setting
const theme = await offlineStorage.getSetting('theme');

Pending Uploads

import { offlineStorage } from '../utils/offlineStorage';

// Add pending upload (when offline)
await offlineStorage.addPendingUpload({
  file: fileData,
  metadata: { title: 'Song' },
});

// Get pending uploads (to sync when online)
const pending = await offlineStorage.getPendingUploads();

// Remove after sync
await offlineStorage.removePendingUpload(uploadId);

Service Worker Communication

Cache Specific URL

import { cacheAudio } from '../utils/pwa';

const url = 'https://example.com/audio/song.mp3';
const success = await cacheAudio(url);
if (success) {
  console.log('Audio cached!');
}

Clear All Caches

import { clearCache } from '../utils/pwa';

const success = await clearCache();
if (success) {
  console.log('Cache cleared!');
}

PWA Utilities

Check Installation Status

import { isInstalled, canInstall } from '../utils/pwa';

if (isInstalled()) {
  console.log('App is installed');
}

if (canInstall()) {
  console.log('Can show install prompt');
}

Check Online Status

import { isOnline } from '../utils/pwa';

if (isOnline()) {
  // Fetch from network
} else {
  // Use cached data
}

Get Cache Size

import { getCacheSize } from '../utils/pwa';

const size = await getCacheSize();
if (size) {
  console.log(`Using ${size.usage} of ${size.quota} bytes`);
  const percent = (size.usage / size.quota) * 100;
  console.log(`${percent.toFixed(1)}% of storage used`);
}

Best Practices

1. Always Handle Offline State

function DataComponent() {
  const { isOnline } = usePWA();
  const [data, setData] = useState(null);

  useEffect(() => {
    if (isOnline) {
      // Fetch from API
      fetchData().then(setData);
    } else {
      // Load from offline storage
      offlineStorage.get('dataStore', 'key').then(setData);
    }
  }, [isOnline]);

  return <div>{/* Render data */}</div>;
}

2. Show Loading States

function MyComponent() {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadData().finally(() => setLoading(false));
  }, []);

  if (loading) {
    return <CircularProgress />;
  }

  return <div>{/* Content */}</div>;
}

3. Provide Offline Feedback

function UploadButton({ file }) {
  const { isOnline } = usePWA();

  const handleUpload = async () => {
    if (!isOnline) {
      // Queue for later
      await offlineStorage.addPendingUpload(file);
      alert('Upload queued. Will sync when online.');
      return;
    }

    // Upload immediately
    await uploadFile(file);
  };

  return (
    <Button onClick={handleUpload}>
      Upload {!isOnline && '(Offline - Will Queue)'}
    </Button>
  );
}

4. Cache Important Assets

function AudioPlayer({ audio }) {
  const { cacheAudio } = usePWA();
  const [isCached, setIsCached] = useState(false);

  useEffect(() => {
    // Pre-cache important audio
    if (audio.is_favorite) {
      cacheAudio(audio.file_url).then(setIsCached);
    }
  }, [audio]);

  return (
    <div>
      <audio src={audio.file_url} />
      {isCached && <Chip label="Available Offline" />}
    </div>
  );
}

5. Clean Up Resources

function MediaPlayer({ audio }) {
  useEffect(() => {
    // Set up media session
    setMediaMetadata(audio);
    setMediaActionHandlers({ /* ... */ });

    // Clean up on unmount
    return () => {
      clearMediaSession();
    };
  }, [audio]);

  return <audio />;
}

Debugging

Check Service Worker Status

// In browser console
navigator.serviceWorker.getRegistration().then(reg => {
  console.log('Service Worker:', reg);
  console.log('Active:', reg?.active);
  console.log('Waiting:', reg?.waiting);
});

List All Caches

// In browser console
caches.keys().then(keys => {
  console.log('Cache keys:', keys);
  keys.forEach(key => {
    caches.open(key).then(cache => {
      cache.keys().then(requests => {
        console.log(`${key}:`, requests.map(r => r.url));
      });
    });
  });
});

Check Offline Storage

// In browser console
const request = indexedDB.open('soundwave-offline');
request.onsuccess = () => {
  const db = request.result;
  console.log('Object stores:', db.objectStoreNames);
};

Force Service Worker Update

// In browser console
navigator.serviceWorker.getRegistration().then(reg => {
  reg?.update().then(() => console.log('Update check complete'));
});

Common Issues

Issue: Service Worker Not Updating

Solution:

// Force skip waiting
navigator.serviceWorker.getRegistration().then(reg => {
  reg?.waiting?.postMessage({ type: 'SKIP_WAITING' });
});

Issue: Install Prompt Not Showing

Checklist:

  • Served over HTTPS
  • Has valid manifest.json
  • Has registered service worker
  • User hasn't dismissed recently
  • User hasn't already installed

Issue: Offline Content Not Loading

Solution:

  1. Check service worker is active
  2. Verify content was visited while online
  3. Check cache in DevTools > Application > Cache Storage
  4. Ensure service worker fetch handler is correct

Issue: Media Session Not Working

Solution:

  • Safari: Limited support, some actions may not work
  • Check if MediaMetadata constructor exists
  • Verify action handlers are set correctly
  • Test in Chrome/Edge first

Performance Tips

1. Lazy Load Components

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

2. Optimize Images

// Use WebP with fallback
<picture>
  <source srcSet="image.webp" type="image/webp" />
  <img src="image.jpg" alt="..." />
</picture>

3. Preload Critical Assets

<link rel="preload" href="/critical.css" as="style" />
<link rel="preload" href="/critical.js" as="script" />

4. Use Code Splitting

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          mui: ['@mui/material'],
        },
      },
    },
  },
});

Happy PWA Development! 🚀

For more information, see: