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:
2025-09-30 08:23:22 +00:00
parent 44831a096f
commit fee36dfdad
7 changed files with 424 additions and 178 deletions

View File

@@ -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