- Add Web App Manifest with app metadata, icons, and installation support - Create Service Worker with intelligent caching strategies for Bible content, static assets, and dynamic content - Implement IndexedDB-based offline storage system for Bible versions, books, chapters, and verses - Add offline download manager component for browsing and downloading Bible versions - Create offline Bible reader component for seamless offline reading experience - Integrate PWA install prompt with platform-specific instructions - Add offline reading interface to existing Bible reader with download buttons - Create dedicated offline page with tabbed interface for reading and downloading - Add PWA and offline-related translations for English and Romanian locales - Implement background sync for Bible downloads and cache management - Add storage usage monitoring and management utilities - Ensure SSR-safe implementation with dynamic imports for client-side components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
// import { useTranslations } from 'next-intl'
|
|
import {
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
Typography,
|
|
Button,
|
|
LinearProgress,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
ListItemSecondaryAction,
|
|
IconButton,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Chip,
|
|
Alert,
|
|
Tooltip
|
|
} from '@mui/material'
|
|
import {
|
|
Download,
|
|
Delete,
|
|
CloudDownload,
|
|
Storage,
|
|
Wifi,
|
|
WifiOff,
|
|
CheckCircle,
|
|
Error,
|
|
Info
|
|
} from '@mui/icons-material'
|
|
import { bibleDownloadManager, type BibleVersion, type DownloadProgress } from '@/lib/offline-storage'
|
|
|
|
interface OfflineDownloadManagerProps {
|
|
availableVersions: BibleVersion[]
|
|
onVersionDownloaded?: (versionId: string) => void
|
|
}
|
|
|
|
export function OfflineDownloadManager({ availableVersions, onVersionDownloaded }: OfflineDownloadManagerProps) {
|
|
// const t = useTranslations('bible')
|
|
const [downloadedVersions, setDownloadedVersions] = useState<BibleVersion[]>([])
|
|
const [downloads, setDownloads] = useState<Record<string, DownloadProgress>>({})
|
|
const [storageInfo, setStorageInfo] = useState({ used: 0, quota: 0, percentage: 0 })
|
|
const [isOnline, setIsOnline] = useState(true)
|
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
loadDownloadedVersions()
|
|
loadStorageInfo()
|
|
checkOnlineStatus()
|
|
|
|
// Listen for online/offline events
|
|
const handleOnline = () => setIsOnline(true)
|
|
const handleOffline = () => setIsOnline(false)
|
|
|
|
window.addEventListener('online', handleOnline)
|
|
window.addEventListener('offline', handleOffline)
|
|
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline)
|
|
window.removeEventListener('offline', handleOffline)
|
|
}
|
|
}, [])
|
|
|
|
const loadDownloadedVersions = async () => {
|
|
try {
|
|
const versions = await bibleDownloadManager.getDownloadedVersions()
|
|
setDownloadedVersions(versions)
|
|
} catch (error: unknown) {
|
|
console.error('Failed to load downloaded versions:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const loadStorageInfo = async () => {
|
|
try {
|
|
const info = await bibleDownloadManager.getStorageInfo()
|
|
setStorageInfo(info)
|
|
} catch (error: unknown) {
|
|
console.error('Failed to load storage info:', error)
|
|
}
|
|
}
|
|
|
|
const checkOnlineStatus = () => {
|
|
setIsOnline(navigator.onLine)
|
|
}
|
|
|
|
const handleDownload = async (version: BibleVersion) => {
|
|
if (!isOnline) {
|
|
alert('You need an internet connection to download Bible versions')
|
|
return
|
|
}
|
|
|
|
try {
|
|
await bibleDownloadManager.downloadVersion(
|
|
version.id,
|
|
(progress: DownloadProgress) => {
|
|
setDownloads(prev => ({
|
|
...prev,
|
|
[version.id]: progress
|
|
}))
|
|
|
|
if (progress.status === 'completed') {
|
|
loadDownloadedVersions()
|
|
loadStorageInfo()
|
|
onVersionDownloaded?.(version.id)
|
|
|
|
// Remove from downloads state after completion
|
|
setTimeout(() => {
|
|
setDownloads(prev => {
|
|
const { [version.id]: _, ...rest } = prev
|
|
return rest
|
|
})
|
|
}, 3000)
|
|
}
|
|
}
|
|
)
|
|
} catch (error: unknown) {
|
|
console.error('Download failed:', error)
|
|
const errorMessage = 'Download failed'
|
|
alert(errorMessage)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (versionId: string) => {
|
|
try {
|
|
await bibleDownloadManager.deleteVersion(versionId)
|
|
setDownloadedVersions(prev => prev.filter(v => v.id !== versionId))
|
|
loadStorageInfo()
|
|
setConfirmDelete(null)
|
|
} catch (error: unknown) {
|
|
console.error('Delete failed:', error)
|
|
alert('Failed to delete version')
|
|
}
|
|
}
|
|
|
|
const formatBytes = (bytes: number): string => {
|
|
if (bytes === 0) return '0 Bytes'
|
|
const k = 1024
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
}
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return <CheckCircle color="success" />
|
|
case 'failed':
|
|
return <Error color="error" />
|
|
case 'downloading':
|
|
return <CloudDownload color="primary" />
|
|
default:
|
|
return <Info color="info" />
|
|
}
|
|
}
|
|
|
|
const isVersionDownloaded = (versionId: string) => {
|
|
return downloadedVersions.some(v => v.id === versionId)
|
|
}
|
|
|
|
const isVersionDownloading = (versionId: string) => {
|
|
return downloads[versionId]?.status === 'downloading'
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box sx={{ p: 3 }}>
|
|
<Typography>Loading offline storage...</Typography>
|
|
<LinearProgress sx={{ mt: 2 }} />
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ p: 3 }}>
|
|
{/* Header */}
|
|
<Box sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Storage color="primary" />
|
|
<Typography variant="h5">Offline Bible Storage</Typography>
|
|
<Chip
|
|
icon={isOnline ? <Wifi /> : <WifiOff />}
|
|
label={isOnline ? 'Online' : 'Offline'}
|
|
color={isOnline ? 'success' : 'error'}
|
|
size="small"
|
|
/>
|
|
</Box>
|
|
|
|
{/* Storage Info */}
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
Storage Usage
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
|
<Box sx={{ flexGrow: 1 }}>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={storageInfo.percentage}
|
|
sx={{ height: 8, borderRadius: 4 }}
|
|
/>
|
|
</Box>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{storageInfo.percentage.toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{formatBytes(storageInfo.used)} of {formatBytes(storageInfo.quota)} used
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{downloadedVersions.length} Bible versions downloaded
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Available Versions for Download */}
|
|
{isOnline && (
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
Available for Download
|
|
</Typography>
|
|
<List dense>
|
|
{availableVersions
|
|
.filter(version => !isVersionDownloaded(version.id))
|
|
.map((version) => (
|
|
<ListItem key={version.id}>
|
|
<ListItemText
|
|
primary={version.name}
|
|
secondary={`${version.abbreviation} - ${version.language}`}
|
|
/>
|
|
<ListItemSecondaryAction>
|
|
<Button
|
|
startIcon={<Download />}
|
|
onClick={() => handleDownload(version)}
|
|
disabled={isVersionDownloading(version.id)}
|
|
variant="outlined"
|
|
size="small"
|
|
>
|
|
Download
|
|
</Button>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
{availableVersions.filter(v => !isVersionDownloaded(v.id)).length === 0 && (
|
|
<Typography color="text.secondary">
|
|
All available versions are already downloaded
|
|
</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Active Downloads */}
|
|
{Object.keys(downloads).length > 0 && (
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
Downloads in Progress
|
|
</Typography>
|
|
<List dense>
|
|
{Object.entries(downloads).map(([versionId, progress]) => {
|
|
const version = availableVersions.find(v => v.id === versionId)
|
|
return (
|
|
<ListItem key={versionId}>
|
|
<ListItemText
|
|
primary={
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{getStatusIcon(progress.status)}
|
|
<Typography>
|
|
{version?.name || versionId}
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
secondary={
|
|
<Box sx={{ mt: 1 }}>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={progress.progress}
|
|
sx={{ mb: 1 }}
|
|
/>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{progress.downloadedChapters} / {progress.totalChapters} chapters
|
|
({progress.progress}%)
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
)
|
|
})}
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Downloaded Versions */}
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
Downloaded Versions
|
|
</Typography>
|
|
{downloadedVersions.length === 0 ? (
|
|
<Alert severity="info">
|
|
No Bible versions downloaded yet. Download versions above to read offline.
|
|
</Alert>
|
|
) : (
|
|
<List dense>
|
|
{downloadedVersions.map((version) => (
|
|
<ListItem key={version.id}>
|
|
<ListItemText
|
|
primary={version.name}
|
|
secondary={
|
|
<Box>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{version.abbreviation} - {version.language}
|
|
</Typography>
|
|
{version.downloadedAt && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
Downloaded: {new Date(version.downloadedAt).toLocaleDateString()}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
}
|
|
/>
|
|
<ListItemSecondaryAction>
|
|
<Tooltip title="Delete from offline storage">
|
|
<IconButton
|
|
edge="end"
|
|
onClick={() => setConfirmDelete(version.id)}
|
|
color="error"
|
|
size="small"
|
|
>
|
|
<Delete />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<Dialog
|
|
open={!!confirmDelete}
|
|
onClose={() => setConfirmDelete(null)}
|
|
>
|
|
<DialogTitle>Delete Bible Version</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>
|
|
Are you sure you want to delete this Bible version from offline storage?
|
|
You'll need to download it again to read offline.
|
|
</Typography>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setConfirmDelete(null)}>Cancel</Button>
|
|
<Button
|
|
onClick={() => confirmDelete && handleDelete(confirmDelete)}
|
|
color="error"
|
|
variant="contained"
|
|
>
|
|
Delete
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Box>
|
|
)
|
|
} |