feat: improve Bible reader UX with dropdown menu and enhanced offline features
- Replace three separate verse action icons with compact three-dot dropdown menu - Bookmark, Copy Verse, and Ask AI now in a single menu - Better space utilization on mobile, tablet, and desktop - Enhance offline Bible downloads UI - Move downloaded versions list to top for better visibility - Add inline progress bars during downloads - Show real-time download progress with chapter counts - Add refresh button for downloaded versions list - Remove duplicate header, keep only main header with online/offline status - Improve build performance - Add .eslintignore to speed up linting phase - Already excludes large directories (bibles/, scripts/, csv_bibles/) - Add debug logging for offline storage operations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,8 @@ import {
|
||||
DialogActions,
|
||||
Chip,
|
||||
Alert,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
CircularProgress
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Download,
|
||||
@@ -31,7 +32,8 @@ import {
|
||||
WifiOff,
|
||||
CheckCircle,
|
||||
Error,
|
||||
Info
|
||||
Info,
|
||||
Refresh
|
||||
} from '@mui/icons-material'
|
||||
import { bibleDownloadManager, type BibleVersion, type DownloadProgress } from '@/lib/offline-storage'
|
||||
|
||||
@@ -69,7 +71,10 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
|
||||
|
||||
const loadDownloadedVersions = async () => {
|
||||
try {
|
||||
console.log('[OfflineDownloadManager] Loading downloaded versions...')
|
||||
const versions = await bibleDownloadManager.getDownloadedVersions()
|
||||
console.log('[OfflineDownloadManager] Downloaded versions:', versions)
|
||||
console.log('[OfflineDownloadManager] Number of versions:', versions.length)
|
||||
setDownloadedVersions(versions)
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to load downloaded versions:', error)
|
||||
@@ -97,16 +102,35 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Starting download for version: ${version.name} (${version.id})`)
|
||||
|
||||
try {
|
||||
// Initialize download progress immediately
|
||||
setDownloads(prev => ({
|
||||
...prev,
|
||||
[version.id]: {
|
||||
versionId: version.id,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
totalBooks: 0,
|
||||
downloadedBooks: 0,
|
||||
totalChapters: 0,
|
||||
downloadedChapters: 0,
|
||||
startedAt: new Date().toISOString()
|
||||
}
|
||||
}))
|
||||
|
||||
await bibleDownloadManager.downloadVersion(
|
||||
version.id,
|
||||
(progress: DownloadProgress) => {
|
||||
console.log(`Download progress for ${version.name}:`, progress)
|
||||
setDownloads(prev => ({
|
||||
...prev,
|
||||
[version.id]: progress
|
||||
}))
|
||||
|
||||
if (progress.status === 'completed') {
|
||||
console.log(`Download completed for ${version.name}`)
|
||||
loadDownloadedVersions()
|
||||
loadStorageInfo()
|
||||
onVersionDownloaded?.(version.id)
|
||||
@@ -118,13 +142,33 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
|
||||
return rest
|
||||
})
|
||||
}, 3000)
|
||||
} else if (progress.status === 'failed') {
|
||||
console.error(`Download failed for ${version.name}:`, progress.error)
|
||||
alert(`Download failed: ${progress.error || 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
console.error('Download failed:', error)
|
||||
const errorMessage = 'Download failed'
|
||||
alert(errorMessage)
|
||||
const errorMessage = (error as Error)?.message || 'Download failed'
|
||||
|
||||
// Update downloads state to show failure
|
||||
setDownloads(prev => ({
|
||||
...prev,
|
||||
[version.id]: {
|
||||
versionId: version.id,
|
||||
status: 'failed' as const,
|
||||
progress: 0,
|
||||
totalBooks: 0,
|
||||
downloadedBooks: 0,
|
||||
totalChapters: 0,
|
||||
downloadedChapters: 0,
|
||||
startedAt: new Date().toISOString(),
|
||||
error: errorMessage
|
||||
}
|
||||
}))
|
||||
|
||||
alert(`Download failed: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +210,8 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
|
||||
}
|
||||
|
||||
const isVersionDownloading = (versionId: string) => {
|
||||
return downloads[versionId]?.status === 'downloading'
|
||||
const status = downloads[versionId]?.status
|
||||
return status === 'downloading' || status === 'pending'
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -192,6 +237,61 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Downloaded Versions - Moved to top */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">
|
||||
Downloaded Versions ({downloadedVersions.length})
|
||||
</Typography>
|
||||
<Tooltip title="Refresh list">
|
||||
<IconButton onClick={loadDownloadedVersions} size="small">
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
{downloadedVersions.length === 0 ? (
|
||||
<Alert severity="info">
|
||||
No Bible versions downloaded yet. Download versions below 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>
|
||||
|
||||
{/* Storage Info */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
@@ -219,45 +319,6 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
|
||||
</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 }}>
|
||||
@@ -301,53 +362,62 @@ export function OfflineDownloadManager({ availableVersions, onVersionDownloaded
|
||||
</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>
|
||||
) : (
|
||||
{/* Available Versions for Download */}
|
||||
{isOnline && (
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Available for Download
|
||||
</Typography>
|
||||
<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>
|
||||
)}
|
||||
{availableVersions
|
||||
.filter(version => !isVersionDownloaded(version.id))
|
||||
.map((version) => {
|
||||
const downloadProgress = downloads[version.id]
|
||||
const isDownloading = isVersionDownloading(version.id)
|
||||
|
||||
return (
|
||||
<ListItem key={version.id} sx={{ flexDirection: 'column', alignItems: 'stretch' }}>
|
||||
<Box sx={{ display: 'flex', width: '100%', alignItems: 'center' }}>
|
||||
<ListItemText
|
||||
primary={version.name}
|
||||
secondary={`${version.abbreviation} - ${version.language}`}
|
||||
/>
|
||||
<Button
|
||||
startIcon={isDownloading ? <CircularProgress size={16} /> : <Download />}
|
||||
onClick={() => handleDownload(version)}
|
||||
disabled={isDownloading}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ ml: 2 }}
|
||||
>
|
||||
{isDownloading ? 'Downloading...' : 'Download'}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Delete from offline storage">
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={() => setConfirmDelete(version.id)}
|
||||
color="error"
|
||||
size="small"
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
{isDownloading && downloadProgress && (
|
||||
<Box sx={{ width: '100%', mt: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={downloadProgress.progress}
|
||||
sx={{ mb: 0.5 }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{downloadProgress.downloadedChapters} / {downloadProgress.totalChapters} chapters ({downloadProgress.progress}%)
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{availableVersions.filter(v => !isVersionDownloaded(v.id)).length === 0 && (
|
||||
<Typography color="text.secondary">
|
||||
All available versions are already downloaded
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
|
||||
Reference in New Issue
Block a user