mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
2cdfaa52cd
- Centered hero section with status badge - Prominent status card with icon - Clean download section with gradient background - Quick access grid with larger icon cards - Improved Bible reader section with styled controls - Technical details collapsed by default - Simplified cached URL list (limit 500 per category) - Cleaner, more modern styling throughout - Better dark mode support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
898 lines
34 KiB
HTML
898 lines
34 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Offline Mode - KJV Study{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.offline-hero {
|
|
text-align: center;
|
|
padding: 2rem 0;
|
|
margin-bottom: 2rem;
|
|
border-bottom: 1px solid var(--border-color, #ddd);
|
|
}
|
|
.offline-hero h1 {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 2.5rem;
|
|
}
|
|
.offline-hero .subtitle {
|
|
font-size: 1.1rem;
|
|
color: var(--text-secondary, #666);
|
|
margin: 0;
|
|
}
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 20px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
margin-left: 0.75rem;
|
|
vertical-align: middle;
|
|
}
|
|
.badge-offline { background: var(--error-color, #c41e3a); color: white; }
|
|
.badge-online { background: var(--success-color, #4a7c59); color: white; }
|
|
|
|
/* Status Card */
|
|
.status-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
padding: 1.5rem 2rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 2rem;
|
|
background: var(--bg-color, #fffff8);
|
|
border: 2px solid var(--border-color, #ddd);
|
|
}
|
|
.status-card.status-ok {
|
|
border-color: var(--success-color, #4a7c59);
|
|
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
|
}
|
|
.status-card.status-warning {
|
|
border-color: #f59e0b;
|
|
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
|
}
|
|
.status-card.status-error {
|
|
border-color: var(--error-color, #c41e3a);
|
|
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
|
}
|
|
[data-theme="dark"] .status-card.status-ok {
|
|
background: linear-gradient(135deg, #14532d 0%, #166534 100%);
|
|
}
|
|
[data-theme="dark"] .status-card.status-warning {
|
|
background: linear-gradient(135deg, #78350f 0%, #92400e 100%);
|
|
}
|
|
[data-theme="dark"] .status-card.status-error {
|
|
background: linear-gradient(135deg, #7f1d1d 0%, #991b1b 100%);
|
|
}
|
|
.status-icon {
|
|
font-size: 3rem;
|
|
line-height: 1;
|
|
}
|
|
.status-content {
|
|
flex: 1;
|
|
}
|
|
.status-title {
|
|
font-weight: 700;
|
|
font-size: 1.25rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
.status-detail {
|
|
color: var(--text-secondary, #666);
|
|
font-size: 0.95rem;
|
|
line-height: 1.5;
|
|
}
|
|
.status-detail code {
|
|
background: rgba(0,0,0,0.1);
|
|
padding: 0.1rem 0.4rem;
|
|
border-radius: 4px;
|
|
font-size: 0.85em;
|
|
}
|
|
[data-theme="dark"] .status-detail code {
|
|
background: rgba(255,255,255,0.1);
|
|
}
|
|
|
|
/* Download Section */
|
|
.download-section {
|
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
padding: 2rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 2rem;
|
|
border: 1px solid var(--border-color, #ddd);
|
|
}
|
|
[data-theme="dark"] .download-section {
|
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
|
}
|
|
.download-section h2 {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 1.5rem;
|
|
}
|
|
.download-section p {
|
|
margin: 0 0 1.5rem 0;
|
|
color: var(--text-secondary, #666);
|
|
}
|
|
.download-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 1rem 2rem;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: var(--success-color, #4a7c59);
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.download-btn:hover {
|
|
filter: brightness(1.1);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(74, 124, 89, 0.3);
|
|
}
|
|
.download-btn:disabled {
|
|
opacity: 0.7;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
.download-progress {
|
|
margin-top: 1.5rem;
|
|
display: none;
|
|
}
|
|
.download-progress.active {
|
|
display: block;
|
|
}
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 12px;
|
|
background: var(--border-color, #ddd);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--success-color, #4a7c59) 0%, #6aa87a 100%);
|
|
width: 0%;
|
|
transition: width 0.3s ease;
|
|
border-radius: 6px;
|
|
}
|
|
.progress-text {
|
|
font-size: 0.9rem;
|
|
color: var(--text-secondary, #666);
|
|
}
|
|
.cache-complete {
|
|
color: var(--success-color, #4a7c59);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Quick Access Grid */
|
|
.quick-access {
|
|
margin-bottom: 2rem;
|
|
}
|
|
.quick-access h2 {
|
|
margin: 0 0 1rem 0;
|
|
}
|
|
.quick-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
.quick-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 1.25rem;
|
|
background: var(--bg-color, #fffff8);
|
|
border: 1px solid var(--border-color, #ddd);
|
|
border-radius: 12px;
|
|
text-decoration: none;
|
|
color: var(--text-color, #111);
|
|
transition: all 0.2s;
|
|
}
|
|
.quick-card:hover {
|
|
border-color: var(--success-color, #4a7c59);
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
|
}
|
|
.quick-card-icon {
|
|
font-size: 2rem;
|
|
}
|
|
.quick-card-label {
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
/* Bible Reader */
|
|
.reader-section {
|
|
background: var(--bg-color, #fffff8);
|
|
border: 1px solid var(--border-color, #ddd);
|
|
border-radius: 12px;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.reader-section h2 {
|
|
margin: 0 0 0.5rem 0;
|
|
}
|
|
.reader-section > p {
|
|
margin: 0 0 1.5rem 0;
|
|
color: var(--text-secondary, #666);
|
|
}
|
|
.reader-controls {
|
|
display: flex;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.reader-controls select {
|
|
padding: 0.75rem 1rem;
|
|
font-size: 1rem;
|
|
border: 2px solid var(--border-color, #ddd);
|
|
border-radius: 8px;
|
|
background: var(--bg-color, #fffff8);
|
|
color: var(--text-color, #111);
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.reader-controls select:focus {
|
|
outline: none;
|
|
border-color: var(--success-color, #4a7c59);
|
|
}
|
|
.nav-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
.nav-btn {
|
|
padding: 0.75rem 1.25rem;
|
|
border: 2px solid var(--border-color, #ddd);
|
|
border-radius: 8px;
|
|
background: var(--bg-color, #fffff8);
|
|
color: var(--text-color, #111);
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
.nav-btn:hover:not(:disabled) {
|
|
border-color: var(--success-color, #4a7c59);
|
|
background: var(--success-color, #4a7c59);
|
|
color: white;
|
|
}
|
|
.nav-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
#chapter-content {
|
|
display: none;
|
|
line-height: 2;
|
|
font-size: 1.15rem;
|
|
}
|
|
#chapter-content.active {
|
|
display: block;
|
|
}
|
|
#chapter-content h3 {
|
|
margin: 0 0 1rem 0;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border-color, #ddd);
|
|
}
|
|
#chapter-content p {
|
|
margin: 0.75rem 0;
|
|
}
|
|
.verse-num {
|
|
font-size: 0.8rem;
|
|
vertical-align: super;
|
|
margin-right: 0.25rem;
|
|
color: var(--success-color, #4a7c59);
|
|
font-weight: 600;
|
|
}
|
|
.keyboard-hint {
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary, #666);
|
|
margin-top: 1rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--border-color, #ddd);
|
|
}
|
|
|
|
/* Technical Details */
|
|
.tech-details {
|
|
margin-top: 3rem;
|
|
}
|
|
.tech-details summary {
|
|
cursor: pointer;
|
|
color: var(--text-secondary, #666);
|
|
font-size: 0.9rem;
|
|
padding: 0.75rem 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
.tech-details summary:hover {
|
|
color: var(--text-color, #111);
|
|
}
|
|
.tech-details[open] summary {
|
|
margin-bottom: 1rem;
|
|
}
|
|
.tech-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.tech-item {
|
|
background: var(--border-color, #ddd);
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
}
|
|
.tech-item dt {
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary, #666);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
.tech-item dd {
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
}
|
|
.status-ok { color: var(--success-color, #4a7c59); }
|
|
.status-error { color: var(--error-color, #c41e3a); }
|
|
|
|
/* Cached URLs */
|
|
.cached-section h3 {
|
|
font-size: 1rem;
|
|
color: var(--text-secondary, #666);
|
|
margin: 1.5rem 0 0.75rem 0;
|
|
}
|
|
.url-categories {
|
|
margin: 0;
|
|
}
|
|
.url-category {
|
|
margin-bottom: 0.5rem;
|
|
border: 1px solid var(--border-color, #ddd);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.url-category-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--border-color, #ddd);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
font-weight: 500;
|
|
font-size: 0.95rem;
|
|
}
|
|
.url-category-header:hover {
|
|
background: var(--text-secondary, #666);
|
|
color: var(--bg-color, #fffff8);
|
|
}
|
|
.url-category.selected .url-category-header {
|
|
outline: 2px solid var(--success-color, #4a7c59);
|
|
outline-offset: -2px;
|
|
}
|
|
.url-category-toggle {
|
|
font-size: 0.75rem;
|
|
transition: transform 0.2s;
|
|
}
|
|
.url-category.expanded .url-category-toggle {
|
|
transform: rotate(90deg);
|
|
}
|
|
.url-category-content {
|
|
display: none;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
padding: 0.5rem;
|
|
background: var(--bg-color, #fffff8);
|
|
}
|
|
.url-category.expanded .url-category-content {
|
|
display: block;
|
|
}
|
|
.url-item {
|
|
display: block;
|
|
padding: 0.4rem 0.75rem;
|
|
color: var(--link-color, #333);
|
|
text-decoration: none;
|
|
font-size: 0.85rem;
|
|
border-radius: 4px;
|
|
}
|
|
.url-item:hover {
|
|
background: var(--border-color, #ddd);
|
|
}
|
|
.url-item.selected {
|
|
background: var(--success-color, #4a7c59);
|
|
color: white;
|
|
}
|
|
.url-count {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary, #666);
|
|
font-weight: normal;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="offline-hero">
|
|
<h1>Offline Mode <span class="badge" id="connection-badge">checking...</span></h1>
|
|
<p class="subtitle">Access the complete KJV Bible without internet</p>
|
|
</div>
|
|
|
|
<div class="status-card" id="status-card">
|
|
<div class="status-icon" id="status-icon">⏳</div>
|
|
<div class="status-content">
|
|
<div class="status-title" id="status-title">Checking status...</div>
|
|
<div class="status-detail" id="status-detail">Please wait</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="download-section">
|
|
<h2>Download for Offline</h2>
|
|
<p>Download all ~48,000 pages including chapters, verse commentary, interlinear text, stories, and study resources.</p>
|
|
<button class="download-btn" id="start-cache-btn">
|
|
<span>Download Everything</span>
|
|
</button>
|
|
<div class="download-progress" id="cache-progress">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progress-fill"></div>
|
|
</div>
|
|
<div class="progress-text" id="progress-text">Preparing...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="quick-access">
|
|
<h2>Quick Access</h2>
|
|
<div class="quick-grid">
|
|
<a href="/books" class="quick-card">
|
|
<span class="quick-card-icon">📖</span>
|
|
<span class="quick-card-label">All Books</span>
|
|
</a>
|
|
<a href="/stories" class="quick-card">
|
|
<span class="quick-card-icon">📚</span>
|
|
<span class="quick-card-label">Stories</span>
|
|
</a>
|
|
<a href="/topics" class="quick-card">
|
|
<span class="quick-card-icon">🏷️</span>
|
|
<span class="quick-card-label">Topics</span>
|
|
</a>
|
|
<a href="/reading-plans" class="quick-card">
|
|
<span class="quick-card-icon">📅</span>
|
|
<span class="quick-card-label">Reading Plans</span>
|
|
</a>
|
|
<a href="/study-guides" class="quick-card">
|
|
<span class="quick-card-icon">📝</span>
|
|
<span class="quick-card-label">Study Guides</span>
|
|
</a>
|
|
<a href="/resources" class="quick-card">
|
|
<span class="quick-card-icon">📂</span>
|
|
<span class="quick-card-label">Resources</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="reader-section">
|
|
<h2>Bible Reader</h2>
|
|
<p>Read directly from cached data (always works offline)</p>
|
|
<div class="reader-controls">
|
|
<select id="book-select">
|
|
<option value="">Select book...</option>
|
|
</select>
|
|
<select id="chapter-select" disabled>
|
|
<option value="">Chapter</option>
|
|
</select>
|
|
<div class="nav-buttons">
|
|
<button class="nav-btn" id="prev-btn" disabled>← Prev</button>
|
|
<button class="nav-btn" id="next-btn" disabled>Next →</button>
|
|
</div>
|
|
</div>
|
|
<div id="chapter-content"></div>
|
|
<p class="keyboard-hint">Keyboard: ←/→ navigate chapters • g = Genesis 1 • ? = help</p>
|
|
</div>
|
|
|
|
<details class="tech-details">
|
|
<summary>Technical Details</summary>
|
|
<div class="tech-grid">
|
|
<div class="tech-item">
|
|
<dt>Service Worker</dt>
|
|
<dd id="sw-status">Checking...</dd>
|
|
</div>
|
|
<div class="tech-item">
|
|
<dt>Cache Storage</dt>
|
|
<dd id="cache-status">Checking...</dd>
|
|
</div>
|
|
<div class="tech-item">
|
|
<dt>Bible Data</dt>
|
|
<dd id="bible-status">Checking...</dd>
|
|
</div>
|
|
<div class="tech-item">
|
|
<dt>Pages Cached</dt>
|
|
<dd id="pages-cached">Checking...</dd>
|
|
</div>
|
|
</div>
|
|
<div class="cached-section">
|
|
<h3>Cached Pages <span id="cached-count" style="color: var(--success-color);">0</span></h3>
|
|
<div id="cached-links" class="url-categories">
|
|
<p style="color: var(--text-secondary); font-size: 0.9rem;">Scanning cache...</p>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
(function() {
|
|
const connectionBadge = document.getElementById('connection-badge');
|
|
const swStatus = document.getElementById('sw-status');
|
|
const cacheStatus = document.getElementById('cache-status');
|
|
const bibleStatus = document.getElementById('bible-status');
|
|
const pagesCached = document.getElementById('pages-cached');
|
|
const cachedLinks = document.getElementById('cached-links');
|
|
const cachedCount = document.getElementById('cached-count');
|
|
const statusCard = document.getElementById('status-card');
|
|
const statusIcon = document.getElementById('status-icon');
|
|
const statusTitle = document.getElementById('status-title');
|
|
const statusDetail = document.getElementById('status-detail');
|
|
|
|
function updateStatusCard(type, icon, title, detail) {
|
|
statusCard.className = 'status-card status-' + type;
|
|
statusIcon.textContent = icon;
|
|
statusTitle.textContent = title;
|
|
statusDetail.innerHTML = detail;
|
|
}
|
|
|
|
function updateConnectionStatus() {
|
|
if (navigator.onLine) {
|
|
connectionBadge.textContent = 'Online';
|
|
connectionBadge.className = 'badge badge-online';
|
|
} else {
|
|
connectionBadge.textContent = 'Offline';
|
|
connectionBadge.className = 'badge badge-offline';
|
|
}
|
|
}
|
|
updateConnectionStatus();
|
|
window.addEventListener('online', updateConnectionStatus);
|
|
window.addEventListener('offline', updateConnectionStatus);
|
|
|
|
async function checkServiceWorker() {
|
|
if ('serviceWorker' in navigator) {
|
|
try {
|
|
const registration = await navigator.serviceWorker.getRegistration();
|
|
if (registration && registration.active) {
|
|
swStatus.innerHTML = '<span class="status-ok">✓ Active</span>';
|
|
|
|
const cacheNames = await caches.keys();
|
|
let totalCached = 0;
|
|
for (const cacheName of cacheNames) {
|
|
const cache = await caches.open(cacheName);
|
|
const keys = await cache.keys();
|
|
totalCached += keys.filter(req => !new URL(req.url).pathname.startsWith('/static/')).length;
|
|
}
|
|
|
|
const TOTAL_SITE_PAGES = 48000;
|
|
const percentCached = Math.round((totalCached / TOTAL_SITE_PAGES) * 100);
|
|
|
|
if (totalCached > 45000) {
|
|
updateStatusCard('ok', '✅', 'Ready for offline use',
|
|
'<strong>' + totalCached.toLocaleString() + '</strong> pages cached. The entire site is available offline.');
|
|
} else if (totalCached > 0) {
|
|
updateStatusCard('warning', '📥', 'Partially cached (' + percentCached + '%)',
|
|
totalCached.toLocaleString() + ' of ' + TOTAL_SITE_PAGES.toLocaleString() + ' pages. Click download to continue.');
|
|
} else {
|
|
updateStatusCard('warning', '📥', 'Ready to download',
|
|
'Click the download button to cache all ' + TOTAL_SITE_PAGES.toLocaleString() + ' pages for offline access.');
|
|
}
|
|
} else {
|
|
swStatus.innerHTML = '<span class="status-error">✗ Not active</span>';
|
|
updateStatusCard('error', '⚠️', 'Service worker not active',
|
|
'Try refreshing the page. If using a content blocker, you may need to whitelist this site.');
|
|
}
|
|
} catch (err) {
|
|
swStatus.innerHTML = '<span class="status-error">✗ Error</span>';
|
|
updateStatusCard('error', '❌', 'Error', err.message);
|
|
}
|
|
} else {
|
|
swStatus.innerHTML = '<span class="status-error">✗ Not supported</span>';
|
|
updateStatusCard('error', '🌐', 'Not supported', 'Your browser does not support service workers.');
|
|
}
|
|
}
|
|
|
|
async function checkCaches() {
|
|
if ('caches' in window) {
|
|
try {
|
|
const cacheNames = await caches.keys();
|
|
cacheStatus.innerHTML = cacheNames.length > 0 ?
|
|
'<span class="status-ok">✓ ' + cacheNames.length + ' cache(s)</span>' :
|
|
'<span class="status-error">✗ Empty</span>';
|
|
|
|
const allUrls = [];
|
|
for (const cacheName of cacheNames) {
|
|
const cache = await caches.open(cacheName);
|
|
const keys = await cache.keys();
|
|
keys.forEach(req => {
|
|
const url = new URL(req.url);
|
|
if (url.pathname && !url.pathname.startsWith('/static/')) {
|
|
allUrls.push(url.pathname);
|
|
}
|
|
});
|
|
}
|
|
|
|
const uniqueUrls = [...new Set(allUrls)].sort();
|
|
pagesCached.innerHTML = '<span class="status-ok">' + uniqueUrls.length.toLocaleString() + '</span>';
|
|
cachedCount.textContent = uniqueUrls.length.toLocaleString();
|
|
|
|
const categories = {
|
|
'Main Pages': [], 'Bible Chapters': [], 'Verse Commentary': [],
|
|
'Interlinear': [], 'Stories': [], 'Topics': [],
|
|
'Study Guides': [], 'Reading Plans': [], 'Resources': [], 'Other': []
|
|
};
|
|
|
|
uniqueUrls.forEach(url => {
|
|
if (url === '/' || url === '/books' || url === '/offline' || url === '/search' || url === '/resources' || url === '/study-guides' || url === '/reading-plans' || url === '/topics' || url === '/stories') {
|
|
categories['Main Pages'].push(url);
|
|
} else if (url.match(/^\/book\/[^\/]+\/\d+$/)) {
|
|
categories['Bible Chapters'].push(url);
|
|
} else if (url.match(/^\/book\/[^\/]+\/\d+\/\d+/)) {
|
|
categories['Verse Commentary'].push(url);
|
|
} else if (url.startsWith('/interlinear/')) {
|
|
categories['Interlinear'].push(url);
|
|
} else if (url.startsWith('/book/')) {
|
|
categories['Bible Chapters'].push(url);
|
|
} else if (url.startsWith('/topics/')) {
|
|
categories['Topics'].push(url);
|
|
} else if (url.startsWith('/stories/')) {
|
|
categories['Stories'].push(url);
|
|
} else if (url.startsWith('/study-guides/')) {
|
|
categories['Study Guides'].push(url);
|
|
} else if (url.startsWith('/reading-plans/')) {
|
|
categories['Reading Plans'].push(url);
|
|
} else {
|
|
categories['Other'].push(url);
|
|
}
|
|
});
|
|
|
|
function formatLabel(url) {
|
|
if (url === '/') return 'Home';
|
|
let label = decodeURIComponent(url).replace(/^\//, '').replace(/-/g, ' ');
|
|
['book/', 'interlinear/', 'stories/', 'topics/', 'study-guides/', 'reading-plans/'].forEach(p => {
|
|
label = label.replace(p.replace('-', ' '), '');
|
|
});
|
|
return label;
|
|
}
|
|
|
|
let html = '';
|
|
for (const [category, urls] of Object.entries(categories)) {
|
|
if (urls.length > 0) {
|
|
html += '<div class="url-category">';
|
|
html += '<div class="url-category-header" onclick="this.parentNode.classList.toggle(\'expanded\')">';
|
|
html += '<span>' + category + ' <span class="url-count">(' + urls.length.toLocaleString() + ')</span></span>';
|
|
html += '<span class="url-category-toggle">▶</span>';
|
|
html += '</div>';
|
|
html += '<div class="url-category-content">';
|
|
urls.slice(0, 500).forEach(url => {
|
|
html += '<a href="' + url + '" class="url-item">' + formatLabel(url) + '</a>';
|
|
});
|
|
if (urls.length > 500) {
|
|
html += '<div style="padding: 0.5rem; color: var(--text-secondary); font-size: 0.85rem;">...and ' + (urls.length - 500).toLocaleString() + ' more</div>';
|
|
}
|
|
html += '</div></div>';
|
|
}
|
|
}
|
|
cachedLinks.innerHTML = html || '<p style="color: var(--text-secondary);">No pages cached yet.</p>';
|
|
} catch (err) {
|
|
cacheStatus.innerHTML = '<span class="status-error">✗ Error</span>';
|
|
}
|
|
}
|
|
}
|
|
|
|
async function checkBibleData() {
|
|
try {
|
|
let found = false;
|
|
const cacheNames = await caches.keys();
|
|
for (const cacheName of cacheNames) {
|
|
const cache = await caches.open(cacheName);
|
|
const response = await cache.match('/static/verses-1769.json');
|
|
if (response) {
|
|
const data = await response.json();
|
|
bibleStatus.innerHTML = '<span class="status-ok">✓ ' + Object.keys(data).length.toLocaleString() + ' verses</span>';
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
bibleStatus.innerHTML = '<span class="status-error">✗ Not cached</span>';
|
|
}
|
|
} catch (err) {
|
|
bibleStatus.innerHTML = '<span class="status-error">✗ Error</span>';
|
|
}
|
|
}
|
|
|
|
checkServiceWorker();
|
|
checkCaches();
|
|
checkBibleData();
|
|
|
|
// Download controls
|
|
const startCacheBtn = document.getElementById('start-cache-btn');
|
|
const cacheProgress = document.getElementById('cache-progress');
|
|
const progressFill = document.getElementById('progress-fill');
|
|
const progressText = document.getElementById('progress-text');
|
|
|
|
startCacheBtn.addEventListener('click', async function() {
|
|
if (!('serviceWorker' in navigator)) return alert('Service workers not supported.');
|
|
const registration = await navigator.serviceWorker.getRegistration();
|
|
if (!registration || !registration.active) return alert('Service worker not active. Refresh and try again.');
|
|
|
|
startCacheBtn.disabled = true;
|
|
startCacheBtn.querySelector('span').textContent = 'Downloading...';
|
|
cacheProgress.classList.add('active');
|
|
progressText.textContent = 'Starting...';
|
|
registration.active.postMessage({ type: 'START_CACHING' });
|
|
});
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.addEventListener('message', function(event) {
|
|
const data = event.data;
|
|
if (data.type === 'CACHE_PROGRESS') {
|
|
cacheProgress.classList.add('active');
|
|
startCacheBtn.disabled = true;
|
|
const pct = Math.round((data.cached / data.total) * 100);
|
|
progressFill.style.width = pct + '%';
|
|
progressText.textContent = data.cached.toLocaleString() + ' / ' + data.total.toLocaleString() + ' (' + pct + '%)';
|
|
} else if (data.type === 'CACHE_COMPLETE') {
|
|
progressFill.style.width = '100%';
|
|
progressText.innerHTML = '<span class="cache-complete">✓ Complete! ' + data.total.toLocaleString() + ' pages cached.</span>';
|
|
startCacheBtn.querySelector('span').textContent = 'Refresh Cache';
|
|
startCacheBtn.disabled = false;
|
|
setTimeout(() => { checkCaches(); checkServiceWorker(); }, 500);
|
|
} else if (data.type === 'CACHE_ERROR') {
|
|
progressText.innerHTML = '<span class="status-error">Error: ' + data.error + '</span>';
|
|
startCacheBtn.disabled = false;
|
|
startCacheBtn.querySelector('span').textContent = 'Try Again';
|
|
}
|
|
});
|
|
|
|
navigator.serviceWorker.ready.then(function(registration) {
|
|
if (registration.active) registration.active.postMessage({ type: 'GET_CACHE_STATUS' });
|
|
});
|
|
}
|
|
|
|
// Bible Reader
|
|
let bibleData = null, bookStructure = {}, currentBook = '', currentChapter = 1;
|
|
const BOOKS = ['Genesis','Exodus','Leviticus','Numbers','Deuteronomy','Joshua','Judges','Ruth','1 Samuel','2 Samuel','1 Kings','2 Kings','1 Chronicles','2 Chronicles','Ezra','Nehemiah','Esther','Job','Psalms','Proverbs','Ecclesiastes','Song of Solomon','Isaiah','Jeremiah','Lamentations','Ezekiel','Daniel','Hosea','Joel','Amos','Obadiah','Jonah','Micah','Nahum','Habakkuk','Zephaniah','Haggai','Zechariah','Malachi','Matthew','Mark','Luke','John','Acts','Romans','1 Corinthians','2 Corinthians','Galatians','Ephesians','Philippians','Colossians','1 Thessalonians','2 Thessalonians','1 Timothy','2 Timothy','Titus','Philemon','Hebrews','James','1 Peter','2 Peter','1 John','2 John','3 John','Jude','Revelation'];
|
|
const bookSelect = document.getElementById('book-select');
|
|
const chapterSelect = document.getElementById('chapter-select');
|
|
const prevBtn = document.getElementById('prev-btn');
|
|
const nextBtn = document.getElementById('next-btn');
|
|
const content = document.getElementById('chapter-content');
|
|
|
|
async function loadBibleData() {
|
|
try {
|
|
let response = null;
|
|
const cacheNames = await caches.keys();
|
|
for (const cacheName of cacheNames) {
|
|
const cache = await caches.open(cacheName);
|
|
response = await cache.match('/static/verses-1769.json');
|
|
if (response) break;
|
|
}
|
|
if (!response) response = await fetch('/static/verses-1769.json');
|
|
if (!response || !response.ok) throw new Error('Could not load Bible data');
|
|
bibleData = await response.json();
|
|
|
|
bookStructure = {};
|
|
for (const ref in bibleData) {
|
|
const m = ref.match(/^(.+) (\d+):(\d+)$/);
|
|
if (m) {
|
|
const [, book, chapter, verse] = m;
|
|
if (!bookStructure[book]) bookStructure[book] = {};
|
|
if (!bookStructure[book][chapter]) bookStructure[book][chapter] = [];
|
|
bookStructure[book][chapter].push(parseInt(verse));
|
|
}
|
|
}
|
|
|
|
BOOKS.forEach(book => {
|
|
if (bookStructure[book]) {
|
|
const opt = document.createElement('option');
|
|
opt.value = book;
|
|
opt.textContent = book;
|
|
bookSelect.appendChild(opt);
|
|
}
|
|
});
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const book = params.get('book'), chapter = params.get('chapter');
|
|
if (book && bookStructure[book]) {
|
|
bookSelect.value = book;
|
|
populateChapters(book);
|
|
if (chapter && bookStructure[book][chapter]) {
|
|
chapterSelect.value = chapter;
|
|
renderChapter(book, chapter);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load Bible:', err);
|
|
}
|
|
}
|
|
|
|
function populateChapters(book) {
|
|
chapterSelect.innerHTML = '<option value="">Chapter</option>';
|
|
if (!bookStructure[book]) return;
|
|
Object.keys(bookStructure[book]).map(Number).sort((a,b) => a-b).forEach(ch => {
|
|
const opt = document.createElement('option');
|
|
opt.value = ch;
|
|
opt.textContent = ch;
|
|
chapterSelect.appendChild(opt);
|
|
});
|
|
chapterSelect.disabled = false;
|
|
}
|
|
|
|
function renderChapter(book, chapter) {
|
|
if (!bookStructure[book] || !bookStructure[book][chapter]) return;
|
|
currentBook = book;
|
|
currentChapter = parseInt(chapter);
|
|
content.classList.add('active');
|
|
|
|
const verses = bookStructure[book][chapter].sort((a,b) => a-b);
|
|
let html = '<h3>' + book + ' ' + chapter + '</h3>';
|
|
verses.forEach(v => {
|
|
const text = (bibleData[book + ' ' + chapter + ':' + v] || '').replace(/^#\s*/, '');
|
|
html += '<p><span class="verse-num">' + v + '</span>' + text + '</p>';
|
|
});
|
|
content.innerHTML = html;
|
|
|
|
const chapters = Object.keys(bookStructure[currentBook]).map(Number).sort((a,b) => a-b);
|
|
const bookIdx = BOOKS.indexOf(currentBook);
|
|
prevBtn.disabled = !(currentChapter > chapters[0] || bookIdx > 0);
|
|
nextBtn.disabled = !(currentChapter < chapters[chapters.length-1] || bookIdx < BOOKS.length-1);
|
|
|
|
history.replaceState(null, '', '/offline?book=' + encodeURIComponent(book) + '&chapter=' + chapter);
|
|
}
|
|
|
|
function navigate(dir) {
|
|
const chapters = Object.keys(bookStructure[currentBook]).map(Number).sort((a,b) => a-b);
|
|
const idx = chapters.indexOf(currentChapter);
|
|
if (dir === -1) {
|
|
if (idx > 0) { chapterSelect.value = chapters[idx-1]; renderChapter(currentBook, chapters[idx-1]); }
|
|
else {
|
|
const bi = BOOKS.indexOf(currentBook);
|
|
if (bi > 0) {
|
|
const pb = BOOKS[bi-1], pc = Object.keys(bookStructure[pb]).map(Number).sort((a,b) => a-b);
|
|
bookSelect.value = pb; populateChapters(pb); chapterSelect.value = pc[pc.length-1];
|
|
renderChapter(pb, pc[pc.length-1]);
|
|
}
|
|
}
|
|
} else {
|
|
if (idx < chapters.length-1) { chapterSelect.value = chapters[idx+1]; renderChapter(currentBook, chapters[idx+1]); }
|
|
else {
|
|
const bi = BOOKS.indexOf(currentBook);
|
|
if (bi < BOOKS.length-1) {
|
|
const nb = BOOKS[bi+1], nc = Object.keys(bookStructure[nb]).map(Number).sort((a,b) => a-b);
|
|
bookSelect.value = nb; populateChapters(nb); chapterSelect.value = nc[0];
|
|
renderChapter(nb, nc[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bookSelect.addEventListener('change', function() {
|
|
if (this.value) {
|
|
populateChapters(this.value);
|
|
const ch = Object.keys(bookStructure[this.value]).map(Number).sort((a,b) => a-b)[0];
|
|
chapterSelect.value = ch;
|
|
renderChapter(this.value, ch);
|
|
}
|
|
});
|
|
chapterSelect.addEventListener('change', function() { if (this.value && currentBook) renderChapter(currentBook, this.value); });
|
|
prevBtn.addEventListener('click', () => navigate(-1));
|
|
nextBtn.addEventListener('click', () => navigate(1));
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.target.tagName === 'SELECT' || e.target.tagName === 'INPUT') return;
|
|
if (e.key === 'ArrowLeft' || e.key === 'h') { e.preventDefault(); if (!prevBtn.disabled) navigate(-1); }
|
|
else if (e.key === 'ArrowRight' || e.key === 'l') { e.preventDefault(); if (!nextBtn.disabled) navigate(1); }
|
|
else if (e.key === 'g') { e.preventDefault(); bookSelect.value = 'Genesis'; populateChapters('Genesis'); chapterSelect.value = '1'; renderChapter('Genesis', '1'); }
|
|
else if (e.key === '?') { e.preventDefault(); alert('Keyboard shortcuts:\n\n←/h - Previous chapter\n→/l - Next chapter\ng - Go to Genesis 1\n? - Show help'); }
|
|
});
|
|
|
|
loadBibleData();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|