Something went wrong. Try again.
The Cavu Reader
Something went wrong. Try again.
3.4 kB · 112 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113/** * The "Read offline" action on a book overview. Saving is explicit and * per-book (lib/offline.ts); once saved the button reports the state, * offers an update when the live bundle has moved past the saved copy * (new chapters), and removes the copy on a confirmed click. Hosted * books only — a foreign book has no bundle to save — and only where * the Cache API exists. */import { useEffect, useState } from 'react'import { getManifest } from '#/lib/bundle'import { getOfflineBook, offlineSupported, removeOffline, saveOffline,} from '#/lib/offline'import type { SaveProgress } from '#/lib/offline'
type State = 'checking' | 'idle' | 'saving' | 'saved' | 'update'
export function OfflineButton({ did, rkey, title, hosted,}: { did: string rkey: string title: string hosted: boolean}) { const supported = offlineSupported() const [state, setState] = useState<State>('checking') const [progress, setProgress] = useState<SaveProgress>() const [error, setError] = useState<string>()
useEffect(() => { if (!supported || !hosted) return let cancelled = false void (async () => { const saved = await getOfflineBook({ did, rkey }) if (cancelled) return if (!saved) { setState('idle') return } // Compare against the live bundle; unreachable (offline) means the // saved copy is simply what there is. const manifest = await getManifest({ did, rkey }).catch(() => undefined) if (cancelled) return setState(manifest && manifest.bundle.hash !== saved.bundleHash ? 'update' : 'saved') })() return () => { cancelled = true } }, [did, rkey, supported, hosted])
if (!supported || !hosted || state === 'checking') return null
const save = async () => { setState('saving') setError(undefined) try { const row = await saveOffline({ did, rkey }, title, setProgress) setState('saved') if (row.imagesMissed > 0) { setError(`Saved; ${row.imagesMissed} image${row.imagesMissed === 1 ? '' : 's'} couldn’t be fetched`) } } catch (err) { setState('idle') setError(err instanceof Error ? err.message : String(err)) } finally { setProgress(undefined) } }
const remove = async () => { if (!window.confirm(`Remove the offline copy of “${title}”?`)) return await removeOffline({ did, rkey }) setState('idle') }
const percent = progress && progress.total > 0 ? Math.round((100 * progress.done) / progress.total) : 0
return ( <> <button className="btn btn-quiet" disabled={state === 'saving'} onClick={() => void (state === 'saved' ? remove() : save())} title={ state === 'saved' ? 'Saved for offline reading — click to remove' : state === 'update' ? 'New chapters since this was saved — click to refresh the copy' : 'Save the whole book for reading without a connection' } type="button" > {state === 'saving' ? `Saving… ${percent}%` : state === 'saved' ? 'Offline ✓' : state === 'update' ? 'Update offline copy' : 'Read offline'} </button> {error && <span className="offline-note">{error}</span>} </> )}