From 004bf7bc409ab801a2687ac405b36f8fb944ca16 Mon Sep 17 00:00:00 2001 From: jd-code76 Date: Sat, 1 Nov 2025 21:24:38 -0400 Subject: [PATCH 1/4] Add files via upload Initial release for GitHub pages. --- public/index.html | 1 + public/main.js | 610 +++++++++++++++++++++++++++++++++++ public/modules/api.js | 115 +++++++ public/modules/navigation.js | 252 +++++++++++++++ public/modules/passage.js | 274 ++++++++++++++++ public/modules/pdf.js | 372 +++++++++++++++++++++ public/modules/settings.js | 274 ++++++++++++++++ public/modules/state.js | 399 +++++++++++++++++++++++ public/modules/strongs.js | 254 +++++++++++++++ public/modules/ui.js | 407 +++++++++++++++++++++++ public/styles.css | 363 +++++++++++++++++++++ public/sw.js | 249 ++++++++++++++ 12 files changed, 3570 insertions(+) create mode 100644 public/index.html create mode 100644 public/main.js create mode 100644 public/modules/api.js create mode 100644 public/modules/navigation.js create mode 100644 public/modules/passage.js create mode 100644 public/modules/pdf.js create mode 100644 public/modules/settings.js create mode 100644 public/modules/state.js create mode 100644 public/modules/strongs.js create mode 100644 public/modules/ui.js create mode 100644 public/styles.css create mode 100644 public/sw.js diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f2aa5e1 --- /dev/null +++ b/public/index.html @@ -0,0 +1 @@ + Provinent Scripture Study

Welcome to Provinent Scripture Study!

A comprehensive Bible study companion with daily passages, notes, and powerful study tools.

Daily Passages

Get a new scripture passage every day with sequential or specific book reading plans.

Highlight & Annotate

Right‑click verses to highlight in colors. Take notes with full markdown support.

Original Languages

Click any verse to access Strong's Concordance, Greek/Hebrew interlinear, and more.

Reference Bible

Open a side‑by‑side reference panel to compare translations while you study.

Export & Import

Save your highlights and notes. Import/export your data anytime.

Offline Mode

Upload a PDF Bible to read and take notes while offline.

Optional: Upload a PDF to View Offline

You can upload a free Berean Standard Bible PDF. This is entirely optional – you can skip this and use the online version.

2. Upload the downloaded PDF here:

Click to select your downloaded PDF

Or drag and drop here

Reference Bible

Page of 0 Zoom: 100%

Provinent Scripture Study

Click any verse for further analysis • Right‑click to highlight

Passage of the Day

Attribution

Scripture quotations are provided by API from bible.helloao.org, without which this app would not have been possible. And a thank you to Berean Bible for their excellent translation work. All copyrights reserved by their respective owners. Currently selected translation: Berean Standard Bible®

Study Notes

\ No newline at end of file diff --git a/public/main.js b/public/main.js new file mode 100644 index 0000000..5d6f768 --- /dev/null +++ b/public/main.js @@ -0,0 +1,610 @@ +import { + initBookChapterControls, + nextPassage, + prevPassage, + randomPassage +} from './modules/navigation.js' +import { loadPassage, setupFootnoteHandlers } from './modules/passage.js' +import { + clearSearch, + currentSearch, + handlePDFUpload, + navigateToSearchResult, + renderPage, + savePDFToIndexedDB, + searchPDF, + setupPDFCleanup, + updateCustomPdfInfo, + updatePDFZoom +} from './modules/pdf.js' +import { + clearCache, + closeSettings, + deleteAllData, + exportData, + importData, + openSettings, + restartReadingPlan, + resumeReadingPlan, + saveSettings +} from './modules/settings.js' +import { + updateBibleGatewayVersion, + loadFromCookies, + loadFromStorage, + saveToCookies, + saveToStorage, + state +} from './modules/state.js' +import { closeStrongsPopup } from './modules/strongs.js' +import { + exportNotes, + initResizeHandles, + insertMarkdown, + makeToggleSticky, + restoreBookChapterUI, + restorePanelStates, + restoreSidebarState, + switchNotesView, + toggleNotes, + togglePanelCollapse, + toggleReferencePanel, + toggleSection, + updateMarkdownPreview, + updateReferencePanel +} from './modules/ui.js' +if (typeof pdfjsLib !== 'undefined') { + pdfjsLib.GlobalWorkerOptions.workerSrc = + 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; +} +if (typeof marked !== 'undefined') { + marked.setOptions({ + breaks: true, + gfm: true + }); +} +function updateDateTime() { + const now = new Date(); + document.getElementById('currentDate').textContent = + now.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); +} +class AppError extends Error { + constructor(message, type, originalError) { + super(message); + this.name = 'AppError'; + this.type = type; + this.originalError = originalError; + } +} +export function handleError(error, context) { + console.error(`Error in ${context}:`, error); + const userMessage = error instanceof AppError + ? error.message + : 'An unexpected error occurred'; + showError(userMessage); + if (window.errorTracker) { + window.errorTracker.log(error, context); + } +} +function setupEventListeners() { + document.getElementById('getStartedBtn') + .addEventListener('click', completeWelcome); + document.getElementById('welcomePdfUploadArea') + .addEventListener('click', () => { + document.getElementById('welcomePdfUpload').click(); + }); + document.getElementById('welcomePdfUpload') + .addEventListener('change', handleWelcomePDFUpload); + document.querySelector('.theme-toggle') + .addEventListener('click', toggleTheme); + document.getElementById('openSettingsBtn') + .addEventListener('click', openSettings); + document.getElementById('exportDataBtn') + .addEventListener('click', exportData); + document.getElementById('importDataBtn') + .addEventListener('click', () => { + document.getElementById('importFile').click(); + }); + document.getElementById('importFile') + .addEventListener('change', importData); + document.querySelector('.toggle-notes') + .addEventListener('click', toggleNotes); + document.getElementById('prevPassageBtn') + .addEventListener('click', prevPassage); + document.getElementById('nextPassageBtn') + .addEventListener('click', nextPassage); + document.getElementById('resumeReadingPlanBtn') + .addEventListener('click', () => { + if (confirm('Return to the daily reading plan where you left off?')) { + resumeReadingPlan(); + } + }); + document.getElementById('randomPassageBtn') + .addEventListener('click', randomPassage); + document.getElementById('referencePanelToggle') + .addEventListener('click', toggleReferencePanel); + document.querySelectorAll('.sidebar-section-header') + .forEach(h => h.addEventListener('click', () => { + const sec = h.dataset.section; + toggleSection(sec); + })); + document.getElementById('referenceTranslation').addEventListener('change', function() { + const tempTranslation = this.value; + const oldTranslation = state.settings.referenceVersion; + state.settings.referenceVersion = tempTranslation; + updateBibleGatewayVersion(); + state.settings.referenceVersion = oldTranslation; + }); + document.addEventListener('DOMContentLoaded', makeToggleSticky); + document.querySelectorAll('.collapse-toggle') + .forEach(btn => btn.addEventListener('click', function () { + const panel = this.closest('[id]'); + if (panel) togglePanelCollapse(panel.id); + })); + document.getElementById('referenceSource') + .addEventListener('change', updateReferencePanel); + document.getElementById('referenceTranslation') + .addEventListener('change', updateReferencePanel); + document.querySelector('.reference-panel-close') + .addEventListener('click', toggleReferencePanel); + document.getElementById('prevPage').addEventListener('click', async () => { + if (!state.pdf.doc || state.pdf.currentPage <= 1) return; + try { + if (state.pdf.renderTask) { + await state.pdf.renderTask.cancel(); + state.pdf.renderTask = null; + } + state.pdf.currentPage--; + await renderPage(state.pdf.currentPage); + } catch (err) { + console.warn('Error navigating to previous page:', err); + await loadPDF(); + } + }); + document.getElementById('nextPage').addEventListener('click', async () => { + if (!state.pdf.doc || state.pdf.currentPage >= state.pdf.doc.numPages) return; + try { + if (state.pdf.renderTask) { + await state.pdf.renderTask.cancel(); + state.pdf.renderTask = null; + } + state.pdf.currentPage++; + await renderPage(state.pdf.currentPage); + } catch (err) { + console.warn('Error navigating to next page:', err); + await loadPDF(); + } + }); + document.getElementById('pageInput').addEventListener('change', async () => { + if (!state.pdf.doc) { + document.getElementById('pageInput').value = state.pdf.currentPage; + return; + } + const inp = document.getElementById('pageInput'); + let p = parseInt(inp.value, 10); + if (Number.isNaN(p)) { + inp.value = state.pdf.currentPage; + return; + } + p = Math.max(1, Math.min(p, state.pdf.doc.numPages)); + try { + state.pdf.currentPage = p; + await renderPage(p); + } catch (err) { + console.warn('Error navigating to page:', err); + inp.value = state.pdf.currentPage; + await loadPDF(); + } + }); + document.getElementById('zoomIn').addEventListener('click', () => { + if (!state.pdf.doc) return; + const newZoom = Math.min(state.pdf.zoomLevel + 0.25, 3.0); + updatePDFZoom(newZoom); + }); + document.getElementById('zoomOut').addEventListener('click', () => { + if (!state.pdf.doc) return; + const newZoom = Math.max(state.pdf.zoomLevel - 0.25, 0.5); + updatePDFZoom(newZoom); + }); + document.getElementById('pdfSearchBtn').addEventListener('click', searchPDF); + document.getElementById('pdfSearchInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') searchPDF(); + }); + document.getElementById('clearSearchBtn').addEventListener('click', clearSearch); + const nextSearchBtn = document.getElementById('nextSearchResult'); + const prevSearchBtn = document.getElementById('prevSearchResult'); + if (nextSearchBtn) { + nextSearchBtn.addEventListener('click', () => { + navigateToSearchResult(currentSearch.currentResult + 1); + }); + } + if (prevSearchBtn) { + prevSearchBtn.addEventListener('click', () => { + navigateToSearchResult(currentSearch.currentResult - 1); + }); + } + document.getElementById('notesInput') + .addEventListener('input', e => { + state.notes = e.target.value; + saveToStorage(); + if (state.settings.notesView === 'markdown') { + updateMarkdownPreview(); + } + }); + document.getElementById('textViewBtn') + .addEventListener('click', () => switchNotesView('text')); + document.getElementById('markdownViewBtn') + .addEventListener('click', () => switchNotesView('markdown')); + document.querySelectorAll('.markdown-btn') + .forEach(btn => btn.addEventListener('click', () => { + const fmt = btn.dataset.format; + insertMarkdown(fmt); + })); + document.querySelectorAll('.notes-controls button') + .forEach(btn => btn.addEventListener('click', () => { + const fmt = btn.dataset.format; + exportNotes(fmt); + })); + document.querySelectorAll('.color-option') + .forEach(opt => opt.addEventListener('click', () => { + const col = opt.dataset.color; + applyHighlight(col); + })); + document.getElementById('removeHighlight') + .addEventListener('click', () => applyHighlight('none')); + document.addEventListener('contextmenu', e => { + const verse = e.target.closest('.verse'); + if (verse) { + e.preventDefault(); + showColorPicker(e, verse); + } + }); + document.addEventListener('click', e => { + const picker = document.getElementById('colorPicker'); + if (!picker.contains(e.target) && !e.target.closest('.verse')) { + picker.classList.remove('active'); + } + }); + document.getElementById('popupOverlay') + .addEventListener('click', closeStrongsPopup); + document.querySelector('#strongsPopup .popup-close') + .addEventListener('click', closeStrongsPopup); + document.getElementById('settingsOverlay') + .addEventListener('click', closeSettings); + document.getElementById('closeSettingsBtn') + .addEventListener('click', closeSettings); + document.getElementById('cancelSettingsBtn') + .addEventListener('click', closeSettings); + document.getElementById('saveSettingsBtn') + .addEventListener('click', saveSettings); + document.getElementById('clearHighlightsBtn') + .addEventListener('click', clearHighlights); + document.getElementById('restartReadingPlanBtn') + .addEventListener('click', () => { + restartReadingPlan(); + }); + document.addEventListener('contentLoaded', () => { + setTimeout(setupFootnoteHandlers, 50); + }); + document.getElementById('clearCacheBtn') + .addEventListener('click', clearCache); + document.getElementById('deleteAllDataBtn') + .addEventListener('click', deleteAllData); + document.getElementById('settingsPdfUploadBtn') + .addEventListener('click', () => { + document.getElementById('settingsPdfUpload').click(); + }); + document.getElementById('settingsPdfUpload') + .addEventListener('change', handlePDFUpload); + document.querySelectorAll('.color-theme-option') + .forEach(opt => opt.addEventListener('click', () => { + const theme = opt.dataset.theme; + selectColorTheme(theme); + })); + document.addEventListener('keydown', e => { + const ta = document.getElementById('notesInput'); + if (document.activeElement !== ta) return; + if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { + switch (e.key.toLowerCase()) { + case 'b': + e.preventDefault(); + insertMarkdown('bold'); + break; + case 'i': + e.preventDefault(); + insertMarkdown('italic'); + break; + } + } + }); +} +export function arrayBufferToBase64(buf) { + let binary = ''; + const bytes = new Uint8Array(buf); + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode.apply( + null, + Array.from(bytes.subarray(i, i + chunk)) + ); + } + return btoa(binary); +} +export function base64ToArrayBuffer(b64) { + const bin = atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) { + arr[i] = bin.charCodeAt(i); + } + return arr.buffer; +} +export function readFileAsArrayBuffer(file) { + return new Promise((resolve, reject) => { + const r = new FileReader(); + r.onload = e => resolve(e.target.result); + r.onerror = () => reject(new Error('Failed to read file')); + r.readAsArrayBuffer(file); + }); +} +export function showLoading(flag) { + document.getElementById('loadingOverlay').classList.toggle('active', flag); +} +export function showError(msg) { + document.getElementById('errorContainer').innerHTML = + `
${msg}
`; +} +export function clearError() { + document.getElementById('errorContainer').innerHTML = ''; +} +async function registerServiceWorker() { + if ('serviceWorker' in navigator) { + try { + const response = await fetch('/sw.js'); + if (!response.ok) { + console.error('Service worker script not found or inaccessible'); + return null; + } + const registration = await navigator.serviceWorker.register('/sw.js', { + scope: '/' + }); + console.log('Service Worker registered successfully:', registration); + return registration; + } catch (err) { + console.error('Service Worker registration failed:', err); + if (err.message.includes('MIME')) { + console.error('MIME type issue - ensure server serves sw.js as application/javascript'); + } + return null; + } + } else { + console.log('Service workers are not supported'); + return null; + } +} +function updateOfflineStatus(isOffline) { + const indicator = document.getElementById('offlineIndicator'); + if (!indicator) { + const newIndicator = document.createElement('div'); + newIndicator.id = 'offlineIndicator'; + newIndicator.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + padding: 10px; + background: ${isOffline ? '#ff6b6b' : '#51cf66'}; + color: white; + border-radius: 5px; + z-index: 10000; + font-size: 14px; + transition: all 0.3s ease; + `; + newIndicator.textContent = isOffline ? 'Offline Mode' : 'Online'; + document.body.appendChild(newIndicator); + setTimeout(() => { + newIndicator.style.opacity = '0'; + setTimeout(() => newIndicator.remove(), 300); + }, 3000); + } else { + indicator.textContent = isOffline ? 'Offline Mode' : 'Online'; + indicator.style.background = isOffline ? '#ff6b6b' : '#51cf66'; + } +} +const offlineStyles = ` +#offlineIndicator { + position: fixed; + top: 10px; + right: 10px; + padding: 10px 15px; + background: #ff6b6b; + color: white; + border-radius: 5px; + z-index: 10000; + font-size: 14px; + font-weight: bold; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + transition: all 0.3s ease; +} +#offlineIndicator.online { + background: #51cf66; +} +`; +function showColorPicker(ev, verseEl) { + const picker = document.getElementById('colorPicker'); + state.currentVerse = verseEl; + picker.style.left = ev.pageX + 'px'; + picker.style.top = ev.pageY + 'px'; + picker.classList.add('active'); +} +function applyHighlight(col) { + if (!state.currentVerse) return; + const verseRef = state.currentVerse.dataset.verse; + state.currentVerse.classList.remove( + 'highlight-yellow', 'highlight-green', 'highlight-blue', + 'highlight-pink', 'highlight-orange', 'highlight-purple' + ); + if (col !== 'none') { + state.currentVerse.classList.add(`highlight-${col}`); + state.highlights[verseRef] = col; + } else { + delete state.highlights[verseRef]; + } + saveToStorage(); + document.getElementById('colorPicker').classList.remove('active'); +} +function clearHighlights() { + if (!confirm('Delete ALL highlights?')) return; + state.highlights = {}; + document.querySelectorAll('.verse') + .forEach(v => v.classList.remove( + 'highlight-yellow', 'highlight-green', 'highlight-blue', + 'highlight-pink', 'highlight-orange', 'highlight-purple' + )); + saveToStorage(); +} +function toggleTheme() { + state.settings.theme = state.settings.theme === 'light' ? 'dark' : 'light'; + applyTheme(); + saveToStorage(); + saveToCookies(); +} +export function applyTheme() { + document.documentElement.setAttribute('data-theme', state.settings.theme); + document.getElementById('themeIcon').textContent = + state.settings.theme === 'light' ? '🌙' : '☀️'; +} +export function selectColorTheme(t) { + state.settings.colorTheme = t; + applyColorTheme(); + document.querySelectorAll('.color-theme-option') + .forEach(o => o.classList.remove('selected')); + document.querySelector(`.color-theme-option[data-theme="${t}"]`) + .classList.add('selected'); +} +export function applyColorTheme() { + document.documentElement.setAttribute('data-color-theme', + state.settings.colorTheme); +} +async function handleWelcomePDFUpload(ev) { + try { + const file = ev.target.files[0]; + if (!file) return; + if (file.size > 50 * 1024 * 1024) { + alert('PDF file is too large (max 50 MiB).'); + ev.target.value = ''; + return; + } + state.welcomePdfFile = file; + document.getElementById('welcomePdfUploadArea').classList.add('has-file'); + document.getElementById('welcomeUploadText').innerHTML = ` + ${file.name}
+ Ready to use for offline mode`; + } catch (err) { + handleError(err, 'handleWelcomePDFUpload'); + } +} +async function completeWelcome() { + showLoading(true); + try { + if (state.welcomePdfFile) { + const reader = new FileReader(); + const arrayBuffer = await new Promise((resolve, reject) => { + reader.onload = (e) => resolve(e.target.result); + reader.onerror = (e) => reject(new Error('Failed to read file')); + reader.readAsArrayBuffer(state.welcomePdfFile); + }); + const bufferCopy = arrayBuffer.slice(0); + const loadingTask = pdfjsLib.getDocument({ data: bufferCopy }); + const pdf = await loadingTask.promise; + const base64 = arrayBufferToBase64(arrayBuffer); + const pdfData = { + name: state.welcomePdfFile.name, + data: base64, + uploadDate: new Date().toISOString(), + numPages: pdf.numPages + }; + await savePDFToIndexedDB(pdfData); + state.settings.customPdf = { + name: pdfData.name, + uploadDate: pdfData.uploadDate, + numPages: pdfData.numPages, + storedInDB: true + }; + } + state.settings.hasSeenWelcome = true; + saveToStorage(); + saveToCookies(); + document.getElementById('welcomeScreen').classList.add('hidden'); + await init(); + } catch (err) { + handleError(err, 'completeWelcome'); + alert('Error processing PDF: ' + err.message + + '. You can continue without offline mode.'); + state.settings.hasSeenWelcome = true; + saveToStorage(); + saveToCookies(); + document.getElementById('welcomeScreen').classList.add('hidden'); + await init(); + } finally { + showLoading(false); + } +} +function attachWelcomeListeners() { + document.getElementById('getStartedBtn') + .addEventListener('click', completeWelcome); + document.getElementById('welcomePdfUploadArea') + .addEventListener('click', () => { + document.getElementById('welcomePdfUpload').click(); + }); + document.getElementById('welcomePdfUpload') + .addEventListener('change', handleWelcomePDFUpload); +} +async function init() { + await loadFromStorage(); + loadFromCookies(); + setupPDFCleanup(); + const style = document.createElement('style'); + style.textContent = offlineStyles; + document.head.appendChild(style); + updateOfflineStatus(!navigator.onLine); + window.addEventListener('online', () => updateOfflineStatus(false)); + window.addEventListener('offline', () => updateOfflineStatus(true)); + if (!state.settings.readingMode) state.settings.readingMode = 'readingPlan'; + if (!state.settings.readingPlanId) state.settings.readingPlanId = 'default'; + initBookChapterControls(); + restoreBookChapterUI(); + if (!state.settings.hasSeenWelcome) { + attachWelcomeListeners(); + return; + } + document.getElementById('welcomeScreen').classList.add('hidden'); + applyTheme(); + applyColorTheme(); + restoreSidebarState(); + restorePanelStates(); + updateDateTime(); + initResizeHandles(); + updateCustomPdfInfo(); + switchNotesView(state.settings.notesView || 'text'); + updateBibleGatewayVersion(); + loadPassage(); + setupEventListeners(); + setInterval(updateDateTime, 1_000); + setTimeout(async () => { + try { + await registerServiceWorker(); + } catch (err) { + handleError(err, 'init'); + } + }, 1000); + console.log('App initialized successfully'); +} +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} \ No newline at end of file diff --git a/public/modules/api.js b/public/modules/api.js new file mode 100644 index 0000000..749cbfb --- /dev/null +++ b/public/modules/api.js @@ -0,0 +1,115 @@ +import { + clearError, + handleError, + showError, + showLoading +} from '../main.js' +import { + afterContentLoad, + displayPassage, + extractVerseText +} from './passage.js' +import { + bookNameMapping, + state +} from './state.js' +const API_BASE_URL = 'https://bible.helloao.org/api'; +const translationMap = { + BSB: 'BSB', + KJV: 'eng_kjv', + NET: 'eng_net', + ASV: 'eng_asv', + GNV: 'eng_gnv' +}; +export function apiTranslationCode(uiCode) { + return translationMap[uiCode] ?? uiCode; +} +export function getApiBookCode(displayName) { + const code = bookNameMapping[displayName]; + if (code) return code; + console.warn('Missing book‑code mapping for:', displayName); + showError(`Cannot load “${displayName}” – unknown book code.`); + throw new Error('Unknown book code'); +} +export async function fetchChapter(translation, book, chapter) { + if (!navigator.onLine) { + throw new Error('Offline mode: Cannot fetch new chapters. Using cached data if available.'); + } + const trans = translation.trim(); + const bk = book.replace(/\s+/g, '').toUpperCase(); + const ch = Number(chapter); + if (!trans || !bk || Number.isNaN(ch) || ch < 1) { + throw new Error('Invalid parameters for Bible API request'); + } + const url = `${API_BASE_URL}/${trans}/${bk}/${ch}.json`; + try { + const resp = await fetch(url, { + method: 'GET', + headers: { Accept: 'application/json' }, + cache: 'no-store' + }); + if (!resp.ok) { + const txt = await resp.text(); + throw new Error(`API error ${resp.status}: ${txt}`); + } + const ct = resp.headers.get('content-type') || ''; + if (!ct.includes('application/json')) { + if (ct.startsWith('<')) { + throw new Error('API returned HTML instead of JSON'); + } + try { + return JSON.parse(await resp.text()); + } catch (_) { + throw new Error('Unable to parse API response as JSON'); + } + } + return resp.json(); + } catch (err) { + handleError(err, 'fetchChapter'); + } +} +export async function loadPassageFromAPI(passageInfo) { + try { + showLoading(true); + const { book, chapter, startVerse, endVerse, displayRef } = passageInfo; + state.currentPassageReference = displayRef; + const apiMap = apiTranslationCode(state.settings.bibleTranslation); + const apiBook = getApiBookCode(book); + const chapterData = await fetchChapter(apiMap, apiBook, chapter); + if (!chapterData || !chapterData.chapter || + !Array.isArray(chapterData.chapter.content)) { + throw new Error('Malformed API response – missing chapter.content'); + } + const chapterFootnotes = chapterData.chapter.footnotes || []; + const footnoteCounter = { value: 1 }; + const verses = chapterData.chapter.content + .filter(v => + v.type === 'verse' && + v.number >= startVerse && + v.number <= endVerse + ) + .map(v => { + const verseData = extractVerseText(v.content, chapterFootnotes, footnoteCounter); + return { + number: v.number, + text: verseData, + reference: `${book} ${chapter}:${v.number}`, + rawContent: v.content + }; + }); + if (verses.length === 0) { + throw new Error('No verses found in the requested range'); + } + displayPassage(verses); + afterContentLoad(); + clearError(); + if (chapterData.translation && chapterData.translation.name) { + document.getElementById('bibleName').textContent = + chapterData.translation.name; + } + } catch (err) { + handleError(err, 'loadPassageFromAPI'); + } finally { + showLoading(false); + } +} \ No newline at end of file diff --git a/public/modules/navigation.js b/public/modules/navigation.js new file mode 100644 index 0000000..9199eed --- /dev/null +++ b/public/modules/navigation.js @@ -0,0 +1,252 @@ +import { + apiTranslationCode, + fetchChapter, + getApiBookCode, + loadPassageFromAPI +} from './api.js' +import { + clearError, + handleError, + showError, + showLoading +} from '../main.js' +import { + displayPassage, + extractVerseText, + loadPassage +} from './passage.js' +import { + BOOK_ORDER, + CHAPTER_COUNTS, + bookNameMapping, + getActivePlan, + saveToStorage, + state +} from './state.js' +import { updateReferencePanel } from './ui.js' +export function populateBookDropdown() { + const bookSel = document.getElementById('bookSelect'); + bookSel.innerHTML = ''; + BOOK_ORDER.forEach(book => { + const opt = document.createElement('option'); + opt.value = book; + opt.textContent = book; + bookSel.appendChild(opt); + }); +} +export function populateChapterDropdown(selectedBook) { + const chapSel = document.getElementById('chapterSelect'); + chapSel.innerHTML = ''; + const max = CHAPTER_COUNTS[selectedBook]; + for (let i = 1; i <= max; i++) { + const opt = document.createElement('option'); + opt.value = i; + opt.textContent = i; + chapSel.appendChild(opt); + } +} +export async function loadSelectedChapter(book = null, chapter = null) { + const selBook = book || document.getElementById('bookSelect').value; + const selChapter = chapter || document.getElementById('chapterSelect').value; + const apiBook = getApiBookCode(selBook); + try { + showLoading(true); + const apiTranslation = apiTranslationCode(state.settings.bibleTranslation); + const chapterData = await fetchChapter( + apiTranslation, + apiBook, + selChapter + ); + const chapterFootnotes = chapterData.chapter.footnotes || []; + const footnoteCounter = { value: 1 }; + const verses = chapterData.chapter.content + .filter(v => v.type === 'verse') + .map(v => ({ + number: v.number, + text: extractVerseText(v.content, chapterFootnotes, footnoteCounter), + reference: `${selBook} ${selChapter}:${v.number}` + })); + document.getElementById('passageReference').textContent = + `${selBook} ${selChapter}`; + state.footnotes = {}; + displayPassage(verses, `${selBook} ${selChapter}`); + clearError(); + document.getElementById('scriptureSection').scrollTop = 0; + if (state.settings.readingMode === 'manual') { + state.settings.manualBook = selBook; + state.settings.manualChapter = Number(selChapter); + saveToStorage(); + } + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + } catch (err) { + handleError(err, 'loadSelectedChapter'); + showError(`Could not load ${selBook} ${selChapter}: ${err.message}`); + } finally { + showLoading(false); + } +} +export function initBookChapterControls() { + populateBookDropdown(); + document.getElementById('bookSelect').addEventListener('change', e => { + const book = e.target.value; + state.settings.readingMode = 'manual'; + populateChapterDropdown(book); + state.settings.manualBook = book; + state.settings.manualChapter = 1; + const chapterSel = document.getElementById('chapterSelect'); + chapterSel.value = '1'; + loadSelectedChapter(book, 1); + saveToStorage(); + }); + document.getElementById('chapterSelect').addEventListener('change', () => { + const book = document.getElementById('bookSelect').value; + const chap = Number(document.getElementById('chapterSelect').value); + state.settings.readingMode = 'manual'; + state.settings.manualBook = book; + state.settings.manualChapter = chap; + loadSelectedChapter(book, chap); + saveToStorage(); + }); + populateChapterDropdown(BOOK_ORDER[0]); +} +export function manualPrevChapter() { + let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook); + let chap = state.settings.manualChapter; + if (chap > 1) { + state.settings.manualChapter = chap - 1; + } else { + if (bookIdx > 0) { + const prevBook = BOOK_ORDER[bookIdx - 1]; + const maxCh = CHAPTER_COUNTS[prevBook]; + state.settings.manualBook = prevBook; + state.settings.manualChapter = maxCh; + } else { + return; + } + } + state.settings.readingMode = 'manual'; + loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter); + syncBookChapterSelectors(); + saveToStorage(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } +} +export function manualNextChapter() { + let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook); + let chap = state.settings.manualChapter; + const maxCh = CHAPTER_COUNTS[state.settings.manualBook]; + if (chap < maxCh) { + state.settings.manualChapter = chap + 1; + } else { + if (bookIdx < BOOK_ORDER.length - 1) { + const nextBook = BOOK_ORDER[bookIdx + 1]; + state.settings.manualBook = nextBook; + state.settings.manualChapter = 1; + } else { + return; + } + } + state.settings.readingMode = 'manual'; + loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter); + syncBookChapterSelectors(); + saveToStorage(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } +} +export function prevPassage() { + if (state.settings.readingMode === 'readingPlan') { + const len = getActivePlan().length; + let newIndex = (state.settings.currentPassageIndex - 1 + len) % len; + state.settings.currentPassageIndex = newIndex; + loadPassage(); + } else { + manualPrevChapter(); + } + document.getElementById('scriptureSection').scrollTop = 0; +} +export function nextPassage() { + if (state.settings.readingMode === 'readingPlan') { + const len = getActivePlan().length; + let newIndex = (state.settings.currentPassageIndex + 1) % len; + if (newIndex < 0) newIndex = len - 1; + state.settings.currentPassageIndex = newIndex; + loadPassage(); + } else { + manualNextChapter(); + } + document.getElementById('scriptureSection').scrollTop = 0; +} +export async function randomPassage() { + try { + state.settings.readingMode = 'manual'; + const randomLoc = await getRandomBibleLocation(); + state.settings.manualBook = randomLoc.book; + state.settings.manualChapter = randomLoc.chapter; + saveToStorage(); + await loadPassageFromAPI(randomLoc); + document.getElementById('passageReference').textContent = randomLoc.displayRef; + state.currentPassageReference = randomLoc.displayRef; + syncBookChapterSelectors(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + } catch (err) { + handleError(err, 'randomPassage'); + showError('Could not load a random passage – see console for details.'); + } +} +export function syncBookChapterSelectors() { + const bookSel = document.getElementById('bookSelect'); + const chapterSel = document.getElementById('chapterSelect'); + if (bookSel.value !== state.settings.manualBook) { + bookSel.value = state.settings.manualBook; + populateChapterDropdown(state.settings.manualBook); + } + const curMax = CHAPTER_COUNTS[state.settings.manualBook]; + const curChap = state.settings.manualChapter; + populateChapterDropdown(state.settings.manualBook); + chapterSel.value = (curChap <= curMax) ? curChap : curMax; +} +export function syncSelectorsToReadingPlan() { + if (state.settings.readingMode !== 'readingPlan') return; + const plan = getActivePlan(); + const passage = plan[state.settings.currentPassageIndex]; + if (!passage || !passage.book) { + console.error('Invalid passage object:', passage); + return; + } + const bookSel = document.getElementById('bookSelect'); + const chapterSel = document.getElementById('chapterSelect'); + state.settings.manualBook = passage.book; + state.settings.manualChapter = passage.chapter; + if (bookSel) bookSel.value = passage.book; + populateChapterDropdown(passage.book); + if (chapterSel) chapterSel.value = passage.chapter; + saveToStorage(); +} +async function getRandomBibleLocation() { + try { + const randomBook = BOOK_ORDER[Math.floor(Math.random() * BOOK_ORDER.length)]; + const maxCh = CHAPTER_COUNTS[randomBook]; + const randomChapter = Math.floor(Math.random() * maxCh) + 1; + const apiMap = apiTranslationCode(state.settings.bibleTranslation); + const apiBook = bookNameMapping[randomBook] || + randomBook.replace(/\s+/g, '').toUpperCase(); + const chapterData = await fetchChapter(apiMap, apiBook, randomChapter); + const verses = chapterData.chapter.content.filter(v => v.type === 'verse'); + const verseCount = verses.length || 1; + return { + book: randomBook, + chapter: randomChapter, + startVerse: 1, + endVerse: verseCount, + displayRef: `${randomBook} ${randomChapter}` + }; + } catch (err) { + handleError(err, 'getRandomBibleLocation'); + } +} \ No newline at end of file diff --git a/public/modules/passage.js b/public/modules/passage.js new file mode 100644 index 0000000..0d35df6 --- /dev/null +++ b/public/modules/passage.js @@ -0,0 +1,274 @@ +import { loadPassageFromAPI } from './api.js' +import { handleError } from '../main.js' +import { syncSelectorsToReadingPlan } from './navigation.js' +import { + getActivePlan, + getCurrentPlanLabel, + getTranslationShorthand, + saveToStorage, + state +} from './state.js' +import { showStrongsReference } from './strongs.js' +import { updateReferencePanel } from './ui.js' +export function displayPassage(verses) { + const container = document.getElementById('scriptureContent'); + const fragment = document.createDocumentFragment(); + state.footnotes = {}; + const allFootnotes = []; + verses.forEach(v => { + const verseDiv = document.createElement('div'); + verseDiv.className = 'verse'; + verseDiv.dataset.verse = v.reference; + verseDiv.dataset.verseNumber = v.number; + let plainText = v.text.text; + plainText = plainText.replace(/<[^>]*>/g, ''); + plainText = plainText.replace(/\s+/g, ' ').trim(); + verseDiv.dataset.verseText = plainText; + const key = v.reference; + if (state.highlights[key]) { + verseDiv.classList.add(`highlight-${state.highlights[key]}`); + } + const numSpan = document.createElement('span'); + numSpan.className = 'verse-number'; + numSpan.textContent = v.number; + const txtSpan = document.createElement('span'); + txtSpan.className = 'verse-text'; + txtSpan.innerHTML = v.text.text; + if (v.text.footnotes && v.text.footnotes.length > 0) { + state.footnotes[v.reference] = v.text.footnotes; + allFootnotes.push(...v.text.footnotes); + } + verseDiv.appendChild(numSpan); + verseDiv.appendChild(txtSpan); + fragment.appendChild(verseDiv); + }); + container.innerHTML = ''; + container.appendChild(fragment); + if (allFootnotes.length > 0) { + const footnotesContainer = document.createElement('div'); + footnotesContainer.className = 'footnotes-container'; + const separator = document.createElement('hr'); + separator.className = 'footnotes-separator'; + const heading = document.createElement('h4'); + heading.className = 'footnotes-heading'; + heading.textContent = 'Footnotes'; + const footnotesFragment = document.createDocumentFragment(); + allFootnotes.sort((a, b) => a.number - b.number).forEach(fn => { + const footnoteElement = document.createElement('div'); + footnoteElement.className = 'footnote'; + footnoteElement.innerHTML = ` + ${fn.number} + ${fn.content} + `; + footnoteElement.dataset.footnoteId = fn.index; + footnoteElement.dataset.footnoteNumber = fn.number; + footnotesFragment.appendChild(footnoteElement); + }); + footnotesContainer.appendChild(footnotesFragment); + container.appendChild(separator); + container.appendChild(heading); + container.appendChild(footnotesContainer); + } + container.addEventListener('click', (e) => { + const verse = e.target.closest('.verse'); + if (verse && !e.target.closest('.footnote-ref')) { + showStrongsReference(verse); + } + }, { once: false }); + setTimeout(() => { + setupFootnoteHandlers(); + }, 100); +} +export function setupFootnoteHandlers() { + const scriptureContent = document.getElementById('scriptureContent'); + if (scriptureContent._footnoteHandler) { + scriptureContent.removeEventListener('click', scriptureContent._footnoteHandler); + } + const footnoteHandler = (e) => { + const footnoteRef = e.target.closest('[class*="footnote-ref"]'); + const footnoteElement = e.target.closest('.footnote'); + if (footnoteRef) { + e.preventDefault(); + e.stopPropagation(); + const footnoteId = (footnoteRef.dataset.footnoteId || '').trim(); + const footnoteNumber = (footnoteRef.dataset.footnoteNumber || '').trim(); + let targetFootnote = null; + if (footnoteId) { + targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-id="${footnoteId}"]`); + } + if (!targetFootnote && footnoteNumber) { + targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-number="${footnoteNumber}"]`); + } + if (!targetFootnote && footnoteId) { + const allFootnotes = scriptureContent.querySelectorAll('.footnote'); + for (const fn of allFootnotes) { + const fnId = (fn.dataset.footnoteId || '').trim(); + const fnNum = (fn.dataset.footnoteNumber || '').trim(); + if (fnId === footnoteId) { + targetFootnote = fn; + break; + } + } + } + if (targetFootnote) { + targetFootnote.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + targetFootnote.style.backgroundColor = 'var(--verse-hover)'; + setTimeout(() => { + targetFootnote.style.backgroundColor = ''; + }, 1000); + } else { + console.log('No target footnote found for ref:', footnoteId, footnoteNumber); + } + } + if (footnoteElement) { + e.preventDefault(); + e.stopPropagation(); + const footnoteId = (footnoteElement.dataset.footnoteId || '').trim(); + const footnoteNumber = (footnoteElement.dataset.footnoteNumber || '').trim(); + let targetRef = null; + const selectors = [ + `[class*="footnote-ref"][data-footnote-id="${footnoteId}"]`, + `[class*="footnote-ref"][data-footnote-number="${footnoteNumber}"]`, + `[class*="footnote-ref"][data-footnote-id="${footnoteNumber}"]`, + `[class*="footnote-ref"][data-footnote-number="${footnoteId}"]` + ]; + for (const selector of selectors) { + targetRef = scriptureContent.querySelector(selector); + if (targetRef) break; + } + if (!targetRef) { + const allRefs = scriptureContent.querySelectorAll('[class*="footnote-ref"]'); + for (const ref of allRefs) { + const refId = (ref.dataset.footnoteId || '').trim(); + const refNum = (ref.dataset.footnoteNumber || '').trim(); + if (refId === footnoteId || refNum === footnoteNumber || + refId === footnoteNumber || refNum === footnoteId) { + targetRef = ref; + break; + } + } + } + if (targetRef) { + targetRef.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + targetRef.style.backgroundColor = 'var(--verse-hover)'; + setTimeout(() => { + targetRef.style.backgroundColor = ''; + }, 1000); + } else { + console.log('No target ref found for footnote:', footnoteId, footnoteNumber); + console.log('Available refs:', scriptureContent.querySelectorAll('[class*="footnote-ref"]')); + } + } + }; + scriptureContent._footnoteHandler = footnoteHandler; + scriptureContent.addEventListener('click', footnoteHandler); +} +export function extractVerseText(content, chapterFootnotes = [], footnoteCounter) { + let txt = ''; + let footnotes = []; + for (const item of content) { + if (typeof item === 'string') { + txt += ensureProperSpacing(item); + } else if (item.text) { + txt += ensureProperSpacing(item.text); + } else if (item.heading) { + txt += ' ' + ensureProperSpacing(item.heading) + ' '; + } else if (item.noteId !== undefined) { + const footnote = chapterFootnotes.find(fn => fn.noteId === item.noteId); + if (footnote) { + const footnoteRef = `${footnoteCounter.value}`; + txt += footnoteRef; + footnotes.push({ + index: footnote.noteId, + number: footnoteCounter.value, + caller: footnote.caller, + content: footnote.text, + reference: footnote.reference + }); + footnoteCounter.value++; + } + } else if (item.type === 'verse') { + txt += ' '; + } else if (item.type === 'chapter') { + txt += ' '; + } else { + if (item.content && Array.isArray(item.content)) { + const nestedResult = extractVerseText(item.content, chapterFootnotes, footnoteCounter); + txt += nestedResult.text; + footnotes.push(...nestedResult.footnotes); + } + } + } + txt = txt.replace(/\s+/g, ' ').trim(); + txt = txt.replace(/\s+([.,;:!?])/g, '$1'); + txt = txt.replace(/([.,;:!?])(?=\w)/g, '$1 '); + txt = txt.replace(/\s*"\s*/g, '" '); + txt = txt.replace(/\s*'\s*/g, "' "); + return { + text: txt, + footnotes: footnotes + }; +} +function ensureProperSpacing(text) { + if (!text) return ''; + let cleanedText = text + .replace(/\s+/g, ' ') + .trim(); + cleanedText = cleanedText + .replace(/([^'"\s])\s+([.,;:!?])/g, '$1$2') + .replace(/([.,;:!?])(?=[A-Za-z])/g, '$1 ') + .replace(/\s*"\s*/g, (match) => { + if (match === '"' || match === ' "') return '"'; + return '" '; + }) + .replace(/\s*'\s*/g, (match) => { + if (match === "'" || match === " '") return "'"; + return "' "; + }); + cleanedText = cleanedText + .replace(/\s+/g, ' ') + .replace(/([.,;:!?]) (["'])/g, '$1$2') + .replace(/(["']) ([.,;:!?])/g, '$1$2') + .trim(); + return cleanedText; +} +export async function loadPassage() { + try { + const plan = getActivePlan(); + if (state.settings.currentPassageIndex < 0 || state.settings.currentPassageIndex >= plan.length) { + state.settings.currentPassageIndex = 0; + } + const passage = plan[state.settings.currentPassageIndex]; + const headerTitleEl = document.getElementById('passageHeaderTitle'); + if (headerTitleEl) { + const transShorthand = getTranslationShorthand(); + headerTitleEl.textContent = `Holy Bible: ${transShorthand}`; + } + const planLabelEl = document.getElementById('planLabel'); + if (planLabelEl) { + planLabelEl.textContent = `Reading plan: ${getCurrentPlanLabel()}`; + } + document.getElementById('passageReference').textContent = passage.displayRef; + state.currentPassageReference = passage.displayRef; + await loadPassageFromAPI(passage); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + saveToStorage(); + if (state.settings.readingMode === 'readingPlan') { + syncSelectorsToReadingPlan(); + } + } catch (err) { + handleError(err, 'loadPassage'); + } +} +export function afterContentLoad() { + const event = new CustomEvent('contentLoaded'); + document.dispatchEvent(event); +} \ No newline at end of file diff --git a/public/modules/pdf.js b/public/modules/pdf.js new file mode 100644 index 0000000..96e586d --- /dev/null +++ b/public/modules/pdf.js @@ -0,0 +1,372 @@ +import { + arrayBufferToBase64, + base64ToArrayBuffer, + handleError, + readFileAsArrayBuffer, + showLoading +} from '../main.js' +import { + saveToStorage, + state +} from './state.js' +import { updateReferencePanel } from './ui.js' +const DB_NAME = 'BibleStudyDB'; +const DB_VERSION = 1; +export const STORE_NAME = 'pdfStore'; +export let currentSearch = { + query: '', + results: [], + currentResult: -1, + highlights: [] +}; +export async function openDB() { + try { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onerror = () => reject(req.error); + req.onsuccess = () => resolve(req.result); + req.onupgradeneeded = ev => { + const db = ev.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + }); + } catch (err) { + handleError(err, 'openDB'); + } +} +export async function savePDFToIndexedDB(pdfData) { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction([STORE_NAME], 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.put(pdfData, 'customPdf'); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch (err) { + handleError(err, 'savePDFToIndexedDB'); + } +} +export async function loadPDFFromIndexedDB() { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction([STORE_NAME], 'readonly'); + const store = tx.objectStore(STORE_NAME); + const req = store.get('customPdf'); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + } catch (err) { + handleError(err, 'loadPDFFromIndexedDB'); + } +} +export async function deletePDFFromIndexedDB() { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction([STORE_NAME], 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.delete('customPdf'); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch (err) { + handleError(err, 'deletePDFFromIndexedDB'); + } +} +export async function handlePDFUpload(ev) { + const file = ev.target.files[0]; + if (!file) return; + if (file.size > 50 * 1024 * 1024) { + alert('PDF file is too large (max 50 MiB).'); + ev.target.value = ''; + return; + } + try { + showLoading(true); + const buf = await readFileAsArrayBuffer(file); + const bufferCopy = buf.slice(0); + const pdf = await pdfjsLib.getDocument({ data: bufferCopy }).promise; + const storageBuffer = buf.slice(0); + const b64 = arrayBufferToBase64(storageBuffer); + const pdfData = { + name: file.name, + data: b64, + uploadDate: new Date().toISOString(), + numPages: pdf.numPages + }; + await savePDFToIndexedDB(pdfData); + state.settings.customPdf = { + name: pdfData.name, + uploadDate: pdfData.uploadDate, + numPages: pdfData.numPages, + storedInDB: true + }; + saveToStorage(); + updateCustomPdfInfo(); + alert('PDF uploaded successfully! You can now use it in the Reference Panel.'); + } catch (e) { + handleError(err, 'handlePDFUpload'); + alert('Error uploading PDF: ' + e.message); + } finally { + showLoading(false); + ev.target.value = ''; + } +} +export function updateCustomPdfInfo() { + const container = document.getElementById('customPdfInfo'); + if (state.settings.customPdf) { + const date = new Date(state.settings.customPdf.uploadDate) + .toLocaleDateString(); + container.innerHTML = ` +
+
+ ${state.settings.customPdf.name} + +
+ Uploaded: ${date} • ${state.settings.customPdf.numPages} pages +
+ `; + document.getElementById('removePdfBtn') + .addEventListener('click', removeCustomPdf); + } else { + container.innerHTML = ` + + No custom PDF uploaded + `; + document.getElementById('pageInput').value = 1; + document.getElementById('pageCount').textContent = '?'; + const zoomDisplay = document.getElementById('zoomLevel'); + if (zoomDisplay) { + zoomDisplay.textContent = '100%'; + } + } +} +async function removeCustomPdf() { + try { + if (!confirm('Delete the uploaded PDF? This cannot be undone.')) return; + await deletePDFFromIndexedDB(); + state.settings.customPdf = null; + state.settings.referenceSource = 'biblegateway'; + saveToStorage(); + updateCustomPdfInfo(); + if (document.getElementById('referenceSource').value === 'pdf') { + document.getElementById('referenceSource').value = 'biblegateway'; + updateReferencePanel(); + } + } catch (err) { + handleError(err, 'removeCustomPdf'); + } +} +export async function loadPDF() { + if (!state.settings.customPdf) { + alert('No custom PDF uploaded. Please upload one first.'); + return; + } + try { + showLoading(true); + if (state.pdf.doc) { + state.pdf.doc.destroy().catch(() => {}); + state.pdf.doc = null; + } + state.pdf.renderTask = null; + const pdfData = await loadPDFFromIndexedDB(); + if (!pdfData) throw new Error('PDF not found in DB'); + const buf = base64ToArrayBuffer(pdfData.data); + const loadingTask = pdfjsLib.getDocument({ + data: buf, + onPassword: (updatePassword, reason) => { + const password = prompt(`This PDF requires a ${reason} password. Please enter the password:`); + if (password) { + updatePassword(password); + } else { + throw new Error(`Password required to open this PDF. ${reason === 1 ? 'Owner' : 'User'} password needed.`); + } + } + }); + state.pdf.doc = await loadingTask.promise; + const savedPage = state.pdf.currentPage || 1; + const validPage = Math.min(savedPage, state.pdf.doc.numPages); + state.pdf.currentPage = validPage; + const savedZoom = state.pdf.zoomLevel || state.settings.pdfZoom; + updatePDFZoom(savedZoom); + document.getElementById('pageCount').textContent = state.pdf.doc.numPages; + document.getElementById('pageInput').max = state.pdf.doc.numPages; + document.getElementById('pageInput').value = validPage; + } catch (err) { + handleError(err, 'loadPDF'); + alert('Could not load PDF: ' + err.message); + state.pdf.doc = null; + state.pdf.renderTask = null; + } finally { + showLoading(false); + } +} +export function updatePDFZoom(zoomLevel) { + state.settings.pdfZoom = zoomLevel; + state.pdf.zoomLevel = zoomLevel; + saveToStorage(); + const zoomDisplay = document.getElementById('zoomLevel'); + if (zoomDisplay) { + zoomDisplay.textContent = `${Math.round(zoomLevel * 100)}%`; + } + if (state.pdf.doc && state.pdf.currentPage) { + renderPage(state.pdf.currentPage); + } +} +export function setupPDFCleanup() { + window.addEventListener('beforeunload', () => { + if (state.pdf.renderTask) { + state.pdf.renderTask.cancel().catch(() => {}); + state.pdf.renderTask = null; + } + }); +} +export async function renderPage(pageNum) { + if (!state.pdf.doc) { + console.warn('PDF document not loaded, attempting to reload...'); + await loadPDF(); + return; + } + try { + if (state.pdf.renderTask) { + try { + await state.pdf.renderTask.cancel(); + } catch (e) { + } + state.pdf.renderTask = null; + } + const page = await state.pdf.doc.getPage(pageNum); + const canvas = document.getElementById('pdfCanvas'); + const ctx = canvas.getContext('2d', { willReadFrequently: false }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + const viewport = page.getViewport({ scale: state.pdf.zoomLevel }); + canvas.style.width = viewport.width + 'px'; + canvas.style.height = viewport.height + 'px'; + canvas.width = viewport.width * window.devicePixelRatio; + canvas.height = viewport.height * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + state.pdf.renderTask = page.render({ + canvasContext: ctx, + viewport: viewport + }); + await state.pdf.renderTask.promise; + page.cleanup(); + state.pdf.renderTask = null; + document.getElementById('pageInput').value = pageNum; + document.getElementById('prevPage').disabled = pageNum <= 1; + document.getElementById('nextPage').disabled = pageNum >= state.pdf.doc.numPages; + state.pdf.currentPage = pageNum; + saveToStorage(); + } catch (err) { + if (err.name === 'RenderingCancelledException') { + console.log('Rendering cancelled normally'); + return; + } + console.warn('Render error, reloading PDF:', err); + await loadPDF(); + handleError(err, 'renderPage'); + } +} +export async function searchPDF() { + const query = document.getElementById('pdfSearchInput').value.trim(); + if (!query || !state.pdf.doc) return; + const resultsSpan = document.getElementById('pdfSearchResults'); + const searchBtn = document.getElementById('pdfSearchBtn'); + resultsSpan.textContent = 'Searching...'; + searchBtn.disabled = true; + searchBtn.textContent = 'Searching...'; + clearSearchHighlights(); + currentSearch = { + query: query, + results: [], + currentResult: -1, + highlights: [] + }; + try { + for (let pageNum = 1; pageNum <= state.pdf.doc.numPages; pageNum++) { + const page = await state.pdf.doc.getPage(pageNum) + const textContent = await page.getTextContent(); + const text = textContent.items.map(item => item.str).join(' '); + const regex = new RegExp(query.replace(/[.*+?^{}()|[\]\\]/g, '\\$&'), 'gi'); + let match; + while ((match = regex.exec(text)) !== null) { + currentSearch.results.push({ + page: pageNum, + index: match.index, + text: match[0] + }); + } + } + if (currentSearch.results.length > 0) { + resultsSpan.textContent = `Found ${currentSearch.results.length} results`; + document.getElementById('clearSearchBtn').style.display = 'inline-block'; + if (currentSearch.results.length > 1) { + document.getElementById('prevSearchResult').style.display = 'inline-block'; + document.getElementById('nextSearchResult').style.display = 'inline-block'; + } + navigateToSearchResult(0); + } else { + resultsSpan.textContent = 'No results found'; + document.getElementById('clearSearchBtn').style.display = 'none'; + document.getElementById('prevSearchResult').style.display = 'none'; + document.getElementById('nextSearchResult').style.display = 'none'; + } + } catch (err) { + handleError(err, 'searchPDF'); + resultsSpan.textContent = 'Search failed'; + document.getElementById('clearSearchBtn').style.display = 'none'; + document.getElementById('prevSearchResult').style.display = 'none'; + document.getElementById('nextSearchResult').style.display = 'none'; + } finally { + searchBtn.disabled = false; + searchBtn.textContent = 'Search'; + } +} +export function clearSearch() { + document.getElementById('pdfSearchInput').value = ''; + document.getElementById('pdfSearchResults').textContent = ''; + document.getElementById('clearSearchBtn').style.display = 'none'; + document.getElementById('prevSearchResult').style.display = 'none'; + document.getElementById('nextSearchResult').style.display = 'none'; + clearSearchHighlights(); + currentSearch = { + query: '', + results: [], + currentResult: -1, + highlights: [] + }; +} +export async function navigateToSearchResult(index) { + try { + if (!currentSearch.results.length || index < 0 || index >= currentSearch.results.length) return; + if (state.pdf.renderTask) { + await state.pdf.renderTask.cancel(); + state.pdf.renderTask = null; + } + currentSearch.currentResult = index; + const result = currentSearch.results[index]; + state.pdf.currentPage = result.page; + await renderPage(result.page); + document.getElementById('pdfSearchResults').textContent = + `Result ${index + 1} of ${currentSearch.results.length}`; + } catch (err) { + handleError(err, 'navigateToSearchResult'); + } +} +function clearSearchHighlights() { + currentSearch.highlights.forEach(highlight => { + if (highlight.animation) { + currentSearch.highlights = []; + } + }); + if (state.pdf.currentPage) { + renderPage(state.pdf.currentPage); + } +} \ No newline at end of file diff --git a/public/modules/settings.js b/public/modules/settings.js new file mode 100644 index 0000000..32d73b3 --- /dev/null +++ b/public/modules/settings.js @@ -0,0 +1,274 @@ +import { + applyColorTheme, + applyTheme, + clearError, + handleError, + showLoading +} from '../main.js' +import { loadPassage } from './passage.js' +import { + deletePDFFromIndexedDB, + openDB, + STORE_NAME, + updateCustomPdfInfo +} from './pdf.js' +import { + APP_VERSION, + BOOK_ORDER, + readingPlan, + saveToCookies, + saveToStorage, + state, + updateBibleGatewayVersion +} from './state.js' +import { + restorePanelStates, + restoreSidebarState, + switchNotesView, + updateMarkdownPreview, + updateReferencePanel +} from './ui.js' +export function exportData() { + const payload = { + version: '2.0', + exportDate: new Date().toISOString(), + highlights: state.highlights, + notes: state.notes, + settings: { ...state.settings } + }; + if (payload.settings.customPdf && payload.settings.customPdf.data) { + const { data, ...meta } = payload.settings.customPdf; + payload.settings.customPdf = meta; + } + const blob = new Blob([JSON.stringify(payload, null, 2)], + { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bible-study-backup-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} +export function importData(ev) { + const file = ev.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = async e => { + try { + const incoming = JSON.parse(e.target.result); + if (!incoming.settings) { + throw new Error('Invalid backup format'); + } + if (!confirm('Import will overwrite all current data (except any uploaded PDF). Continue?')) { + return; + } + Object.assign(state.settings, incoming.settings); + if (incoming.highlights) { + state.highlights = incoming.highlights; + } + if (incoming.notes) { + state.notes = incoming.notes; + } + saveToStorage(); + applyTheme(); + applyColorTheme(); + restoreSidebarState(); + restorePanelStates(); + updateCustomPdfInfo(); + switchNotesView(state.settings.notesView || 'text'); + await loadPassage(); + document.getElementById('notesInput').value = state.notes; + updateMarkdownPreview(); + alert('Backup imported successfully!'); + } catch (err) { + console.error('Import error:', err); + alert('Failed to import backup – see console for details.'); + } + }; + reader.readAsText(file); + ev.target.value = ''; +} +export function resumeReadingPlan() { + const curBook = state.settings.manualBook; + const curChapter = state.settings.manualChapter; + const idx = findReadingPlanIndex(curBook, curChapter); + if (idx !== -1) { + state.settings.currentPassageIndex = idx; + } + state.settings.readingMode = 'readingPlan'; + loadPassage(); +} +function findReadingPlanIndex(book, chapter) { + for (let i = 0; i < readingPlan.length; i++) { + const p = readingPlan[i]; + if (p.book === book && p.chapter === chapter) { + return i; + } + } + return -1; +} +export function openSettings() { + document.getElementById('bibleTranslationSetting').value = + state.settings.bibleTranslation; + document.getElementById('referenceVersionSetting').value = + state.settings.referenceVersion; + document.getElementById('readingPlanId').value = + state.settings.readingPlanId || 'default'; + document.querySelectorAll('.color-theme-option') + .forEach(o => o.classList.toggle('selected', + o.dataset.theme === state.settings.colorTheme)); + document.getElementById('settingsModal').classList.add('active'); + document.getElementById('settingsOverlay').classList.add('active'); + document.getElementById('appVersion').textContent = APP_VERSION; +} +export function closeSettings() { + document.getElementById('settingsModal').classList.remove('active'); + document.getElementById('settingsOverlay').classList.remove('active'); +} +export async function saveSettings() { + try { + const newPlanId = document.getElementById('readingPlanId').value; + const newTranslation = document.getElementById('bibleTranslationSetting').value; + const newReferenceVersion = document.getElementById('referenceVersionSetting').value; + const oldPlanId = state.settings.readingPlanId; + state.settings.bibleTranslation = newTranslation; + state.settings.referenceVersion = newReferenceVersion; + state.settings.readingPlanId = newPlanId; + if (newReferenceVersion === 'BSB' && state.settings.referenceSource === 'biblegateway') { + state.settings.referenceSource = 'biblehub'; + document.getElementById('referenceSource').value = 'biblehub'; + } else if (newReferenceVersion === 'NASB1995' && state.settings.referenceSource === 'biblehub') { + state.settings.referenceSource = 'biblegateway'; + document.getElementById('referenceSource').value = 'biblegateway'; + } + if (oldPlanId !== newPlanId) { + state.settings.currentPassageIndex = 0; + state.settings.readingMode = 'readingPlan'; + } + const selectedTheme = document.querySelector('.color-theme-option.selected'); + if (selectedTheme) { + state.settings.colorTheme = selectedTheme.dataset.theme; + applyColorTheme(); + } + document.getElementById('referenceTranslation').value = state.settings.referenceVersion; + updateBibleGatewayVersion(); + saveToStorage(); + saveToCookies(); + closeSettings(); + await loadPassage(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + alert('Settings saved!' + (oldPlanId !== newPlanId ? ' Starting from beginning of new reading plan.' : '')); + } catch (err) { + handleError(err, 'saveSettings'); + } +} +export function restartReadingPlan() { + if (confirm('Reset the reading plan to the very first passage? Highlights and notes will stay unchanged.')) { + state.settings.currentPassageIndex = 0; + state.settings.readingMode = 'readingPlan'; + saveToStorage(); + loadPassage(); + alert('Reading plan restarted – you are now at the beginning.'); + } +} +export async function clearCache() { + if (confirm('Clear all cached Bible data? This will remove offline access to previously viewed passages.')) { + try { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' }); + } + const db = await openDB(); + const tx = db.transaction([STORE_NAME], 'readwrite'); + const store = tx.objectStore(STORE_NAME); + await store.clear(); + alert('Cache cleared successfully'); + } catch (err) { + handleError(err, 'clearCache'); + alert('Error clearing cache: ' + err.message); + } + } +} +export async function deleteAllData() { + const confirmDelete = confirm('WARNING: This will delete ALL your data. Would you like to create a backup first?'); + if (confirmDelete) { + exportData(); + const proceed = confirm('Backup created. Proceed with deletion?'); + if (!proceed) return; + } + try { + showLoading(true); + localStorage.removeItem('bibleStudyState'); + document.cookie = 'bibleStudySettings=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + try { + await deletePDFFromIndexedDB(); + } catch (e) { + console.warn('Could not delete PDF from IndexedDB:', e); + } + const defaultState = { + currentVerse: null, + currentVerseData: null, + highlights: {}, + notes: '', + settings: { + bibleTranslation: 'BSB', + referenceVersion: 'NASB1995', + passageType: 'default', + readingMode: 'readingPlan', + manualBook: BOOK_ORDER[0], + manualChapter: 1, + lastUpdate: null, + currentPassageIndex: 0, + theme: 'light', + colorTheme: 'blue', + notesView: 'text', + hasSeenWelcome: true, + referencePanelOpen: false, + referenceSource: 'biblegateway', + collapsedSections: {}, + collapsedPanels: {}, + panelWidths: { + sidebar: 280, + referencePanel: 400, + scriptureSection: null, + notesSection: 400 + }, + customPdf: null, + pdfZoom: 1 + }, + currentPassageReference: '', + pdf: { + doc: null, + currentPage: 1, + renderTask: null, + zoomLevel: 1 + }, + welcomePdfFile: null + }; + Object.assign(state, defaultState); + document.getElementById('notesInput').value = ''; + updateMarkdownPreview(); + updateCustomPdfInfo(); + applyTheme(); + applyColorTheme(); + updateBibleGatewayVersion(); + document.getElementById('pageInput').value = 1; + document.getElementById('pageCount').textContent = '?'; + const zoomDisplay = document.getElementById('zoomLevel'); + if (zoomDisplay) { + zoomDisplay.textContent = '100%'; + } + await loadPassage(); + clearError(); + closeSettings(); + alert('All data has been deleted. The app has been reset to defaults.'); + } catch (err) { + handleError(err, 'deleteAllData'); + alert('Error deleting data. See console for details.'); + } finally { + showLoading(false); + } +} \ No newline at end of file diff --git a/public/modules/state.js b/public/modules/state.js new file mode 100644 index 0000000..847efc2 --- /dev/null +++ b/public/modules/state.js @@ -0,0 +1,399 @@ +import { handleError } from '../main.js' +import { loadPDFFromIndexedDB } from './pdf.js' +export const APP_VERSION = '1.0.2025.11.01'; +let saveTimeout = null; +const SAVE_DEBOUNCE_MS = 500; +export const BOOK_ORDER = [ + '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' +]; +export const CHAPTER_COUNTS = { + Genesis: 50, Exodus: 40, Leviticus: 27, Numbers: 36, Deuteronomy: 34, + Joshua: 24, Judges: 21, Ruth: 4, '1 Samuel': 31, '2 Samuel': 24, + '1 Kings': 22, '2 Kings': 25, '1 Chronicles': 29, '2 Chronicles': 36, + Ezra: 10, Nehemiah: 13, Esther: 10, + Job: 42, Psalms: 150, Proverbs: 31, Ecclesiastes: 12, 'Song of Solomon': 8, + Isaiah: 66, Jeremiah: 52, Lamentations: 5, Ezekiel: 48, Daniel: 12, + Hosea: 14, Joel: 3, Amos: 9, Obadiah: 1, Jonah: 4, Micah: 7, + Nahum: 3, Habakkuk: 3, Zephaniah: 3, Haggai: 2, Zechariah: 14, Malachi: 4, + Matthew: 28, Mark: 16, Luke: 24, John: 21, + Acts: 28, + Romans: 16, '1 Corinthians': 16, '2 Corinthians': 13, Galatians: 6, + Ephesians: 6, Philippians: 4, Colossians: 4, '1 Thessalonians': 5, + '2 Thessalonians': 3, '1 Timothy': 6, '2 Timothy': 4, Titus: 3, Philemon: 1, + Hebrews: 13, James: 5, '1 Peter': 5, '2 Peter': 3, '1 John': 5, + '2 John': 1, '3 John': 1, Jude: 1, + Revelation: 22 +}; +export const state = { + currentVerse: null, + currentVerseData: null, + highlights: {}, + notes: '', // User's study notes (plain text/markdown) + settings: { + bibleTranslation: 'BSB', + referenceVersion: 'NASB1995', + footnotes: {}, + passageType: 'default', + readingMode: 'readingPlan', // 'readingPlan' | 'manual' + manualBook: BOOK_ORDER[0], + manualChapter: 1, + lastUpdate: null, + currentPassageIndex: 0, + theme: 'light', // 'light' | 'dark' + colorTheme: 'blue', + notesView: 'text', // 'text' | 'markdown' + hasSeenWelcome: false, + referencePanelOpen: false, + referenceSource: 'biblegateway',// 'biblegateway' | 'biblehub' | 'pdf' + collapsedSections: {}, + collapsedPanels: {}, + panelWidths: { + sidebar: 280, + referencePanel: 400, + scriptureSection: null, + notesSection: 400 + }, + customPdf: null, + pdfZoom: 1 + }, + currentPassageReference: '', + pdf: { + doc: null, + currentPage: 1, + renderTask: null, + zoomLevel: 1 + }, + welcomePdfFile: null +}; +export function formatBookNameForSource(bookName, source) { + const book = bookName.toLowerCase(); + switch(source) { + case 'biblecom': + const bibleComCodes = { + 'genesis': 'GEN', 'exodus': 'EXO', 'leviticus': 'LEV', 'numbers': 'NUM', + 'deuteronomy': 'DEU', 'joshua': 'JOS', 'judges': 'JDG', 'ruth': 'RUT', + '1 samuel': '1SA', '2 samuel': '2SA', '1 kings': '1KI', '2 kings': '2KI', + '1 chronicles': '1CH', '2 chronicles': '2CH', 'ezra': 'EZR', 'nehemiah': 'NEH', + 'esther': 'EST', 'job': 'JOB', 'psalms': 'PSA', 'proverbs': 'PRO', + 'ecclesiastes': 'ECC', 'song of solomon': 'SNG', 'isaiah': 'ISA', 'jeremiah': 'JER', + 'lamentations': 'LAM', 'ezekiel': 'EZK', 'daniel': 'DAN', 'hosea': 'HOS', + 'joel': 'JOL', 'amos': 'AMO', 'obadiah': 'OBA', 'jonah': 'JON', + 'micah': 'MIC', 'nahum': 'NAM', 'habakkuk': 'HAB', 'zephaniah': 'ZEP', + 'haggai': 'HAG', 'zechariah': 'ZEC', 'malachi': 'MAL', 'matthew': 'MAT', + 'mark': 'MRK', 'luke': 'LUK', 'john': 'JHN', 'acts': 'ACT', + 'romans': 'ROM', '1 corinthians': '1CO', '2 corinthians': '2CO', 'galatians': 'GAL', + 'ephesians': 'EPH', 'philippians': 'PHP', 'colossians': 'COL', '1 thessalonians': '1TH', + '2 thessalonians': '2TH', '1 timothy': '1TI', '2 timothy': '2TI', 'titus': 'TIT', + 'philemon': 'PHM', 'hebrews': 'HEB', 'james': 'JAS', '1 peter': '1PE', + '2 peter': '2PE', '1 john': '1JN', '2 john': '2JN', '3 john': '3JN', + 'jude': 'JUD', 'revelation': 'REV' + }; + return bibleComCodes[book] || book.substring(0, 3).toUpperCase(); + case 'ebibleorg': + if (book === 'psalms') return 'PS1'; + return book.substring(0, 3).toUpperCase() + '1'; + default: + return book.replace(/\s+/g, '_'); + } +} +export function saveToStorage() { + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + try { + const cleanState = { + currentVerse: null, + currentVerseData: state.currentVerseData, + highlights: state.highlights, + notes: state.notes, + settings: { ...state.settings }, + currentPassageReference: state.currentPassageReference, + pdf: { + currentPage: state.pdf.currentPage, + zoomLevel: state.pdf.zoomLevel + }, + welcomePdfFile: null + }; + if (cleanState.settings.customPdf && cleanState.settings.customPdf.data) { + const { data, ...meta } = cleanState.settings.customPdf; + cleanState.settings.customPdf = { ...meta, storedInDB: true }; + } + localStorage.setItem('bibleStudyState', JSON.stringify(cleanState)); + saveToCookies(); + } catch (e) { + console.error('Storage error:', e); + } + }, SAVE_DEBOUNCE_MS); +} +export async function loadFromStorage() { + const raw = localStorage.getItem('bibleStudyState'); + if (!raw) return; + try { + const parsed = JSON.parse(raw); + Object.assign(state, parsed); + if (parsed.pdf) { + state.pdf.currentPage = parsed.pdf.currentPage || 1; + state.pdf.zoomLevel = parsed.pdf.zoomLevel || state.settings.pdfZoom; + } + if (state.settings.customPdf && state.settings.customPdf.storedInDB) { + const pdfMeta = await loadPDFFromIndexedDB(); + if (!pdfMeta) { + console.warn('PDF metadata present but DB entry missing'); + state.settings.customPdf = null; + } + } + document.getElementById('notesInput').value = state.notes; + } catch (e) { + handleError(err, 'loadFromStorage'); + } +} +export function saveToCookies() { + const exp = new Date(); + exp.setFullYear(exp.getFullYear() + 10); + const cookieVal = encodeURIComponent(JSON.stringify({ + ...state.settings, + customPdf: undefined + })); + document.cookie = `bibleStudySettings=${cookieVal}; expires=${exp.toUTCString()}; path=/; SameSite=Strict`; +} +export function loadFromCookies() { + const pairs = document.cookie.split(';'); + for (let pair of pairs) { + const [k, v] = pair.trim().split('='); + if (k === 'bibleStudySettings') { + try { + const settings = JSON.parse(decodeURIComponent(v)); + Object.assign(state.settings, settings); + } catch (e) { + console.error('Cookie parse error:', e); + } + } + } +} +export const readingPlan = [ + { book: 'Genesis', chapter: 1, startVerse: 1, endVerse: 31, displayRef: 'Genesis 1' }, + { book: 'Genesis', chapter: 2, startVerse: 1, endVerse: 25, displayRef: 'Genesis 2' }, + { book: 'Genesis', chapter: 3, startVerse: 1, endVerse: 24, displayRef: 'Genesis 3' }, + { book: 'Genesis', chapter: 6, startVerse: 5, endVerse: 22, displayRef: 'Genesis 6:5-22' }, + { book: 'Genesis', chapter: 12, startVerse: 1, endVerse: 9, displayRef: 'Genesis 12:1-9' }, + { book: 'Genesis', chapter: 22, startVerse: 1, endVerse: 19, displayRef: 'Genesis 22:1-19' }, + { book: 'Exodus', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Exodus 3' }, + { book: 'Exodus', chapter: 20, startVerse: 1, endVerse: 21, displayRef: 'Exodus 20:1-21' }, + { book: 'Leviticus', chapter: 19, startVerse: 1, endVerse: 18, displayRef: 'Leviticus 19:1-18' }, + { book: 'Numbers', chapter: 14, startVerse: 1, endVerse: 38, displayRef: 'Numbers 14:1-38' }, + { book: 'Deuteronomy', chapter: 6, startVerse: 1, endVerse: 25, displayRef: 'Deuteronomy 6' }, + { book: 'Deuteronomy', chapter: 30, startVerse: 1, endVerse: 20, displayRef: 'Deuteronomy 30' }, + { book: 'Joshua', chapter: 1, startVerse: 1, endVerse: 9, displayRef: 'Joshua 1:1-9' }, + { book: 'Judges', chapter: 2, startVerse: 6, endVerse: 23, displayRef: 'Judges 2:6-23' }, + { book: 'Ruth', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Ruth 1' }, + { book: '1 Samuel', chapter: 16, startVerse: 1, endVerse: 13, displayRef: '1 Samuel 16:1-13' }, + { book: '2 Samuel', chapter: 7, startVerse: 1, endVerse: 29, displayRef: '2 Samuel 7' }, + { book: '1 Kings', chapter: 18, startVerse: 1, endVerse: 46, displayRef: '1 Kings 18' }, + { book: '2 Kings', chapter: 22, startVerse: 1, endVerse: 20, displayRef: '2 Kings 22' }, + { book: 'Ezra', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Ezra 1' }, + { book: 'Nehemiah', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Nehemiah 1' }, + { book: 'Esther', chapter: 4, startVerse: 1, endVerse: 17, displayRef: 'Esther 4' }, + { book: 'Job', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Job 1' }, + { book: 'Psalms', chapter: 1, startVerse: 1, endVerse: 6, displayRef: 'Psalm 1' }, + { book: 'Psalms', chapter: 19, startVerse: 1, endVerse: 14, displayRef: 'Psalm 19' }, + { book: 'Psalms', chapter: 23, startVerse: 1, endVerse: 6, displayRef: 'Psalm 23' }, + { book: 'Psalms', chapter: 51, startVerse: 1, endVerse: 19, displayRef: 'Psalm 51' }, + { book: 'Psalms', chapter: 103, startVerse: 1, endVerse: 22, displayRef: 'Psalm 103' }, + { book: 'Psalms', chapter: 119, startVerse: 1, endVerse: 16, displayRef: 'Psalm 119:1-16' }, + { book: 'Proverbs', chapter: 3, startVerse: 1, endVerse: 12, displayRef: 'Proverbs 3:1-12' }, + { book: 'Ecclesiastes', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Ecclesiastes 3' }, + { book: 'Song of Solomon', chapter: 2, startVerse: 1, endVerse: 17, displayRef: 'Song of Solomon 2' }, + { book: 'Isaiah', chapter: 6, startVerse: 1, endVerse: 13, displayRef: 'Isaiah 6' }, + { book: 'Isaiah', chapter: 9, startVerse: 1, endVerse: 7, displayRef: 'Isaiah 9:1-7' }, + { book: 'Isaiah', chapter: 40, startVerse: 1, endVerse: 31, displayRef: 'Isaiah 40' }, + { book: 'Isaiah', chapter: 53, startVerse: 1, endVerse: 12, displayRef: 'Isaiah 53' }, + { book: 'Jeremiah', chapter: 29, startVerse: 1, endVerse: 14, displayRef: 'Jeremiah 29:1-14' }, + { book: 'Lamentations', chapter: 3, startVerse: 1, endVerse: 33, displayRef: 'Lamentations 3:1-33' }, + { book: 'Ezekiel', chapter: 36, startVerse: 22, endVerse: 38, displayRef: 'Ezekiel 36:22-38' }, + { book: 'Daniel', chapter: 3, startVerse: 1, endVerse: 30, displayRef: 'Daniel 3' }, + { book: 'Daniel', chapter: 6, startVerse: 1, endVerse: 28, displayRef: 'Daniel 6' }, + { book: 'Hosea', chapter: 6, startVerse: 1, endVerse: 11, displayRef: 'Hosea 6' }, + { book: 'Joel', chapter: 2, startVerse: 12, endVerse: 32, displayRef: 'Joel 2:12-32' }, + { book: 'Jonah', chapter: 1, startVerse: 1, endVerse: 17, displayRef: 'Jonah 1' }, + { book: 'Micah', chapter: 6, startVerse: 1, endVerse: 16, displayRef: 'Micah 6' }, + { book: 'Habakkuk', chapter: 3, startVerse: 1, endVerse: 19, displayRef: 'Habakkuk 3' }, + { book: 'Malachi', chapter: 3, startVerse: 1, endVerse: 18, displayRef: 'Malachi 3' }, + { book: 'Matthew', chapter: 5, startVerse: 1, endVerse: 30, displayRef: 'Matthew 5:1-30' }, + { book: 'Matthew', chapter: 6, startVerse: 1, endVerse: 34, displayRef: 'Matthew 6' }, + { book: 'Matthew', chapter: 7, startVerse: 1, endVerse: 29, displayRef: 'Matthew 7' }, + { book: 'Mark', chapter: 10, startVerse: 17, endVerse: 45, displayRef: 'Mark 10:17-45' }, + { book: 'Luke', chapter: 15, startVerse: 1, endVerse: 32, displayRef: 'Luke 15' }, + { book: 'John', chapter: 1, startVerse: 1, endVerse: 51, displayRef: 'John 1:1-51' }, + { book: 'John', chapter: 3, startVerse: 1, endVerse: 36, displayRef: 'John 3:1-36' }, + { book: 'John', chapter: 6, startVerse:30, endVerse: 66, displayRef: 'John 6:30-66' }, + { book: 'John', chapter: 14, startVerse: 1, endVerse: 31, displayRef: 'John 14' }, + { book: 'Acts', chapter: 2, startVerse: 1, endVerse: 47, displayRef: 'Acts 2' }, + { book: 'Romans', chapter: 1, startVerse: 1, endVerse: 32, displayRef: 'Romans 1' }, + { book: 'Romans', chapter: 3, startVerse: 1, endVerse: 31, displayRef: 'Romans 3' }, + { book: 'Romans', chapter: 8, startVerse: 1, endVerse: 39, displayRef: 'Romans 8' }, + { book: 'Romans', chapter: 12, startVerse: 1, endVerse: 21, displayRef: 'Romans 12' }, + { book: '1 Corinthians', chapter: 13, startVerse: 1, endVerse: 13, displayRef: '1 Corinthians 13' }, + { book: '2 Corinthians', chapter: 5, startVerse: 1, endVerse: 21, displayRef: '2 Corinthians 5' }, + { book: 'Galatians', chapter: 5, startVerse: 16, endVerse: 26, displayRef: 'Galatians 5:16-26' }, + { book: 'Ephesians', chapter: 2, startVerse: 1, endVerse: 22, displayRef: 'Ephesians 2' }, + { book: 'Philippians', chapter: 2, startVerse: 1, endVerse: 18, displayRef: 'Philippians 2:1-18' }, + { book: 'Colossians', chapter: 1, startVerse: 1, endVerse: 29, displayRef: 'Colossians 1' }, + { book: '1 Thessalonians', chapter: 4, startVerse: 1, endVerse: 18, displayRef: '1 Thessalonians 4' }, + { book: '1 Timothy', chapter: 3, startVerse: 1, endVerse: 16, displayRef: '1 Timothy 3' }, + { book: 'Hebrews', chapter: 11, startVerse: 1, endVerse: 40, displayRef: 'Hebrews 11' }, + { book: 'James', chapter: 1, startVerse: 1, endVerse: 27, displayRef: 'James 1' }, + { book: '1 Peter', chapter: 1, startVerse: 1, endVerse: 25, displayRef: '1 Peter 1' }, + { book: '1 John', chapter: 4, startVerse: 1, endVerse: 21, displayRef: '1 John 4' }, + { book: 'Revelation', chapter: 1, startVerse: 1, endVerse: 20, displayRef: 'Revelation 1' }, + { book: 'Revelation', chapter: 21, startVerse: 1, endVerse: 27, displayRef: 'Revelation 21' }, + { book: 'Revelation', chapter: 22, startVerse: 1, endVerse: 21, displayRef: 'Revelation 22' } +]; +function buildFullBookPlan(bookName) { + const maxChapters = CHAPTER_COUNTS[bookName]; + if (!maxChapters) { + console.warn(`No chapter count for "${bookName}" – skipping plan`); + return []; + } + const plan = []; + for (let ch = 1; ch <= maxChapters; ch++) { + plan.push({ + book: bookName, + chapter: ch, + startVerse: 1, + endVerse: 999, + displayRef: `${bookName} ${ch}` + }); + } + return plan; +} +const READING_PLANS = { + default: readingPlan, + genesis: buildFullBookPlan('Genesis'), + psalms: buildFullBookPlan('Psalms'), + proverbs: buildFullBookPlan('Proverbs'), + ecclesiastes: buildFullBookPlan('Ecclesiastes'), + romans: buildFullBookPlan('Romans'), + revelation: buildFullBookPlan('Revelation') +}; +export function getActivePlan() { + const id = state.settings.readingPlanId || 'default'; + return READING_PLANS[id] || READING_PLANS['default']; +} +const PLAN_LABELS = { + default: '90‑Day Sequential', + genesis: 'Genesis', + psalms: 'Psalms', + proverbs: 'Proverbs', + ecclesiastes: 'Ecclesiastes', + romans: 'Romans', + revelation: 'Revelation' +}; +export function getCurrentPlanLabel() { + const id = state.settings.readingPlanId || 'default'; + return PLAN_LABELS[id] || id; +} +export const bookNameMapping = { + Genesis: 'GEN', Exodus: 'EXO', Leviticus: 'LEV', Numbers: 'NUM', Deuteronomy: 'DEU', + Joshua: 'JOS', Judges: 'JDG', Ruth: 'RUT', '1 Samuel': '1SA', '2 Samuel': '2SA', + '1 Kings': '1KI', '2 Kings': '2KI', '1 Chronicles': '1CH', '2 Chronicles': '2CH', + Ezra: 'EZR', Nehemiah: 'NEH', Esther: 'EST', Job: 'JOB', Psalms: 'PSA', + Proverbs: 'PRO', Ecclesiastes: 'ECC', 'Song of Solomon': 'SNG', Isaiah: 'ISA', Jeremiah: 'JER', + Lamentations: 'LAM', Ezekiel: 'EZK', Daniel: 'DAN', Hosea: 'HOS', Joel: 'JOL', + Amos: 'AMO', Obadiah: 'OBA', Jonah: 'JON', Micah: 'MIC', Nahum: 'NAM', + Habakkuk: 'HAB', Zephaniah: 'ZEP', Haggai: 'HAG', Zechariah: 'ZEC', Malachi: 'MAL', + Matthew: 'MAT', Mark: 'MRK', Luke: 'LUK', John: 'JHN', Acts: 'ACT', + Romans: 'ROM', '1 Corinthians': '1CO', '2 Corinthians': '2CO', Galatians: 'GAL', + Ephesians: 'EPH', Philippians: 'PHP', Colossians: 'COL', '1 Thessalonians': '1TH', + '2 Thessalonians': '2TH', '1 Timothy': '1TI', '2 Timothy': '2TI', Titus: 'TIT', + Philemon: 'PHM', Hebrews: 'HEB', James: 'JAS', '1 Peter': '1PE', '2 Peter': '2PE', + '1 John': '1JN', '2 John': '2JN', '3 John': '3JN', Jude: 'JUD', Revelation: 'REV' +}; +export const bibleHubUrlMap = { + 'NASB1995': 'nasb', // Bible Hub uses 'nasb' for NASB 1995 + 'NASB': 'nasb_', // Bible Hub uses 'nasb_' for NASB 2020 + 'ASV': 'asv', + 'ESV': 'esv', + 'KJV': 'kjv', + 'NKJV': 'nkjv', + 'BSB': 'bsb', + 'CSB': 'csb', + 'NET': 'net', + 'NIV': 'niv', + 'NLT': 'nlt' +}; +export const bibleComUrlMap = { + 'NASB1995': '100', + 'NASB': '2692', + 'ASV': '12', + 'ESV': '59', + 'KJV': '1', + 'GNV': '2163', + 'NKJV': '114', + 'BSB': '3034', + 'CSB': '1713', + 'NET': '107', + 'NIV': '111', + 'NLT': '116' +}; +export const ebibleOrgUrlMap = { + 'NASB1995': 'local:engnasb', + 'ASV': 'local:eng-asv', + 'KJV': 'local:eng-kjv2006', + 'GNV': 'local:enggnv', + 'BSB': 'local:engbsb', + 'NET': 'local:engnet' +}; +export const stepBibleUrlMap = { + 'NASB1995': 'NASB1995', + 'NASB': 'NASB2020', + 'ASV': 'ASV', + 'ESV': 'ESV', + 'KJV': 'KJV', + 'GNV': 'Gen', + 'BSB': 'BSB', + 'NET': 'NET2full', + 'NIV': 'NIV' +}; +export function getTranslationShorthand() { + return state.settings.bibleTranslation || 'BSB'; +} +function getBibleGatewayVersionCode(appTranslation) { + const versionMap = { + 'NASB1995': 'NASB1995', + 'NASB': 'NASB', + 'ASV': 'ASV', + 'ESV': 'ESV', + 'KJV': 'KJV', + 'GNV': 'GNV', + 'NKJV': 'NKJV', + 'BSB': 'BSB', + 'CSB': 'CSB', + 'NET': 'NET', + 'NIV': 'NIV', + 'NLT': 'NLT' + }; + return versionMap[appTranslation] || 'NASB1995'; +} +export function updateBibleGatewayVersion() { + const versionCode = getBibleGatewayVersionCode(state.settings.referenceVersion); + const versionInput = document.getElementById('bgVersion'); + if (versionCode === 'BSB') { + versionInput.value = 'NASB1995'; + } else { + versionInput.value = versionCode; + } +} \ No newline at end of file diff --git a/public/modules/strongs.js b/public/modules/strongs.js new file mode 100644 index 0000000..5a3d7c0 --- /dev/null +++ b/public/modules/strongs.js @@ -0,0 +1,254 @@ +import { state } from './state.js' +import { getStepBibleUrl } from './ui.js' +export function showStrongsReference(verseEl) { + const ref = verseEl.dataset.verse; + state.currentVerseElement = verseEl; + const textSpan = verseEl.querySelector('.verse-text'); + let verseText = ''; + if (textSpan) { + verseText = textSpan.innerHTML; + if (!verseText || verseText.trim() === '') { + verseText = textSpan.textContent || ''; + if (!verseText || verseText.trim() === '') { + verseText = verseEl.dataset.verseText || ''; + } + } + } else { + verseText = verseEl.dataset.verseText || ''; + } + if (!verseText || verseText.trim() === '') { + verseText = 'Verse text not available'; + } + state.currentVerseData = { reference: ref, text: verseText }; + const content = document.getElementById('strongsContent'); + const m = ref.match(/^([\w\s]+)\s+(\d+):(\d+)/); + let book = '', chapter = '', verse = ''; + if (m) { + book = m[1].trim().replace(/\s+/g, '_').toLowerCase(); + chapter = m[2]; + verse = m[3]; + } + const greekUrl = `https://biblehub.com/interlinear/${book}/${chapter}-${verse}.htm`; + const currentTranslation = state.settings.referenceVersion; + const stepUrl = getStepBibleUrl(ref, currentTranslation); + content.innerHTML = ` +
+
+ + ${ref} + +
+ +
+
+ ${verseText} +
+ +
+
+
+
+

BibleHub Interlinear

+
+ +
+
+ +
+
+
+

STEP Bible Analysis

+
+ +
+
+ +
+
+

+ These resources provide detailed word-by-word analysis. + Use the "Pop Out" button to open them in a new tab. +

+
+

Quick Links

+
+ BibleHub Strong's Exhaustive Concordance
+ NET Bible (with comprehensive notes)
+
+
+ `; + populateStrongsFootnotes(ref); + document.getElementById('strongsPopup').classList.add('active'); + document.getElementById('popupOverlay').classList.add('active'); + setTimeout(() => { + const copyBtn = document.getElementById('copyVerseBtn'); + if (copyBtn) { + copyBtn.addEventListener('click', copyVerseText); + } + const prevBtn = document.getElementById('prevVerseBtn'); + const nextBtn = document.getElementById('nextVerseBtn'); + if (prevBtn) { + prevBtn.addEventListener('click', navigateToPreviousVerse); + } + if (nextBtn) { + nextBtn.addEventListener('click', navigateToNextVerse); + } + document.querySelectorAll('.resource-frame-btn').forEach(btn => { + btn.addEventListener('click', function() { + const url = this.dataset.url; + const title = this.dataset.title; + popOutResource(url, title); + }); + }); + setupStrongsFootnoteHandlers(); + }, 0); +} +function navigateToPreviousVerse() { + const currentVerseEl = state.currentVerseElement; + if (!currentVerseEl) return; + const allVerses = Array.from(document.querySelectorAll('.verse')); + const currentIndex = allVerses.indexOf(currentVerseEl); + if (currentIndex > 0) { + const prevVerseEl = allVerses[currentIndex - 1]; + showStrongsReference(prevVerseEl); + } +} +function navigateToNextVerse() { + const currentVerseEl = state.currentVerseElement; + if (!currentVerseEl) return; + const allVerses = Array.from(document.querySelectorAll('.verse')); + const currentIndex = allVerses.indexOf(currentVerseEl); + if (currentIndex < allVerses.length - 1) { + const nextVerseEl = allVerses[currentIndex + 1]; + showStrongsReference(nextVerseEl); + } +} +export function closeStrongsPopup() { + document.getElementById('strongsPopup').classList.remove('active'); + document.getElementById('popupOverlay').classList.remove('active'); + state.currentVerseData = null; +} +function copyVerseText() { + if (!state.currentVerseData) return; + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = state.currentVerseData.text; + let plainText = tempDiv.textContent || tempDiv.innerText || ''; + plainText = plainText + .replace(/\s+/g, ' ') + .replace(/\s([.,;:!?])/g, '$1') + .replace(/([.,;:!?])(?=\w)/g, '$1 ') + .trim(); + const txt = `${state.currentVerseData.reference} – ${plainText}`; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(txt) + .then(() => { + const btn = document.getElementById('copyVerseBtn'); + const original = btn.innerHTML; + btn.innerHTML = ' Copied!'; + btn.classList.add('copied'); + setTimeout(() => { + btn.innerHTML = original; + btn.classList.remove('copied'); + }, 2000); + }) + .catch(err => { + console.error('Copy failed:', err); + copyVerseFallback(txt); + }); + } else { + copyVerseFallback(txt); + } +} +function populateStrongsFootnotes(verseRef) { + const container = document.getElementById('strongsFootnotesContainer'); + if (!container) return; + container.innerHTML = ''; + const verseFootnotes = state.footnotes[verseRef]; + if (!verseFootnotes || verseFootnotes.length === 0) { + container.innerHTML = '

No footnotes available for this verse

'; + return; + } + console.log('Found stored footnotes:', verseFootnotes); + container.innerHTML = ` +
+

Footnotes

+ `; + verseFootnotes.forEach(fn => { + const footnoteDiv = document.createElement('div'); + footnoteDiv.className = 'footnote'; + footnoteDiv.innerHTML = ` + ${fn.number} + ${fn.content} + `; + container.appendChild(footnoteDiv); + }); + container.style.display = 'block'; +} +function setupStrongsFootnoteHandlers() { + document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => { + const newRef = ref.cloneNode(true); + ref.parentNode.replaceChild(newRef, ref); + }); + document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => { + ref.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + const footnoteNumber = this.dataset.footnoteNumber; + const footnoteElement = document.querySelector(`#strongsFootnotesContainer .footnote[data-footnote-number="${footnoteNumber}"]`); + if (footnoteElement) { + footnoteElement.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + footnoteElement.style.backgroundColor = 'var(--verse-hover)'; + setTimeout(() => { + footnoteElement.style.backgroundColor = ''; + }, 2000); + } + }); + }); +} +function copyVerseFallback(text) { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + const btn = document.getElementById('copyVerseBtn'); + const original = btn.textContent; + btn.textContent = '✓ Copied!'; + btn.classList.add('copied'); + setTimeout(() => { + btn.textContent = original; + btn.classList.remove('copied'); + }, 2000); + } catch (err) { + console.error('Copy fallback failed:', err); + alert('Could not copy verse text.'); + } finally { + document.body.removeChild(textarea); + } +} +function popOutResource(url, title) { + window.open(url, title, + 'width=800,height=600,menubar=no,toolbar=no,location=no'); +} \ No newline at end of file diff --git a/public/modules/ui.js b/public/modules/ui.js new file mode 100644 index 0000000..1fa2216 --- /dev/null +++ b/public/modules/ui.js @@ -0,0 +1,407 @@ +import { handleError } from '../main.js' +import { + loadSelectedChapter, + populateBookDropdown, + populateChapterDropdown +} from './navigation.js' +import { loadPDF } from './pdf.js' +import { + BOOK_ORDER, + CHAPTER_COUNTS, + bibleComUrlMap, + bibleHubUrlMap, + ebibleOrgUrlMap, + formatBookNameForSource, + getActivePlan, + saveToStorage, + state, + stepBibleUrlMap +} from './state.js' +export function switchNotesView(view) { + state.settings.notesView = view; + const txtBtn = document.getElementById('textViewBtn'); + const mdBtn = document.getElementById('markdownViewBtn'); + const input = document.getElementById('notesInput'); + const display = document.getElementById('notesDisplay'); + if (view === 'text') { + txtBtn.classList.add('active'); + mdBtn.classList.remove('active'); + input.style.display = 'block'; + display.style.display = 'none'; + } else { + txtBtn.classList.remove('active'); + mdBtn.classList.add('active'); + input.style.display = 'none'; + display.style.display = 'block'; + updateMarkdownPreview(); + } + saveToStorage(); +} +export function updateMarkdownPreview() { + if (state.settings.notesView !== 'markdown' || typeof marked === 'undefined') return; + const out = document.getElementById('notesDisplay'); + try { + out.innerHTML = marked.parse(state.notes); + } catch (e) { + console.error('Markdown error:', e); + out.innerHTML = '

Error rendering markdown

'; + } +} +export function insertMarkdown(type) { + const ta = document.getElementById('notesInput'); + const start = ta.selectionStart; + const end = ta.selectionEnd; + const sel = ta.value.substring(start, end); + let repl = ''; + let cursorAdj = 0; + switch (type) { + case 'bold': repl = `**${sel || 'bold text'}**`; cursorAdj = sel ? 0 : -2; break; + case 'italic': repl = `*${sel || 'italic text'}*`; cursorAdj = sel ? 0 : -1; break; + case 'h1': repl = `# ${sel || 'Heading 1'}`; cursorAdj = sel ? 0 : -10; break; + case 'h2': repl = `## ${sel || 'Heading 2'}`; cursorAdj = sel ? 0 : -10; break; + case 'h3': repl = `### ${sel || 'Heading 3'}`; cursorAdj = sel ? 0 : -10; break; + case 'ul': repl = `- ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break; + case 'ol': repl = `1. ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break; + case 'quote': repl = `> ${sel || 'Quote'}`; cursorAdj = sel ? 0 : -6; break; + case 'code': repl = `\`${sel || 'code'}\``; cursorAdj = sel ? 0 : -1; break; + case 'link': repl = `[${sel || 'link text'}](url)`; cursorAdj = sel ? -4 : -14; break; + } + ta.value = ta.value.slice(0, start) + repl + ta.value.slice(end); + const newPos = start + repl.length + cursorAdj; + ta.setSelectionRange(newPos, newPos); + ta.focus(); + state.notes = ta.value; + saveToStorage(); + updateMarkdownPreview(); +} +export function exportNotes(ext) { + if (!state.notes || state.notes.trim() === '') { + alert('No notes to export!'); + return; + } + const blob = new Blob([state.notes], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `study-notes-${new Date().toISOString().split('T')[0]}.${ext}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} +export function toggleNotes() { + document.getElementById('notesSection').classList.toggle('hidden'); +} +export function togglePanelCollapse(panelId) { + const panel = document.getElementById(panelId); + const collapsed = panel.classList.contains('panel-collapsed'); + if (collapsed) { + panel.classList.remove('panel-collapsed'); + if (state.settings.panelWidths[panelId]) { + panel.style.width = state.settings.panelWidths[panelId] + 'px'; + } + state.settings.collapsedPanels[panelId] = false; + } else { + panel.classList.add('panel-collapsed'); + state.settings.collapsedPanels[panelId] = true; + } + saveToStorage(); +} +export function toggleSection(sectionId) { + const content = document.getElementById(`content-${sectionId}`); + const header = document.querySelector(`[data-section="${sectionId}"]`); + const toggle = header.querySelector('.section-toggle'); + const nowCollapsed = content.classList.contains('collapsed'); + content.classList.toggle('collapsed'); + toggle.classList.toggle('collapsed'); + state.settings.collapsedSections[sectionId] = !nowCollapsed; + saveToStorage(); +} +export function toggleReferencePanel() { + const panel = document.getElementById('referencePanel'); + const nowOpen = panel.classList.contains('active'); + if (nowOpen) { + panel.classList.remove('active'); + state.settings.referencePanelOpen = false; + } else { + panel.classList.add('active'); + state.settings.referencePanelOpen = true; + updateReferencePanel(); + } + saveToStorage(); +} +export async function updateReferencePanel() { + try { + const sourceSelect = document.getElementById('referenceSource'); + const source = sourceSelect.value; + const iframe = document.getElementById('referenceIframe'); + const pdfViewer = document.getElementById('pdfViewer'); + const transSel = document.getElementById('referenceTranslation'); + state.settings.referenceSource = source; + state.settings.referenceVersion = transSel.value; + transSel.style.display = source === 'pdf' ? 'none' : 'block'; + filterTranslationOptions(source, transSel); + const actualSource = document.getElementById('referenceSource').value; + const translation = transSel.value; + let passage; + if (state.settings.readingMode === 'readingPlan') { + passage = getActivePlan()[state.settings.currentPassageIndex]; + } else { + passage = { + book: state.settings.manualBook, + chapter: state.settings.manualChapter, + displayRef: `${state.settings.manualBook} ${state.settings.manualChapter}` + }; + } + const bookName = passage.book.toLowerCase().replace(/\s+/g, '_'); + const bookAbbr = bookName.substring(0, 3).toUpperCase(); + const chapter = passage.chapter; + if (actualSource === 'pdf') { + if (!state.settings.customPdf) { + alert('No PDF uploaded. Please upload one in Settings first.'); + document.getElementById('referenceSource').value = 'biblegateway'; + transSel.style.display = 'block'; + filterTranslationOptions('biblegateway', transSel); + return; + } + iframe.style.display = 'none'; + pdfViewer.classList.add('active'); + document.getElementById('zoomLevel').textContent = + Math.round(state.settings.pdfZoom * 100) + '%'; + await loadPDF(); + } else if (actualSource === 'biblehub') { + const bibleHubCode = bibleHubUrlMap[translation] || translation.toLowerCase(); + const url = `https://biblehub.com/${bibleHubCode}/${bookName}/${chapter}.htm`; + iframe.src = url; + } else if (actualSource === 'biblecom') { + const bibleComCode = bibleComUrlMap[translation]; + if (!bibleComCode) { + alert(`Bible.com doesn't support ${translation}. Please choose another translation.`); + return; + } + const formattedBook = formatBookNameForSource(passage.book, 'biblecom'); + const urlFormats = [ + `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}?interface=embed`, + `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}`, + `https://www.bible.com/bible/${bibleComCode}/${chapter}.${translation}?${formattedBook}=${chapter}` + ]; + let currentUrlIndex = 0; + function tryNextUrl() { + if (currentUrlIndex >= urlFormats.length) { + alert('Could not load Bible.com. Please try another reference source.'); + return; + } + iframe.src = urlFormats[currentUrlIndex]; + currentUrlIndex++; + } + iframe.onload = function() { + console.log('Bible.com loaded successfully with format', currentUrlIndex); + }; + iframe.onerror = function() { + console.log('Trying next Bible.com URL format...'); + tryNextUrl(); + }; + tryNextUrl(); + } else if (actualSource === 'ebibleorg') { + const ebibleOrgCode = ebibleOrgUrlMap[translation]; + if (!ebibleOrgCode) { + alert(`eBible.org doesn't support ${translation}. Please choose another translation.`); + return; + } + const bookRef = bookName === 'psalms' ? 'PS1' : `${bookAbbr}1`; + const url = `https://ebible.org/study/?w1=bible&t1=${encodeURIComponent(ebibleOrgCode)}&v1=${bookRef}_${chapter}`; + iframe.src = url; + } else if (actualSource === 'stepbible') { + const stepBibleCode = stepBibleUrlMap[translation]; + if (!stepBibleCode) { + alert(`STEP Bible doesn't support ${translation}. Please choose another translation.`); + return; + } + const url = getStepBibleUrl(passage.displayRef, translation); + iframe.src = url; + } else { + const query = passage.displayRef.replace(/\s+/g, '+'); + let version = translation; + if (translation === 'GNV') version = 'GNV'; + const url = `https://www.biblegateway.com/passage/?search=${query}&version=${version}&interface=print`; + iframe.src = url; + } + saveToStorage(); + } catch (err) { + handleError(err, 'updateReferencePanel'); + } +} +function filterTranslationOptions(source, selectElement) { + const unsupportedTranslations = { + biblecom: [], + biblehub: ['GNV'], + biblegateway: ['BSB'], + stepbible: ['NKJV', 'CSB', 'NLT'], + ebibleorg: ['NASB', 'ASV', 'ESV', 'NKJV', 'CSB', 'NIV', 'NLT'], + pdf: ['NASB1995', 'NASB', 'ASV', 'ESV', 'KJV', 'GNV', 'NKJV', 'BSB', 'CSB', 'NET', 'NIV', 'NLT'] + }; + const allOptions = selectElement.querySelectorAll('option'); + const currentValue = selectElement.value; + let needsNewSelection = false; + let needsSourceChange = false; + allOptions.forEach(option => { + const value = option.value; + const isUnsupported = unsupportedTranslations[source]?.includes(value); + if (isUnsupported) { + option.style.display = 'none'; + option.disabled = true; + if (value === currentValue) { + needsNewSelection = true; + if (value === 'BSB' && source === 'biblegateway') { + needsSourceChange = true; + } + } + } else { + option.style.display = 'block'; + option.disabled = false; + } + }); + if (needsSourceChange) { + document.getElementById('referenceSource').value = 'biblehub'; + state.settings.referenceSource = 'biblehub'; + const sourceSelect = document.getElementById('referenceSource'); + sourceSelect.value = 'biblehub'; + selectElement.value = 'BSB'; + state.settings.referenceVersion = 'BSB'; + } + else if (needsNewSelection) { + let fallbackValue = 'NASB1995'; + if (source === 'biblehub') { + fallbackValue = 'NASB'; + } + selectElement.value = fallbackValue; + state.settings.referenceVersion = fallbackValue; + } + if (needsSourceChange || needsNewSelection) { + saveToStorage(); + } +} +export function restoreBookChapterUI() { + populateBookDropdown(); + const bookSel = document.getElementById('bookSelect'); + const chapterSel = document.getElementById('chapterSelect'); + const savedBook = state.settings.manualBook || BOOK_ORDER[0]; + const bookIdx = BOOK_ORDER.indexOf(savedBook); + const book = bookIdx >= 0 ? BOOK_ORDER[bookIdx] : BOOK_ORDER[0]; + populateBookDropdown(); + bookSel.value = book; + populateChapterDropdown(book); + const savedChap = Number(state.settings.manualChapter) || 1; + const maxChap = CHAPTER_COUNTS[book]; + const chapter = Math.min(savedChap, maxChap); + chapterSel.value = String(chapter); + state.settings.manualBook = book; + state.settings.manualChapter = chapter; + loadSelectedChapter(book, chapter); +} +export function initResizeHandles() { + const handles = document.querySelectorAll('.resize-handle'); + const SPEED_FACTOR = 1.8; + const limits = { + sidebar: { min: 150, max: 600 }, + referencePanel: { min: 250, max: 800 }, + scriptureSection: { min: 300, max: 1200 }, + notesSection: { min: 250, max: 800 } + }; + let resizing = false, + startX = 0, + startW = 0, + panel = null, + invert = false, + pendingRAF = false; + handles.forEach(handle => { + handle.addEventListener('mousedown', e => { + resizing = true; + startX = e.clientX; + const panelId = handle.dataset.panel; + panel = document.getElementById(panelId); + startW = panel.offsetWidth; + invert = (panel.id === 'notesSection'); + document.body.style.cursor = 'ew-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + }); + document.addEventListener('mousemove', e => { + if (!resizing || !panel) return; + let delta = (invert ? startX - e.clientX : e.clientX - startX) * SPEED_FACTOR; + let newW = startW + delta; + const { min, max } = limits[panel.id] || { min: 150, max: 1200 }; + if (newW < min) newW = min; + if (newW > max) newW = max; + if (!pendingRAF) { + pendingRAF = true; + requestAnimationFrame(() => { + panel.style.width = newW + 'px'; + state.settings.panelWidths[panel.id] = newW; + pendingRAF = false; + }); + } + }); + document.addEventListener('mouseup', () => { + if (resizing) { + resizing = false; + panel = null; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + saveToStorage(); + } + }); +} +export function popOutResource(url, title) { + window.open(url, title, + 'width=800,height=600,menubar=no,toolbar=no,location=no'); +} +export function getStepBibleUrl(reference, translation) { + const stepBibleCode = stepBibleUrlMap[translation] || translation; + return `https://www.stepbible.org/?q=version=${stepBibleCode}@reference=${encodeURIComponent(reference)}&options=HNVUG`; +} +export function makeToggleSticky() { + const sidebar = document.getElementById('sidebar'); + const toggle = sidebar.querySelector('.collapse-toggle'); + if (!toggle) return; + toggle.style.position = 'sticky'; + toggle.style.top = '10px'; + toggle.style.zIndex = '1000'; + toggle.style.marginLeft = 'auto'; + toggle.style.marginRight = '10px'; +} +export function restoreSidebarState() { + Object.entries(state.settings.collapsedSections || {}) + .forEach(([sec, collapsed]) => { + if (collapsed) { + const content = document.getElementById(`content-${sec}`); + const header = document.querySelector(`[data-section="${sec}"]`); + const toggle = header?.querySelector('.section-toggle'); + if (content && toggle) { + content.classList.add('collapsed'); + toggle.classList.add('collapsed'); + } + } + }); +} +export function restorePanelStates() { + Object.entries(state.settings.panelWidths || {}) + .forEach(([id, w]) => { + const el = document.getElementById(id); + if (el && w) el.style.width = w + 'px'; + }); + Object.entries(state.settings.collapsedPanels || {}) + .forEach(([id, collapsed]) => { + const el = document.getElementById(id); + if (el && collapsed) el.classList.add('panel-collapsed'); + }); + if (state.settings.referencePanelOpen) { + document.getElementById('referencePanel').classList.add('active'); + document.getElementById('referenceSource').value = + state.settings.referenceSource || 'biblegateway'; + document.getElementById('referenceTranslation').value = + state.settings.referenceVersion || 'NASB1995'; + updateReferencePanel(); + } +} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..8d5580b --- /dev/null +++ b/public/styles.css @@ -0,0 +1,363 @@ +:root{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db;--background:#ecf0f1;--card-background:#ffffff;--toolbar-background:#f8f9fa;--notes-background:#f8f9fa;--sidebar-background:#2c3e50;--header-background:#ffffff;--popup-background:#ffffff;--text-color:#2c3e50;--sidebar-text:#ecf0f1;--border-color:#bdc3c7;--verse-hover:#f0f0f0;--shadow:rgba(0,0,0,0.1);--shadow-strong:rgba(0,0,0,0.3);--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--transition-fast:150ms ease;--transition-normal:300ms ease;--transition-slow:500ms ease} +[data-color-theme="blue"]{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db} +[data-color-theme="green"]{--primary-color:#1b5e20;--secondary-color:#2e7d32;--accent-color:#4caf50} +[data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#6a1b9a;--accent-color:#9c27b0} +[data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#c62828;--accent-color:#f44336} +[data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#ef6c00;--accent-color:#ff9800} +[data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00695c;--accent-color:#009688} +[data-theme="dark"]{--background:#0d1117;--card-background:#161b22;--toolbar-background:#161b22;--notes-background:#0d1117;--sidebar-background:#0d1117;--header-background:#161b22;--popup-background:#161b22;--text-color:#e1e1e1;--sidebar-text:#c9d1d9;--border-color:#444;--verse-hover:#21262d;--shadow:rgba(0,0,0,0.4);--shadow-strong:rgba(0,0,0,0.6)} +[data-theme="dark"][data-color-theme="blue"]{--primary-color:#1a3a52;--secondary-color:#2c5f7f;--accent-color:#5dade2} +[data-theme="dark"][data-color-theme="green"]{--primary-color:#1b4d20;--secondary-color:#2e6d32;--accent-color:#66bb6a} +[data-theme="dark"][data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#7b1fa2;--accent-color:#ab47bc} +[data-theme="dark"][data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#d32f2f;--accent-color:#ef5350} +[data-theme="dark"][data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#f57c00;--accent-color:#ffa726} +[data-theme="dark"][data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00796b;--accent-color:#26a69a} + +*,*::before,*::after{margin:0;padding:0;box-sizing:border-box} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important;scroll-behavior:auto!important} +} +:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px} +@media(prefers-contrast:high){:root{--border-color:#000000;--shadow:rgba(0,0,0,0.8);--shadow-strong:rgba(0,0,0,0.9)} +.verse:hover{outline:2px solid var(--accent-color)} +} + +body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background-color:var(--background);color:var(--text-color);line-height:1.6;transition:background-color 0.3s,color 0.3s} +.container{display:flex;height:100vh;overflow:hidden} +.panel-collapsed{min-width:50px!important;max-width:50px!important} +.panel-collapsed>*:not(.collapse-toggle):not(.resize-handle){display:none!important} +.collapse-toggle{will-change:transform;position:absolute;top:55%;right:5px;transform:translateY(-50%);background:var(--accent-color);color:white;border:none;width:40px;height:40px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;z-index:100;transition:transform 0.3s ease,background-color 0.3s ease;box-shadow:0 2px 8px rgba(0,0,0,0.2)} +.collapse-toggle:hover{transform:translateY(-50%)scale(1.1);box-shadow:0 4px 12px rgba(0,0,0,0.3)} +.collapse-toggle:focus{outline:2px solid white;outline-offset:2px} +.panel-collapsed .collapse-toggle{right:5px} +.collapse-toggle::before{content:'\25C0';transition:transform 0.3s ease} +.panel-collapsed .collapse-toggle::before{content:'\25B6'} +#scriptureSection .collapse-toggle::before{content:'\25B6'} +#scriptureSection.panel-collapsed .collapse-toggle::before{content:'\25C0'} +#notesSection .collapse-toggle::before{content:'\25B6'} +#notesSection.panel-collapsed .collapse-toggle::before{content:'\25C0'} +.scripture-section{scroll-behavior:smooth;scroll-padding-top:20px} +.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0} +.content-area{flex:1;display:flex;overflow:hidden} + +.welcome-screen{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,var(--primary-color),var(--accent-color));z-index:5000;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto} +.welcome-screen.hidden{display:none} +.welcome-content{background:var(--card-background);border-radius:20px;padding:40px;max-width:900px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,0.3);margin:auto;max-height:90vh;overflow-y:auto} +.welcome-content h1{color:var(--primary-color);text-align:center;margin-bottom:15px;font-size:2.5em} +.welcome-content>p{color:var(--text-color);text-align:center;margin-bottom:30px;font-size:1.2em;opacity:0.8} +.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin:30px 0} +.feature-card{padding:20px;background:var(--background);border-radius:10px;border:2px solid var(--border-color);transition:all 0.3s} +.feature-card:hover{transform:translateY(-5px);box-shadow:0 5px 20px var(--shadow);border-color:var(--accent-color)} +.feature-icon{font-size:2.5em;margin-bottom:10px} +.feature-card h3{color:var(--primary-color);margin-bottom:10px} +.feature-card p{color:var(--text-color);opacity:0.8;font-size:0.95em} +.offline-setup{margin:30px 0;padding:25px;background:var(--background);border-radius:10px;border:2px solid var(--accent-color)} +.offline-setup h2{color:var(--primary-color);margin-bottom:15px;display:flex;align-items:center;gap:10px} +.pdf-upload-area{border:2px dashed var(--border-color);border-radius:8px;padding:30px;text-align:center;cursor:pointer;transition:all 0.3s;background:var(--card-background);margin:15px 0} +.pdf-upload-area:hover{border-color:var(--accent-color);background-color:rgba(52,152,219,0.05)} +.pdf-upload-area.has-file{border-color:var(--accent-color);background-color:rgba(52,152,219,0.1)} +.pdf-download-options{margin-top:15px;padding:15px;background-color:var(--card-background);border-radius:5px} +.pdf-download-options h4{margin-bottom:10px;color:var(--text-color)} +.pdf-download-link{display:block;padding:8px 12px;margin-bottom:8px;background-color:var(--background);border:1px solid var(--border-color);border-radius:4px;color:var(--accent-color);text-decoration:none;transition:all 0.2s} +.pdf-download-link:hover{background-color:var(--accent-color);color:white} +.welcome-actions{display:flex;gap:15px;margin-top:30px;justify-content:center} +.welcome-actions button{padding:15px 40px;border:none;border-radius:8px;font-size:16px;font-weight:bold;cursor:pointer;transition:all 0.3s} + +.sidebar{width:280px;background-color:var(--sidebar-background);color:var(--sidebar-text);padding:20px;overflow-y:auto;flex-shrink:0;border-right:1px solid var(--border-color);position:relative;min-width:50px;transition:all 0.3s ease} +.sidebar h2{margin-bottom:20px;font-size:1.3em;border-bottom:2px solid var(--accent-color);padding-bottom:10px;color:var(--sidebar-text)} +.sidebar-section{margin-bottom:10px} +.sidebar-section-header{display:flex;justify-content:space-between;align-items:center;cursor:pointer;padding:10px;margin-top:15px;background-color:rgba(255,255,255,0.05);border-radius:5px;transition:background-color 0.2s} +.sidebar-section-header:hover{background-color:rgba(255,255,255,0.1)} +.sidebar-section-header:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.sidebar-section-header h3{margin:0;font-size:1.1em;color:var(--accent-color)} +.section-toggle{font-size:0.9em;transition:transform 0.3s} +.section-toggle.collapsed{transform:rotate(-90deg)} +.sidebar-section-content{max-height:1000px;overflow:hidden;transition:max-height 0.3s ease-out,opacity 0.3s ease-out;opacity:1} +.sidebar-section-content.collapsed{max-height:0;opacity:0} +.sidebar-links{list-style:none;margin-top:10px} +.sidebar-links li{margin-bottom:8px} +.sidebar-links a{color:var(--sidebar-text);text-decoration:none;display:block;padding:8px 12px;border-radius:5px;transition:background-color 0.3s;opacity:0.9} +.sidebar-links a:hover{background-color:var(--secondary-color);opacity:1} +.sidebar-links a:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.reference-panel-toggle{margin-top:20px;padding:12px;background:var(--accent-color);color:white;border:none;border-radius:5px;cursor:pointer;width:100%;font-size:14px;font-weight:bold;transition:opacity 0.3s} +.reference-panel-toggle:hover{opacity:0.8} +.reference-panel-toggle:focus{outline:2px solid white;outline-offset:2px} + +.reference-panel{display:none;width:400px;background-color:var(--card-background);border-right:1px solid var(--border-color);position:relative;flex-shrink:0;min-width:50px;transition:all 0.3s ease} +.reference-panel.active{display:flex;flex-direction:column} +.reference-panel-header{padding:15px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px} +.reference-panel-header h3{color:var(--text-color);margin:0} +.reference-panel-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap} +.reference-panel-controls select{padding:5px 10px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px} +.reference-panel-controls select:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.reference-panel-close{background:none;border:none;color:var(--text-color);cursor:pointer;font-size:20px;padding:5px 10px;border-radius:4px;transition:background-color 0.2s;margin-left:10px} +.reference-panel-close:hover{background-color:var(--verse-hover)} +.reference-panel-close:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.reference-panel-content{flex:1;overflow:hidden;position:relative} +.reference-panel-iframe{width:100%;height:100%;border:none} +.pdf-viewer{width:100%;height:100%;display:none;flex-direction:column;background-color:var(--card-background)} +.pdf-viewer.active{display:flex} +.pdf-controls{padding:10px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap} +.pdf-controls button{padding:5px 15px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px} +.pdf-controls button:hover{opacity:0.8} +.pdf-controls button:focus{outline:2px solid white;outline-offset:2px} +.pdf-controls button:disabled{opacity:0.5;cursor:not-allowed} +.pdf-controls span{color:var(--text-color);font-size:13px} +.pdf-controls input[type="number"]{width:60px;padding:5px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px} +.pdf-controls input[type="number"]:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.pdf-canvas-container{flex:1;overflow:auto;display:flex;justify-content:center;align-items:flex-start;padding:20px;background-color:var(--background)} +#pdfCanvas{box-shadow:0 2px 10px var(--shadow)} + +.resize-handle{position:absolute;top:0;bottom:0;width:10px;cursor:ew-resize;background-color:transparent;transition:background-color 0.2s;z-index:10;display:flex;align-items:center;justify-content:center} +.resize-handle::after{content:'\22EE\22EE';color:var(--border-color);font-size:18px;opacity:0;transition:opacity 0.2s} +.resize-handle:hover{background-color:rgba(52,152,219,0.1)} +.resize-handle:hover::after{opacity:1;color:var(--accent-color)} +.resize-handle-right{right:0} +.resize-handle-left{left:0} + +.header{background-color:var(--header-background);padding:20px 30px;box-shadow:0 2px 5px var(--shadow);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)} +.header h1{color:var(--text-color);font-size:1.8em} +.header-controls{display:flex;gap:10px;align-items:center} +.theme-toggle{background:none;border:2px solid var(--border-color);color:var(--text-color);width:44px;height:44px;border-radius:50%;cursor:pointer;font-size:20px;display:flex;align-items:center;justify-content:center;transition:all 0.3s} +.theme-toggle:hover{background-color:var(--accent-color);border-color:var(--accent-color);color:white;transform:rotate(180deg)} +.theme-toggle:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.btn{position:relative;padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-size:14px;transition:all 0.3s;background-color:var(--accent-color);color:white} +.btn:hover{opacity:0.8;transform:translateY(-1px)} +.btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.btn:focus::after{content:'';position:absolute;top:-4px;left:-4px;right:-4px;bottom:-4px;border:2px solid var(--accent-color);border-radius:7px;pointer-events:none} +.btn-secondary{background-color:var(--secondary-color)} +.btn:disabled{opacity:0.5;cursor:not-allowed;transform:none} +.btn-primary{background:var(--accent-color);color:white} +.btn-primary:hover{opacity:0.9;transform:translateY(-2px);box-shadow:0 5px 15px var(--shadow)} +.btn-primary:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.btn-danger{background-color:#f44336!important;color:white} +.btn-danger:hover{background-color:#d32f2f!important;opacity:0.9} +.toolbar{background-color:var(--toolbar-background);padding:15px 30px;border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;flex-wrap:wrap} +.toolbar button{padding:8px 15px;font-size:13px} +.toolbar-info{margin-left:auto;color:var(--text-color);opacity:0.7;font-size:14px} +.toolbar-divider{width:1px;height:24px;margin:0 12px;background-color:var(--border-color);align-self:center;opacity:0.6} + +.scripture-section{flex:1;padding:30px;overflow-y:auto;background-color:var(--card-background);margin:var(--spacing-xl);padding-inline:var(--spacing-xl);padding-block:var(--spacing-lg);border-radius:10px;box-shadow:0 2px 10px var(--shadow);border:1px solid var(--border-color);position:relative;min-width:300px;transition:all 0.3s ease;contain:layout style} +.scripture-content{font-size:1.1em;line-height:1.8;text-rendering:optimizeLegibility;font-feature-settings:"kern" 1,"liga" 1,"calt" 1} +.passage-header{background:linear-gradient(135deg,var(--primary-color),var(--primary-color));color:white;padding:20px;border-radius:8px;margin-bottom:20px} +.passage-header h2{font-size:1.5em;margin-bottom:5px;color:white;border:none} +.passage-header .date{opacity:0.9;font-size:0.9em} +.passage-reference{font-weight:bold;font-size:1.2em;margin-bottom:15px;color:var(--accent-color)} +.plan-label{font-size:1.1rem;font-weight:500;color:var(--accent-color);margin-top:8px;margin-bottom:12px} +.verse{margin-bottom:8px;padding:var(--spacing-sm);border-radius:var(--radius-sm);cursor:pointer;transition:background-color var(--transition-fast);position:relative;contain:content} +.verse:hover,.verse:active{background-color:var(--verse-hover);will-change:background-color} +.verse:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.verse-number{font-weight:bold;color:var(--accent-color);margin-right:8px;font-size:0.85em;vertical-align:super} +.verse-text{display:inline} +.highlight-yellow{background-color:#fff59d;color:#000} +.highlight-green{background-color:#a5d6a7;color:#000} +.highlight-blue{background-color:#90caf9;color:#000} +.highlight-pink{background-color:#f48fb1;color:#000} +.highlight-orange{background-color:#ffcc80;color:#000} +.highlight-purple{background-color:#ce93d8;color:#000} +.footnote-ref{position:relative;color:var(--accent-color);font-size:0.7em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease} +.footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)} +.footnote-ref::before{content:'['} +.footnote-ref::after{content:']'} +.footnotes-container{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:6px;border-left:3px solid var(--accent-color);font-size:0.9em;overflow-y:auto;max-height:300px;position:relative;z-index:100} +.footnotes-separator{margin:30px 0 15px 0;border:none;border-top:1px solid var(--border-color);opacity:0.5} +.footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600} +.footnote{cursor:pointer;transition:all 0.2s ease;padding:8px 12px;border-radius:4px;margin-bottom:8px} +.footnote:hover{background-color:var(--verse-hover);transform:translateX(2px)} +.footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;background-color:rgba(0,0,0,0.08);padding:2px 6px;border-radius:3px;transition:all 0.2s ease} +.footnote:hover .footnote-number{background-color:var(--accent-color);color:white} +.footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95} +.footnote.highlighted{background-color:var(--verse-hover);border-left-color:var(--accent-color)} +.strongs-footnotes-container{display:block!important;max-height:200px;overflow-y:auto;padding:2px;background-color:var(--toolbar-background)} +.strongs-footnotes-container .footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600} +.strongs-footnotes-container .footnote{margin-bottom:12px;padding:10px;border-radius:4px;transition:all 0.3s ease} +.strongs-footnotes-container .footnote:hover{background-color:rgba(0,0,0,0.03)} +.strongs-footnotes-container .footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;font-size:0.85em;background-color:rgba(0,0,0,0.08);padding:3px 6px;border-radius:3px} +.strongs-footnotes-container .footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95} +.strongs-footnotes-container .footnote-ref{color:var(--accent-color);font-size:0.75em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease} +.strongs-footnotes-container .footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)} +#strongsFootnotesContainer .footnote{margin-bottom:12px;padding:10px;background-color:var(--toolbar-background);border-radius:4px} +#strongsFootnotesContainer .footnote-number{font-weight:bold;color:var(--accent-color);margin-right:8px} +#strongsFootnotesContainer .footnote-content{display:inline} +#strongsPopup .footnote{cursor:pointer;transition:all 0.2s ease} +#strongsPopup .footnote:hover{background-color:var(--verse-hover)} +#strongsPopup .footnote:hover .footnote-number{background-color:var(--accent-color);color:white} +.verse-navigation{display:flex;align-items:center;gap:10px;margin-right:auto} +.nav-btn{background:var(--button-bg);border:1px solid var(--border-color);border-radius:4px;padding:6px 10px;cursor:pointer;font-size:12px;color:var(--text-color);transition:all 0.2s ease} +.nav-btn:hover{background:var(--button-hover-bg);border-color:var(--accent-color)} +.nav-btn:disabled{opacity:0.5;cursor:not-allowed} + +.color-picker{transform:translateZ(0);display:none;position:absolute;background:var(--popup-background);border:1px solid var(--border-color);border-radius:5px;padding:10px;box-shadow:0 4px 15px var(--shadow-strong);z-index:1000} +.color-picker.active{display:block} +.color-options{display:grid;grid-template-columns:repeat(3,1fr);gap:8px} +.color-option{width:40px;height:40px;border-radius:5px;cursor:pointer;border:2px solid transparent;transition:all 0.2s} +.color-option:hover{border-color:var(--accent-color);transform:scale(1.1)} +.color-option:focus{outline:2px solid var(--accent-color);outline-offset:2px} + +.strongs-popup{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:25px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:90vw;width:90%;max-height:90vh;overflow-y:auto;border:1px solid var(--border-color)} +.strongs-popup.active{display:block} +.popup-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1999} +.popup-overlay.active{display:block} +.popup-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;border-bottom:2px solid var(--accent-color);padding-bottom:10px} +.popup-header h2{color:var(--text-color)} +.popup-close{cursor:pointer;font-size:24px;color:var(--text-color);background:none;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color 0.2s} +.popup-close:hover{background-color:var(--verse-hover)} +.popup-close:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.strongs-content{line-height:1.8;color:var(--text-color)} +.verse-reference-display{font-size:1.2em;font-weight:bold;color:var(--accent-color);margin-bottom:15px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px} +.verse-text-display{padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px;margin-bottom:20px;font-size:1.1em;line-height:1.6} +.copy-verse-btn{padding:6px 12px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px;transition:all 0.2s} +.copy-verse-btn:hover{opacity:0.8} +.copy-verse-btn:focus{outline:2px solid white;outline-offset:2px} +.copy-verse-btn.copied{background-color:#4caf50} +.embedded-resources{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:20px} +.resource-frame{border:1px solid var(--border-color);border-radius:5px;overflow:hidden;background:var(--card-background)} +.resource-frame-header{background:var(--toolbar-background);padding:10px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)} +.resource-frame-header h4{margin:0;font-size:0.9em;color:var(--text-color)} +.resource-frame-actions{display:flex;gap:5px} +.resource-frame-btn{padding:4px 8px;font-size:11px;background:var(--accent-color);color:white;border:none;border-radius:3px;cursor:pointer;transition:opacity 0.2s} +.resource-frame-btn:hover{opacity:0.8} +.resource-frame-btn:focus{outline:2px solid white;outline-offset:2px} +.resource-frame iframe{width:100%;height:400px;border:none} +.strongs-definition{margin-top:15px;padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px} +.strongs-definition h3{color:var(--text-color);margin-bottom:10px} +.api-attribution{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:5px;border:1px solid var(--border-color);font-size:0.85em;opacity:0.8} +.api-attribution a{color:var(--accent-color);text-decoration:none} +.api-attribution a:hover{text-decoration:underline} +.api-attribution a:focus{outline:2px solid var(--accent-color);outline-offset:2px} + +.notes-section{width:400px;padding:20px;background-color:var(--notes-background);border-left:1px solid var(--border-color);display:flex;flex-direction:column;overflow:hidden;position:relative;min-width:50px;transition:all 0.3s ease} +.notes-section.hidden{display:none} +.notes-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px} +.notes-header h3{color:var(--text-color)} +.notes-controls{display:flex;gap:5px} +.notes-controls button{padding:5px 10px;font-size:12px} +.notes-view-toggle{display:flex;gap:5px;padding:5px;background-color:var(--toolbar-background);border-radius:5px;margin-bottom:10px} +.view-toggle-btn{flex:1;padding:8px;border:none;border-radius:4px;cursor:pointer;font-size:13px;background-color:transparent;color:var(--text-color);transition:all 0.2s} +.view-toggle-btn.active{background-color:var(--accent-color);color:white} +.view-toggle-btn:hover:not(.active){background-color:var(--verse-hover)} +.view-toggle-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.markdown-toolbar{display:flex;gap:5px;padding:10px;background-color:var(--toolbar-background);border:1px solid var(--border-color);border-radius:5px 5px 0 0;flex-wrap:wrap} +.markdown-btn{padding:6px 12px;background-color:var(--card-background);border:1px solid var(--border-color);border-radius:4px;cursor:pointer;font-size:13px;color:var(--text-color);transition:all 0.2s;display:flex;align-items:center;gap:5px} +.markdown-btn:hover{background-color:var(--accent-color);color:white;border-color:var(--accent-color)} +.markdown-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.markdown-btn:active{transform:scale(0.95)} +#notesInput,#notesDisplay{flex:1;width:100%;border:1px solid var(--border-color);border-radius:0 0 5px 5px;padding:15px;margin-bottom:10px;background-color:var(--card-background);color:var(--text-color)} +#notesInput{font-family:'Courier New',monospace;font-size:14px;resize:none} +#notesInput:focus{outline:2px solid var(--accent-color);outline-offset:2px} +#notesDisplay{overflow-y:auto;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;font-size:14px} +#notesDisplay h1,#notesDisplay h2,#notesDisplay h3{margin-top:20px;margin-bottom:10px;color:var(--text-color)} +#notesDisplay h1{font-size:2em;border-bottom:2px solid var(--border-color);padding-bottom:10px} +#notesDisplay h2{font-size:1.5em} +#notesDisplay h3{font-size:1.2em} +#notesDisplay code{background-color:var(--toolbar-background);padding:2px 6px;border-radius:3px;font-family:'Courier New',monospace;border:1px solid var(--border-color)} +#notesDisplay pre{background-color:var(--toolbar-background);padding:15px;border-radius:5px;overflow-x:auto;border:1px solid var(--border-color)} +#notesDisplay pre code{background:none;border:none;padding:0} +#notesDisplay blockquote{border-left:4px solid var(--accent-color);padding-left:15px;margin:15px 0;opacity:0.8} +#notesDisplay ul,#notesDisplay ol{margin-left:25px;margin-bottom:15px} +#notesDisplay a{color:var(--accent-color)} +#notesDisplay a:focus{outline:2px solid var(--accent-color);outline-offset:2px} +#notesDisplay p{margin-bottom:10px} +#notesDisplay table{border-collapse:collapse;width:100%;margin:15px 0} +#notesDisplay table th,#notesDisplay table td{border:1px solid var(--border-color);padding:8px;text-align:left} +#notesDisplay table th{background-color:var(--toolbar-background);font-weight:bold} + +.settings-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:30px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:600px;width:90%;border:1px solid var(--border-color);max-height:80vh;overflow-y:auto} +.settings-modal.active{display:block} +.settings-group{margin-bottom:20px} +.settings-group label{display:block;margin-bottom:8px;font-weight:bold;color:var(--text-color)} +.settings-group input,.settings-group select{width:100%;padding:10px;border:1px solid var(--border-color);border-radius:5px;font-size:14px;background-color:var(--card-background);color:var(--text-color)} +.settings-group input:focus,.settings-group select:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.settings-group small{display:block;margin-top:5px;opacity:0.7;color:var(--text-color)} +.settings-group small a{color:var(--accent-color)} +.settings-group small a:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.color-theme-options{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px} +.color-theme-option{padding:15px;border:2px solid var(--border-color);border-radius:8px;cursor:pointer;text-align:center;transition:all 0.3s;background:var(--card-background)} +.color-theme-option:hover{transform:scale(1.05)} +.color-theme-option:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.color-theme-option.selected{border-color:var(--accent-color);background-color:var(--accent-color);color:white} +.color-theme-preview{width:100%;height:40px;border-radius:5px;margin-bottom:8px} +.settings-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:25px} +.settings-section{margin-bottom:25px;padding-bottom:20px;border-bottom:1px solid var(--border-color)} +.about-content{line-height:1.6} +.about-creator{margin-bottom:12px;font-size:.9em} +.about-description{margin-bottom:5px;padding:5px;background-color:var(--bg-secondary);border-radius:5px} +.about-description p{margin:0;color:var(--text-color)} +.attribution-links{margin-top:20px;padding-top:15px;border-top:1px solid var(--border-color)} +.attribution-links h4{margin-bottom:12px;color:var(--text-color);font-size:1em;font-weight:600} +.attribution-link{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-bottom:8px;background-color:var(--bg-secondary);border-radius:6px;text-decoration:none;color:var(--text-color);transition:all 0.2s ease;border:1px solid transparent} +.attribution-link:hover{background-color:var(--button-hover-bg);border-color:var(--accent-color);transform:translateY(-1px)} +.attribution-link i{font-size:1.2em;width:20px;text-align:center} +.attribution-link.gab{color:#4cf278} +.attribution-link.gab:hover{background-color:rgba(151,246,202,0.1)} +.attribution-link.lumo{color:#8c67cd} +.attribution-link.lumo:hover{background-color:rgba(197,151,246,0.1)} +.link-description{font-size:0.85em;opacity:0.8;margin-top:4px} +.version-info{font-family:monospace;padding:2px 4px;background-color:var(--bg-secondary);border-radius:6px;display:inline-block} + +.loading-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);display:none;justify-content:center;align-items:center;z-index:3000} +.loading-overlay.active{display:flex} +.loading-spinner{width:60px;height:60px;border:6px solid rgba(255,255,255,0.3);border-top-color:var(--accent-color);border-radius:50%;animation:spin 1s linear infinite} +@keyframes spin{to{transform:rotate(360deg)} +} + +.error-message{background-color:#ff5252;color:white;padding:15px;border-radius:5px;margin-bottom:20px} + +@media(max-width:1024px){.sidebar{width:250px} +.notes-section{width:350px} +.reference-panel{width:350px} +.feature-grid{grid-template-columns:1fr} +} +@media(max-width:768px){.container{flex-direction:column} +.sidebar{width:100%;height:auto;max-height:200px} +.content-area{flex-direction:column} +.notes-section{width:100%;border-left:none;border-top:1px solid var(--border-color)} +.reference-panel{width:100%;border-left:none;border-top:1px solid var(--border-color)} +.footnote{padding:6px;margin-bottom:8px} +.toolbar-info{display:none} +.embedded-resources{grid-template-columns:1fr} +.welcome-content h1{font-size:2em} +.welcome-actions{flex-direction:column} +} +@container(max-width:768px){.feature-grid{grid-template-columns:1fr} +.toolbar{flex-direction:column;gap:15px} +} +.toggle-notes{display:none} +@media(max-width:768px){.toggle-notes{display:inline-block} +} +.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} +@media print{.sidebar,.header-controls,.toolbar,.notes-section,.reference-panel-toggle{display:none!important} +.scripture-section{margin:0;box-shadow:none;border:none} +body{background:white;color:black} +} +html{font-size:16px} +@media(max-width:768px){html{font-size:14px} +} +@media(hover:none)and(pointer:coarse){.verse:hover{background-color:transparent} +.sidebar-links a{padding:var(--spacing-md)var(--spacing-lg)} +.btn{min-height:44px;min-width:44px} +} +@media(prefers-contrast:high){.verse-number{font-weight:900} +.passage-header{border:2px solid var(--text-color)} +} +@keyframes spin{0%{transform:rotate(0deg)} +100%{transform:rotate(360deg)} +} +.loading-spinner{animation:spin 1s linear infinite;transform:translateZ(0)} +.settings-modal:focus{outline:none} +.settings-modal *:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px} +@media print{.verse{page-break-inside:avoid} +.passage-header{break-after:avoid} +body{background:white!important;color:black!important} +.verse{cursor:default!important} +.highlight-yellow{background-color:#fff59d!important} +} + +::-webkit-scrollbar{width:10px;height:10px} +::-webkit-scrollbar-track{background:var(--background)} +::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:5px} +::-webkit-scrollbar-thumb:hover{background:var(--secondary-color)} +[data-theme="dark"]::-webkit-scrollbar-thumb{background:var(--secondary-color)} +[data-theme="dark"]::-webkit-scrollbar-thumb:hover{background:var(--accent-color)} +*{scrollbar-width:thin;scrollbar-color:var(--border-color)var(--background)} +[data-theme="dark"]*{scrollbar-color:var(--secondary-color)var(--background)} + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..67c6375 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,249 @@ +import { APP_VERSION } from "./modules/state"; +const CACHE_VERSION = { + static: 'v2', + api: 'v1', + pdf: 'v1' +}; +const CACHE_NAME = `provinent-scripture-${APP_VERSION}-${CACHE_VERSION.static}`; +const API_CACHE_NAME = `provinent-api-cache-${CACHE_VERSION.api}`; +const OFFLINE_PDF_CACHE = `provinent-pdf-cache-${CACHE_VERSION.pdf}`; +const MAX_CACHE_AGE = 24 * 60 * 60 * 1000; +const PDF_SIZE_LIMIT = 10 * 1024 * 1024; +const PRECACHE_URLS = [ + '/', + '/index.html', + '/api.js', + '/main.js', + '/navigation.js', + '/passage.js', + '/pdf.js', + '/settings.js', + '/state.js', + '/strongs.js', + '/ui.js', + '/styles.css', + '/manifest.json' +]; +self.addEventListener('install', (event) => { + console.log('[Service Worker] Installing and pre-caching static assets'); + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + console.log('[Service Worker] Caching static assets:', PRECACHE_URLS); + return cache.addAll(PRECACHE_URLS); + }) + .then(() => { + console.log('[Service Worker] Pre-caching complete, skipping waiting'); + return self.skipWaiting(); + }) + .catch((error) => { + console.error('[Service Worker] Pre-caching failed:', error); + }) + ); +}); +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys() + .then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME && + cacheName !== API_CACHE_NAME && + cacheName !== OFFLINE_PDF_CACHE) { + console.log('[Service Worker] Deleting old cache:', cacheName); + return caches.delete(cacheName) + .catch((error) => { + console.warn('[Service Worker] Failed to delete cache:', cacheName, error); + }); + } + }) + ); + }) + .then(() => { + return Promise.all([ + manageCacheSize(API_CACHE_NAME, 100), + manageCacheSize(OFFLINE_PDF_CACHE, 5) + ]); + }) + .then(() => { + console.log('[Service Worker] Cache cleanup complete, claiming clients'); + return self.clients.claim(); + }) + ); +}); +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + if (url.origin === 'https://bible.helloao.org') { + event.respondWith(handleApiRequest(event.request)); + } + else if (url.hostname.includes('biblehub.com') || + url.hostname.includes('biblegateway.com') || + url.hostname.includes('bible.com')) { + event.respondWith(handleExternalBibleResource(event.request)); + } + else if (url.pathname.endsWith('.pdf')) { + event.respondWith(handlePdfRequest(event.request)); + } + else if (PRECACHE_URLS.includes(url.pathname) || + PRECACHE_URLS.includes(url.pathname + '/')) { + event.respondWith(handleStaticAssetRequest(event.request)); + } +}); +async function handleApiRequest(request) { + const cache = await caches.open(API_CACHE_NAME); + const cachedResponse = await cache.match(request); + if (cachedResponse) { + const cacheTime = new Date(cachedResponse.headers.get('sw-cache-time')); + if (Date.now() - cacheTime.getTime() < MAX_CACHE_AGE) { + console.log('[Service Worker] Serving fresh cached API response'); + return cachedResponse; + } + } + try { + console.log('[Service Worker] Fetching fresh API response from network'); + const networkResponse = await fetch(request); + if (networkResponse.ok) { + const headers = new Headers(networkResponse.headers); + headers.set('sw-cache-time', new Date().toISOString()); + const responseToCache = new Response( + await networkResponse.clone().blob(), + { + status: networkResponse.status, + statusText: networkResponse.statusText, + headers: headers + } + ); + await cache.put(request, responseToCache); + console.log('[Service Worker] Cached fresh API response'); + await manageCacheSize(API_CACHE_NAME, 100); + } + return networkResponse; + } + catch (error) { + console.error('[Service Worker] API request failed, returning offline response'); + return new Response( + JSON.stringify({ + error: 'Offline mode - Bible data not available', + message: 'Please connect to the internet to access Scripture data' + }), + { + status: 503, + headers: { 'Content-Type': 'application/json' } + } + ); + } +} +async function handleExternalBibleResource(request) { + try { + console.log('[Service Worker] Fetching external Bible resource:', request.url); + return await fetch(request); + } + catch (error) { + console.warn('[Service Worker] External resource unavailable offline:', request.url); + return new Response( + ` + +

Offline Mode

+

External Bible resources from ${new URL(request.url).hostname} + are not available offline.

+

Please connect to the internet to access this resource.

+ + `, + { + headers: { 'Content-Type': 'text/html' } + } + ); + } +} +async function handlePdfRequest(request) { + const cache = await caches.open(OFFLINE_PDF_CACHE); + const cachedPdf = await cache.match(request); + if (cachedPdf) { + console.log('[Service Worker] Serving cached PDF:', request.url); + return cachedPdf; + } + try { + console.log('[Service Worker] Fetching PDF from network:', request.url); + const response = await fetch(request); + if (response.status === 200) { + const contentLength = response.headers.get('content-length'); + if (!contentLength || parseInt(contentLength) <= PDF_SIZE_LIMIT) { + await cache.put(request, response.clone()); + console.log('[Service Worker] Cached PDF:', request.url); + await manageCacheSize(OFFLINE_PDF_CACHE, 5); + } + } + return response; + } + catch (error) { + console.warn('[Service Worker] PDF unavailable offline:', request.url); + return new Response( + 'PDF not available offline. Please connect to the internet to access this resource.', + { + status: 503, + headers: { 'Content-Type': 'text/plain' } + } + ); + } +} +async function handleStaticAssetRequest(request) { + try { + const cachedResponse = await caches.match(request); + if (cachedResponse) { + console.log('[Service Worker] Serving cached static asset:', request.url); + return cachedResponse; + } + console.log('[Service Worker] Fetching static asset from network:', request.url); + return await fetch(request); + } + catch (error) { + console.error('[Service Worker] Static asset unavailable:', request.url); + if (event.request.destination === 'document') { + return new Response( + ` + +

Offline

+

Provident Scripture Study is currently offline.

+

Some features may be limited without an internet connection.

+ + `, + { + headers: { 'Content-Type': 'text/html' } + } + ); + } + throw error; + } +} +self.addEventListener('message', (event) => { + switch (event.data.type) { + case 'CLEAR_CACHE': + console.log('[Service Worker] Clearing caches per client request'); + caches.delete(CACHE_NAME).catch(console.warn); + caches.delete(API_CACHE_NAME).catch(console.warn); + break; + case 'CACHE_PDF': + console.log('[Service Worker] Caching PDF from client data:', event.data.url); + caches.open(OFFLINE_PDF_CACHE) + .then(cache => cache.put(event.data.url, new Response(event.data.data))) + .catch(console.error); + break; + default: + console.log('[Service Worker] Received unknown message:', event.data.type); + } +}); +async function manageCacheSize(cacheName, maxSize = 50) { + try { + const cache = await caches.open(cacheName); + const requests = await cache.keys(); + if (requests.length > maxSize) { + const excessCount = requests.length - maxSize; + const excessRequests = requests.slice(0, excessCount); + await Promise.all(excessRequests.map(request => cache.delete(request))); + console.log(`[Service Worker] Trimmed ${excessCount} entries from ${cacheName}`); + } + } + catch (error) { + console.error(`[Service Worker] Failed to manage cache size for ${cacheName}:`, error); + } +} \ No newline at end of file From c65c70f27ab77ab60f8df5793fc28f389b229d92 Mon Sep 17 00:00:00 2001 From: jd-code76 Date: Sat, 1 Nov 2025 21:28:30 -0400 Subject: [PATCH 2/4] Delete public directory --- public/index.html | 1 - public/main.js | 610 ----------------------------------- public/modules/api.js | 115 ------- public/modules/navigation.js | 252 --------------- public/modules/passage.js | 274 ---------------- public/modules/pdf.js | 372 --------------------- public/modules/settings.js | 274 ---------------- public/modules/state.js | 399 ----------------------- public/modules/strongs.js | 254 --------------- public/modules/ui.js | 407 ----------------------- public/styles.css | 363 --------------------- public/sw.js | 249 -------------- 12 files changed, 3570 deletions(-) delete mode 100644 public/index.html delete mode 100644 public/main.js delete mode 100644 public/modules/api.js delete mode 100644 public/modules/navigation.js delete mode 100644 public/modules/passage.js delete mode 100644 public/modules/pdf.js delete mode 100644 public/modules/settings.js delete mode 100644 public/modules/state.js delete mode 100644 public/modules/strongs.js delete mode 100644 public/modules/ui.js delete mode 100644 public/styles.css delete mode 100644 public/sw.js diff --git a/public/index.html b/public/index.html deleted file mode 100644 index f2aa5e1..0000000 --- a/public/index.html +++ /dev/null @@ -1 +0,0 @@ - Provinent Scripture Study

Welcome to Provinent Scripture Study!

A comprehensive Bible study companion with daily passages, notes, and powerful study tools.

Daily Passages

Get a new scripture passage every day with sequential or specific book reading plans.

Highlight & Annotate

Right‑click verses to highlight in colors. Take notes with full markdown support.

Original Languages

Click any verse to access Strong's Concordance, Greek/Hebrew interlinear, and more.

Reference Bible

Open a side‑by‑side reference panel to compare translations while you study.

Export & Import

Save your highlights and notes. Import/export your data anytime.

Offline Mode

Upload a PDF Bible to read and take notes while offline.

Optional: Upload a PDF to View Offline

You can upload a free Berean Standard Bible PDF. This is entirely optional – you can skip this and use the online version.

2. Upload the downloaded PDF here:

Click to select your downloaded PDF

Or drag and drop here

Reference Bible

Page of 0 Zoom: 100%

Provinent Scripture Study

Click any verse for further analysis • Right‑click to highlight

Passage of the Day

Attribution

Scripture quotations are provided by API from bible.helloao.org, without which this app would not have been possible. And a thank you to Berean Bible for their excellent translation work. All copyrights reserved by their respective owners. Currently selected translation: Berean Standard Bible®

Study Notes

\ No newline at end of file diff --git a/public/main.js b/public/main.js deleted file mode 100644 index 5d6f768..0000000 --- a/public/main.js +++ /dev/null @@ -1,610 +0,0 @@ -import { - initBookChapterControls, - nextPassage, - prevPassage, - randomPassage -} from './modules/navigation.js' -import { loadPassage, setupFootnoteHandlers } from './modules/passage.js' -import { - clearSearch, - currentSearch, - handlePDFUpload, - navigateToSearchResult, - renderPage, - savePDFToIndexedDB, - searchPDF, - setupPDFCleanup, - updateCustomPdfInfo, - updatePDFZoom -} from './modules/pdf.js' -import { - clearCache, - closeSettings, - deleteAllData, - exportData, - importData, - openSettings, - restartReadingPlan, - resumeReadingPlan, - saveSettings -} from './modules/settings.js' -import { - updateBibleGatewayVersion, - loadFromCookies, - loadFromStorage, - saveToCookies, - saveToStorage, - state -} from './modules/state.js' -import { closeStrongsPopup } from './modules/strongs.js' -import { - exportNotes, - initResizeHandles, - insertMarkdown, - makeToggleSticky, - restoreBookChapterUI, - restorePanelStates, - restoreSidebarState, - switchNotesView, - toggleNotes, - togglePanelCollapse, - toggleReferencePanel, - toggleSection, - updateMarkdownPreview, - updateReferencePanel -} from './modules/ui.js' -if (typeof pdfjsLib !== 'undefined') { - pdfjsLib.GlobalWorkerOptions.workerSrc = - 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; -} -if (typeof marked !== 'undefined') { - marked.setOptions({ - breaks: true, - gfm: true - }); -} -function updateDateTime() { - const now = new Date(); - document.getElementById('currentDate').textContent = - now.toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - }); -} -class AppError extends Error { - constructor(message, type, originalError) { - super(message); - this.name = 'AppError'; - this.type = type; - this.originalError = originalError; - } -} -export function handleError(error, context) { - console.error(`Error in ${context}:`, error); - const userMessage = error instanceof AppError - ? error.message - : 'An unexpected error occurred'; - showError(userMessage); - if (window.errorTracker) { - window.errorTracker.log(error, context); - } -} -function setupEventListeners() { - document.getElementById('getStartedBtn') - .addEventListener('click', completeWelcome); - document.getElementById('welcomePdfUploadArea') - .addEventListener('click', () => { - document.getElementById('welcomePdfUpload').click(); - }); - document.getElementById('welcomePdfUpload') - .addEventListener('change', handleWelcomePDFUpload); - document.querySelector('.theme-toggle') - .addEventListener('click', toggleTheme); - document.getElementById('openSettingsBtn') - .addEventListener('click', openSettings); - document.getElementById('exportDataBtn') - .addEventListener('click', exportData); - document.getElementById('importDataBtn') - .addEventListener('click', () => { - document.getElementById('importFile').click(); - }); - document.getElementById('importFile') - .addEventListener('change', importData); - document.querySelector('.toggle-notes') - .addEventListener('click', toggleNotes); - document.getElementById('prevPassageBtn') - .addEventListener('click', prevPassage); - document.getElementById('nextPassageBtn') - .addEventListener('click', nextPassage); - document.getElementById('resumeReadingPlanBtn') - .addEventListener('click', () => { - if (confirm('Return to the daily reading plan where you left off?')) { - resumeReadingPlan(); - } - }); - document.getElementById('randomPassageBtn') - .addEventListener('click', randomPassage); - document.getElementById('referencePanelToggle') - .addEventListener('click', toggleReferencePanel); - document.querySelectorAll('.sidebar-section-header') - .forEach(h => h.addEventListener('click', () => { - const sec = h.dataset.section; - toggleSection(sec); - })); - document.getElementById('referenceTranslation').addEventListener('change', function() { - const tempTranslation = this.value; - const oldTranslation = state.settings.referenceVersion; - state.settings.referenceVersion = tempTranslation; - updateBibleGatewayVersion(); - state.settings.referenceVersion = oldTranslation; - }); - document.addEventListener('DOMContentLoaded', makeToggleSticky); - document.querySelectorAll('.collapse-toggle') - .forEach(btn => btn.addEventListener('click', function () { - const panel = this.closest('[id]'); - if (panel) togglePanelCollapse(panel.id); - })); - document.getElementById('referenceSource') - .addEventListener('change', updateReferencePanel); - document.getElementById('referenceTranslation') - .addEventListener('change', updateReferencePanel); - document.querySelector('.reference-panel-close') - .addEventListener('click', toggleReferencePanel); - document.getElementById('prevPage').addEventListener('click', async () => { - if (!state.pdf.doc || state.pdf.currentPage <= 1) return; - try { - if (state.pdf.renderTask) { - await state.pdf.renderTask.cancel(); - state.pdf.renderTask = null; - } - state.pdf.currentPage--; - await renderPage(state.pdf.currentPage); - } catch (err) { - console.warn('Error navigating to previous page:', err); - await loadPDF(); - } - }); - document.getElementById('nextPage').addEventListener('click', async () => { - if (!state.pdf.doc || state.pdf.currentPage >= state.pdf.doc.numPages) return; - try { - if (state.pdf.renderTask) { - await state.pdf.renderTask.cancel(); - state.pdf.renderTask = null; - } - state.pdf.currentPage++; - await renderPage(state.pdf.currentPage); - } catch (err) { - console.warn('Error navigating to next page:', err); - await loadPDF(); - } - }); - document.getElementById('pageInput').addEventListener('change', async () => { - if (!state.pdf.doc) { - document.getElementById('pageInput').value = state.pdf.currentPage; - return; - } - const inp = document.getElementById('pageInput'); - let p = parseInt(inp.value, 10); - if (Number.isNaN(p)) { - inp.value = state.pdf.currentPage; - return; - } - p = Math.max(1, Math.min(p, state.pdf.doc.numPages)); - try { - state.pdf.currentPage = p; - await renderPage(p); - } catch (err) { - console.warn('Error navigating to page:', err); - inp.value = state.pdf.currentPage; - await loadPDF(); - } - }); - document.getElementById('zoomIn').addEventListener('click', () => { - if (!state.pdf.doc) return; - const newZoom = Math.min(state.pdf.zoomLevel + 0.25, 3.0); - updatePDFZoom(newZoom); - }); - document.getElementById('zoomOut').addEventListener('click', () => { - if (!state.pdf.doc) return; - const newZoom = Math.max(state.pdf.zoomLevel - 0.25, 0.5); - updatePDFZoom(newZoom); - }); - document.getElementById('pdfSearchBtn').addEventListener('click', searchPDF); - document.getElementById('pdfSearchInput').addEventListener('keypress', (e) => { - if (e.key === 'Enter') searchPDF(); - }); - document.getElementById('clearSearchBtn').addEventListener('click', clearSearch); - const nextSearchBtn = document.getElementById('nextSearchResult'); - const prevSearchBtn = document.getElementById('prevSearchResult'); - if (nextSearchBtn) { - nextSearchBtn.addEventListener('click', () => { - navigateToSearchResult(currentSearch.currentResult + 1); - }); - } - if (prevSearchBtn) { - prevSearchBtn.addEventListener('click', () => { - navigateToSearchResult(currentSearch.currentResult - 1); - }); - } - document.getElementById('notesInput') - .addEventListener('input', e => { - state.notes = e.target.value; - saveToStorage(); - if (state.settings.notesView === 'markdown') { - updateMarkdownPreview(); - } - }); - document.getElementById('textViewBtn') - .addEventListener('click', () => switchNotesView('text')); - document.getElementById('markdownViewBtn') - .addEventListener('click', () => switchNotesView('markdown')); - document.querySelectorAll('.markdown-btn') - .forEach(btn => btn.addEventListener('click', () => { - const fmt = btn.dataset.format; - insertMarkdown(fmt); - })); - document.querySelectorAll('.notes-controls button') - .forEach(btn => btn.addEventListener('click', () => { - const fmt = btn.dataset.format; - exportNotes(fmt); - })); - document.querySelectorAll('.color-option') - .forEach(opt => opt.addEventListener('click', () => { - const col = opt.dataset.color; - applyHighlight(col); - })); - document.getElementById('removeHighlight') - .addEventListener('click', () => applyHighlight('none')); - document.addEventListener('contextmenu', e => { - const verse = e.target.closest('.verse'); - if (verse) { - e.preventDefault(); - showColorPicker(e, verse); - } - }); - document.addEventListener('click', e => { - const picker = document.getElementById('colorPicker'); - if (!picker.contains(e.target) && !e.target.closest('.verse')) { - picker.classList.remove('active'); - } - }); - document.getElementById('popupOverlay') - .addEventListener('click', closeStrongsPopup); - document.querySelector('#strongsPopup .popup-close') - .addEventListener('click', closeStrongsPopup); - document.getElementById('settingsOverlay') - .addEventListener('click', closeSettings); - document.getElementById('closeSettingsBtn') - .addEventListener('click', closeSettings); - document.getElementById('cancelSettingsBtn') - .addEventListener('click', closeSettings); - document.getElementById('saveSettingsBtn') - .addEventListener('click', saveSettings); - document.getElementById('clearHighlightsBtn') - .addEventListener('click', clearHighlights); - document.getElementById('restartReadingPlanBtn') - .addEventListener('click', () => { - restartReadingPlan(); - }); - document.addEventListener('contentLoaded', () => { - setTimeout(setupFootnoteHandlers, 50); - }); - document.getElementById('clearCacheBtn') - .addEventListener('click', clearCache); - document.getElementById('deleteAllDataBtn') - .addEventListener('click', deleteAllData); - document.getElementById('settingsPdfUploadBtn') - .addEventListener('click', () => { - document.getElementById('settingsPdfUpload').click(); - }); - document.getElementById('settingsPdfUpload') - .addEventListener('change', handlePDFUpload); - document.querySelectorAll('.color-theme-option') - .forEach(opt => opt.addEventListener('click', () => { - const theme = opt.dataset.theme; - selectColorTheme(theme); - })); - document.addEventListener('keydown', e => { - const ta = document.getElementById('notesInput'); - if (document.activeElement !== ta) return; - if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { - switch (e.key.toLowerCase()) { - case 'b': - e.preventDefault(); - insertMarkdown('bold'); - break; - case 'i': - e.preventDefault(); - insertMarkdown('italic'); - break; - } - } - }); -} -export function arrayBufferToBase64(buf) { - let binary = ''; - const bytes = new Uint8Array(buf); - const chunk = 0x8000; - for (let i = 0; i < bytes.length; i += chunk) { - binary += String.fromCharCode.apply( - null, - Array.from(bytes.subarray(i, i + chunk)) - ); - } - return btoa(binary); -} -export function base64ToArrayBuffer(b64) { - const bin = atob(b64); - const arr = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) { - arr[i] = bin.charCodeAt(i); - } - return arr.buffer; -} -export function readFileAsArrayBuffer(file) { - return new Promise((resolve, reject) => { - const r = new FileReader(); - r.onload = e => resolve(e.target.result); - r.onerror = () => reject(new Error('Failed to read file')); - r.readAsArrayBuffer(file); - }); -} -export function showLoading(flag) { - document.getElementById('loadingOverlay').classList.toggle('active', flag); -} -export function showError(msg) { - document.getElementById('errorContainer').innerHTML = - `
${msg}
`; -} -export function clearError() { - document.getElementById('errorContainer').innerHTML = ''; -} -async function registerServiceWorker() { - if ('serviceWorker' in navigator) { - try { - const response = await fetch('/sw.js'); - if (!response.ok) { - console.error('Service worker script not found or inaccessible'); - return null; - } - const registration = await navigator.serviceWorker.register('/sw.js', { - scope: '/' - }); - console.log('Service Worker registered successfully:', registration); - return registration; - } catch (err) { - console.error('Service Worker registration failed:', err); - if (err.message.includes('MIME')) { - console.error('MIME type issue - ensure server serves sw.js as application/javascript'); - } - return null; - } - } else { - console.log('Service workers are not supported'); - return null; - } -} -function updateOfflineStatus(isOffline) { - const indicator = document.getElementById('offlineIndicator'); - if (!indicator) { - const newIndicator = document.createElement('div'); - newIndicator.id = 'offlineIndicator'; - newIndicator.style.cssText = ` - position: fixed; - top: 10px; - right: 10px; - padding: 10px; - background: ${isOffline ? '#ff6b6b' : '#51cf66'}; - color: white; - border-radius: 5px; - z-index: 10000; - font-size: 14px; - transition: all 0.3s ease; - `; - newIndicator.textContent = isOffline ? 'Offline Mode' : 'Online'; - document.body.appendChild(newIndicator); - setTimeout(() => { - newIndicator.style.opacity = '0'; - setTimeout(() => newIndicator.remove(), 300); - }, 3000); - } else { - indicator.textContent = isOffline ? 'Offline Mode' : 'Online'; - indicator.style.background = isOffline ? '#ff6b6b' : '#51cf66'; - } -} -const offlineStyles = ` -#offlineIndicator { - position: fixed; - top: 10px; - right: 10px; - padding: 10px 15px; - background: #ff6b6b; - color: white; - border-radius: 5px; - z-index: 10000; - font-size: 14px; - font-weight: bold; - box-shadow: 0 2px 10px rgba(0,0,0,0.2); - transition: all 0.3s ease; -} -#offlineIndicator.online { - background: #51cf66; -} -`; -function showColorPicker(ev, verseEl) { - const picker = document.getElementById('colorPicker'); - state.currentVerse = verseEl; - picker.style.left = ev.pageX + 'px'; - picker.style.top = ev.pageY + 'px'; - picker.classList.add('active'); -} -function applyHighlight(col) { - if (!state.currentVerse) return; - const verseRef = state.currentVerse.dataset.verse; - state.currentVerse.classList.remove( - 'highlight-yellow', 'highlight-green', 'highlight-blue', - 'highlight-pink', 'highlight-orange', 'highlight-purple' - ); - if (col !== 'none') { - state.currentVerse.classList.add(`highlight-${col}`); - state.highlights[verseRef] = col; - } else { - delete state.highlights[verseRef]; - } - saveToStorage(); - document.getElementById('colorPicker').classList.remove('active'); -} -function clearHighlights() { - if (!confirm('Delete ALL highlights?')) return; - state.highlights = {}; - document.querySelectorAll('.verse') - .forEach(v => v.classList.remove( - 'highlight-yellow', 'highlight-green', 'highlight-blue', - 'highlight-pink', 'highlight-orange', 'highlight-purple' - )); - saveToStorage(); -} -function toggleTheme() { - state.settings.theme = state.settings.theme === 'light' ? 'dark' : 'light'; - applyTheme(); - saveToStorage(); - saveToCookies(); -} -export function applyTheme() { - document.documentElement.setAttribute('data-theme', state.settings.theme); - document.getElementById('themeIcon').textContent = - state.settings.theme === 'light' ? '🌙' : '☀️'; -} -export function selectColorTheme(t) { - state.settings.colorTheme = t; - applyColorTheme(); - document.querySelectorAll('.color-theme-option') - .forEach(o => o.classList.remove('selected')); - document.querySelector(`.color-theme-option[data-theme="${t}"]`) - .classList.add('selected'); -} -export function applyColorTheme() { - document.documentElement.setAttribute('data-color-theme', - state.settings.colorTheme); -} -async function handleWelcomePDFUpload(ev) { - try { - const file = ev.target.files[0]; - if (!file) return; - if (file.size > 50 * 1024 * 1024) { - alert('PDF file is too large (max 50 MiB).'); - ev.target.value = ''; - return; - } - state.welcomePdfFile = file; - document.getElementById('welcomePdfUploadArea').classList.add('has-file'); - document.getElementById('welcomeUploadText').innerHTML = ` - ${file.name}
- Ready to use for offline mode`; - } catch (err) { - handleError(err, 'handleWelcomePDFUpload'); - } -} -async function completeWelcome() { - showLoading(true); - try { - if (state.welcomePdfFile) { - const reader = new FileReader(); - const arrayBuffer = await new Promise((resolve, reject) => { - reader.onload = (e) => resolve(e.target.result); - reader.onerror = (e) => reject(new Error('Failed to read file')); - reader.readAsArrayBuffer(state.welcomePdfFile); - }); - const bufferCopy = arrayBuffer.slice(0); - const loadingTask = pdfjsLib.getDocument({ data: bufferCopy }); - const pdf = await loadingTask.promise; - const base64 = arrayBufferToBase64(arrayBuffer); - const pdfData = { - name: state.welcomePdfFile.name, - data: base64, - uploadDate: new Date().toISOString(), - numPages: pdf.numPages - }; - await savePDFToIndexedDB(pdfData); - state.settings.customPdf = { - name: pdfData.name, - uploadDate: pdfData.uploadDate, - numPages: pdfData.numPages, - storedInDB: true - }; - } - state.settings.hasSeenWelcome = true; - saveToStorage(); - saveToCookies(); - document.getElementById('welcomeScreen').classList.add('hidden'); - await init(); - } catch (err) { - handleError(err, 'completeWelcome'); - alert('Error processing PDF: ' + err.message + - '. You can continue without offline mode.'); - state.settings.hasSeenWelcome = true; - saveToStorage(); - saveToCookies(); - document.getElementById('welcomeScreen').classList.add('hidden'); - await init(); - } finally { - showLoading(false); - } -} -function attachWelcomeListeners() { - document.getElementById('getStartedBtn') - .addEventListener('click', completeWelcome); - document.getElementById('welcomePdfUploadArea') - .addEventListener('click', () => { - document.getElementById('welcomePdfUpload').click(); - }); - document.getElementById('welcomePdfUpload') - .addEventListener('change', handleWelcomePDFUpload); -} -async function init() { - await loadFromStorage(); - loadFromCookies(); - setupPDFCleanup(); - const style = document.createElement('style'); - style.textContent = offlineStyles; - document.head.appendChild(style); - updateOfflineStatus(!navigator.onLine); - window.addEventListener('online', () => updateOfflineStatus(false)); - window.addEventListener('offline', () => updateOfflineStatus(true)); - if (!state.settings.readingMode) state.settings.readingMode = 'readingPlan'; - if (!state.settings.readingPlanId) state.settings.readingPlanId = 'default'; - initBookChapterControls(); - restoreBookChapterUI(); - if (!state.settings.hasSeenWelcome) { - attachWelcomeListeners(); - return; - } - document.getElementById('welcomeScreen').classList.add('hidden'); - applyTheme(); - applyColorTheme(); - restoreSidebarState(); - restorePanelStates(); - updateDateTime(); - initResizeHandles(); - updateCustomPdfInfo(); - switchNotesView(state.settings.notesView || 'text'); - updateBibleGatewayVersion(); - loadPassage(); - setupEventListeners(); - setInterval(updateDateTime, 1_000); - setTimeout(async () => { - try { - await registerServiceWorker(); - } catch (err) { - handleError(err, 'init'); - } - }, 1000); - console.log('App initialized successfully'); -} -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); -} else { - init(); -} \ No newline at end of file diff --git a/public/modules/api.js b/public/modules/api.js deleted file mode 100644 index 749cbfb..0000000 --- a/public/modules/api.js +++ /dev/null @@ -1,115 +0,0 @@ -import { - clearError, - handleError, - showError, - showLoading -} from '../main.js' -import { - afterContentLoad, - displayPassage, - extractVerseText -} from './passage.js' -import { - bookNameMapping, - state -} from './state.js' -const API_BASE_URL = 'https://bible.helloao.org/api'; -const translationMap = { - BSB: 'BSB', - KJV: 'eng_kjv', - NET: 'eng_net', - ASV: 'eng_asv', - GNV: 'eng_gnv' -}; -export function apiTranslationCode(uiCode) { - return translationMap[uiCode] ?? uiCode; -} -export function getApiBookCode(displayName) { - const code = bookNameMapping[displayName]; - if (code) return code; - console.warn('Missing book‑code mapping for:', displayName); - showError(`Cannot load “${displayName}” – unknown book code.`); - throw new Error('Unknown book code'); -} -export async function fetchChapter(translation, book, chapter) { - if (!navigator.onLine) { - throw new Error('Offline mode: Cannot fetch new chapters. Using cached data if available.'); - } - const trans = translation.trim(); - const bk = book.replace(/\s+/g, '').toUpperCase(); - const ch = Number(chapter); - if (!trans || !bk || Number.isNaN(ch) || ch < 1) { - throw new Error('Invalid parameters for Bible API request'); - } - const url = `${API_BASE_URL}/${trans}/${bk}/${ch}.json`; - try { - const resp = await fetch(url, { - method: 'GET', - headers: { Accept: 'application/json' }, - cache: 'no-store' - }); - if (!resp.ok) { - const txt = await resp.text(); - throw new Error(`API error ${resp.status}: ${txt}`); - } - const ct = resp.headers.get('content-type') || ''; - if (!ct.includes('application/json')) { - if (ct.startsWith('<')) { - throw new Error('API returned HTML instead of JSON'); - } - try { - return JSON.parse(await resp.text()); - } catch (_) { - throw new Error('Unable to parse API response as JSON'); - } - } - return resp.json(); - } catch (err) { - handleError(err, 'fetchChapter'); - } -} -export async function loadPassageFromAPI(passageInfo) { - try { - showLoading(true); - const { book, chapter, startVerse, endVerse, displayRef } = passageInfo; - state.currentPassageReference = displayRef; - const apiMap = apiTranslationCode(state.settings.bibleTranslation); - const apiBook = getApiBookCode(book); - const chapterData = await fetchChapter(apiMap, apiBook, chapter); - if (!chapterData || !chapterData.chapter || - !Array.isArray(chapterData.chapter.content)) { - throw new Error('Malformed API response – missing chapter.content'); - } - const chapterFootnotes = chapterData.chapter.footnotes || []; - const footnoteCounter = { value: 1 }; - const verses = chapterData.chapter.content - .filter(v => - v.type === 'verse' && - v.number >= startVerse && - v.number <= endVerse - ) - .map(v => { - const verseData = extractVerseText(v.content, chapterFootnotes, footnoteCounter); - return { - number: v.number, - text: verseData, - reference: `${book} ${chapter}:${v.number}`, - rawContent: v.content - }; - }); - if (verses.length === 0) { - throw new Error('No verses found in the requested range'); - } - displayPassage(verses); - afterContentLoad(); - clearError(); - if (chapterData.translation && chapterData.translation.name) { - document.getElementById('bibleName').textContent = - chapterData.translation.name; - } - } catch (err) { - handleError(err, 'loadPassageFromAPI'); - } finally { - showLoading(false); - } -} \ No newline at end of file diff --git a/public/modules/navigation.js b/public/modules/navigation.js deleted file mode 100644 index 9199eed..0000000 --- a/public/modules/navigation.js +++ /dev/null @@ -1,252 +0,0 @@ -import { - apiTranslationCode, - fetchChapter, - getApiBookCode, - loadPassageFromAPI -} from './api.js' -import { - clearError, - handleError, - showError, - showLoading -} from '../main.js' -import { - displayPassage, - extractVerseText, - loadPassage -} from './passage.js' -import { - BOOK_ORDER, - CHAPTER_COUNTS, - bookNameMapping, - getActivePlan, - saveToStorage, - state -} from './state.js' -import { updateReferencePanel } from './ui.js' -export function populateBookDropdown() { - const bookSel = document.getElementById('bookSelect'); - bookSel.innerHTML = ''; - BOOK_ORDER.forEach(book => { - const opt = document.createElement('option'); - opt.value = book; - opt.textContent = book; - bookSel.appendChild(opt); - }); -} -export function populateChapterDropdown(selectedBook) { - const chapSel = document.getElementById('chapterSelect'); - chapSel.innerHTML = ''; - const max = CHAPTER_COUNTS[selectedBook]; - for (let i = 1; i <= max; i++) { - const opt = document.createElement('option'); - opt.value = i; - opt.textContent = i; - chapSel.appendChild(opt); - } -} -export async function loadSelectedChapter(book = null, chapter = null) { - const selBook = book || document.getElementById('bookSelect').value; - const selChapter = chapter || document.getElementById('chapterSelect').value; - const apiBook = getApiBookCode(selBook); - try { - showLoading(true); - const apiTranslation = apiTranslationCode(state.settings.bibleTranslation); - const chapterData = await fetchChapter( - apiTranslation, - apiBook, - selChapter - ); - const chapterFootnotes = chapterData.chapter.footnotes || []; - const footnoteCounter = { value: 1 }; - const verses = chapterData.chapter.content - .filter(v => v.type === 'verse') - .map(v => ({ - number: v.number, - text: extractVerseText(v.content, chapterFootnotes, footnoteCounter), - reference: `${selBook} ${selChapter}:${v.number}` - })); - document.getElementById('passageReference').textContent = - `${selBook} ${selChapter}`; - state.footnotes = {}; - displayPassage(verses, `${selBook} ${selChapter}`); - clearError(); - document.getElementById('scriptureSection').scrollTop = 0; - if (state.settings.readingMode === 'manual') { - state.settings.manualBook = selBook; - state.settings.manualChapter = Number(selChapter); - saveToStorage(); - } - if (state.settings.referencePanelOpen) { - updateReferencePanel(); - } - } catch (err) { - handleError(err, 'loadSelectedChapter'); - showError(`Could not load ${selBook} ${selChapter}: ${err.message}`); - } finally { - showLoading(false); - } -} -export function initBookChapterControls() { - populateBookDropdown(); - document.getElementById('bookSelect').addEventListener('change', e => { - const book = e.target.value; - state.settings.readingMode = 'manual'; - populateChapterDropdown(book); - state.settings.manualBook = book; - state.settings.manualChapter = 1; - const chapterSel = document.getElementById('chapterSelect'); - chapterSel.value = '1'; - loadSelectedChapter(book, 1); - saveToStorage(); - }); - document.getElementById('chapterSelect').addEventListener('change', () => { - const book = document.getElementById('bookSelect').value; - const chap = Number(document.getElementById('chapterSelect').value); - state.settings.readingMode = 'manual'; - state.settings.manualBook = book; - state.settings.manualChapter = chap; - loadSelectedChapter(book, chap); - saveToStorage(); - }); - populateChapterDropdown(BOOK_ORDER[0]); -} -export function manualPrevChapter() { - let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook); - let chap = state.settings.manualChapter; - if (chap > 1) { - state.settings.manualChapter = chap - 1; - } else { - if (bookIdx > 0) { - const prevBook = BOOK_ORDER[bookIdx - 1]; - const maxCh = CHAPTER_COUNTS[prevBook]; - state.settings.manualBook = prevBook; - state.settings.manualChapter = maxCh; - } else { - return; - } - } - state.settings.readingMode = 'manual'; - loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter); - syncBookChapterSelectors(); - saveToStorage(); - if (state.settings.referencePanelOpen) { - updateReferencePanel(); - } -} -export function manualNextChapter() { - let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook); - let chap = state.settings.manualChapter; - const maxCh = CHAPTER_COUNTS[state.settings.manualBook]; - if (chap < maxCh) { - state.settings.manualChapter = chap + 1; - } else { - if (bookIdx < BOOK_ORDER.length - 1) { - const nextBook = BOOK_ORDER[bookIdx + 1]; - state.settings.manualBook = nextBook; - state.settings.manualChapter = 1; - } else { - return; - } - } - state.settings.readingMode = 'manual'; - loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter); - syncBookChapterSelectors(); - saveToStorage(); - if (state.settings.referencePanelOpen) { - updateReferencePanel(); - } -} -export function prevPassage() { - if (state.settings.readingMode === 'readingPlan') { - const len = getActivePlan().length; - let newIndex = (state.settings.currentPassageIndex - 1 + len) % len; - state.settings.currentPassageIndex = newIndex; - loadPassage(); - } else { - manualPrevChapter(); - } - document.getElementById('scriptureSection').scrollTop = 0; -} -export function nextPassage() { - if (state.settings.readingMode === 'readingPlan') { - const len = getActivePlan().length; - let newIndex = (state.settings.currentPassageIndex + 1) % len; - if (newIndex < 0) newIndex = len - 1; - state.settings.currentPassageIndex = newIndex; - loadPassage(); - } else { - manualNextChapter(); - } - document.getElementById('scriptureSection').scrollTop = 0; -} -export async function randomPassage() { - try { - state.settings.readingMode = 'manual'; - const randomLoc = await getRandomBibleLocation(); - state.settings.manualBook = randomLoc.book; - state.settings.manualChapter = randomLoc.chapter; - saveToStorage(); - await loadPassageFromAPI(randomLoc); - document.getElementById('passageReference').textContent = randomLoc.displayRef; - state.currentPassageReference = randomLoc.displayRef; - syncBookChapterSelectors(); - if (state.settings.referencePanelOpen) { - updateReferencePanel(); - } - } catch (err) { - handleError(err, 'randomPassage'); - showError('Could not load a random passage – see console for details.'); - } -} -export function syncBookChapterSelectors() { - const bookSel = document.getElementById('bookSelect'); - const chapterSel = document.getElementById('chapterSelect'); - if (bookSel.value !== state.settings.manualBook) { - bookSel.value = state.settings.manualBook; - populateChapterDropdown(state.settings.manualBook); - } - const curMax = CHAPTER_COUNTS[state.settings.manualBook]; - const curChap = state.settings.manualChapter; - populateChapterDropdown(state.settings.manualBook); - chapterSel.value = (curChap <= curMax) ? curChap : curMax; -} -export function syncSelectorsToReadingPlan() { - if (state.settings.readingMode !== 'readingPlan') return; - const plan = getActivePlan(); - const passage = plan[state.settings.currentPassageIndex]; - if (!passage || !passage.book) { - console.error('Invalid passage object:', passage); - return; - } - const bookSel = document.getElementById('bookSelect'); - const chapterSel = document.getElementById('chapterSelect'); - state.settings.manualBook = passage.book; - state.settings.manualChapter = passage.chapter; - if (bookSel) bookSel.value = passage.book; - populateChapterDropdown(passage.book); - if (chapterSel) chapterSel.value = passage.chapter; - saveToStorage(); -} -async function getRandomBibleLocation() { - try { - const randomBook = BOOK_ORDER[Math.floor(Math.random() * BOOK_ORDER.length)]; - const maxCh = CHAPTER_COUNTS[randomBook]; - const randomChapter = Math.floor(Math.random() * maxCh) + 1; - const apiMap = apiTranslationCode(state.settings.bibleTranslation); - const apiBook = bookNameMapping[randomBook] || - randomBook.replace(/\s+/g, '').toUpperCase(); - const chapterData = await fetchChapter(apiMap, apiBook, randomChapter); - const verses = chapterData.chapter.content.filter(v => v.type === 'verse'); - const verseCount = verses.length || 1; - return { - book: randomBook, - chapter: randomChapter, - startVerse: 1, - endVerse: verseCount, - displayRef: `${randomBook} ${randomChapter}` - }; - } catch (err) { - handleError(err, 'getRandomBibleLocation'); - } -} \ No newline at end of file diff --git a/public/modules/passage.js b/public/modules/passage.js deleted file mode 100644 index 0d35df6..0000000 --- a/public/modules/passage.js +++ /dev/null @@ -1,274 +0,0 @@ -import { loadPassageFromAPI } from './api.js' -import { handleError } from '../main.js' -import { syncSelectorsToReadingPlan } from './navigation.js' -import { - getActivePlan, - getCurrentPlanLabel, - getTranslationShorthand, - saveToStorage, - state -} from './state.js' -import { showStrongsReference } from './strongs.js' -import { updateReferencePanel } from './ui.js' -export function displayPassage(verses) { - const container = document.getElementById('scriptureContent'); - const fragment = document.createDocumentFragment(); - state.footnotes = {}; - const allFootnotes = []; - verses.forEach(v => { - const verseDiv = document.createElement('div'); - verseDiv.className = 'verse'; - verseDiv.dataset.verse = v.reference; - verseDiv.dataset.verseNumber = v.number; - let plainText = v.text.text; - plainText = plainText.replace(/<[^>]*>/g, ''); - plainText = plainText.replace(/\s+/g, ' ').trim(); - verseDiv.dataset.verseText = plainText; - const key = v.reference; - if (state.highlights[key]) { - verseDiv.classList.add(`highlight-${state.highlights[key]}`); - } - const numSpan = document.createElement('span'); - numSpan.className = 'verse-number'; - numSpan.textContent = v.number; - const txtSpan = document.createElement('span'); - txtSpan.className = 'verse-text'; - txtSpan.innerHTML = v.text.text; - if (v.text.footnotes && v.text.footnotes.length > 0) { - state.footnotes[v.reference] = v.text.footnotes; - allFootnotes.push(...v.text.footnotes); - } - verseDiv.appendChild(numSpan); - verseDiv.appendChild(txtSpan); - fragment.appendChild(verseDiv); - }); - container.innerHTML = ''; - container.appendChild(fragment); - if (allFootnotes.length > 0) { - const footnotesContainer = document.createElement('div'); - footnotesContainer.className = 'footnotes-container'; - const separator = document.createElement('hr'); - separator.className = 'footnotes-separator'; - const heading = document.createElement('h4'); - heading.className = 'footnotes-heading'; - heading.textContent = 'Footnotes'; - const footnotesFragment = document.createDocumentFragment(); - allFootnotes.sort((a, b) => a.number - b.number).forEach(fn => { - const footnoteElement = document.createElement('div'); - footnoteElement.className = 'footnote'; - footnoteElement.innerHTML = ` - ${fn.number} - ${fn.content} - `; - footnoteElement.dataset.footnoteId = fn.index; - footnoteElement.dataset.footnoteNumber = fn.number; - footnotesFragment.appendChild(footnoteElement); - }); - footnotesContainer.appendChild(footnotesFragment); - container.appendChild(separator); - container.appendChild(heading); - container.appendChild(footnotesContainer); - } - container.addEventListener('click', (e) => { - const verse = e.target.closest('.verse'); - if (verse && !e.target.closest('.footnote-ref')) { - showStrongsReference(verse); - } - }, { once: false }); - setTimeout(() => { - setupFootnoteHandlers(); - }, 100); -} -export function setupFootnoteHandlers() { - const scriptureContent = document.getElementById('scriptureContent'); - if (scriptureContent._footnoteHandler) { - scriptureContent.removeEventListener('click', scriptureContent._footnoteHandler); - } - const footnoteHandler = (e) => { - const footnoteRef = e.target.closest('[class*="footnote-ref"]'); - const footnoteElement = e.target.closest('.footnote'); - if (footnoteRef) { - e.preventDefault(); - e.stopPropagation(); - const footnoteId = (footnoteRef.dataset.footnoteId || '').trim(); - const footnoteNumber = (footnoteRef.dataset.footnoteNumber || '').trim(); - let targetFootnote = null; - if (footnoteId) { - targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-id="${footnoteId}"]`); - } - if (!targetFootnote && footnoteNumber) { - targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-number="${footnoteNumber}"]`); - } - if (!targetFootnote && footnoteId) { - const allFootnotes = scriptureContent.querySelectorAll('.footnote'); - for (const fn of allFootnotes) { - const fnId = (fn.dataset.footnoteId || '').trim(); - const fnNum = (fn.dataset.footnoteNumber || '').trim(); - if (fnId === footnoteId) { - targetFootnote = fn; - break; - } - } - } - if (targetFootnote) { - targetFootnote.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - targetFootnote.style.backgroundColor = 'var(--verse-hover)'; - setTimeout(() => { - targetFootnote.style.backgroundColor = ''; - }, 1000); - } else { - console.log('No target footnote found for ref:', footnoteId, footnoteNumber); - } - } - if (footnoteElement) { - e.preventDefault(); - e.stopPropagation(); - const footnoteId = (footnoteElement.dataset.footnoteId || '').trim(); - const footnoteNumber = (footnoteElement.dataset.footnoteNumber || '').trim(); - let targetRef = null; - const selectors = [ - `[class*="footnote-ref"][data-footnote-id="${footnoteId}"]`, - `[class*="footnote-ref"][data-footnote-number="${footnoteNumber}"]`, - `[class*="footnote-ref"][data-footnote-id="${footnoteNumber}"]`, - `[class*="footnote-ref"][data-footnote-number="${footnoteId}"]` - ]; - for (const selector of selectors) { - targetRef = scriptureContent.querySelector(selector); - if (targetRef) break; - } - if (!targetRef) { - const allRefs = scriptureContent.querySelectorAll('[class*="footnote-ref"]'); - for (const ref of allRefs) { - const refId = (ref.dataset.footnoteId || '').trim(); - const refNum = (ref.dataset.footnoteNumber || '').trim(); - if (refId === footnoteId || refNum === footnoteNumber || - refId === footnoteNumber || refNum === footnoteId) { - targetRef = ref; - break; - } - } - } - if (targetRef) { - targetRef.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - targetRef.style.backgroundColor = 'var(--verse-hover)'; - setTimeout(() => { - targetRef.style.backgroundColor = ''; - }, 1000); - } else { - console.log('No target ref found for footnote:', footnoteId, footnoteNumber); - console.log('Available refs:', scriptureContent.querySelectorAll('[class*="footnote-ref"]')); - } - } - }; - scriptureContent._footnoteHandler = footnoteHandler; - scriptureContent.addEventListener('click', footnoteHandler); -} -export function extractVerseText(content, chapterFootnotes = [], footnoteCounter) { - let txt = ''; - let footnotes = []; - for (const item of content) { - if (typeof item === 'string') { - txt += ensureProperSpacing(item); - } else if (item.text) { - txt += ensureProperSpacing(item.text); - } else if (item.heading) { - txt += ' ' + ensureProperSpacing(item.heading) + ' '; - } else if (item.noteId !== undefined) { - const footnote = chapterFootnotes.find(fn => fn.noteId === item.noteId); - if (footnote) { - const footnoteRef = `${footnoteCounter.value}`; - txt += footnoteRef; - footnotes.push({ - index: footnote.noteId, - number: footnoteCounter.value, - caller: footnote.caller, - content: footnote.text, - reference: footnote.reference - }); - footnoteCounter.value++; - } - } else if (item.type === 'verse') { - txt += ' '; - } else if (item.type === 'chapter') { - txt += ' '; - } else { - if (item.content && Array.isArray(item.content)) { - const nestedResult = extractVerseText(item.content, chapterFootnotes, footnoteCounter); - txt += nestedResult.text; - footnotes.push(...nestedResult.footnotes); - } - } - } - txt = txt.replace(/\s+/g, ' ').trim(); - txt = txt.replace(/\s+([.,;:!?])/g, '$1'); - txt = txt.replace(/([.,;:!?])(?=\w)/g, '$1 '); - txt = txt.replace(/\s*"\s*/g, '" '); - txt = txt.replace(/\s*'\s*/g, "' "); - return { - text: txt, - footnotes: footnotes - }; -} -function ensureProperSpacing(text) { - if (!text) return ''; - let cleanedText = text - .replace(/\s+/g, ' ') - .trim(); - cleanedText = cleanedText - .replace(/([^'"\s])\s+([.,;:!?])/g, '$1$2') - .replace(/([.,;:!?])(?=[A-Za-z])/g, '$1 ') - .replace(/\s*"\s*/g, (match) => { - if (match === '"' || match === ' "') return '"'; - return '" '; - }) - .replace(/\s*'\s*/g, (match) => { - if (match === "'" || match === " '") return "'"; - return "' "; - }); - cleanedText = cleanedText - .replace(/\s+/g, ' ') - .replace(/([.,;:!?]) (["'])/g, '$1$2') - .replace(/(["']) ([.,;:!?])/g, '$1$2') - .trim(); - return cleanedText; -} -export async function loadPassage() { - try { - const plan = getActivePlan(); - if (state.settings.currentPassageIndex < 0 || state.settings.currentPassageIndex >= plan.length) { - state.settings.currentPassageIndex = 0; - } - const passage = plan[state.settings.currentPassageIndex]; - const headerTitleEl = document.getElementById('passageHeaderTitle'); - if (headerTitleEl) { - const transShorthand = getTranslationShorthand(); - headerTitleEl.textContent = `Holy Bible: ${transShorthand}`; - } - const planLabelEl = document.getElementById('planLabel'); - if (planLabelEl) { - planLabelEl.textContent = `Reading plan: ${getCurrentPlanLabel()}`; - } - document.getElementById('passageReference').textContent = passage.displayRef; - state.currentPassageReference = passage.displayRef; - await loadPassageFromAPI(passage); - if (state.settings.referencePanelOpen) { - updateReferencePanel(); - } - saveToStorage(); - if (state.settings.readingMode === 'readingPlan') { - syncSelectorsToReadingPlan(); - } - } catch (err) { - handleError(err, 'loadPassage'); - } -} -export function afterContentLoad() { - const event = new CustomEvent('contentLoaded'); - document.dispatchEvent(event); -} \ No newline at end of file diff --git a/public/modules/pdf.js b/public/modules/pdf.js deleted file mode 100644 index 96e586d..0000000 --- a/public/modules/pdf.js +++ /dev/null @@ -1,372 +0,0 @@ -import { - arrayBufferToBase64, - base64ToArrayBuffer, - handleError, - readFileAsArrayBuffer, - showLoading -} from '../main.js' -import { - saveToStorage, - state -} from './state.js' -import { updateReferencePanel } from './ui.js' -const DB_NAME = 'BibleStudyDB'; -const DB_VERSION = 1; -export const STORE_NAME = 'pdfStore'; -export let currentSearch = { - query: '', - results: [], - currentResult: -1, - highlights: [] -}; -export async function openDB() { - try { - return new Promise((resolve, reject) => { - const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onerror = () => reject(req.error); - req.onsuccess = () => resolve(req.result); - req.onupgradeneeded = ev => { - const db = ev.target.result; - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME); - } - }; - }); - } catch (err) { - handleError(err, 'openDB'); - } -} -export async function savePDFToIndexedDB(pdfData) { - try { - const db = await openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction([STORE_NAME], 'readwrite'); - const store = tx.objectStore(STORE_NAME); - const req = store.put(pdfData, 'customPdf'); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); - } catch (err) { - handleError(err, 'savePDFToIndexedDB'); - } -} -export async function loadPDFFromIndexedDB() { - try { - const db = await openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction([STORE_NAME], 'readonly'); - const store = tx.objectStore(STORE_NAME); - const req = store.get('customPdf'); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); - } catch (err) { - handleError(err, 'loadPDFFromIndexedDB'); - } -} -export async function deletePDFFromIndexedDB() { - try { - const db = await openDB(); - return new Promise((resolve, reject) => { - const tx = db.transaction([STORE_NAME], 'readwrite'); - const store = tx.objectStore(STORE_NAME); - const req = store.delete('customPdf'); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); - } catch (err) { - handleError(err, 'deletePDFFromIndexedDB'); - } -} -export async function handlePDFUpload(ev) { - const file = ev.target.files[0]; - if (!file) return; - if (file.size > 50 * 1024 * 1024) { - alert('PDF file is too large (max 50 MiB).'); - ev.target.value = ''; - return; - } - try { - showLoading(true); - const buf = await readFileAsArrayBuffer(file); - const bufferCopy = buf.slice(0); - const pdf = await pdfjsLib.getDocument({ data: bufferCopy }).promise; - const storageBuffer = buf.slice(0); - const b64 = arrayBufferToBase64(storageBuffer); - const pdfData = { - name: file.name, - data: b64, - uploadDate: new Date().toISOString(), - numPages: pdf.numPages - }; - await savePDFToIndexedDB(pdfData); - state.settings.customPdf = { - name: pdfData.name, - uploadDate: pdfData.uploadDate, - numPages: pdfData.numPages, - storedInDB: true - }; - saveToStorage(); - updateCustomPdfInfo(); - alert('PDF uploaded successfully! You can now use it in the Reference Panel.'); - } catch (e) { - handleError(err, 'handlePDFUpload'); - alert('Error uploading PDF: ' + e.message); - } finally { - showLoading(false); - ev.target.value = ''; - } -} -export function updateCustomPdfInfo() { - const container = document.getElementById('customPdfInfo'); - if (state.settings.customPdf) { - const date = new Date(state.settings.customPdf.uploadDate) - .toLocaleDateString(); - container.innerHTML = ` -
-
- ${state.settings.customPdf.name} - -
- Uploaded: ${date} • ${state.settings.customPdf.numPages} pages -
- `; - document.getElementById('removePdfBtn') - .addEventListener('click', removeCustomPdf); - } else { - container.innerHTML = ` - - No custom PDF uploaded - `; - document.getElementById('pageInput').value = 1; - document.getElementById('pageCount').textContent = '?'; - const zoomDisplay = document.getElementById('zoomLevel'); - if (zoomDisplay) { - zoomDisplay.textContent = '100%'; - } - } -} -async function removeCustomPdf() { - try { - if (!confirm('Delete the uploaded PDF? This cannot be undone.')) return; - await deletePDFFromIndexedDB(); - state.settings.customPdf = null; - state.settings.referenceSource = 'biblegateway'; - saveToStorage(); - updateCustomPdfInfo(); - if (document.getElementById('referenceSource').value === 'pdf') { - document.getElementById('referenceSource').value = 'biblegateway'; - updateReferencePanel(); - } - } catch (err) { - handleError(err, 'removeCustomPdf'); - } -} -export async function loadPDF() { - if (!state.settings.customPdf) { - alert('No custom PDF uploaded. Please upload one first.'); - return; - } - try { - showLoading(true); - if (state.pdf.doc) { - state.pdf.doc.destroy().catch(() => {}); - state.pdf.doc = null; - } - state.pdf.renderTask = null; - const pdfData = await loadPDFFromIndexedDB(); - if (!pdfData) throw new Error('PDF not found in DB'); - const buf = base64ToArrayBuffer(pdfData.data); - const loadingTask = pdfjsLib.getDocument({ - data: buf, - onPassword: (updatePassword, reason) => { - const password = prompt(`This PDF requires a ${reason} password. Please enter the password:`); - if (password) { - updatePassword(password); - } else { - throw new Error(`Password required to open this PDF. ${reason === 1 ? 'Owner' : 'User'} password needed.`); - } - } - }); - state.pdf.doc = await loadingTask.promise; - const savedPage = state.pdf.currentPage || 1; - const validPage = Math.min(savedPage, state.pdf.doc.numPages); - state.pdf.currentPage = validPage; - const savedZoom = state.pdf.zoomLevel || state.settings.pdfZoom; - updatePDFZoom(savedZoom); - document.getElementById('pageCount').textContent = state.pdf.doc.numPages; - document.getElementById('pageInput').max = state.pdf.doc.numPages; - document.getElementById('pageInput').value = validPage; - } catch (err) { - handleError(err, 'loadPDF'); - alert('Could not load PDF: ' + err.message); - state.pdf.doc = null; - state.pdf.renderTask = null; - } finally { - showLoading(false); - } -} -export function updatePDFZoom(zoomLevel) { - state.settings.pdfZoom = zoomLevel; - state.pdf.zoomLevel = zoomLevel; - saveToStorage(); - const zoomDisplay = document.getElementById('zoomLevel'); - if (zoomDisplay) { - zoomDisplay.textContent = `${Math.round(zoomLevel * 100)}%`; - } - if (state.pdf.doc && state.pdf.currentPage) { - renderPage(state.pdf.currentPage); - } -} -export function setupPDFCleanup() { - window.addEventListener('beforeunload', () => { - if (state.pdf.renderTask) { - state.pdf.renderTask.cancel().catch(() => {}); - state.pdf.renderTask = null; - } - }); -} -export async function renderPage(pageNum) { - if (!state.pdf.doc) { - console.warn('PDF document not loaded, attempting to reload...'); - await loadPDF(); - return; - } - try { - if (state.pdf.renderTask) { - try { - await state.pdf.renderTask.cancel(); - } catch (e) { - } - state.pdf.renderTask = null; - } - const page = await state.pdf.doc.getPage(pageNum); - const canvas = document.getElementById('pdfCanvas'); - const ctx = canvas.getContext('2d', { willReadFrequently: false }); - ctx.clearRect(0, 0, canvas.width, canvas.height); - const viewport = page.getViewport({ scale: state.pdf.zoomLevel }); - canvas.style.width = viewport.width + 'px'; - canvas.style.height = viewport.height + 'px'; - canvas.width = viewport.width * window.devicePixelRatio; - canvas.height = viewport.height * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - state.pdf.renderTask = page.render({ - canvasContext: ctx, - viewport: viewport - }); - await state.pdf.renderTask.promise; - page.cleanup(); - state.pdf.renderTask = null; - document.getElementById('pageInput').value = pageNum; - document.getElementById('prevPage').disabled = pageNum <= 1; - document.getElementById('nextPage').disabled = pageNum >= state.pdf.doc.numPages; - state.pdf.currentPage = pageNum; - saveToStorage(); - } catch (err) { - if (err.name === 'RenderingCancelledException') { - console.log('Rendering cancelled normally'); - return; - } - console.warn('Render error, reloading PDF:', err); - await loadPDF(); - handleError(err, 'renderPage'); - } -} -export async function searchPDF() { - const query = document.getElementById('pdfSearchInput').value.trim(); - if (!query || !state.pdf.doc) return; - const resultsSpan = document.getElementById('pdfSearchResults'); - const searchBtn = document.getElementById('pdfSearchBtn'); - resultsSpan.textContent = 'Searching...'; - searchBtn.disabled = true; - searchBtn.textContent = 'Searching...'; - clearSearchHighlights(); - currentSearch = { - query: query, - results: [], - currentResult: -1, - highlights: [] - }; - try { - for (let pageNum = 1; pageNum <= state.pdf.doc.numPages; pageNum++) { - const page = await state.pdf.doc.getPage(pageNum) - const textContent = await page.getTextContent(); - const text = textContent.items.map(item => item.str).join(' '); - const regex = new RegExp(query.replace(/[.*+?^{}()|[\]\\]/g, '\\$&'), 'gi'); - let match; - while ((match = regex.exec(text)) !== null) { - currentSearch.results.push({ - page: pageNum, - index: match.index, - text: match[0] - }); - } - } - if (currentSearch.results.length > 0) { - resultsSpan.textContent = `Found ${currentSearch.results.length} results`; - document.getElementById('clearSearchBtn').style.display = 'inline-block'; - if (currentSearch.results.length > 1) { - document.getElementById('prevSearchResult').style.display = 'inline-block'; - document.getElementById('nextSearchResult').style.display = 'inline-block'; - } - navigateToSearchResult(0); - } else { - resultsSpan.textContent = 'No results found'; - document.getElementById('clearSearchBtn').style.display = 'none'; - document.getElementById('prevSearchResult').style.display = 'none'; - document.getElementById('nextSearchResult').style.display = 'none'; - } - } catch (err) { - handleError(err, 'searchPDF'); - resultsSpan.textContent = 'Search failed'; - document.getElementById('clearSearchBtn').style.display = 'none'; - document.getElementById('prevSearchResult').style.display = 'none'; - document.getElementById('nextSearchResult').style.display = 'none'; - } finally { - searchBtn.disabled = false; - searchBtn.textContent = 'Search'; - } -} -export function clearSearch() { - document.getElementById('pdfSearchInput').value = ''; - document.getElementById('pdfSearchResults').textContent = ''; - document.getElementById('clearSearchBtn').style.display = 'none'; - document.getElementById('prevSearchResult').style.display = 'none'; - document.getElementById('nextSearchResult').style.display = 'none'; - clearSearchHighlights(); - currentSearch = { - query: '', - results: [], - currentResult: -1, - highlights: [] - }; -} -export async function navigateToSearchResult(index) { - try { - if (!currentSearch.results.length || index < 0 || index >= currentSearch.results.length) return; - if (state.pdf.renderTask) { - await state.pdf.renderTask.cancel(); - state.pdf.renderTask = null; - } - currentSearch.currentResult = index; - const result = currentSearch.results[index]; - state.pdf.currentPage = result.page; - await renderPage(result.page); - document.getElementById('pdfSearchResults').textContent = - `Result ${index + 1} of ${currentSearch.results.length}`; - } catch (err) { - handleError(err, 'navigateToSearchResult'); - } -} -function clearSearchHighlights() { - currentSearch.highlights.forEach(highlight => { - if (highlight.animation) { - currentSearch.highlights = []; - } - }); - if (state.pdf.currentPage) { - renderPage(state.pdf.currentPage); - } -} \ No newline at end of file diff --git a/public/modules/settings.js b/public/modules/settings.js deleted file mode 100644 index 32d73b3..0000000 --- a/public/modules/settings.js +++ /dev/null @@ -1,274 +0,0 @@ -import { - applyColorTheme, - applyTheme, - clearError, - handleError, - showLoading -} from '../main.js' -import { loadPassage } from './passage.js' -import { - deletePDFFromIndexedDB, - openDB, - STORE_NAME, - updateCustomPdfInfo -} from './pdf.js' -import { - APP_VERSION, - BOOK_ORDER, - readingPlan, - saveToCookies, - saveToStorage, - state, - updateBibleGatewayVersion -} from './state.js' -import { - restorePanelStates, - restoreSidebarState, - switchNotesView, - updateMarkdownPreview, - updateReferencePanel -} from './ui.js' -export function exportData() { - const payload = { - version: '2.0', - exportDate: new Date().toISOString(), - highlights: state.highlights, - notes: state.notes, - settings: { ...state.settings } - }; - if (payload.settings.customPdf && payload.settings.customPdf.data) { - const { data, ...meta } = payload.settings.customPdf; - payload.settings.customPdf = meta; - } - const blob = new Blob([JSON.stringify(payload, null, 2)], - { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `bible-study-backup-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} -export function importData(ev) { - const file = ev.target.files[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = async e => { - try { - const incoming = JSON.parse(e.target.result); - if (!incoming.settings) { - throw new Error('Invalid backup format'); - } - if (!confirm('Import will overwrite all current data (except any uploaded PDF). Continue?')) { - return; - } - Object.assign(state.settings, incoming.settings); - if (incoming.highlights) { - state.highlights = incoming.highlights; - } - if (incoming.notes) { - state.notes = incoming.notes; - } - saveToStorage(); - applyTheme(); - applyColorTheme(); - restoreSidebarState(); - restorePanelStates(); - updateCustomPdfInfo(); - switchNotesView(state.settings.notesView || 'text'); - await loadPassage(); - document.getElementById('notesInput').value = state.notes; - updateMarkdownPreview(); - alert('Backup imported successfully!'); - } catch (err) { - console.error('Import error:', err); - alert('Failed to import backup – see console for details.'); - } - }; - reader.readAsText(file); - ev.target.value = ''; -} -export function resumeReadingPlan() { - const curBook = state.settings.manualBook; - const curChapter = state.settings.manualChapter; - const idx = findReadingPlanIndex(curBook, curChapter); - if (idx !== -1) { - state.settings.currentPassageIndex = idx; - } - state.settings.readingMode = 'readingPlan'; - loadPassage(); -} -function findReadingPlanIndex(book, chapter) { - for (let i = 0; i < readingPlan.length; i++) { - const p = readingPlan[i]; - if (p.book === book && p.chapter === chapter) { - return i; - } - } - return -1; -} -export function openSettings() { - document.getElementById('bibleTranslationSetting').value = - state.settings.bibleTranslation; - document.getElementById('referenceVersionSetting').value = - state.settings.referenceVersion; - document.getElementById('readingPlanId').value = - state.settings.readingPlanId || 'default'; - document.querySelectorAll('.color-theme-option') - .forEach(o => o.classList.toggle('selected', - o.dataset.theme === state.settings.colorTheme)); - document.getElementById('settingsModal').classList.add('active'); - document.getElementById('settingsOverlay').classList.add('active'); - document.getElementById('appVersion').textContent = APP_VERSION; -} -export function closeSettings() { - document.getElementById('settingsModal').classList.remove('active'); - document.getElementById('settingsOverlay').classList.remove('active'); -} -export async function saveSettings() { - try { - const newPlanId = document.getElementById('readingPlanId').value; - const newTranslation = document.getElementById('bibleTranslationSetting').value; - const newReferenceVersion = document.getElementById('referenceVersionSetting').value; - const oldPlanId = state.settings.readingPlanId; - state.settings.bibleTranslation = newTranslation; - state.settings.referenceVersion = newReferenceVersion; - state.settings.readingPlanId = newPlanId; - if (newReferenceVersion === 'BSB' && state.settings.referenceSource === 'biblegateway') { - state.settings.referenceSource = 'biblehub'; - document.getElementById('referenceSource').value = 'biblehub'; - } else if (newReferenceVersion === 'NASB1995' && state.settings.referenceSource === 'biblehub') { - state.settings.referenceSource = 'biblegateway'; - document.getElementById('referenceSource').value = 'biblegateway'; - } - if (oldPlanId !== newPlanId) { - state.settings.currentPassageIndex = 0; - state.settings.readingMode = 'readingPlan'; - } - const selectedTheme = document.querySelector('.color-theme-option.selected'); - if (selectedTheme) { - state.settings.colorTheme = selectedTheme.dataset.theme; - applyColorTheme(); - } - document.getElementById('referenceTranslation').value = state.settings.referenceVersion; - updateBibleGatewayVersion(); - saveToStorage(); - saveToCookies(); - closeSettings(); - await loadPassage(); - if (state.settings.referencePanelOpen) { - updateReferencePanel(); - } - alert('Settings saved!' + (oldPlanId !== newPlanId ? ' Starting from beginning of new reading plan.' : '')); - } catch (err) { - handleError(err, 'saveSettings'); - } -} -export function restartReadingPlan() { - if (confirm('Reset the reading plan to the very first passage? Highlights and notes will stay unchanged.')) { - state.settings.currentPassageIndex = 0; - state.settings.readingMode = 'readingPlan'; - saveToStorage(); - loadPassage(); - alert('Reading plan restarted – you are now at the beginning.'); - } -} -export async function clearCache() { - if (confirm('Clear all cached Bible data? This will remove offline access to previously viewed passages.')) { - try { - if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' }); - } - const db = await openDB(); - const tx = db.transaction([STORE_NAME], 'readwrite'); - const store = tx.objectStore(STORE_NAME); - await store.clear(); - alert('Cache cleared successfully'); - } catch (err) { - handleError(err, 'clearCache'); - alert('Error clearing cache: ' + err.message); - } - } -} -export async function deleteAllData() { - const confirmDelete = confirm('WARNING: This will delete ALL your data. Would you like to create a backup first?'); - if (confirmDelete) { - exportData(); - const proceed = confirm('Backup created. Proceed with deletion?'); - if (!proceed) return; - } - try { - showLoading(true); - localStorage.removeItem('bibleStudyState'); - document.cookie = 'bibleStudySettings=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - try { - await deletePDFFromIndexedDB(); - } catch (e) { - console.warn('Could not delete PDF from IndexedDB:', e); - } - const defaultState = { - currentVerse: null, - currentVerseData: null, - highlights: {}, - notes: '', - settings: { - bibleTranslation: 'BSB', - referenceVersion: 'NASB1995', - passageType: 'default', - readingMode: 'readingPlan', - manualBook: BOOK_ORDER[0], - manualChapter: 1, - lastUpdate: null, - currentPassageIndex: 0, - theme: 'light', - colorTheme: 'blue', - notesView: 'text', - hasSeenWelcome: true, - referencePanelOpen: false, - referenceSource: 'biblegateway', - collapsedSections: {}, - collapsedPanels: {}, - panelWidths: { - sidebar: 280, - referencePanel: 400, - scriptureSection: null, - notesSection: 400 - }, - customPdf: null, - pdfZoom: 1 - }, - currentPassageReference: '', - pdf: { - doc: null, - currentPage: 1, - renderTask: null, - zoomLevel: 1 - }, - welcomePdfFile: null - }; - Object.assign(state, defaultState); - document.getElementById('notesInput').value = ''; - updateMarkdownPreview(); - updateCustomPdfInfo(); - applyTheme(); - applyColorTheme(); - updateBibleGatewayVersion(); - document.getElementById('pageInput').value = 1; - document.getElementById('pageCount').textContent = '?'; - const zoomDisplay = document.getElementById('zoomLevel'); - if (zoomDisplay) { - zoomDisplay.textContent = '100%'; - } - await loadPassage(); - clearError(); - closeSettings(); - alert('All data has been deleted. The app has been reset to defaults.'); - } catch (err) { - handleError(err, 'deleteAllData'); - alert('Error deleting data. See console for details.'); - } finally { - showLoading(false); - } -} \ No newline at end of file diff --git a/public/modules/state.js b/public/modules/state.js deleted file mode 100644 index 847efc2..0000000 --- a/public/modules/state.js +++ /dev/null @@ -1,399 +0,0 @@ -import { handleError } from '../main.js' -import { loadPDFFromIndexedDB } from './pdf.js' -export const APP_VERSION = '1.0.2025.11.01'; -let saveTimeout = null; -const SAVE_DEBOUNCE_MS = 500; -export const BOOK_ORDER = [ - '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' -]; -export const CHAPTER_COUNTS = { - Genesis: 50, Exodus: 40, Leviticus: 27, Numbers: 36, Deuteronomy: 34, - Joshua: 24, Judges: 21, Ruth: 4, '1 Samuel': 31, '2 Samuel': 24, - '1 Kings': 22, '2 Kings': 25, '1 Chronicles': 29, '2 Chronicles': 36, - Ezra: 10, Nehemiah: 13, Esther: 10, - Job: 42, Psalms: 150, Proverbs: 31, Ecclesiastes: 12, 'Song of Solomon': 8, - Isaiah: 66, Jeremiah: 52, Lamentations: 5, Ezekiel: 48, Daniel: 12, - Hosea: 14, Joel: 3, Amos: 9, Obadiah: 1, Jonah: 4, Micah: 7, - Nahum: 3, Habakkuk: 3, Zephaniah: 3, Haggai: 2, Zechariah: 14, Malachi: 4, - Matthew: 28, Mark: 16, Luke: 24, John: 21, - Acts: 28, - Romans: 16, '1 Corinthians': 16, '2 Corinthians': 13, Galatians: 6, - Ephesians: 6, Philippians: 4, Colossians: 4, '1 Thessalonians': 5, - '2 Thessalonians': 3, '1 Timothy': 6, '2 Timothy': 4, Titus: 3, Philemon: 1, - Hebrews: 13, James: 5, '1 Peter': 5, '2 Peter': 3, '1 John': 5, - '2 John': 1, '3 John': 1, Jude: 1, - Revelation: 22 -}; -export const state = { - currentVerse: null, - currentVerseData: null, - highlights: {}, - notes: '', // User's study notes (plain text/markdown) - settings: { - bibleTranslation: 'BSB', - referenceVersion: 'NASB1995', - footnotes: {}, - passageType: 'default', - readingMode: 'readingPlan', // 'readingPlan' | 'manual' - manualBook: BOOK_ORDER[0], - manualChapter: 1, - lastUpdate: null, - currentPassageIndex: 0, - theme: 'light', // 'light' | 'dark' - colorTheme: 'blue', - notesView: 'text', // 'text' | 'markdown' - hasSeenWelcome: false, - referencePanelOpen: false, - referenceSource: 'biblegateway',// 'biblegateway' | 'biblehub' | 'pdf' - collapsedSections: {}, - collapsedPanels: {}, - panelWidths: { - sidebar: 280, - referencePanel: 400, - scriptureSection: null, - notesSection: 400 - }, - customPdf: null, - pdfZoom: 1 - }, - currentPassageReference: '', - pdf: { - doc: null, - currentPage: 1, - renderTask: null, - zoomLevel: 1 - }, - welcomePdfFile: null -}; -export function formatBookNameForSource(bookName, source) { - const book = bookName.toLowerCase(); - switch(source) { - case 'biblecom': - const bibleComCodes = { - 'genesis': 'GEN', 'exodus': 'EXO', 'leviticus': 'LEV', 'numbers': 'NUM', - 'deuteronomy': 'DEU', 'joshua': 'JOS', 'judges': 'JDG', 'ruth': 'RUT', - '1 samuel': '1SA', '2 samuel': '2SA', '1 kings': '1KI', '2 kings': '2KI', - '1 chronicles': '1CH', '2 chronicles': '2CH', 'ezra': 'EZR', 'nehemiah': 'NEH', - 'esther': 'EST', 'job': 'JOB', 'psalms': 'PSA', 'proverbs': 'PRO', - 'ecclesiastes': 'ECC', 'song of solomon': 'SNG', 'isaiah': 'ISA', 'jeremiah': 'JER', - 'lamentations': 'LAM', 'ezekiel': 'EZK', 'daniel': 'DAN', 'hosea': 'HOS', - 'joel': 'JOL', 'amos': 'AMO', 'obadiah': 'OBA', 'jonah': 'JON', - 'micah': 'MIC', 'nahum': 'NAM', 'habakkuk': 'HAB', 'zephaniah': 'ZEP', - 'haggai': 'HAG', 'zechariah': 'ZEC', 'malachi': 'MAL', 'matthew': 'MAT', - 'mark': 'MRK', 'luke': 'LUK', 'john': 'JHN', 'acts': 'ACT', - 'romans': 'ROM', '1 corinthians': '1CO', '2 corinthians': '2CO', 'galatians': 'GAL', - 'ephesians': 'EPH', 'philippians': 'PHP', 'colossians': 'COL', '1 thessalonians': '1TH', - '2 thessalonians': '2TH', '1 timothy': '1TI', '2 timothy': '2TI', 'titus': 'TIT', - 'philemon': 'PHM', 'hebrews': 'HEB', 'james': 'JAS', '1 peter': '1PE', - '2 peter': '2PE', '1 john': '1JN', '2 john': '2JN', '3 john': '3JN', - 'jude': 'JUD', 'revelation': 'REV' - }; - return bibleComCodes[book] || book.substring(0, 3).toUpperCase(); - case 'ebibleorg': - if (book === 'psalms') return 'PS1'; - return book.substring(0, 3).toUpperCase() + '1'; - default: - return book.replace(/\s+/g, '_'); - } -} -export function saveToStorage() { - if (saveTimeout) { - clearTimeout(saveTimeout); - } - saveTimeout = setTimeout(() => { - try { - const cleanState = { - currentVerse: null, - currentVerseData: state.currentVerseData, - highlights: state.highlights, - notes: state.notes, - settings: { ...state.settings }, - currentPassageReference: state.currentPassageReference, - pdf: { - currentPage: state.pdf.currentPage, - zoomLevel: state.pdf.zoomLevel - }, - welcomePdfFile: null - }; - if (cleanState.settings.customPdf && cleanState.settings.customPdf.data) { - const { data, ...meta } = cleanState.settings.customPdf; - cleanState.settings.customPdf = { ...meta, storedInDB: true }; - } - localStorage.setItem('bibleStudyState', JSON.stringify(cleanState)); - saveToCookies(); - } catch (e) { - console.error('Storage error:', e); - } - }, SAVE_DEBOUNCE_MS); -} -export async function loadFromStorage() { - const raw = localStorage.getItem('bibleStudyState'); - if (!raw) return; - try { - const parsed = JSON.parse(raw); - Object.assign(state, parsed); - if (parsed.pdf) { - state.pdf.currentPage = parsed.pdf.currentPage || 1; - state.pdf.zoomLevel = parsed.pdf.zoomLevel || state.settings.pdfZoom; - } - if (state.settings.customPdf && state.settings.customPdf.storedInDB) { - const pdfMeta = await loadPDFFromIndexedDB(); - if (!pdfMeta) { - console.warn('PDF metadata present but DB entry missing'); - state.settings.customPdf = null; - } - } - document.getElementById('notesInput').value = state.notes; - } catch (e) { - handleError(err, 'loadFromStorage'); - } -} -export function saveToCookies() { - const exp = new Date(); - exp.setFullYear(exp.getFullYear() + 10); - const cookieVal = encodeURIComponent(JSON.stringify({ - ...state.settings, - customPdf: undefined - })); - document.cookie = `bibleStudySettings=${cookieVal}; expires=${exp.toUTCString()}; path=/; SameSite=Strict`; -} -export function loadFromCookies() { - const pairs = document.cookie.split(';'); - for (let pair of pairs) { - const [k, v] = pair.trim().split('='); - if (k === 'bibleStudySettings') { - try { - const settings = JSON.parse(decodeURIComponent(v)); - Object.assign(state.settings, settings); - } catch (e) { - console.error('Cookie parse error:', e); - } - } - } -} -export const readingPlan = [ - { book: 'Genesis', chapter: 1, startVerse: 1, endVerse: 31, displayRef: 'Genesis 1' }, - { book: 'Genesis', chapter: 2, startVerse: 1, endVerse: 25, displayRef: 'Genesis 2' }, - { book: 'Genesis', chapter: 3, startVerse: 1, endVerse: 24, displayRef: 'Genesis 3' }, - { book: 'Genesis', chapter: 6, startVerse: 5, endVerse: 22, displayRef: 'Genesis 6:5-22' }, - { book: 'Genesis', chapter: 12, startVerse: 1, endVerse: 9, displayRef: 'Genesis 12:1-9' }, - { book: 'Genesis', chapter: 22, startVerse: 1, endVerse: 19, displayRef: 'Genesis 22:1-19' }, - { book: 'Exodus', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Exodus 3' }, - { book: 'Exodus', chapter: 20, startVerse: 1, endVerse: 21, displayRef: 'Exodus 20:1-21' }, - { book: 'Leviticus', chapter: 19, startVerse: 1, endVerse: 18, displayRef: 'Leviticus 19:1-18' }, - { book: 'Numbers', chapter: 14, startVerse: 1, endVerse: 38, displayRef: 'Numbers 14:1-38' }, - { book: 'Deuteronomy', chapter: 6, startVerse: 1, endVerse: 25, displayRef: 'Deuteronomy 6' }, - { book: 'Deuteronomy', chapter: 30, startVerse: 1, endVerse: 20, displayRef: 'Deuteronomy 30' }, - { book: 'Joshua', chapter: 1, startVerse: 1, endVerse: 9, displayRef: 'Joshua 1:1-9' }, - { book: 'Judges', chapter: 2, startVerse: 6, endVerse: 23, displayRef: 'Judges 2:6-23' }, - { book: 'Ruth', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Ruth 1' }, - { book: '1 Samuel', chapter: 16, startVerse: 1, endVerse: 13, displayRef: '1 Samuel 16:1-13' }, - { book: '2 Samuel', chapter: 7, startVerse: 1, endVerse: 29, displayRef: '2 Samuel 7' }, - { book: '1 Kings', chapter: 18, startVerse: 1, endVerse: 46, displayRef: '1 Kings 18' }, - { book: '2 Kings', chapter: 22, startVerse: 1, endVerse: 20, displayRef: '2 Kings 22' }, - { book: 'Ezra', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Ezra 1' }, - { book: 'Nehemiah', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Nehemiah 1' }, - { book: 'Esther', chapter: 4, startVerse: 1, endVerse: 17, displayRef: 'Esther 4' }, - { book: 'Job', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Job 1' }, - { book: 'Psalms', chapter: 1, startVerse: 1, endVerse: 6, displayRef: 'Psalm 1' }, - { book: 'Psalms', chapter: 19, startVerse: 1, endVerse: 14, displayRef: 'Psalm 19' }, - { book: 'Psalms', chapter: 23, startVerse: 1, endVerse: 6, displayRef: 'Psalm 23' }, - { book: 'Psalms', chapter: 51, startVerse: 1, endVerse: 19, displayRef: 'Psalm 51' }, - { book: 'Psalms', chapter: 103, startVerse: 1, endVerse: 22, displayRef: 'Psalm 103' }, - { book: 'Psalms', chapter: 119, startVerse: 1, endVerse: 16, displayRef: 'Psalm 119:1-16' }, - { book: 'Proverbs', chapter: 3, startVerse: 1, endVerse: 12, displayRef: 'Proverbs 3:1-12' }, - { book: 'Ecclesiastes', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Ecclesiastes 3' }, - { book: 'Song of Solomon', chapter: 2, startVerse: 1, endVerse: 17, displayRef: 'Song of Solomon 2' }, - { book: 'Isaiah', chapter: 6, startVerse: 1, endVerse: 13, displayRef: 'Isaiah 6' }, - { book: 'Isaiah', chapter: 9, startVerse: 1, endVerse: 7, displayRef: 'Isaiah 9:1-7' }, - { book: 'Isaiah', chapter: 40, startVerse: 1, endVerse: 31, displayRef: 'Isaiah 40' }, - { book: 'Isaiah', chapter: 53, startVerse: 1, endVerse: 12, displayRef: 'Isaiah 53' }, - { book: 'Jeremiah', chapter: 29, startVerse: 1, endVerse: 14, displayRef: 'Jeremiah 29:1-14' }, - { book: 'Lamentations', chapter: 3, startVerse: 1, endVerse: 33, displayRef: 'Lamentations 3:1-33' }, - { book: 'Ezekiel', chapter: 36, startVerse: 22, endVerse: 38, displayRef: 'Ezekiel 36:22-38' }, - { book: 'Daniel', chapter: 3, startVerse: 1, endVerse: 30, displayRef: 'Daniel 3' }, - { book: 'Daniel', chapter: 6, startVerse: 1, endVerse: 28, displayRef: 'Daniel 6' }, - { book: 'Hosea', chapter: 6, startVerse: 1, endVerse: 11, displayRef: 'Hosea 6' }, - { book: 'Joel', chapter: 2, startVerse: 12, endVerse: 32, displayRef: 'Joel 2:12-32' }, - { book: 'Jonah', chapter: 1, startVerse: 1, endVerse: 17, displayRef: 'Jonah 1' }, - { book: 'Micah', chapter: 6, startVerse: 1, endVerse: 16, displayRef: 'Micah 6' }, - { book: 'Habakkuk', chapter: 3, startVerse: 1, endVerse: 19, displayRef: 'Habakkuk 3' }, - { book: 'Malachi', chapter: 3, startVerse: 1, endVerse: 18, displayRef: 'Malachi 3' }, - { book: 'Matthew', chapter: 5, startVerse: 1, endVerse: 30, displayRef: 'Matthew 5:1-30' }, - { book: 'Matthew', chapter: 6, startVerse: 1, endVerse: 34, displayRef: 'Matthew 6' }, - { book: 'Matthew', chapter: 7, startVerse: 1, endVerse: 29, displayRef: 'Matthew 7' }, - { book: 'Mark', chapter: 10, startVerse: 17, endVerse: 45, displayRef: 'Mark 10:17-45' }, - { book: 'Luke', chapter: 15, startVerse: 1, endVerse: 32, displayRef: 'Luke 15' }, - { book: 'John', chapter: 1, startVerse: 1, endVerse: 51, displayRef: 'John 1:1-51' }, - { book: 'John', chapter: 3, startVerse: 1, endVerse: 36, displayRef: 'John 3:1-36' }, - { book: 'John', chapter: 6, startVerse:30, endVerse: 66, displayRef: 'John 6:30-66' }, - { book: 'John', chapter: 14, startVerse: 1, endVerse: 31, displayRef: 'John 14' }, - { book: 'Acts', chapter: 2, startVerse: 1, endVerse: 47, displayRef: 'Acts 2' }, - { book: 'Romans', chapter: 1, startVerse: 1, endVerse: 32, displayRef: 'Romans 1' }, - { book: 'Romans', chapter: 3, startVerse: 1, endVerse: 31, displayRef: 'Romans 3' }, - { book: 'Romans', chapter: 8, startVerse: 1, endVerse: 39, displayRef: 'Romans 8' }, - { book: 'Romans', chapter: 12, startVerse: 1, endVerse: 21, displayRef: 'Romans 12' }, - { book: '1 Corinthians', chapter: 13, startVerse: 1, endVerse: 13, displayRef: '1 Corinthians 13' }, - { book: '2 Corinthians', chapter: 5, startVerse: 1, endVerse: 21, displayRef: '2 Corinthians 5' }, - { book: 'Galatians', chapter: 5, startVerse: 16, endVerse: 26, displayRef: 'Galatians 5:16-26' }, - { book: 'Ephesians', chapter: 2, startVerse: 1, endVerse: 22, displayRef: 'Ephesians 2' }, - { book: 'Philippians', chapter: 2, startVerse: 1, endVerse: 18, displayRef: 'Philippians 2:1-18' }, - { book: 'Colossians', chapter: 1, startVerse: 1, endVerse: 29, displayRef: 'Colossians 1' }, - { book: '1 Thessalonians', chapter: 4, startVerse: 1, endVerse: 18, displayRef: '1 Thessalonians 4' }, - { book: '1 Timothy', chapter: 3, startVerse: 1, endVerse: 16, displayRef: '1 Timothy 3' }, - { book: 'Hebrews', chapter: 11, startVerse: 1, endVerse: 40, displayRef: 'Hebrews 11' }, - { book: 'James', chapter: 1, startVerse: 1, endVerse: 27, displayRef: 'James 1' }, - { book: '1 Peter', chapter: 1, startVerse: 1, endVerse: 25, displayRef: '1 Peter 1' }, - { book: '1 John', chapter: 4, startVerse: 1, endVerse: 21, displayRef: '1 John 4' }, - { book: 'Revelation', chapter: 1, startVerse: 1, endVerse: 20, displayRef: 'Revelation 1' }, - { book: 'Revelation', chapter: 21, startVerse: 1, endVerse: 27, displayRef: 'Revelation 21' }, - { book: 'Revelation', chapter: 22, startVerse: 1, endVerse: 21, displayRef: 'Revelation 22' } -]; -function buildFullBookPlan(bookName) { - const maxChapters = CHAPTER_COUNTS[bookName]; - if (!maxChapters) { - console.warn(`No chapter count for "${bookName}" – skipping plan`); - return []; - } - const plan = []; - for (let ch = 1; ch <= maxChapters; ch++) { - plan.push({ - book: bookName, - chapter: ch, - startVerse: 1, - endVerse: 999, - displayRef: `${bookName} ${ch}` - }); - } - return plan; -} -const READING_PLANS = { - default: readingPlan, - genesis: buildFullBookPlan('Genesis'), - psalms: buildFullBookPlan('Psalms'), - proverbs: buildFullBookPlan('Proverbs'), - ecclesiastes: buildFullBookPlan('Ecclesiastes'), - romans: buildFullBookPlan('Romans'), - revelation: buildFullBookPlan('Revelation') -}; -export function getActivePlan() { - const id = state.settings.readingPlanId || 'default'; - return READING_PLANS[id] || READING_PLANS['default']; -} -const PLAN_LABELS = { - default: '90‑Day Sequential', - genesis: 'Genesis', - psalms: 'Psalms', - proverbs: 'Proverbs', - ecclesiastes: 'Ecclesiastes', - romans: 'Romans', - revelation: 'Revelation' -}; -export function getCurrentPlanLabel() { - const id = state.settings.readingPlanId || 'default'; - return PLAN_LABELS[id] || id; -} -export const bookNameMapping = { - Genesis: 'GEN', Exodus: 'EXO', Leviticus: 'LEV', Numbers: 'NUM', Deuteronomy: 'DEU', - Joshua: 'JOS', Judges: 'JDG', Ruth: 'RUT', '1 Samuel': '1SA', '2 Samuel': '2SA', - '1 Kings': '1KI', '2 Kings': '2KI', '1 Chronicles': '1CH', '2 Chronicles': '2CH', - Ezra: 'EZR', Nehemiah: 'NEH', Esther: 'EST', Job: 'JOB', Psalms: 'PSA', - Proverbs: 'PRO', Ecclesiastes: 'ECC', 'Song of Solomon': 'SNG', Isaiah: 'ISA', Jeremiah: 'JER', - Lamentations: 'LAM', Ezekiel: 'EZK', Daniel: 'DAN', Hosea: 'HOS', Joel: 'JOL', - Amos: 'AMO', Obadiah: 'OBA', Jonah: 'JON', Micah: 'MIC', Nahum: 'NAM', - Habakkuk: 'HAB', Zephaniah: 'ZEP', Haggai: 'HAG', Zechariah: 'ZEC', Malachi: 'MAL', - Matthew: 'MAT', Mark: 'MRK', Luke: 'LUK', John: 'JHN', Acts: 'ACT', - Romans: 'ROM', '1 Corinthians': '1CO', '2 Corinthians': '2CO', Galatians: 'GAL', - Ephesians: 'EPH', Philippians: 'PHP', Colossians: 'COL', '1 Thessalonians': '1TH', - '2 Thessalonians': '2TH', '1 Timothy': '1TI', '2 Timothy': '2TI', Titus: 'TIT', - Philemon: 'PHM', Hebrews: 'HEB', James: 'JAS', '1 Peter': '1PE', '2 Peter': '2PE', - '1 John': '1JN', '2 John': '2JN', '3 John': '3JN', Jude: 'JUD', Revelation: 'REV' -}; -export const bibleHubUrlMap = { - 'NASB1995': 'nasb', // Bible Hub uses 'nasb' for NASB 1995 - 'NASB': 'nasb_', // Bible Hub uses 'nasb_' for NASB 2020 - 'ASV': 'asv', - 'ESV': 'esv', - 'KJV': 'kjv', - 'NKJV': 'nkjv', - 'BSB': 'bsb', - 'CSB': 'csb', - 'NET': 'net', - 'NIV': 'niv', - 'NLT': 'nlt' -}; -export const bibleComUrlMap = { - 'NASB1995': '100', - 'NASB': '2692', - 'ASV': '12', - 'ESV': '59', - 'KJV': '1', - 'GNV': '2163', - 'NKJV': '114', - 'BSB': '3034', - 'CSB': '1713', - 'NET': '107', - 'NIV': '111', - 'NLT': '116' -}; -export const ebibleOrgUrlMap = { - 'NASB1995': 'local:engnasb', - 'ASV': 'local:eng-asv', - 'KJV': 'local:eng-kjv2006', - 'GNV': 'local:enggnv', - 'BSB': 'local:engbsb', - 'NET': 'local:engnet' -}; -export const stepBibleUrlMap = { - 'NASB1995': 'NASB1995', - 'NASB': 'NASB2020', - 'ASV': 'ASV', - 'ESV': 'ESV', - 'KJV': 'KJV', - 'GNV': 'Gen', - 'BSB': 'BSB', - 'NET': 'NET2full', - 'NIV': 'NIV' -}; -export function getTranslationShorthand() { - return state.settings.bibleTranslation || 'BSB'; -} -function getBibleGatewayVersionCode(appTranslation) { - const versionMap = { - 'NASB1995': 'NASB1995', - 'NASB': 'NASB', - 'ASV': 'ASV', - 'ESV': 'ESV', - 'KJV': 'KJV', - 'GNV': 'GNV', - 'NKJV': 'NKJV', - 'BSB': 'BSB', - 'CSB': 'CSB', - 'NET': 'NET', - 'NIV': 'NIV', - 'NLT': 'NLT' - }; - return versionMap[appTranslation] || 'NASB1995'; -} -export function updateBibleGatewayVersion() { - const versionCode = getBibleGatewayVersionCode(state.settings.referenceVersion); - const versionInput = document.getElementById('bgVersion'); - if (versionCode === 'BSB') { - versionInput.value = 'NASB1995'; - } else { - versionInput.value = versionCode; - } -} \ No newline at end of file diff --git a/public/modules/strongs.js b/public/modules/strongs.js deleted file mode 100644 index 5a3d7c0..0000000 --- a/public/modules/strongs.js +++ /dev/null @@ -1,254 +0,0 @@ -import { state } from './state.js' -import { getStepBibleUrl } from './ui.js' -export function showStrongsReference(verseEl) { - const ref = verseEl.dataset.verse; - state.currentVerseElement = verseEl; - const textSpan = verseEl.querySelector('.verse-text'); - let verseText = ''; - if (textSpan) { - verseText = textSpan.innerHTML; - if (!verseText || verseText.trim() === '') { - verseText = textSpan.textContent || ''; - if (!verseText || verseText.trim() === '') { - verseText = verseEl.dataset.verseText || ''; - } - } - } else { - verseText = verseEl.dataset.verseText || ''; - } - if (!verseText || verseText.trim() === '') { - verseText = 'Verse text not available'; - } - state.currentVerseData = { reference: ref, text: verseText }; - const content = document.getElementById('strongsContent'); - const m = ref.match(/^([\w\s]+)\s+(\d+):(\d+)/); - let book = '', chapter = '', verse = ''; - if (m) { - book = m[1].trim().replace(/\s+/g, '_').toLowerCase(); - chapter = m[2]; - verse = m[3]; - } - const greekUrl = `https://biblehub.com/interlinear/${book}/${chapter}-${verse}.htm`; - const currentTranslation = state.settings.referenceVersion; - const stepUrl = getStepBibleUrl(ref, currentTranslation); - content.innerHTML = ` -
-
- - ${ref} - -
- -
-
- ${verseText} -
- -
-
-
-
-

BibleHub Interlinear

-
- -
-
- -
-
-
-

STEP Bible Analysis

-
- -
-
- -
-
-

- These resources provide detailed word-by-word analysis. - Use the "Pop Out" button to open them in a new tab. -

- - `; - populateStrongsFootnotes(ref); - document.getElementById('strongsPopup').classList.add('active'); - document.getElementById('popupOverlay').classList.add('active'); - setTimeout(() => { - const copyBtn = document.getElementById('copyVerseBtn'); - if (copyBtn) { - copyBtn.addEventListener('click', copyVerseText); - } - const prevBtn = document.getElementById('prevVerseBtn'); - const nextBtn = document.getElementById('nextVerseBtn'); - if (prevBtn) { - prevBtn.addEventListener('click', navigateToPreviousVerse); - } - if (nextBtn) { - nextBtn.addEventListener('click', navigateToNextVerse); - } - document.querySelectorAll('.resource-frame-btn').forEach(btn => { - btn.addEventListener('click', function() { - const url = this.dataset.url; - const title = this.dataset.title; - popOutResource(url, title); - }); - }); - setupStrongsFootnoteHandlers(); - }, 0); -} -function navigateToPreviousVerse() { - const currentVerseEl = state.currentVerseElement; - if (!currentVerseEl) return; - const allVerses = Array.from(document.querySelectorAll('.verse')); - const currentIndex = allVerses.indexOf(currentVerseEl); - if (currentIndex > 0) { - const prevVerseEl = allVerses[currentIndex - 1]; - showStrongsReference(prevVerseEl); - } -} -function navigateToNextVerse() { - const currentVerseEl = state.currentVerseElement; - if (!currentVerseEl) return; - const allVerses = Array.from(document.querySelectorAll('.verse')); - const currentIndex = allVerses.indexOf(currentVerseEl); - if (currentIndex < allVerses.length - 1) { - const nextVerseEl = allVerses[currentIndex + 1]; - showStrongsReference(nextVerseEl); - } -} -export function closeStrongsPopup() { - document.getElementById('strongsPopup').classList.remove('active'); - document.getElementById('popupOverlay').classList.remove('active'); - state.currentVerseData = null; -} -function copyVerseText() { - if (!state.currentVerseData) return; - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = state.currentVerseData.text; - let plainText = tempDiv.textContent || tempDiv.innerText || ''; - plainText = plainText - .replace(/\s+/g, ' ') - .replace(/\s([.,;:!?])/g, '$1') - .replace(/([.,;:!?])(?=\w)/g, '$1 ') - .trim(); - const txt = `${state.currentVerseData.reference} – ${plainText}`; - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(txt) - .then(() => { - const btn = document.getElementById('copyVerseBtn'); - const original = btn.innerHTML; - btn.innerHTML = ' Copied!'; - btn.classList.add('copied'); - setTimeout(() => { - btn.innerHTML = original; - btn.classList.remove('copied'); - }, 2000); - }) - .catch(err => { - console.error('Copy failed:', err); - copyVerseFallback(txt); - }); - } else { - copyVerseFallback(txt); - } -} -function populateStrongsFootnotes(verseRef) { - const container = document.getElementById('strongsFootnotesContainer'); - if (!container) return; - container.innerHTML = ''; - const verseFootnotes = state.footnotes[verseRef]; - if (!verseFootnotes || verseFootnotes.length === 0) { - container.innerHTML = '

No footnotes available for this verse

'; - return; - } - console.log('Found stored footnotes:', verseFootnotes); - container.innerHTML = ` -
-

Footnotes

- `; - verseFootnotes.forEach(fn => { - const footnoteDiv = document.createElement('div'); - footnoteDiv.className = 'footnote'; - footnoteDiv.innerHTML = ` - ${fn.number} - ${fn.content} - `; - container.appendChild(footnoteDiv); - }); - container.style.display = 'block'; -} -function setupStrongsFootnoteHandlers() { - document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => { - const newRef = ref.cloneNode(true); - ref.parentNode.replaceChild(newRef, ref); - }); - document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => { - ref.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - const footnoteNumber = this.dataset.footnoteNumber; - const footnoteElement = document.querySelector(`#strongsFootnotesContainer .footnote[data-footnote-number="${footnoteNumber}"]`); - if (footnoteElement) { - footnoteElement.scrollIntoView({ - behavior: 'smooth', - block: 'nearest' - }); - footnoteElement.style.backgroundColor = 'var(--verse-hover)'; - setTimeout(() => { - footnoteElement.style.backgroundColor = ''; - }, 2000); - } - }); - }); -} -function copyVerseFallback(text) { - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand('copy'); - const btn = document.getElementById('copyVerseBtn'); - const original = btn.textContent; - btn.textContent = '✓ Copied!'; - btn.classList.add('copied'); - setTimeout(() => { - btn.textContent = original; - btn.classList.remove('copied'); - }, 2000); - } catch (err) { - console.error('Copy fallback failed:', err); - alert('Could not copy verse text.'); - } finally { - document.body.removeChild(textarea); - } -} -function popOutResource(url, title) { - window.open(url, title, - 'width=800,height=600,menubar=no,toolbar=no,location=no'); -} \ No newline at end of file diff --git a/public/modules/ui.js b/public/modules/ui.js deleted file mode 100644 index 1fa2216..0000000 --- a/public/modules/ui.js +++ /dev/null @@ -1,407 +0,0 @@ -import { handleError } from '../main.js' -import { - loadSelectedChapter, - populateBookDropdown, - populateChapterDropdown -} from './navigation.js' -import { loadPDF } from './pdf.js' -import { - BOOK_ORDER, - CHAPTER_COUNTS, - bibleComUrlMap, - bibleHubUrlMap, - ebibleOrgUrlMap, - formatBookNameForSource, - getActivePlan, - saveToStorage, - state, - stepBibleUrlMap -} from './state.js' -export function switchNotesView(view) { - state.settings.notesView = view; - const txtBtn = document.getElementById('textViewBtn'); - const mdBtn = document.getElementById('markdownViewBtn'); - const input = document.getElementById('notesInput'); - const display = document.getElementById('notesDisplay'); - if (view === 'text') { - txtBtn.classList.add('active'); - mdBtn.classList.remove('active'); - input.style.display = 'block'; - display.style.display = 'none'; - } else { - txtBtn.classList.remove('active'); - mdBtn.classList.add('active'); - input.style.display = 'none'; - display.style.display = 'block'; - updateMarkdownPreview(); - } - saveToStorage(); -} -export function updateMarkdownPreview() { - if (state.settings.notesView !== 'markdown' || typeof marked === 'undefined') return; - const out = document.getElementById('notesDisplay'); - try { - out.innerHTML = marked.parse(state.notes); - } catch (e) { - console.error('Markdown error:', e); - out.innerHTML = '

Error rendering markdown

'; - } -} -export function insertMarkdown(type) { - const ta = document.getElementById('notesInput'); - const start = ta.selectionStart; - const end = ta.selectionEnd; - const sel = ta.value.substring(start, end); - let repl = ''; - let cursorAdj = 0; - switch (type) { - case 'bold': repl = `**${sel || 'bold text'}**`; cursorAdj = sel ? 0 : -2; break; - case 'italic': repl = `*${sel || 'italic text'}*`; cursorAdj = sel ? 0 : -1; break; - case 'h1': repl = `# ${sel || 'Heading 1'}`; cursorAdj = sel ? 0 : -10; break; - case 'h2': repl = `## ${sel || 'Heading 2'}`; cursorAdj = sel ? 0 : -10; break; - case 'h3': repl = `### ${sel || 'Heading 3'}`; cursorAdj = sel ? 0 : -10; break; - case 'ul': repl = `- ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break; - case 'ol': repl = `1. ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break; - case 'quote': repl = `> ${sel || 'Quote'}`; cursorAdj = sel ? 0 : -6; break; - case 'code': repl = `\`${sel || 'code'}\``; cursorAdj = sel ? 0 : -1; break; - case 'link': repl = `[${sel || 'link text'}](url)`; cursorAdj = sel ? -4 : -14; break; - } - ta.value = ta.value.slice(0, start) + repl + ta.value.slice(end); - const newPos = start + repl.length + cursorAdj; - ta.setSelectionRange(newPos, newPos); - ta.focus(); - state.notes = ta.value; - saveToStorage(); - updateMarkdownPreview(); -} -export function exportNotes(ext) { - if (!state.notes || state.notes.trim() === '') { - alert('No notes to export!'); - return; - } - const blob = new Blob([state.notes], { type: 'text/plain;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `study-notes-${new Date().toISOString().split('T')[0]}.${ext}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} -export function toggleNotes() { - document.getElementById('notesSection').classList.toggle('hidden'); -} -export function togglePanelCollapse(panelId) { - const panel = document.getElementById(panelId); - const collapsed = panel.classList.contains('panel-collapsed'); - if (collapsed) { - panel.classList.remove('panel-collapsed'); - if (state.settings.panelWidths[panelId]) { - panel.style.width = state.settings.panelWidths[panelId] + 'px'; - } - state.settings.collapsedPanels[panelId] = false; - } else { - panel.classList.add('panel-collapsed'); - state.settings.collapsedPanels[panelId] = true; - } - saveToStorage(); -} -export function toggleSection(sectionId) { - const content = document.getElementById(`content-${sectionId}`); - const header = document.querySelector(`[data-section="${sectionId}"]`); - const toggle = header.querySelector('.section-toggle'); - const nowCollapsed = content.classList.contains('collapsed'); - content.classList.toggle('collapsed'); - toggle.classList.toggle('collapsed'); - state.settings.collapsedSections[sectionId] = !nowCollapsed; - saveToStorage(); -} -export function toggleReferencePanel() { - const panel = document.getElementById('referencePanel'); - const nowOpen = panel.classList.contains('active'); - if (nowOpen) { - panel.classList.remove('active'); - state.settings.referencePanelOpen = false; - } else { - panel.classList.add('active'); - state.settings.referencePanelOpen = true; - updateReferencePanel(); - } - saveToStorage(); -} -export async function updateReferencePanel() { - try { - const sourceSelect = document.getElementById('referenceSource'); - const source = sourceSelect.value; - const iframe = document.getElementById('referenceIframe'); - const pdfViewer = document.getElementById('pdfViewer'); - const transSel = document.getElementById('referenceTranslation'); - state.settings.referenceSource = source; - state.settings.referenceVersion = transSel.value; - transSel.style.display = source === 'pdf' ? 'none' : 'block'; - filterTranslationOptions(source, transSel); - const actualSource = document.getElementById('referenceSource').value; - const translation = transSel.value; - let passage; - if (state.settings.readingMode === 'readingPlan') { - passage = getActivePlan()[state.settings.currentPassageIndex]; - } else { - passage = { - book: state.settings.manualBook, - chapter: state.settings.manualChapter, - displayRef: `${state.settings.manualBook} ${state.settings.manualChapter}` - }; - } - const bookName = passage.book.toLowerCase().replace(/\s+/g, '_'); - const bookAbbr = bookName.substring(0, 3).toUpperCase(); - const chapter = passage.chapter; - if (actualSource === 'pdf') { - if (!state.settings.customPdf) { - alert('No PDF uploaded. Please upload one in Settings first.'); - document.getElementById('referenceSource').value = 'biblegateway'; - transSel.style.display = 'block'; - filterTranslationOptions('biblegateway', transSel); - return; - } - iframe.style.display = 'none'; - pdfViewer.classList.add('active'); - document.getElementById('zoomLevel').textContent = - Math.round(state.settings.pdfZoom * 100) + '%'; - await loadPDF(); - } else if (actualSource === 'biblehub') { - const bibleHubCode = bibleHubUrlMap[translation] || translation.toLowerCase(); - const url = `https://biblehub.com/${bibleHubCode}/${bookName}/${chapter}.htm`; - iframe.src = url; - } else if (actualSource === 'biblecom') { - const bibleComCode = bibleComUrlMap[translation]; - if (!bibleComCode) { - alert(`Bible.com doesn't support ${translation}. Please choose another translation.`); - return; - } - const formattedBook = formatBookNameForSource(passage.book, 'biblecom'); - const urlFormats = [ - `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}?interface=embed`, - `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}`, - `https://www.bible.com/bible/${bibleComCode}/${chapter}.${translation}?${formattedBook}=${chapter}` - ]; - let currentUrlIndex = 0; - function tryNextUrl() { - if (currentUrlIndex >= urlFormats.length) { - alert('Could not load Bible.com. Please try another reference source.'); - return; - } - iframe.src = urlFormats[currentUrlIndex]; - currentUrlIndex++; - } - iframe.onload = function() { - console.log('Bible.com loaded successfully with format', currentUrlIndex); - }; - iframe.onerror = function() { - console.log('Trying next Bible.com URL format...'); - tryNextUrl(); - }; - tryNextUrl(); - } else if (actualSource === 'ebibleorg') { - const ebibleOrgCode = ebibleOrgUrlMap[translation]; - if (!ebibleOrgCode) { - alert(`eBible.org doesn't support ${translation}. Please choose another translation.`); - return; - } - const bookRef = bookName === 'psalms' ? 'PS1' : `${bookAbbr}1`; - const url = `https://ebible.org/study/?w1=bible&t1=${encodeURIComponent(ebibleOrgCode)}&v1=${bookRef}_${chapter}`; - iframe.src = url; - } else if (actualSource === 'stepbible') { - const stepBibleCode = stepBibleUrlMap[translation]; - if (!stepBibleCode) { - alert(`STEP Bible doesn't support ${translation}. Please choose another translation.`); - return; - } - const url = getStepBibleUrl(passage.displayRef, translation); - iframe.src = url; - } else { - const query = passage.displayRef.replace(/\s+/g, '+'); - let version = translation; - if (translation === 'GNV') version = 'GNV'; - const url = `https://www.biblegateway.com/passage/?search=${query}&version=${version}&interface=print`; - iframe.src = url; - } - saveToStorage(); - } catch (err) { - handleError(err, 'updateReferencePanel'); - } -} -function filterTranslationOptions(source, selectElement) { - const unsupportedTranslations = { - biblecom: [], - biblehub: ['GNV'], - biblegateway: ['BSB'], - stepbible: ['NKJV', 'CSB', 'NLT'], - ebibleorg: ['NASB', 'ASV', 'ESV', 'NKJV', 'CSB', 'NIV', 'NLT'], - pdf: ['NASB1995', 'NASB', 'ASV', 'ESV', 'KJV', 'GNV', 'NKJV', 'BSB', 'CSB', 'NET', 'NIV', 'NLT'] - }; - const allOptions = selectElement.querySelectorAll('option'); - const currentValue = selectElement.value; - let needsNewSelection = false; - let needsSourceChange = false; - allOptions.forEach(option => { - const value = option.value; - const isUnsupported = unsupportedTranslations[source]?.includes(value); - if (isUnsupported) { - option.style.display = 'none'; - option.disabled = true; - if (value === currentValue) { - needsNewSelection = true; - if (value === 'BSB' && source === 'biblegateway') { - needsSourceChange = true; - } - } - } else { - option.style.display = 'block'; - option.disabled = false; - } - }); - if (needsSourceChange) { - document.getElementById('referenceSource').value = 'biblehub'; - state.settings.referenceSource = 'biblehub'; - const sourceSelect = document.getElementById('referenceSource'); - sourceSelect.value = 'biblehub'; - selectElement.value = 'BSB'; - state.settings.referenceVersion = 'BSB'; - } - else if (needsNewSelection) { - let fallbackValue = 'NASB1995'; - if (source === 'biblehub') { - fallbackValue = 'NASB'; - } - selectElement.value = fallbackValue; - state.settings.referenceVersion = fallbackValue; - } - if (needsSourceChange || needsNewSelection) { - saveToStorage(); - } -} -export function restoreBookChapterUI() { - populateBookDropdown(); - const bookSel = document.getElementById('bookSelect'); - const chapterSel = document.getElementById('chapterSelect'); - const savedBook = state.settings.manualBook || BOOK_ORDER[0]; - const bookIdx = BOOK_ORDER.indexOf(savedBook); - const book = bookIdx >= 0 ? BOOK_ORDER[bookIdx] : BOOK_ORDER[0]; - populateBookDropdown(); - bookSel.value = book; - populateChapterDropdown(book); - const savedChap = Number(state.settings.manualChapter) || 1; - const maxChap = CHAPTER_COUNTS[book]; - const chapter = Math.min(savedChap, maxChap); - chapterSel.value = String(chapter); - state.settings.manualBook = book; - state.settings.manualChapter = chapter; - loadSelectedChapter(book, chapter); -} -export function initResizeHandles() { - const handles = document.querySelectorAll('.resize-handle'); - const SPEED_FACTOR = 1.8; - const limits = { - sidebar: { min: 150, max: 600 }, - referencePanel: { min: 250, max: 800 }, - scriptureSection: { min: 300, max: 1200 }, - notesSection: { min: 250, max: 800 } - }; - let resizing = false, - startX = 0, - startW = 0, - panel = null, - invert = false, - pendingRAF = false; - handles.forEach(handle => { - handle.addEventListener('mousedown', e => { - resizing = true; - startX = e.clientX; - const panelId = handle.dataset.panel; - panel = document.getElementById(panelId); - startW = panel.offsetWidth; - invert = (panel.id === 'notesSection'); - document.body.style.cursor = 'ew-resize'; - document.body.style.userSelect = 'none'; - e.preventDefault(); - }); - }); - document.addEventListener('mousemove', e => { - if (!resizing || !panel) return; - let delta = (invert ? startX - e.clientX : e.clientX - startX) * SPEED_FACTOR; - let newW = startW + delta; - const { min, max } = limits[panel.id] || { min: 150, max: 1200 }; - if (newW < min) newW = min; - if (newW > max) newW = max; - if (!pendingRAF) { - pendingRAF = true; - requestAnimationFrame(() => { - panel.style.width = newW + 'px'; - state.settings.panelWidths[panel.id] = newW; - pendingRAF = false; - }); - } - }); - document.addEventListener('mouseup', () => { - if (resizing) { - resizing = false; - panel = null; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - saveToStorage(); - } - }); -} -export function popOutResource(url, title) { - window.open(url, title, - 'width=800,height=600,menubar=no,toolbar=no,location=no'); -} -export function getStepBibleUrl(reference, translation) { - const stepBibleCode = stepBibleUrlMap[translation] || translation; - return `https://www.stepbible.org/?q=version=${stepBibleCode}@reference=${encodeURIComponent(reference)}&options=HNVUG`; -} -export function makeToggleSticky() { - const sidebar = document.getElementById('sidebar'); - const toggle = sidebar.querySelector('.collapse-toggle'); - if (!toggle) return; - toggle.style.position = 'sticky'; - toggle.style.top = '10px'; - toggle.style.zIndex = '1000'; - toggle.style.marginLeft = 'auto'; - toggle.style.marginRight = '10px'; -} -export function restoreSidebarState() { - Object.entries(state.settings.collapsedSections || {}) - .forEach(([sec, collapsed]) => { - if (collapsed) { - const content = document.getElementById(`content-${sec}`); - const header = document.querySelector(`[data-section="${sec}"]`); - const toggle = header?.querySelector('.section-toggle'); - if (content && toggle) { - content.classList.add('collapsed'); - toggle.classList.add('collapsed'); - } - } - }); -} -export function restorePanelStates() { - Object.entries(state.settings.panelWidths || {}) - .forEach(([id, w]) => { - const el = document.getElementById(id); - if (el && w) el.style.width = w + 'px'; - }); - Object.entries(state.settings.collapsedPanels || {}) - .forEach(([id, collapsed]) => { - const el = document.getElementById(id); - if (el && collapsed) el.classList.add('panel-collapsed'); - }); - if (state.settings.referencePanelOpen) { - document.getElementById('referencePanel').classList.add('active'); - document.getElementById('referenceSource').value = - state.settings.referenceSource || 'biblegateway'; - document.getElementById('referenceTranslation').value = - state.settings.referenceVersion || 'NASB1995'; - updateReferencePanel(); - } -} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css deleted file mode 100644 index 8d5580b..0000000 --- a/public/styles.css +++ /dev/null @@ -1,363 +0,0 @@ -:root{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db;--background:#ecf0f1;--card-background:#ffffff;--toolbar-background:#f8f9fa;--notes-background:#f8f9fa;--sidebar-background:#2c3e50;--header-background:#ffffff;--popup-background:#ffffff;--text-color:#2c3e50;--sidebar-text:#ecf0f1;--border-color:#bdc3c7;--verse-hover:#f0f0f0;--shadow:rgba(0,0,0,0.1);--shadow-strong:rgba(0,0,0,0.3);--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--transition-fast:150ms ease;--transition-normal:300ms ease;--transition-slow:500ms ease} -[data-color-theme="blue"]{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db} -[data-color-theme="green"]{--primary-color:#1b5e20;--secondary-color:#2e7d32;--accent-color:#4caf50} -[data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#6a1b9a;--accent-color:#9c27b0} -[data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#c62828;--accent-color:#f44336} -[data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#ef6c00;--accent-color:#ff9800} -[data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00695c;--accent-color:#009688} -[data-theme="dark"]{--background:#0d1117;--card-background:#161b22;--toolbar-background:#161b22;--notes-background:#0d1117;--sidebar-background:#0d1117;--header-background:#161b22;--popup-background:#161b22;--text-color:#e1e1e1;--sidebar-text:#c9d1d9;--border-color:#444;--verse-hover:#21262d;--shadow:rgba(0,0,0,0.4);--shadow-strong:rgba(0,0,0,0.6)} -[data-theme="dark"][data-color-theme="blue"]{--primary-color:#1a3a52;--secondary-color:#2c5f7f;--accent-color:#5dade2} -[data-theme="dark"][data-color-theme="green"]{--primary-color:#1b4d20;--secondary-color:#2e6d32;--accent-color:#66bb6a} -[data-theme="dark"][data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#7b1fa2;--accent-color:#ab47bc} -[data-theme="dark"][data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#d32f2f;--accent-color:#ef5350} -[data-theme="dark"][data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#f57c00;--accent-color:#ffa726} -[data-theme="dark"][data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00796b;--accent-color:#26a69a} - -*,*::before,*::after{margin:0;padding:0;box-sizing:border-box} -@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important;scroll-behavior:auto!important} -} -:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px} -@media(prefers-contrast:high){:root{--border-color:#000000;--shadow:rgba(0,0,0,0.8);--shadow-strong:rgba(0,0,0,0.9)} -.verse:hover{outline:2px solid var(--accent-color)} -} - -body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background-color:var(--background);color:var(--text-color);line-height:1.6;transition:background-color 0.3s,color 0.3s} -.container{display:flex;height:100vh;overflow:hidden} -.panel-collapsed{min-width:50px!important;max-width:50px!important} -.panel-collapsed>*:not(.collapse-toggle):not(.resize-handle){display:none!important} -.collapse-toggle{will-change:transform;position:absolute;top:55%;right:5px;transform:translateY(-50%);background:var(--accent-color);color:white;border:none;width:40px;height:40px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;z-index:100;transition:transform 0.3s ease,background-color 0.3s ease;box-shadow:0 2px 8px rgba(0,0,0,0.2)} -.collapse-toggle:hover{transform:translateY(-50%)scale(1.1);box-shadow:0 4px 12px rgba(0,0,0,0.3)} -.collapse-toggle:focus{outline:2px solid white;outline-offset:2px} -.panel-collapsed .collapse-toggle{right:5px} -.collapse-toggle::before{content:'\25C0';transition:transform 0.3s ease} -.panel-collapsed .collapse-toggle::before{content:'\25B6'} -#scriptureSection .collapse-toggle::before{content:'\25B6'} -#scriptureSection.panel-collapsed .collapse-toggle::before{content:'\25C0'} -#notesSection .collapse-toggle::before{content:'\25B6'} -#notesSection.panel-collapsed .collapse-toggle::before{content:'\25C0'} -.scripture-section{scroll-behavior:smooth;scroll-padding-top:20px} -.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0} -.content-area{flex:1;display:flex;overflow:hidden} - -.welcome-screen{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,var(--primary-color),var(--accent-color));z-index:5000;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto} -.welcome-screen.hidden{display:none} -.welcome-content{background:var(--card-background);border-radius:20px;padding:40px;max-width:900px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,0.3);margin:auto;max-height:90vh;overflow-y:auto} -.welcome-content h1{color:var(--primary-color);text-align:center;margin-bottom:15px;font-size:2.5em} -.welcome-content>p{color:var(--text-color);text-align:center;margin-bottom:30px;font-size:1.2em;opacity:0.8} -.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin:30px 0} -.feature-card{padding:20px;background:var(--background);border-radius:10px;border:2px solid var(--border-color);transition:all 0.3s} -.feature-card:hover{transform:translateY(-5px);box-shadow:0 5px 20px var(--shadow);border-color:var(--accent-color)} -.feature-icon{font-size:2.5em;margin-bottom:10px} -.feature-card h3{color:var(--primary-color);margin-bottom:10px} -.feature-card p{color:var(--text-color);opacity:0.8;font-size:0.95em} -.offline-setup{margin:30px 0;padding:25px;background:var(--background);border-radius:10px;border:2px solid var(--accent-color)} -.offline-setup h2{color:var(--primary-color);margin-bottom:15px;display:flex;align-items:center;gap:10px} -.pdf-upload-area{border:2px dashed var(--border-color);border-radius:8px;padding:30px;text-align:center;cursor:pointer;transition:all 0.3s;background:var(--card-background);margin:15px 0} -.pdf-upload-area:hover{border-color:var(--accent-color);background-color:rgba(52,152,219,0.05)} -.pdf-upload-area.has-file{border-color:var(--accent-color);background-color:rgba(52,152,219,0.1)} -.pdf-download-options{margin-top:15px;padding:15px;background-color:var(--card-background);border-radius:5px} -.pdf-download-options h4{margin-bottom:10px;color:var(--text-color)} -.pdf-download-link{display:block;padding:8px 12px;margin-bottom:8px;background-color:var(--background);border:1px solid var(--border-color);border-radius:4px;color:var(--accent-color);text-decoration:none;transition:all 0.2s} -.pdf-download-link:hover{background-color:var(--accent-color);color:white} -.welcome-actions{display:flex;gap:15px;margin-top:30px;justify-content:center} -.welcome-actions button{padding:15px 40px;border:none;border-radius:8px;font-size:16px;font-weight:bold;cursor:pointer;transition:all 0.3s} - -.sidebar{width:280px;background-color:var(--sidebar-background);color:var(--sidebar-text);padding:20px;overflow-y:auto;flex-shrink:0;border-right:1px solid var(--border-color);position:relative;min-width:50px;transition:all 0.3s ease} -.sidebar h2{margin-bottom:20px;font-size:1.3em;border-bottom:2px solid var(--accent-color);padding-bottom:10px;color:var(--sidebar-text)} -.sidebar-section{margin-bottom:10px} -.sidebar-section-header{display:flex;justify-content:space-between;align-items:center;cursor:pointer;padding:10px;margin-top:15px;background-color:rgba(255,255,255,0.05);border-radius:5px;transition:background-color 0.2s} -.sidebar-section-header:hover{background-color:rgba(255,255,255,0.1)} -.sidebar-section-header:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.sidebar-section-header h3{margin:0;font-size:1.1em;color:var(--accent-color)} -.section-toggle{font-size:0.9em;transition:transform 0.3s} -.section-toggle.collapsed{transform:rotate(-90deg)} -.sidebar-section-content{max-height:1000px;overflow:hidden;transition:max-height 0.3s ease-out,opacity 0.3s ease-out;opacity:1} -.sidebar-section-content.collapsed{max-height:0;opacity:0} -.sidebar-links{list-style:none;margin-top:10px} -.sidebar-links li{margin-bottom:8px} -.sidebar-links a{color:var(--sidebar-text);text-decoration:none;display:block;padding:8px 12px;border-radius:5px;transition:background-color 0.3s;opacity:0.9} -.sidebar-links a:hover{background-color:var(--secondary-color);opacity:1} -.sidebar-links a:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.reference-panel-toggle{margin-top:20px;padding:12px;background:var(--accent-color);color:white;border:none;border-radius:5px;cursor:pointer;width:100%;font-size:14px;font-weight:bold;transition:opacity 0.3s} -.reference-panel-toggle:hover{opacity:0.8} -.reference-panel-toggle:focus{outline:2px solid white;outline-offset:2px} - -.reference-panel{display:none;width:400px;background-color:var(--card-background);border-right:1px solid var(--border-color);position:relative;flex-shrink:0;min-width:50px;transition:all 0.3s ease} -.reference-panel.active{display:flex;flex-direction:column} -.reference-panel-header{padding:15px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px} -.reference-panel-header h3{color:var(--text-color);margin:0} -.reference-panel-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap} -.reference-panel-controls select{padding:5px 10px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px} -.reference-panel-controls select:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.reference-panel-close{background:none;border:none;color:var(--text-color);cursor:pointer;font-size:20px;padding:5px 10px;border-radius:4px;transition:background-color 0.2s;margin-left:10px} -.reference-panel-close:hover{background-color:var(--verse-hover)} -.reference-panel-close:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.reference-panel-content{flex:1;overflow:hidden;position:relative} -.reference-panel-iframe{width:100%;height:100%;border:none} -.pdf-viewer{width:100%;height:100%;display:none;flex-direction:column;background-color:var(--card-background)} -.pdf-viewer.active{display:flex} -.pdf-controls{padding:10px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap} -.pdf-controls button{padding:5px 15px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px} -.pdf-controls button:hover{opacity:0.8} -.pdf-controls button:focus{outline:2px solid white;outline-offset:2px} -.pdf-controls button:disabled{opacity:0.5;cursor:not-allowed} -.pdf-controls span{color:var(--text-color);font-size:13px} -.pdf-controls input[type="number"]{width:60px;padding:5px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px} -.pdf-controls input[type="number"]:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.pdf-canvas-container{flex:1;overflow:auto;display:flex;justify-content:center;align-items:flex-start;padding:20px;background-color:var(--background)} -#pdfCanvas{box-shadow:0 2px 10px var(--shadow)} - -.resize-handle{position:absolute;top:0;bottom:0;width:10px;cursor:ew-resize;background-color:transparent;transition:background-color 0.2s;z-index:10;display:flex;align-items:center;justify-content:center} -.resize-handle::after{content:'\22EE\22EE';color:var(--border-color);font-size:18px;opacity:0;transition:opacity 0.2s} -.resize-handle:hover{background-color:rgba(52,152,219,0.1)} -.resize-handle:hover::after{opacity:1;color:var(--accent-color)} -.resize-handle-right{right:0} -.resize-handle-left{left:0} - -.header{background-color:var(--header-background);padding:20px 30px;box-shadow:0 2px 5px var(--shadow);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)} -.header h1{color:var(--text-color);font-size:1.8em} -.header-controls{display:flex;gap:10px;align-items:center} -.theme-toggle{background:none;border:2px solid var(--border-color);color:var(--text-color);width:44px;height:44px;border-radius:50%;cursor:pointer;font-size:20px;display:flex;align-items:center;justify-content:center;transition:all 0.3s} -.theme-toggle:hover{background-color:var(--accent-color);border-color:var(--accent-color);color:white;transform:rotate(180deg)} -.theme-toggle:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.btn{position:relative;padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-size:14px;transition:all 0.3s;background-color:var(--accent-color);color:white} -.btn:hover{opacity:0.8;transform:translateY(-1px)} -.btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.btn:focus::after{content:'';position:absolute;top:-4px;left:-4px;right:-4px;bottom:-4px;border:2px solid var(--accent-color);border-radius:7px;pointer-events:none} -.btn-secondary{background-color:var(--secondary-color)} -.btn:disabled{opacity:0.5;cursor:not-allowed;transform:none} -.btn-primary{background:var(--accent-color);color:white} -.btn-primary:hover{opacity:0.9;transform:translateY(-2px);box-shadow:0 5px 15px var(--shadow)} -.btn-primary:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.btn-danger{background-color:#f44336!important;color:white} -.btn-danger:hover{background-color:#d32f2f!important;opacity:0.9} -.toolbar{background-color:var(--toolbar-background);padding:15px 30px;border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;flex-wrap:wrap} -.toolbar button{padding:8px 15px;font-size:13px} -.toolbar-info{margin-left:auto;color:var(--text-color);opacity:0.7;font-size:14px} -.toolbar-divider{width:1px;height:24px;margin:0 12px;background-color:var(--border-color);align-self:center;opacity:0.6} - -.scripture-section{flex:1;padding:30px;overflow-y:auto;background-color:var(--card-background);margin:var(--spacing-xl);padding-inline:var(--spacing-xl);padding-block:var(--spacing-lg);border-radius:10px;box-shadow:0 2px 10px var(--shadow);border:1px solid var(--border-color);position:relative;min-width:300px;transition:all 0.3s ease;contain:layout style} -.scripture-content{font-size:1.1em;line-height:1.8;text-rendering:optimizeLegibility;font-feature-settings:"kern" 1,"liga" 1,"calt" 1} -.passage-header{background:linear-gradient(135deg,var(--primary-color),var(--primary-color));color:white;padding:20px;border-radius:8px;margin-bottom:20px} -.passage-header h2{font-size:1.5em;margin-bottom:5px;color:white;border:none} -.passage-header .date{opacity:0.9;font-size:0.9em} -.passage-reference{font-weight:bold;font-size:1.2em;margin-bottom:15px;color:var(--accent-color)} -.plan-label{font-size:1.1rem;font-weight:500;color:var(--accent-color);margin-top:8px;margin-bottom:12px} -.verse{margin-bottom:8px;padding:var(--spacing-sm);border-radius:var(--radius-sm);cursor:pointer;transition:background-color var(--transition-fast);position:relative;contain:content} -.verse:hover,.verse:active{background-color:var(--verse-hover);will-change:background-color} -.verse:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.verse-number{font-weight:bold;color:var(--accent-color);margin-right:8px;font-size:0.85em;vertical-align:super} -.verse-text{display:inline} -.highlight-yellow{background-color:#fff59d;color:#000} -.highlight-green{background-color:#a5d6a7;color:#000} -.highlight-blue{background-color:#90caf9;color:#000} -.highlight-pink{background-color:#f48fb1;color:#000} -.highlight-orange{background-color:#ffcc80;color:#000} -.highlight-purple{background-color:#ce93d8;color:#000} -.footnote-ref{position:relative;color:var(--accent-color);font-size:0.7em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease} -.footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)} -.footnote-ref::before{content:'['} -.footnote-ref::after{content:']'} -.footnotes-container{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:6px;border-left:3px solid var(--accent-color);font-size:0.9em;overflow-y:auto;max-height:300px;position:relative;z-index:100} -.footnotes-separator{margin:30px 0 15px 0;border:none;border-top:1px solid var(--border-color);opacity:0.5} -.footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600} -.footnote{cursor:pointer;transition:all 0.2s ease;padding:8px 12px;border-radius:4px;margin-bottom:8px} -.footnote:hover{background-color:var(--verse-hover);transform:translateX(2px)} -.footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;background-color:rgba(0,0,0,0.08);padding:2px 6px;border-radius:3px;transition:all 0.2s ease} -.footnote:hover .footnote-number{background-color:var(--accent-color);color:white} -.footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95} -.footnote.highlighted{background-color:var(--verse-hover);border-left-color:var(--accent-color)} -.strongs-footnotes-container{display:block!important;max-height:200px;overflow-y:auto;padding:2px;background-color:var(--toolbar-background)} -.strongs-footnotes-container .footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600} -.strongs-footnotes-container .footnote{margin-bottom:12px;padding:10px;border-radius:4px;transition:all 0.3s ease} -.strongs-footnotes-container .footnote:hover{background-color:rgba(0,0,0,0.03)} -.strongs-footnotes-container .footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;font-size:0.85em;background-color:rgba(0,0,0,0.08);padding:3px 6px;border-radius:3px} -.strongs-footnotes-container .footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95} -.strongs-footnotes-container .footnote-ref{color:var(--accent-color);font-size:0.75em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease} -.strongs-footnotes-container .footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)} -#strongsFootnotesContainer .footnote{margin-bottom:12px;padding:10px;background-color:var(--toolbar-background);border-radius:4px} -#strongsFootnotesContainer .footnote-number{font-weight:bold;color:var(--accent-color);margin-right:8px} -#strongsFootnotesContainer .footnote-content{display:inline} -#strongsPopup .footnote{cursor:pointer;transition:all 0.2s ease} -#strongsPopup .footnote:hover{background-color:var(--verse-hover)} -#strongsPopup .footnote:hover .footnote-number{background-color:var(--accent-color);color:white} -.verse-navigation{display:flex;align-items:center;gap:10px;margin-right:auto} -.nav-btn{background:var(--button-bg);border:1px solid var(--border-color);border-radius:4px;padding:6px 10px;cursor:pointer;font-size:12px;color:var(--text-color);transition:all 0.2s ease} -.nav-btn:hover{background:var(--button-hover-bg);border-color:var(--accent-color)} -.nav-btn:disabled{opacity:0.5;cursor:not-allowed} - -.color-picker{transform:translateZ(0);display:none;position:absolute;background:var(--popup-background);border:1px solid var(--border-color);border-radius:5px;padding:10px;box-shadow:0 4px 15px var(--shadow-strong);z-index:1000} -.color-picker.active{display:block} -.color-options{display:grid;grid-template-columns:repeat(3,1fr);gap:8px} -.color-option{width:40px;height:40px;border-radius:5px;cursor:pointer;border:2px solid transparent;transition:all 0.2s} -.color-option:hover{border-color:var(--accent-color);transform:scale(1.1)} -.color-option:focus{outline:2px solid var(--accent-color);outline-offset:2px} - -.strongs-popup{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:25px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:90vw;width:90%;max-height:90vh;overflow-y:auto;border:1px solid var(--border-color)} -.strongs-popup.active{display:block} -.popup-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1999} -.popup-overlay.active{display:block} -.popup-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;border-bottom:2px solid var(--accent-color);padding-bottom:10px} -.popup-header h2{color:var(--text-color)} -.popup-close{cursor:pointer;font-size:24px;color:var(--text-color);background:none;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color 0.2s} -.popup-close:hover{background-color:var(--verse-hover)} -.popup-close:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.strongs-content{line-height:1.8;color:var(--text-color)} -.verse-reference-display{font-size:1.2em;font-weight:bold;color:var(--accent-color);margin-bottom:15px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px} -.verse-text-display{padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px;margin-bottom:20px;font-size:1.1em;line-height:1.6} -.copy-verse-btn{padding:6px 12px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px;transition:all 0.2s} -.copy-verse-btn:hover{opacity:0.8} -.copy-verse-btn:focus{outline:2px solid white;outline-offset:2px} -.copy-verse-btn.copied{background-color:#4caf50} -.embedded-resources{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:20px} -.resource-frame{border:1px solid var(--border-color);border-radius:5px;overflow:hidden;background:var(--card-background)} -.resource-frame-header{background:var(--toolbar-background);padding:10px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)} -.resource-frame-header h4{margin:0;font-size:0.9em;color:var(--text-color)} -.resource-frame-actions{display:flex;gap:5px} -.resource-frame-btn{padding:4px 8px;font-size:11px;background:var(--accent-color);color:white;border:none;border-radius:3px;cursor:pointer;transition:opacity 0.2s} -.resource-frame-btn:hover{opacity:0.8} -.resource-frame-btn:focus{outline:2px solid white;outline-offset:2px} -.resource-frame iframe{width:100%;height:400px;border:none} -.strongs-definition{margin-top:15px;padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px} -.strongs-definition h3{color:var(--text-color);margin-bottom:10px} -.api-attribution{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:5px;border:1px solid var(--border-color);font-size:0.85em;opacity:0.8} -.api-attribution a{color:var(--accent-color);text-decoration:none} -.api-attribution a:hover{text-decoration:underline} -.api-attribution a:focus{outline:2px solid var(--accent-color);outline-offset:2px} - -.notes-section{width:400px;padding:20px;background-color:var(--notes-background);border-left:1px solid var(--border-color);display:flex;flex-direction:column;overflow:hidden;position:relative;min-width:50px;transition:all 0.3s ease} -.notes-section.hidden{display:none} -.notes-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px} -.notes-header h3{color:var(--text-color)} -.notes-controls{display:flex;gap:5px} -.notes-controls button{padding:5px 10px;font-size:12px} -.notes-view-toggle{display:flex;gap:5px;padding:5px;background-color:var(--toolbar-background);border-radius:5px;margin-bottom:10px} -.view-toggle-btn{flex:1;padding:8px;border:none;border-radius:4px;cursor:pointer;font-size:13px;background-color:transparent;color:var(--text-color);transition:all 0.2s} -.view-toggle-btn.active{background-color:var(--accent-color);color:white} -.view-toggle-btn:hover:not(.active){background-color:var(--verse-hover)} -.view-toggle-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.markdown-toolbar{display:flex;gap:5px;padding:10px;background-color:var(--toolbar-background);border:1px solid var(--border-color);border-radius:5px 5px 0 0;flex-wrap:wrap} -.markdown-btn{padding:6px 12px;background-color:var(--card-background);border:1px solid var(--border-color);border-radius:4px;cursor:pointer;font-size:13px;color:var(--text-color);transition:all 0.2s;display:flex;align-items:center;gap:5px} -.markdown-btn:hover{background-color:var(--accent-color);color:white;border-color:var(--accent-color)} -.markdown-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.markdown-btn:active{transform:scale(0.95)} -#notesInput,#notesDisplay{flex:1;width:100%;border:1px solid var(--border-color);border-radius:0 0 5px 5px;padding:15px;margin-bottom:10px;background-color:var(--card-background);color:var(--text-color)} -#notesInput{font-family:'Courier New',monospace;font-size:14px;resize:none} -#notesInput:focus{outline:2px solid var(--accent-color);outline-offset:2px} -#notesDisplay{overflow-y:auto;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;font-size:14px} -#notesDisplay h1,#notesDisplay h2,#notesDisplay h3{margin-top:20px;margin-bottom:10px;color:var(--text-color)} -#notesDisplay h1{font-size:2em;border-bottom:2px solid var(--border-color);padding-bottom:10px} -#notesDisplay h2{font-size:1.5em} -#notesDisplay h3{font-size:1.2em} -#notesDisplay code{background-color:var(--toolbar-background);padding:2px 6px;border-radius:3px;font-family:'Courier New',monospace;border:1px solid var(--border-color)} -#notesDisplay pre{background-color:var(--toolbar-background);padding:15px;border-radius:5px;overflow-x:auto;border:1px solid var(--border-color)} -#notesDisplay pre code{background:none;border:none;padding:0} -#notesDisplay blockquote{border-left:4px solid var(--accent-color);padding-left:15px;margin:15px 0;opacity:0.8} -#notesDisplay ul,#notesDisplay ol{margin-left:25px;margin-bottom:15px} -#notesDisplay a{color:var(--accent-color)} -#notesDisplay a:focus{outline:2px solid var(--accent-color);outline-offset:2px} -#notesDisplay p{margin-bottom:10px} -#notesDisplay table{border-collapse:collapse;width:100%;margin:15px 0} -#notesDisplay table th,#notesDisplay table td{border:1px solid var(--border-color);padding:8px;text-align:left} -#notesDisplay table th{background-color:var(--toolbar-background);font-weight:bold} - -.settings-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:30px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:600px;width:90%;border:1px solid var(--border-color);max-height:80vh;overflow-y:auto} -.settings-modal.active{display:block} -.settings-group{margin-bottom:20px} -.settings-group label{display:block;margin-bottom:8px;font-weight:bold;color:var(--text-color)} -.settings-group input,.settings-group select{width:100%;padding:10px;border:1px solid var(--border-color);border-radius:5px;font-size:14px;background-color:var(--card-background);color:var(--text-color)} -.settings-group input:focus,.settings-group select:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.settings-group small{display:block;margin-top:5px;opacity:0.7;color:var(--text-color)} -.settings-group small a{color:var(--accent-color)} -.settings-group small a:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.color-theme-options{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px} -.color-theme-option{padding:15px;border:2px solid var(--border-color);border-radius:8px;cursor:pointer;text-align:center;transition:all 0.3s;background:var(--card-background)} -.color-theme-option:hover{transform:scale(1.05)} -.color-theme-option:focus{outline:2px solid var(--accent-color);outline-offset:2px} -.color-theme-option.selected{border-color:var(--accent-color);background-color:var(--accent-color);color:white} -.color-theme-preview{width:100%;height:40px;border-radius:5px;margin-bottom:8px} -.settings-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:25px} -.settings-section{margin-bottom:25px;padding-bottom:20px;border-bottom:1px solid var(--border-color)} -.about-content{line-height:1.6} -.about-creator{margin-bottom:12px;font-size:.9em} -.about-description{margin-bottom:5px;padding:5px;background-color:var(--bg-secondary);border-radius:5px} -.about-description p{margin:0;color:var(--text-color)} -.attribution-links{margin-top:20px;padding-top:15px;border-top:1px solid var(--border-color)} -.attribution-links h4{margin-bottom:12px;color:var(--text-color);font-size:1em;font-weight:600} -.attribution-link{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-bottom:8px;background-color:var(--bg-secondary);border-radius:6px;text-decoration:none;color:var(--text-color);transition:all 0.2s ease;border:1px solid transparent} -.attribution-link:hover{background-color:var(--button-hover-bg);border-color:var(--accent-color);transform:translateY(-1px)} -.attribution-link i{font-size:1.2em;width:20px;text-align:center} -.attribution-link.gab{color:#4cf278} -.attribution-link.gab:hover{background-color:rgba(151,246,202,0.1)} -.attribution-link.lumo{color:#8c67cd} -.attribution-link.lumo:hover{background-color:rgba(197,151,246,0.1)} -.link-description{font-size:0.85em;opacity:0.8;margin-top:4px} -.version-info{font-family:monospace;padding:2px 4px;background-color:var(--bg-secondary);border-radius:6px;display:inline-block} - -.loading-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);display:none;justify-content:center;align-items:center;z-index:3000} -.loading-overlay.active{display:flex} -.loading-spinner{width:60px;height:60px;border:6px solid rgba(255,255,255,0.3);border-top-color:var(--accent-color);border-radius:50%;animation:spin 1s linear infinite} -@keyframes spin{to{transform:rotate(360deg)} -} - -.error-message{background-color:#ff5252;color:white;padding:15px;border-radius:5px;margin-bottom:20px} - -@media(max-width:1024px){.sidebar{width:250px} -.notes-section{width:350px} -.reference-panel{width:350px} -.feature-grid{grid-template-columns:1fr} -} -@media(max-width:768px){.container{flex-direction:column} -.sidebar{width:100%;height:auto;max-height:200px} -.content-area{flex-direction:column} -.notes-section{width:100%;border-left:none;border-top:1px solid var(--border-color)} -.reference-panel{width:100%;border-left:none;border-top:1px solid var(--border-color)} -.footnote{padding:6px;margin-bottom:8px} -.toolbar-info{display:none} -.embedded-resources{grid-template-columns:1fr} -.welcome-content h1{font-size:2em} -.welcome-actions{flex-direction:column} -} -@container(max-width:768px){.feature-grid{grid-template-columns:1fr} -.toolbar{flex-direction:column;gap:15px} -} -.toggle-notes{display:none} -@media(max-width:768px){.toggle-notes{display:inline-block} -} -.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} -@media print{.sidebar,.header-controls,.toolbar,.notes-section,.reference-panel-toggle{display:none!important} -.scripture-section{margin:0;box-shadow:none;border:none} -body{background:white;color:black} -} -html{font-size:16px} -@media(max-width:768px){html{font-size:14px} -} -@media(hover:none)and(pointer:coarse){.verse:hover{background-color:transparent} -.sidebar-links a{padding:var(--spacing-md)var(--spacing-lg)} -.btn{min-height:44px;min-width:44px} -} -@media(prefers-contrast:high){.verse-number{font-weight:900} -.passage-header{border:2px solid var(--text-color)} -} -@keyframes spin{0%{transform:rotate(0deg)} -100%{transform:rotate(360deg)} -} -.loading-spinner{animation:spin 1s linear infinite;transform:translateZ(0)} -.settings-modal:focus{outline:none} -.settings-modal *:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px} -@media print{.verse{page-break-inside:avoid} -.passage-header{break-after:avoid} -body{background:white!important;color:black!important} -.verse{cursor:default!important} -.highlight-yellow{background-color:#fff59d!important} -} - -::-webkit-scrollbar{width:10px;height:10px} -::-webkit-scrollbar-track{background:var(--background)} -::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:5px} -::-webkit-scrollbar-thumb:hover{background:var(--secondary-color)} -[data-theme="dark"]::-webkit-scrollbar-thumb{background:var(--secondary-color)} -[data-theme="dark"]::-webkit-scrollbar-thumb:hover{background:var(--accent-color)} -*{scrollbar-width:thin;scrollbar-color:var(--border-color)var(--background)} -[data-theme="dark"]*{scrollbar-color:var(--secondary-color)var(--background)} - diff --git a/public/sw.js b/public/sw.js deleted file mode 100644 index 67c6375..0000000 --- a/public/sw.js +++ /dev/null @@ -1,249 +0,0 @@ -import { APP_VERSION } from "./modules/state"; -const CACHE_VERSION = { - static: 'v2', - api: 'v1', - pdf: 'v1' -}; -const CACHE_NAME = `provinent-scripture-${APP_VERSION}-${CACHE_VERSION.static}`; -const API_CACHE_NAME = `provinent-api-cache-${CACHE_VERSION.api}`; -const OFFLINE_PDF_CACHE = `provinent-pdf-cache-${CACHE_VERSION.pdf}`; -const MAX_CACHE_AGE = 24 * 60 * 60 * 1000; -const PDF_SIZE_LIMIT = 10 * 1024 * 1024; -const PRECACHE_URLS = [ - '/', - '/index.html', - '/api.js', - '/main.js', - '/navigation.js', - '/passage.js', - '/pdf.js', - '/settings.js', - '/state.js', - '/strongs.js', - '/ui.js', - '/styles.css', - '/manifest.json' -]; -self.addEventListener('install', (event) => { - console.log('[Service Worker] Installing and pre-caching static assets'); - event.waitUntil( - caches.open(CACHE_NAME) - .then((cache) => { - console.log('[Service Worker] Caching static assets:', PRECACHE_URLS); - return cache.addAll(PRECACHE_URLS); - }) - .then(() => { - console.log('[Service Worker] Pre-caching complete, skipping waiting'); - return self.skipWaiting(); - }) - .catch((error) => { - console.error('[Service Worker] Pre-caching failed:', error); - }) - ); -}); -self.addEventListener('activate', (event) => { - event.waitUntil( - caches.keys() - .then((cacheNames) => { - return Promise.all( - cacheNames.map((cacheName) => { - if (cacheName !== CACHE_NAME && - cacheName !== API_CACHE_NAME && - cacheName !== OFFLINE_PDF_CACHE) { - console.log('[Service Worker] Deleting old cache:', cacheName); - return caches.delete(cacheName) - .catch((error) => { - console.warn('[Service Worker] Failed to delete cache:', cacheName, error); - }); - } - }) - ); - }) - .then(() => { - return Promise.all([ - manageCacheSize(API_CACHE_NAME, 100), - manageCacheSize(OFFLINE_PDF_CACHE, 5) - ]); - }) - .then(() => { - console.log('[Service Worker] Cache cleanup complete, claiming clients'); - return self.clients.claim(); - }) - ); -}); -self.addEventListener('fetch', (event) => { - const url = new URL(event.request.url); - if (url.origin === 'https://bible.helloao.org') { - event.respondWith(handleApiRequest(event.request)); - } - else if (url.hostname.includes('biblehub.com') || - url.hostname.includes('biblegateway.com') || - url.hostname.includes('bible.com')) { - event.respondWith(handleExternalBibleResource(event.request)); - } - else if (url.pathname.endsWith('.pdf')) { - event.respondWith(handlePdfRequest(event.request)); - } - else if (PRECACHE_URLS.includes(url.pathname) || - PRECACHE_URLS.includes(url.pathname + '/')) { - event.respondWith(handleStaticAssetRequest(event.request)); - } -}); -async function handleApiRequest(request) { - const cache = await caches.open(API_CACHE_NAME); - const cachedResponse = await cache.match(request); - if (cachedResponse) { - const cacheTime = new Date(cachedResponse.headers.get('sw-cache-time')); - if (Date.now() - cacheTime.getTime() < MAX_CACHE_AGE) { - console.log('[Service Worker] Serving fresh cached API response'); - return cachedResponse; - } - } - try { - console.log('[Service Worker] Fetching fresh API response from network'); - const networkResponse = await fetch(request); - if (networkResponse.ok) { - const headers = new Headers(networkResponse.headers); - headers.set('sw-cache-time', new Date().toISOString()); - const responseToCache = new Response( - await networkResponse.clone().blob(), - { - status: networkResponse.status, - statusText: networkResponse.statusText, - headers: headers - } - ); - await cache.put(request, responseToCache); - console.log('[Service Worker] Cached fresh API response'); - await manageCacheSize(API_CACHE_NAME, 100); - } - return networkResponse; - } - catch (error) { - console.error('[Service Worker] API request failed, returning offline response'); - return new Response( - JSON.stringify({ - error: 'Offline mode - Bible data not available', - message: 'Please connect to the internet to access Scripture data' - }), - { - status: 503, - headers: { 'Content-Type': 'application/json' } - } - ); - } -} -async function handleExternalBibleResource(request) { - try { - console.log('[Service Worker] Fetching external Bible resource:', request.url); - return await fetch(request); - } - catch (error) { - console.warn('[Service Worker] External resource unavailable offline:', request.url); - return new Response( - ` - -

Offline Mode

-

External Bible resources from ${new URL(request.url).hostname} - are not available offline.

-

Please connect to the internet to access this resource.

- - `, - { - headers: { 'Content-Type': 'text/html' } - } - ); - } -} -async function handlePdfRequest(request) { - const cache = await caches.open(OFFLINE_PDF_CACHE); - const cachedPdf = await cache.match(request); - if (cachedPdf) { - console.log('[Service Worker] Serving cached PDF:', request.url); - return cachedPdf; - } - try { - console.log('[Service Worker] Fetching PDF from network:', request.url); - const response = await fetch(request); - if (response.status === 200) { - const contentLength = response.headers.get('content-length'); - if (!contentLength || parseInt(contentLength) <= PDF_SIZE_LIMIT) { - await cache.put(request, response.clone()); - console.log('[Service Worker] Cached PDF:', request.url); - await manageCacheSize(OFFLINE_PDF_CACHE, 5); - } - } - return response; - } - catch (error) { - console.warn('[Service Worker] PDF unavailable offline:', request.url); - return new Response( - 'PDF not available offline. Please connect to the internet to access this resource.', - { - status: 503, - headers: { 'Content-Type': 'text/plain' } - } - ); - } -} -async function handleStaticAssetRequest(request) { - try { - const cachedResponse = await caches.match(request); - if (cachedResponse) { - console.log('[Service Worker] Serving cached static asset:', request.url); - return cachedResponse; - } - console.log('[Service Worker] Fetching static asset from network:', request.url); - return await fetch(request); - } - catch (error) { - console.error('[Service Worker] Static asset unavailable:', request.url); - if (event.request.destination === 'document') { - return new Response( - ` - -

Offline

-

Provident Scripture Study is currently offline.

-

Some features may be limited without an internet connection.

- - `, - { - headers: { 'Content-Type': 'text/html' } - } - ); - } - throw error; - } -} -self.addEventListener('message', (event) => { - switch (event.data.type) { - case 'CLEAR_CACHE': - console.log('[Service Worker] Clearing caches per client request'); - caches.delete(CACHE_NAME).catch(console.warn); - caches.delete(API_CACHE_NAME).catch(console.warn); - break; - case 'CACHE_PDF': - console.log('[Service Worker] Caching PDF from client data:', event.data.url); - caches.open(OFFLINE_PDF_CACHE) - .then(cache => cache.put(event.data.url, new Response(event.data.data))) - .catch(console.error); - break; - default: - console.log('[Service Worker] Received unknown message:', event.data.type); - } -}); -async function manageCacheSize(cacheName, maxSize = 50) { - try { - const cache = await caches.open(cacheName); - const requests = await cache.keys(); - if (requests.length > maxSize) { - const excessCount = requests.length - maxSize; - const excessRequests = requests.slice(0, excessCount); - await Promise.all(excessRequests.map(request => cache.delete(request))); - console.log(`[Service Worker] Trimmed ${excessCount} entries from ${cacheName}`); - } - } - catch (error) { - console.error(`[Service Worker] Failed to manage cache size for ${cacheName}:`, error); - } -} \ No newline at end of file From f65810afbd7a8ba890be9f4a9faf4305dd6ba2d1 Mon Sep 17 00:00:00 2001 From: jd-code76 Date: Sat, 1 Nov 2025 21:29:12 -0400 Subject: [PATCH 3/4] Add files via upload Initial release (actually) for GitHub Pages --- index.html | 1 + main.js | 610 ++++++++++++++++++++++++++++++++++++++++++ modules/api.js | 115 ++++++++ modules/navigation.js | 252 +++++++++++++++++ modules/passage.js | 274 +++++++++++++++++++ modules/pdf.js | 372 ++++++++++++++++++++++++++ modules/settings.js | 274 +++++++++++++++++++ modules/state.js | 399 +++++++++++++++++++++++++++ modules/strongs.js | 254 ++++++++++++++++++ modules/ui.js | 407 ++++++++++++++++++++++++++++ styles.css | 363 +++++++++++++++++++++++++ sw.js | 249 +++++++++++++++++ 12 files changed, 3570 insertions(+) create mode 100644 index.html create mode 100644 main.js create mode 100644 modules/api.js create mode 100644 modules/navigation.js create mode 100644 modules/passage.js create mode 100644 modules/pdf.js create mode 100644 modules/settings.js create mode 100644 modules/state.js create mode 100644 modules/strongs.js create mode 100644 modules/ui.js create mode 100644 styles.css create mode 100644 sw.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..f2aa5e1 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ + Provinent Scripture Study

Welcome to Provinent Scripture Study!

A comprehensive Bible study companion with daily passages, notes, and powerful study tools.

Daily Passages

Get a new scripture passage every day with sequential or specific book reading plans.

Highlight & Annotate

Right‑click verses to highlight in colors. Take notes with full markdown support.

Original Languages

Click any verse to access Strong's Concordance, Greek/Hebrew interlinear, and more.

Reference Bible

Open a side‑by‑side reference panel to compare translations while you study.

Export & Import

Save your highlights and notes. Import/export your data anytime.

Offline Mode

Upload a PDF Bible to read and take notes while offline.

Optional: Upload a PDF to View Offline

You can upload a free Berean Standard Bible PDF. This is entirely optional – you can skip this and use the online version.

2. Upload the downloaded PDF here:

Click to select your downloaded PDF

Or drag and drop here

Reference Bible

Page of 0 Zoom: 100%

Provinent Scripture Study

Click any verse for further analysis • Right‑click to highlight

Passage of the Day

Attribution

Scripture quotations are provided by API from bible.helloao.org, without which this app would not have been possible. And a thank you to Berean Bible for their excellent translation work. All copyrights reserved by their respective owners. Currently selected translation: Berean Standard Bible®

Study Notes

\ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..5d6f768 --- /dev/null +++ b/main.js @@ -0,0 +1,610 @@ +import { + initBookChapterControls, + nextPassage, + prevPassage, + randomPassage +} from './modules/navigation.js' +import { loadPassage, setupFootnoteHandlers } from './modules/passage.js' +import { + clearSearch, + currentSearch, + handlePDFUpload, + navigateToSearchResult, + renderPage, + savePDFToIndexedDB, + searchPDF, + setupPDFCleanup, + updateCustomPdfInfo, + updatePDFZoom +} from './modules/pdf.js' +import { + clearCache, + closeSettings, + deleteAllData, + exportData, + importData, + openSettings, + restartReadingPlan, + resumeReadingPlan, + saveSettings +} from './modules/settings.js' +import { + updateBibleGatewayVersion, + loadFromCookies, + loadFromStorage, + saveToCookies, + saveToStorage, + state +} from './modules/state.js' +import { closeStrongsPopup } from './modules/strongs.js' +import { + exportNotes, + initResizeHandles, + insertMarkdown, + makeToggleSticky, + restoreBookChapterUI, + restorePanelStates, + restoreSidebarState, + switchNotesView, + toggleNotes, + togglePanelCollapse, + toggleReferencePanel, + toggleSection, + updateMarkdownPreview, + updateReferencePanel +} from './modules/ui.js' +if (typeof pdfjsLib !== 'undefined') { + pdfjsLib.GlobalWorkerOptions.workerSrc = + 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; +} +if (typeof marked !== 'undefined') { + marked.setOptions({ + breaks: true, + gfm: true + }); +} +function updateDateTime() { + const now = new Date(); + document.getElementById('currentDate').textContent = + now.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); +} +class AppError extends Error { + constructor(message, type, originalError) { + super(message); + this.name = 'AppError'; + this.type = type; + this.originalError = originalError; + } +} +export function handleError(error, context) { + console.error(`Error in ${context}:`, error); + const userMessage = error instanceof AppError + ? error.message + : 'An unexpected error occurred'; + showError(userMessage); + if (window.errorTracker) { + window.errorTracker.log(error, context); + } +} +function setupEventListeners() { + document.getElementById('getStartedBtn') + .addEventListener('click', completeWelcome); + document.getElementById('welcomePdfUploadArea') + .addEventListener('click', () => { + document.getElementById('welcomePdfUpload').click(); + }); + document.getElementById('welcomePdfUpload') + .addEventListener('change', handleWelcomePDFUpload); + document.querySelector('.theme-toggle') + .addEventListener('click', toggleTheme); + document.getElementById('openSettingsBtn') + .addEventListener('click', openSettings); + document.getElementById('exportDataBtn') + .addEventListener('click', exportData); + document.getElementById('importDataBtn') + .addEventListener('click', () => { + document.getElementById('importFile').click(); + }); + document.getElementById('importFile') + .addEventListener('change', importData); + document.querySelector('.toggle-notes') + .addEventListener('click', toggleNotes); + document.getElementById('prevPassageBtn') + .addEventListener('click', prevPassage); + document.getElementById('nextPassageBtn') + .addEventListener('click', nextPassage); + document.getElementById('resumeReadingPlanBtn') + .addEventListener('click', () => { + if (confirm('Return to the daily reading plan where you left off?')) { + resumeReadingPlan(); + } + }); + document.getElementById('randomPassageBtn') + .addEventListener('click', randomPassage); + document.getElementById('referencePanelToggle') + .addEventListener('click', toggleReferencePanel); + document.querySelectorAll('.sidebar-section-header') + .forEach(h => h.addEventListener('click', () => { + const sec = h.dataset.section; + toggleSection(sec); + })); + document.getElementById('referenceTranslation').addEventListener('change', function() { + const tempTranslation = this.value; + const oldTranslation = state.settings.referenceVersion; + state.settings.referenceVersion = tempTranslation; + updateBibleGatewayVersion(); + state.settings.referenceVersion = oldTranslation; + }); + document.addEventListener('DOMContentLoaded', makeToggleSticky); + document.querySelectorAll('.collapse-toggle') + .forEach(btn => btn.addEventListener('click', function () { + const panel = this.closest('[id]'); + if (panel) togglePanelCollapse(panel.id); + })); + document.getElementById('referenceSource') + .addEventListener('change', updateReferencePanel); + document.getElementById('referenceTranslation') + .addEventListener('change', updateReferencePanel); + document.querySelector('.reference-panel-close') + .addEventListener('click', toggleReferencePanel); + document.getElementById('prevPage').addEventListener('click', async () => { + if (!state.pdf.doc || state.pdf.currentPage <= 1) return; + try { + if (state.pdf.renderTask) { + await state.pdf.renderTask.cancel(); + state.pdf.renderTask = null; + } + state.pdf.currentPage--; + await renderPage(state.pdf.currentPage); + } catch (err) { + console.warn('Error navigating to previous page:', err); + await loadPDF(); + } + }); + document.getElementById('nextPage').addEventListener('click', async () => { + if (!state.pdf.doc || state.pdf.currentPage >= state.pdf.doc.numPages) return; + try { + if (state.pdf.renderTask) { + await state.pdf.renderTask.cancel(); + state.pdf.renderTask = null; + } + state.pdf.currentPage++; + await renderPage(state.pdf.currentPage); + } catch (err) { + console.warn('Error navigating to next page:', err); + await loadPDF(); + } + }); + document.getElementById('pageInput').addEventListener('change', async () => { + if (!state.pdf.doc) { + document.getElementById('pageInput').value = state.pdf.currentPage; + return; + } + const inp = document.getElementById('pageInput'); + let p = parseInt(inp.value, 10); + if (Number.isNaN(p)) { + inp.value = state.pdf.currentPage; + return; + } + p = Math.max(1, Math.min(p, state.pdf.doc.numPages)); + try { + state.pdf.currentPage = p; + await renderPage(p); + } catch (err) { + console.warn('Error navigating to page:', err); + inp.value = state.pdf.currentPage; + await loadPDF(); + } + }); + document.getElementById('zoomIn').addEventListener('click', () => { + if (!state.pdf.doc) return; + const newZoom = Math.min(state.pdf.zoomLevel + 0.25, 3.0); + updatePDFZoom(newZoom); + }); + document.getElementById('zoomOut').addEventListener('click', () => { + if (!state.pdf.doc) return; + const newZoom = Math.max(state.pdf.zoomLevel - 0.25, 0.5); + updatePDFZoom(newZoom); + }); + document.getElementById('pdfSearchBtn').addEventListener('click', searchPDF); + document.getElementById('pdfSearchInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') searchPDF(); + }); + document.getElementById('clearSearchBtn').addEventListener('click', clearSearch); + const nextSearchBtn = document.getElementById('nextSearchResult'); + const prevSearchBtn = document.getElementById('prevSearchResult'); + if (nextSearchBtn) { + nextSearchBtn.addEventListener('click', () => { + navigateToSearchResult(currentSearch.currentResult + 1); + }); + } + if (prevSearchBtn) { + prevSearchBtn.addEventListener('click', () => { + navigateToSearchResult(currentSearch.currentResult - 1); + }); + } + document.getElementById('notesInput') + .addEventListener('input', e => { + state.notes = e.target.value; + saveToStorage(); + if (state.settings.notesView === 'markdown') { + updateMarkdownPreview(); + } + }); + document.getElementById('textViewBtn') + .addEventListener('click', () => switchNotesView('text')); + document.getElementById('markdownViewBtn') + .addEventListener('click', () => switchNotesView('markdown')); + document.querySelectorAll('.markdown-btn') + .forEach(btn => btn.addEventListener('click', () => { + const fmt = btn.dataset.format; + insertMarkdown(fmt); + })); + document.querySelectorAll('.notes-controls button') + .forEach(btn => btn.addEventListener('click', () => { + const fmt = btn.dataset.format; + exportNotes(fmt); + })); + document.querySelectorAll('.color-option') + .forEach(opt => opt.addEventListener('click', () => { + const col = opt.dataset.color; + applyHighlight(col); + })); + document.getElementById('removeHighlight') + .addEventListener('click', () => applyHighlight('none')); + document.addEventListener('contextmenu', e => { + const verse = e.target.closest('.verse'); + if (verse) { + e.preventDefault(); + showColorPicker(e, verse); + } + }); + document.addEventListener('click', e => { + const picker = document.getElementById('colorPicker'); + if (!picker.contains(e.target) && !e.target.closest('.verse')) { + picker.classList.remove('active'); + } + }); + document.getElementById('popupOverlay') + .addEventListener('click', closeStrongsPopup); + document.querySelector('#strongsPopup .popup-close') + .addEventListener('click', closeStrongsPopup); + document.getElementById('settingsOverlay') + .addEventListener('click', closeSettings); + document.getElementById('closeSettingsBtn') + .addEventListener('click', closeSettings); + document.getElementById('cancelSettingsBtn') + .addEventListener('click', closeSettings); + document.getElementById('saveSettingsBtn') + .addEventListener('click', saveSettings); + document.getElementById('clearHighlightsBtn') + .addEventListener('click', clearHighlights); + document.getElementById('restartReadingPlanBtn') + .addEventListener('click', () => { + restartReadingPlan(); + }); + document.addEventListener('contentLoaded', () => { + setTimeout(setupFootnoteHandlers, 50); + }); + document.getElementById('clearCacheBtn') + .addEventListener('click', clearCache); + document.getElementById('deleteAllDataBtn') + .addEventListener('click', deleteAllData); + document.getElementById('settingsPdfUploadBtn') + .addEventListener('click', () => { + document.getElementById('settingsPdfUpload').click(); + }); + document.getElementById('settingsPdfUpload') + .addEventListener('change', handlePDFUpload); + document.querySelectorAll('.color-theme-option') + .forEach(opt => opt.addEventListener('click', () => { + const theme = opt.dataset.theme; + selectColorTheme(theme); + })); + document.addEventListener('keydown', e => { + const ta = document.getElementById('notesInput'); + if (document.activeElement !== ta) return; + if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { + switch (e.key.toLowerCase()) { + case 'b': + e.preventDefault(); + insertMarkdown('bold'); + break; + case 'i': + e.preventDefault(); + insertMarkdown('italic'); + break; + } + } + }); +} +export function arrayBufferToBase64(buf) { + let binary = ''; + const bytes = new Uint8Array(buf); + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode.apply( + null, + Array.from(bytes.subarray(i, i + chunk)) + ); + } + return btoa(binary); +} +export function base64ToArrayBuffer(b64) { + const bin = atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) { + arr[i] = bin.charCodeAt(i); + } + return arr.buffer; +} +export function readFileAsArrayBuffer(file) { + return new Promise((resolve, reject) => { + const r = new FileReader(); + r.onload = e => resolve(e.target.result); + r.onerror = () => reject(new Error('Failed to read file')); + r.readAsArrayBuffer(file); + }); +} +export function showLoading(flag) { + document.getElementById('loadingOverlay').classList.toggle('active', flag); +} +export function showError(msg) { + document.getElementById('errorContainer').innerHTML = + `
${msg}
`; +} +export function clearError() { + document.getElementById('errorContainer').innerHTML = ''; +} +async function registerServiceWorker() { + if ('serviceWorker' in navigator) { + try { + const response = await fetch('/sw.js'); + if (!response.ok) { + console.error('Service worker script not found or inaccessible'); + return null; + } + const registration = await navigator.serviceWorker.register('/sw.js', { + scope: '/' + }); + console.log('Service Worker registered successfully:', registration); + return registration; + } catch (err) { + console.error('Service Worker registration failed:', err); + if (err.message.includes('MIME')) { + console.error('MIME type issue - ensure server serves sw.js as application/javascript'); + } + return null; + } + } else { + console.log('Service workers are not supported'); + return null; + } +} +function updateOfflineStatus(isOffline) { + const indicator = document.getElementById('offlineIndicator'); + if (!indicator) { + const newIndicator = document.createElement('div'); + newIndicator.id = 'offlineIndicator'; + newIndicator.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + padding: 10px; + background: ${isOffline ? '#ff6b6b' : '#51cf66'}; + color: white; + border-radius: 5px; + z-index: 10000; + font-size: 14px; + transition: all 0.3s ease; + `; + newIndicator.textContent = isOffline ? 'Offline Mode' : 'Online'; + document.body.appendChild(newIndicator); + setTimeout(() => { + newIndicator.style.opacity = '0'; + setTimeout(() => newIndicator.remove(), 300); + }, 3000); + } else { + indicator.textContent = isOffline ? 'Offline Mode' : 'Online'; + indicator.style.background = isOffline ? '#ff6b6b' : '#51cf66'; + } +} +const offlineStyles = ` +#offlineIndicator { + position: fixed; + top: 10px; + right: 10px; + padding: 10px 15px; + background: #ff6b6b; + color: white; + border-radius: 5px; + z-index: 10000; + font-size: 14px; + font-weight: bold; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + transition: all 0.3s ease; +} +#offlineIndicator.online { + background: #51cf66; +} +`; +function showColorPicker(ev, verseEl) { + const picker = document.getElementById('colorPicker'); + state.currentVerse = verseEl; + picker.style.left = ev.pageX + 'px'; + picker.style.top = ev.pageY + 'px'; + picker.classList.add('active'); +} +function applyHighlight(col) { + if (!state.currentVerse) return; + const verseRef = state.currentVerse.dataset.verse; + state.currentVerse.classList.remove( + 'highlight-yellow', 'highlight-green', 'highlight-blue', + 'highlight-pink', 'highlight-orange', 'highlight-purple' + ); + if (col !== 'none') { + state.currentVerse.classList.add(`highlight-${col}`); + state.highlights[verseRef] = col; + } else { + delete state.highlights[verseRef]; + } + saveToStorage(); + document.getElementById('colorPicker').classList.remove('active'); +} +function clearHighlights() { + if (!confirm('Delete ALL highlights?')) return; + state.highlights = {}; + document.querySelectorAll('.verse') + .forEach(v => v.classList.remove( + 'highlight-yellow', 'highlight-green', 'highlight-blue', + 'highlight-pink', 'highlight-orange', 'highlight-purple' + )); + saveToStorage(); +} +function toggleTheme() { + state.settings.theme = state.settings.theme === 'light' ? 'dark' : 'light'; + applyTheme(); + saveToStorage(); + saveToCookies(); +} +export function applyTheme() { + document.documentElement.setAttribute('data-theme', state.settings.theme); + document.getElementById('themeIcon').textContent = + state.settings.theme === 'light' ? '🌙' : '☀️'; +} +export function selectColorTheme(t) { + state.settings.colorTheme = t; + applyColorTheme(); + document.querySelectorAll('.color-theme-option') + .forEach(o => o.classList.remove('selected')); + document.querySelector(`.color-theme-option[data-theme="${t}"]`) + .classList.add('selected'); +} +export function applyColorTheme() { + document.documentElement.setAttribute('data-color-theme', + state.settings.colorTheme); +} +async function handleWelcomePDFUpload(ev) { + try { + const file = ev.target.files[0]; + if (!file) return; + if (file.size > 50 * 1024 * 1024) { + alert('PDF file is too large (max 50 MiB).'); + ev.target.value = ''; + return; + } + state.welcomePdfFile = file; + document.getElementById('welcomePdfUploadArea').classList.add('has-file'); + document.getElementById('welcomeUploadText').innerHTML = ` + ${file.name}
+ Ready to use for offline mode`; + } catch (err) { + handleError(err, 'handleWelcomePDFUpload'); + } +} +async function completeWelcome() { + showLoading(true); + try { + if (state.welcomePdfFile) { + const reader = new FileReader(); + const arrayBuffer = await new Promise((resolve, reject) => { + reader.onload = (e) => resolve(e.target.result); + reader.onerror = (e) => reject(new Error('Failed to read file')); + reader.readAsArrayBuffer(state.welcomePdfFile); + }); + const bufferCopy = arrayBuffer.slice(0); + const loadingTask = pdfjsLib.getDocument({ data: bufferCopy }); + const pdf = await loadingTask.promise; + const base64 = arrayBufferToBase64(arrayBuffer); + const pdfData = { + name: state.welcomePdfFile.name, + data: base64, + uploadDate: new Date().toISOString(), + numPages: pdf.numPages + }; + await savePDFToIndexedDB(pdfData); + state.settings.customPdf = { + name: pdfData.name, + uploadDate: pdfData.uploadDate, + numPages: pdfData.numPages, + storedInDB: true + }; + } + state.settings.hasSeenWelcome = true; + saveToStorage(); + saveToCookies(); + document.getElementById('welcomeScreen').classList.add('hidden'); + await init(); + } catch (err) { + handleError(err, 'completeWelcome'); + alert('Error processing PDF: ' + err.message + + '. You can continue without offline mode.'); + state.settings.hasSeenWelcome = true; + saveToStorage(); + saveToCookies(); + document.getElementById('welcomeScreen').classList.add('hidden'); + await init(); + } finally { + showLoading(false); + } +} +function attachWelcomeListeners() { + document.getElementById('getStartedBtn') + .addEventListener('click', completeWelcome); + document.getElementById('welcomePdfUploadArea') + .addEventListener('click', () => { + document.getElementById('welcomePdfUpload').click(); + }); + document.getElementById('welcomePdfUpload') + .addEventListener('change', handleWelcomePDFUpload); +} +async function init() { + await loadFromStorage(); + loadFromCookies(); + setupPDFCleanup(); + const style = document.createElement('style'); + style.textContent = offlineStyles; + document.head.appendChild(style); + updateOfflineStatus(!navigator.onLine); + window.addEventListener('online', () => updateOfflineStatus(false)); + window.addEventListener('offline', () => updateOfflineStatus(true)); + if (!state.settings.readingMode) state.settings.readingMode = 'readingPlan'; + if (!state.settings.readingPlanId) state.settings.readingPlanId = 'default'; + initBookChapterControls(); + restoreBookChapterUI(); + if (!state.settings.hasSeenWelcome) { + attachWelcomeListeners(); + return; + } + document.getElementById('welcomeScreen').classList.add('hidden'); + applyTheme(); + applyColorTheme(); + restoreSidebarState(); + restorePanelStates(); + updateDateTime(); + initResizeHandles(); + updateCustomPdfInfo(); + switchNotesView(state.settings.notesView || 'text'); + updateBibleGatewayVersion(); + loadPassage(); + setupEventListeners(); + setInterval(updateDateTime, 1_000); + setTimeout(async () => { + try { + await registerServiceWorker(); + } catch (err) { + handleError(err, 'init'); + } + }, 1000); + console.log('App initialized successfully'); +} +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} \ No newline at end of file diff --git a/modules/api.js b/modules/api.js new file mode 100644 index 0000000..749cbfb --- /dev/null +++ b/modules/api.js @@ -0,0 +1,115 @@ +import { + clearError, + handleError, + showError, + showLoading +} from '../main.js' +import { + afterContentLoad, + displayPassage, + extractVerseText +} from './passage.js' +import { + bookNameMapping, + state +} from './state.js' +const API_BASE_URL = 'https://bible.helloao.org/api'; +const translationMap = { + BSB: 'BSB', + KJV: 'eng_kjv', + NET: 'eng_net', + ASV: 'eng_asv', + GNV: 'eng_gnv' +}; +export function apiTranslationCode(uiCode) { + return translationMap[uiCode] ?? uiCode; +} +export function getApiBookCode(displayName) { + const code = bookNameMapping[displayName]; + if (code) return code; + console.warn('Missing book‑code mapping for:', displayName); + showError(`Cannot load “${displayName}” – unknown book code.`); + throw new Error('Unknown book code'); +} +export async function fetchChapter(translation, book, chapter) { + if (!navigator.onLine) { + throw new Error('Offline mode: Cannot fetch new chapters. Using cached data if available.'); + } + const trans = translation.trim(); + const bk = book.replace(/\s+/g, '').toUpperCase(); + const ch = Number(chapter); + if (!trans || !bk || Number.isNaN(ch) || ch < 1) { + throw new Error('Invalid parameters for Bible API request'); + } + const url = `${API_BASE_URL}/${trans}/${bk}/${ch}.json`; + try { + const resp = await fetch(url, { + method: 'GET', + headers: { Accept: 'application/json' }, + cache: 'no-store' + }); + if (!resp.ok) { + const txt = await resp.text(); + throw new Error(`API error ${resp.status}: ${txt}`); + } + const ct = resp.headers.get('content-type') || ''; + if (!ct.includes('application/json')) { + if (ct.startsWith('<')) { + throw new Error('API returned HTML instead of JSON'); + } + try { + return JSON.parse(await resp.text()); + } catch (_) { + throw new Error('Unable to parse API response as JSON'); + } + } + return resp.json(); + } catch (err) { + handleError(err, 'fetchChapter'); + } +} +export async function loadPassageFromAPI(passageInfo) { + try { + showLoading(true); + const { book, chapter, startVerse, endVerse, displayRef } = passageInfo; + state.currentPassageReference = displayRef; + const apiMap = apiTranslationCode(state.settings.bibleTranslation); + const apiBook = getApiBookCode(book); + const chapterData = await fetchChapter(apiMap, apiBook, chapter); + if (!chapterData || !chapterData.chapter || + !Array.isArray(chapterData.chapter.content)) { + throw new Error('Malformed API response – missing chapter.content'); + } + const chapterFootnotes = chapterData.chapter.footnotes || []; + const footnoteCounter = { value: 1 }; + const verses = chapterData.chapter.content + .filter(v => + v.type === 'verse' && + v.number >= startVerse && + v.number <= endVerse + ) + .map(v => { + const verseData = extractVerseText(v.content, chapterFootnotes, footnoteCounter); + return { + number: v.number, + text: verseData, + reference: `${book} ${chapter}:${v.number}`, + rawContent: v.content + }; + }); + if (verses.length === 0) { + throw new Error('No verses found in the requested range'); + } + displayPassage(verses); + afterContentLoad(); + clearError(); + if (chapterData.translation && chapterData.translation.name) { + document.getElementById('bibleName').textContent = + chapterData.translation.name; + } + } catch (err) { + handleError(err, 'loadPassageFromAPI'); + } finally { + showLoading(false); + } +} \ No newline at end of file diff --git a/modules/navigation.js b/modules/navigation.js new file mode 100644 index 0000000..9199eed --- /dev/null +++ b/modules/navigation.js @@ -0,0 +1,252 @@ +import { + apiTranslationCode, + fetchChapter, + getApiBookCode, + loadPassageFromAPI +} from './api.js' +import { + clearError, + handleError, + showError, + showLoading +} from '../main.js' +import { + displayPassage, + extractVerseText, + loadPassage +} from './passage.js' +import { + BOOK_ORDER, + CHAPTER_COUNTS, + bookNameMapping, + getActivePlan, + saveToStorage, + state +} from './state.js' +import { updateReferencePanel } from './ui.js' +export function populateBookDropdown() { + const bookSel = document.getElementById('bookSelect'); + bookSel.innerHTML = ''; + BOOK_ORDER.forEach(book => { + const opt = document.createElement('option'); + opt.value = book; + opt.textContent = book; + bookSel.appendChild(opt); + }); +} +export function populateChapterDropdown(selectedBook) { + const chapSel = document.getElementById('chapterSelect'); + chapSel.innerHTML = ''; + const max = CHAPTER_COUNTS[selectedBook]; + for (let i = 1; i <= max; i++) { + const opt = document.createElement('option'); + opt.value = i; + opt.textContent = i; + chapSel.appendChild(opt); + } +} +export async function loadSelectedChapter(book = null, chapter = null) { + const selBook = book || document.getElementById('bookSelect').value; + const selChapter = chapter || document.getElementById('chapterSelect').value; + const apiBook = getApiBookCode(selBook); + try { + showLoading(true); + const apiTranslation = apiTranslationCode(state.settings.bibleTranslation); + const chapterData = await fetchChapter( + apiTranslation, + apiBook, + selChapter + ); + const chapterFootnotes = chapterData.chapter.footnotes || []; + const footnoteCounter = { value: 1 }; + const verses = chapterData.chapter.content + .filter(v => v.type === 'verse') + .map(v => ({ + number: v.number, + text: extractVerseText(v.content, chapterFootnotes, footnoteCounter), + reference: `${selBook} ${selChapter}:${v.number}` + })); + document.getElementById('passageReference').textContent = + `${selBook} ${selChapter}`; + state.footnotes = {}; + displayPassage(verses, `${selBook} ${selChapter}`); + clearError(); + document.getElementById('scriptureSection').scrollTop = 0; + if (state.settings.readingMode === 'manual') { + state.settings.manualBook = selBook; + state.settings.manualChapter = Number(selChapter); + saveToStorage(); + } + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + } catch (err) { + handleError(err, 'loadSelectedChapter'); + showError(`Could not load ${selBook} ${selChapter}: ${err.message}`); + } finally { + showLoading(false); + } +} +export function initBookChapterControls() { + populateBookDropdown(); + document.getElementById('bookSelect').addEventListener('change', e => { + const book = e.target.value; + state.settings.readingMode = 'manual'; + populateChapterDropdown(book); + state.settings.manualBook = book; + state.settings.manualChapter = 1; + const chapterSel = document.getElementById('chapterSelect'); + chapterSel.value = '1'; + loadSelectedChapter(book, 1); + saveToStorage(); + }); + document.getElementById('chapterSelect').addEventListener('change', () => { + const book = document.getElementById('bookSelect').value; + const chap = Number(document.getElementById('chapterSelect').value); + state.settings.readingMode = 'manual'; + state.settings.manualBook = book; + state.settings.manualChapter = chap; + loadSelectedChapter(book, chap); + saveToStorage(); + }); + populateChapterDropdown(BOOK_ORDER[0]); +} +export function manualPrevChapter() { + let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook); + let chap = state.settings.manualChapter; + if (chap > 1) { + state.settings.manualChapter = chap - 1; + } else { + if (bookIdx > 0) { + const prevBook = BOOK_ORDER[bookIdx - 1]; + const maxCh = CHAPTER_COUNTS[prevBook]; + state.settings.manualBook = prevBook; + state.settings.manualChapter = maxCh; + } else { + return; + } + } + state.settings.readingMode = 'manual'; + loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter); + syncBookChapterSelectors(); + saveToStorage(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } +} +export function manualNextChapter() { + let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook); + let chap = state.settings.manualChapter; + const maxCh = CHAPTER_COUNTS[state.settings.manualBook]; + if (chap < maxCh) { + state.settings.manualChapter = chap + 1; + } else { + if (bookIdx < BOOK_ORDER.length - 1) { + const nextBook = BOOK_ORDER[bookIdx + 1]; + state.settings.manualBook = nextBook; + state.settings.manualChapter = 1; + } else { + return; + } + } + state.settings.readingMode = 'manual'; + loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter); + syncBookChapterSelectors(); + saveToStorage(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } +} +export function prevPassage() { + if (state.settings.readingMode === 'readingPlan') { + const len = getActivePlan().length; + let newIndex = (state.settings.currentPassageIndex - 1 + len) % len; + state.settings.currentPassageIndex = newIndex; + loadPassage(); + } else { + manualPrevChapter(); + } + document.getElementById('scriptureSection').scrollTop = 0; +} +export function nextPassage() { + if (state.settings.readingMode === 'readingPlan') { + const len = getActivePlan().length; + let newIndex = (state.settings.currentPassageIndex + 1) % len; + if (newIndex < 0) newIndex = len - 1; + state.settings.currentPassageIndex = newIndex; + loadPassage(); + } else { + manualNextChapter(); + } + document.getElementById('scriptureSection').scrollTop = 0; +} +export async function randomPassage() { + try { + state.settings.readingMode = 'manual'; + const randomLoc = await getRandomBibleLocation(); + state.settings.manualBook = randomLoc.book; + state.settings.manualChapter = randomLoc.chapter; + saveToStorage(); + await loadPassageFromAPI(randomLoc); + document.getElementById('passageReference').textContent = randomLoc.displayRef; + state.currentPassageReference = randomLoc.displayRef; + syncBookChapterSelectors(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + } catch (err) { + handleError(err, 'randomPassage'); + showError('Could not load a random passage – see console for details.'); + } +} +export function syncBookChapterSelectors() { + const bookSel = document.getElementById('bookSelect'); + const chapterSel = document.getElementById('chapterSelect'); + if (bookSel.value !== state.settings.manualBook) { + bookSel.value = state.settings.manualBook; + populateChapterDropdown(state.settings.manualBook); + } + const curMax = CHAPTER_COUNTS[state.settings.manualBook]; + const curChap = state.settings.manualChapter; + populateChapterDropdown(state.settings.manualBook); + chapterSel.value = (curChap <= curMax) ? curChap : curMax; +} +export function syncSelectorsToReadingPlan() { + if (state.settings.readingMode !== 'readingPlan') return; + const plan = getActivePlan(); + const passage = plan[state.settings.currentPassageIndex]; + if (!passage || !passage.book) { + console.error('Invalid passage object:', passage); + return; + } + const bookSel = document.getElementById('bookSelect'); + const chapterSel = document.getElementById('chapterSelect'); + state.settings.manualBook = passage.book; + state.settings.manualChapter = passage.chapter; + if (bookSel) bookSel.value = passage.book; + populateChapterDropdown(passage.book); + if (chapterSel) chapterSel.value = passage.chapter; + saveToStorage(); +} +async function getRandomBibleLocation() { + try { + const randomBook = BOOK_ORDER[Math.floor(Math.random() * BOOK_ORDER.length)]; + const maxCh = CHAPTER_COUNTS[randomBook]; + const randomChapter = Math.floor(Math.random() * maxCh) + 1; + const apiMap = apiTranslationCode(state.settings.bibleTranslation); + const apiBook = bookNameMapping[randomBook] || + randomBook.replace(/\s+/g, '').toUpperCase(); + const chapterData = await fetchChapter(apiMap, apiBook, randomChapter); + const verses = chapterData.chapter.content.filter(v => v.type === 'verse'); + const verseCount = verses.length || 1; + return { + book: randomBook, + chapter: randomChapter, + startVerse: 1, + endVerse: verseCount, + displayRef: `${randomBook} ${randomChapter}` + }; + } catch (err) { + handleError(err, 'getRandomBibleLocation'); + } +} \ No newline at end of file diff --git a/modules/passage.js b/modules/passage.js new file mode 100644 index 0000000..0d35df6 --- /dev/null +++ b/modules/passage.js @@ -0,0 +1,274 @@ +import { loadPassageFromAPI } from './api.js' +import { handleError } from '../main.js' +import { syncSelectorsToReadingPlan } from './navigation.js' +import { + getActivePlan, + getCurrentPlanLabel, + getTranslationShorthand, + saveToStorage, + state +} from './state.js' +import { showStrongsReference } from './strongs.js' +import { updateReferencePanel } from './ui.js' +export function displayPassage(verses) { + const container = document.getElementById('scriptureContent'); + const fragment = document.createDocumentFragment(); + state.footnotes = {}; + const allFootnotes = []; + verses.forEach(v => { + const verseDiv = document.createElement('div'); + verseDiv.className = 'verse'; + verseDiv.dataset.verse = v.reference; + verseDiv.dataset.verseNumber = v.number; + let plainText = v.text.text; + plainText = plainText.replace(/<[^>]*>/g, ''); + plainText = plainText.replace(/\s+/g, ' ').trim(); + verseDiv.dataset.verseText = plainText; + const key = v.reference; + if (state.highlights[key]) { + verseDiv.classList.add(`highlight-${state.highlights[key]}`); + } + const numSpan = document.createElement('span'); + numSpan.className = 'verse-number'; + numSpan.textContent = v.number; + const txtSpan = document.createElement('span'); + txtSpan.className = 'verse-text'; + txtSpan.innerHTML = v.text.text; + if (v.text.footnotes && v.text.footnotes.length > 0) { + state.footnotes[v.reference] = v.text.footnotes; + allFootnotes.push(...v.text.footnotes); + } + verseDiv.appendChild(numSpan); + verseDiv.appendChild(txtSpan); + fragment.appendChild(verseDiv); + }); + container.innerHTML = ''; + container.appendChild(fragment); + if (allFootnotes.length > 0) { + const footnotesContainer = document.createElement('div'); + footnotesContainer.className = 'footnotes-container'; + const separator = document.createElement('hr'); + separator.className = 'footnotes-separator'; + const heading = document.createElement('h4'); + heading.className = 'footnotes-heading'; + heading.textContent = 'Footnotes'; + const footnotesFragment = document.createDocumentFragment(); + allFootnotes.sort((a, b) => a.number - b.number).forEach(fn => { + const footnoteElement = document.createElement('div'); + footnoteElement.className = 'footnote'; + footnoteElement.innerHTML = ` + ${fn.number} + ${fn.content} + `; + footnoteElement.dataset.footnoteId = fn.index; + footnoteElement.dataset.footnoteNumber = fn.number; + footnotesFragment.appendChild(footnoteElement); + }); + footnotesContainer.appendChild(footnotesFragment); + container.appendChild(separator); + container.appendChild(heading); + container.appendChild(footnotesContainer); + } + container.addEventListener('click', (e) => { + const verse = e.target.closest('.verse'); + if (verse && !e.target.closest('.footnote-ref')) { + showStrongsReference(verse); + } + }, { once: false }); + setTimeout(() => { + setupFootnoteHandlers(); + }, 100); +} +export function setupFootnoteHandlers() { + const scriptureContent = document.getElementById('scriptureContent'); + if (scriptureContent._footnoteHandler) { + scriptureContent.removeEventListener('click', scriptureContent._footnoteHandler); + } + const footnoteHandler = (e) => { + const footnoteRef = e.target.closest('[class*="footnote-ref"]'); + const footnoteElement = e.target.closest('.footnote'); + if (footnoteRef) { + e.preventDefault(); + e.stopPropagation(); + const footnoteId = (footnoteRef.dataset.footnoteId || '').trim(); + const footnoteNumber = (footnoteRef.dataset.footnoteNumber || '').trim(); + let targetFootnote = null; + if (footnoteId) { + targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-id="${footnoteId}"]`); + } + if (!targetFootnote && footnoteNumber) { + targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-number="${footnoteNumber}"]`); + } + if (!targetFootnote && footnoteId) { + const allFootnotes = scriptureContent.querySelectorAll('.footnote'); + for (const fn of allFootnotes) { + const fnId = (fn.dataset.footnoteId || '').trim(); + const fnNum = (fn.dataset.footnoteNumber || '').trim(); + if (fnId === footnoteId) { + targetFootnote = fn; + break; + } + } + } + if (targetFootnote) { + targetFootnote.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + targetFootnote.style.backgroundColor = 'var(--verse-hover)'; + setTimeout(() => { + targetFootnote.style.backgroundColor = ''; + }, 1000); + } else { + console.log('No target footnote found for ref:', footnoteId, footnoteNumber); + } + } + if (footnoteElement) { + e.preventDefault(); + e.stopPropagation(); + const footnoteId = (footnoteElement.dataset.footnoteId || '').trim(); + const footnoteNumber = (footnoteElement.dataset.footnoteNumber || '').trim(); + let targetRef = null; + const selectors = [ + `[class*="footnote-ref"][data-footnote-id="${footnoteId}"]`, + `[class*="footnote-ref"][data-footnote-number="${footnoteNumber}"]`, + `[class*="footnote-ref"][data-footnote-id="${footnoteNumber}"]`, + `[class*="footnote-ref"][data-footnote-number="${footnoteId}"]` + ]; + for (const selector of selectors) { + targetRef = scriptureContent.querySelector(selector); + if (targetRef) break; + } + if (!targetRef) { + const allRefs = scriptureContent.querySelectorAll('[class*="footnote-ref"]'); + for (const ref of allRefs) { + const refId = (ref.dataset.footnoteId || '').trim(); + const refNum = (ref.dataset.footnoteNumber || '').trim(); + if (refId === footnoteId || refNum === footnoteNumber || + refId === footnoteNumber || refNum === footnoteId) { + targetRef = ref; + break; + } + } + } + if (targetRef) { + targetRef.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }); + targetRef.style.backgroundColor = 'var(--verse-hover)'; + setTimeout(() => { + targetRef.style.backgroundColor = ''; + }, 1000); + } else { + console.log('No target ref found for footnote:', footnoteId, footnoteNumber); + console.log('Available refs:', scriptureContent.querySelectorAll('[class*="footnote-ref"]')); + } + } + }; + scriptureContent._footnoteHandler = footnoteHandler; + scriptureContent.addEventListener('click', footnoteHandler); +} +export function extractVerseText(content, chapterFootnotes = [], footnoteCounter) { + let txt = ''; + let footnotes = []; + for (const item of content) { + if (typeof item === 'string') { + txt += ensureProperSpacing(item); + } else if (item.text) { + txt += ensureProperSpacing(item.text); + } else if (item.heading) { + txt += ' ' + ensureProperSpacing(item.heading) + ' '; + } else if (item.noteId !== undefined) { + const footnote = chapterFootnotes.find(fn => fn.noteId === item.noteId); + if (footnote) { + const footnoteRef = `${footnoteCounter.value}`; + txt += footnoteRef; + footnotes.push({ + index: footnote.noteId, + number: footnoteCounter.value, + caller: footnote.caller, + content: footnote.text, + reference: footnote.reference + }); + footnoteCounter.value++; + } + } else if (item.type === 'verse') { + txt += ' '; + } else if (item.type === 'chapter') { + txt += ' '; + } else { + if (item.content && Array.isArray(item.content)) { + const nestedResult = extractVerseText(item.content, chapterFootnotes, footnoteCounter); + txt += nestedResult.text; + footnotes.push(...nestedResult.footnotes); + } + } + } + txt = txt.replace(/\s+/g, ' ').trim(); + txt = txt.replace(/\s+([.,;:!?])/g, '$1'); + txt = txt.replace(/([.,;:!?])(?=\w)/g, '$1 '); + txt = txt.replace(/\s*"\s*/g, '" '); + txt = txt.replace(/\s*'\s*/g, "' "); + return { + text: txt, + footnotes: footnotes + }; +} +function ensureProperSpacing(text) { + if (!text) return ''; + let cleanedText = text + .replace(/\s+/g, ' ') + .trim(); + cleanedText = cleanedText + .replace(/([^'"\s])\s+([.,;:!?])/g, '$1$2') + .replace(/([.,;:!?])(?=[A-Za-z])/g, '$1 ') + .replace(/\s*"\s*/g, (match) => { + if (match === '"' || match === ' "') return '"'; + return '" '; + }) + .replace(/\s*'\s*/g, (match) => { + if (match === "'" || match === " '") return "'"; + return "' "; + }); + cleanedText = cleanedText + .replace(/\s+/g, ' ') + .replace(/([.,;:!?]) (["'])/g, '$1$2') + .replace(/(["']) ([.,;:!?])/g, '$1$2') + .trim(); + return cleanedText; +} +export async function loadPassage() { + try { + const plan = getActivePlan(); + if (state.settings.currentPassageIndex < 0 || state.settings.currentPassageIndex >= plan.length) { + state.settings.currentPassageIndex = 0; + } + const passage = plan[state.settings.currentPassageIndex]; + const headerTitleEl = document.getElementById('passageHeaderTitle'); + if (headerTitleEl) { + const transShorthand = getTranslationShorthand(); + headerTitleEl.textContent = `Holy Bible: ${transShorthand}`; + } + const planLabelEl = document.getElementById('planLabel'); + if (planLabelEl) { + planLabelEl.textContent = `Reading plan: ${getCurrentPlanLabel()}`; + } + document.getElementById('passageReference').textContent = passage.displayRef; + state.currentPassageReference = passage.displayRef; + await loadPassageFromAPI(passage); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + saveToStorage(); + if (state.settings.readingMode === 'readingPlan') { + syncSelectorsToReadingPlan(); + } + } catch (err) { + handleError(err, 'loadPassage'); + } +} +export function afterContentLoad() { + const event = new CustomEvent('contentLoaded'); + document.dispatchEvent(event); +} \ No newline at end of file diff --git a/modules/pdf.js b/modules/pdf.js new file mode 100644 index 0000000..96e586d --- /dev/null +++ b/modules/pdf.js @@ -0,0 +1,372 @@ +import { + arrayBufferToBase64, + base64ToArrayBuffer, + handleError, + readFileAsArrayBuffer, + showLoading +} from '../main.js' +import { + saveToStorage, + state +} from './state.js' +import { updateReferencePanel } from './ui.js' +const DB_NAME = 'BibleStudyDB'; +const DB_VERSION = 1; +export const STORE_NAME = 'pdfStore'; +export let currentSearch = { + query: '', + results: [], + currentResult: -1, + highlights: [] +}; +export async function openDB() { + try { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onerror = () => reject(req.error); + req.onsuccess = () => resolve(req.result); + req.onupgradeneeded = ev => { + const db = ev.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + }); + } catch (err) { + handleError(err, 'openDB'); + } +} +export async function savePDFToIndexedDB(pdfData) { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction([STORE_NAME], 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.put(pdfData, 'customPdf'); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch (err) { + handleError(err, 'savePDFToIndexedDB'); + } +} +export async function loadPDFFromIndexedDB() { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction([STORE_NAME], 'readonly'); + const store = tx.objectStore(STORE_NAME); + const req = store.get('customPdf'); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + } catch (err) { + handleError(err, 'loadPDFFromIndexedDB'); + } +} +export async function deletePDFFromIndexedDB() { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction([STORE_NAME], 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.delete('customPdf'); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } catch (err) { + handleError(err, 'deletePDFFromIndexedDB'); + } +} +export async function handlePDFUpload(ev) { + const file = ev.target.files[0]; + if (!file) return; + if (file.size > 50 * 1024 * 1024) { + alert('PDF file is too large (max 50 MiB).'); + ev.target.value = ''; + return; + } + try { + showLoading(true); + const buf = await readFileAsArrayBuffer(file); + const bufferCopy = buf.slice(0); + const pdf = await pdfjsLib.getDocument({ data: bufferCopy }).promise; + const storageBuffer = buf.slice(0); + const b64 = arrayBufferToBase64(storageBuffer); + const pdfData = { + name: file.name, + data: b64, + uploadDate: new Date().toISOString(), + numPages: pdf.numPages + }; + await savePDFToIndexedDB(pdfData); + state.settings.customPdf = { + name: pdfData.name, + uploadDate: pdfData.uploadDate, + numPages: pdfData.numPages, + storedInDB: true + }; + saveToStorage(); + updateCustomPdfInfo(); + alert('PDF uploaded successfully! You can now use it in the Reference Panel.'); + } catch (e) { + handleError(err, 'handlePDFUpload'); + alert('Error uploading PDF: ' + e.message); + } finally { + showLoading(false); + ev.target.value = ''; + } +} +export function updateCustomPdfInfo() { + const container = document.getElementById('customPdfInfo'); + if (state.settings.customPdf) { + const date = new Date(state.settings.customPdf.uploadDate) + .toLocaleDateString(); + container.innerHTML = ` +
+
+ ${state.settings.customPdf.name} + +
+ Uploaded: ${date} • ${state.settings.customPdf.numPages} pages +
+ `; + document.getElementById('removePdfBtn') + .addEventListener('click', removeCustomPdf); + } else { + container.innerHTML = ` + + No custom PDF uploaded + `; + document.getElementById('pageInput').value = 1; + document.getElementById('pageCount').textContent = '?'; + const zoomDisplay = document.getElementById('zoomLevel'); + if (zoomDisplay) { + zoomDisplay.textContent = '100%'; + } + } +} +async function removeCustomPdf() { + try { + if (!confirm('Delete the uploaded PDF? This cannot be undone.')) return; + await deletePDFFromIndexedDB(); + state.settings.customPdf = null; + state.settings.referenceSource = 'biblegateway'; + saveToStorage(); + updateCustomPdfInfo(); + if (document.getElementById('referenceSource').value === 'pdf') { + document.getElementById('referenceSource').value = 'biblegateway'; + updateReferencePanel(); + } + } catch (err) { + handleError(err, 'removeCustomPdf'); + } +} +export async function loadPDF() { + if (!state.settings.customPdf) { + alert('No custom PDF uploaded. Please upload one first.'); + return; + } + try { + showLoading(true); + if (state.pdf.doc) { + state.pdf.doc.destroy().catch(() => {}); + state.pdf.doc = null; + } + state.pdf.renderTask = null; + const pdfData = await loadPDFFromIndexedDB(); + if (!pdfData) throw new Error('PDF not found in DB'); + const buf = base64ToArrayBuffer(pdfData.data); + const loadingTask = pdfjsLib.getDocument({ + data: buf, + onPassword: (updatePassword, reason) => { + const password = prompt(`This PDF requires a ${reason} password. Please enter the password:`); + if (password) { + updatePassword(password); + } else { + throw new Error(`Password required to open this PDF. ${reason === 1 ? 'Owner' : 'User'} password needed.`); + } + } + }); + state.pdf.doc = await loadingTask.promise; + const savedPage = state.pdf.currentPage || 1; + const validPage = Math.min(savedPage, state.pdf.doc.numPages); + state.pdf.currentPage = validPage; + const savedZoom = state.pdf.zoomLevel || state.settings.pdfZoom; + updatePDFZoom(savedZoom); + document.getElementById('pageCount').textContent = state.pdf.doc.numPages; + document.getElementById('pageInput').max = state.pdf.doc.numPages; + document.getElementById('pageInput').value = validPage; + } catch (err) { + handleError(err, 'loadPDF'); + alert('Could not load PDF: ' + err.message); + state.pdf.doc = null; + state.pdf.renderTask = null; + } finally { + showLoading(false); + } +} +export function updatePDFZoom(zoomLevel) { + state.settings.pdfZoom = zoomLevel; + state.pdf.zoomLevel = zoomLevel; + saveToStorage(); + const zoomDisplay = document.getElementById('zoomLevel'); + if (zoomDisplay) { + zoomDisplay.textContent = `${Math.round(zoomLevel * 100)}%`; + } + if (state.pdf.doc && state.pdf.currentPage) { + renderPage(state.pdf.currentPage); + } +} +export function setupPDFCleanup() { + window.addEventListener('beforeunload', () => { + if (state.pdf.renderTask) { + state.pdf.renderTask.cancel().catch(() => {}); + state.pdf.renderTask = null; + } + }); +} +export async function renderPage(pageNum) { + if (!state.pdf.doc) { + console.warn('PDF document not loaded, attempting to reload...'); + await loadPDF(); + return; + } + try { + if (state.pdf.renderTask) { + try { + await state.pdf.renderTask.cancel(); + } catch (e) { + } + state.pdf.renderTask = null; + } + const page = await state.pdf.doc.getPage(pageNum); + const canvas = document.getElementById('pdfCanvas'); + const ctx = canvas.getContext('2d', { willReadFrequently: false }); + ctx.clearRect(0, 0, canvas.width, canvas.height); + const viewport = page.getViewport({ scale: state.pdf.zoomLevel }); + canvas.style.width = viewport.width + 'px'; + canvas.style.height = viewport.height + 'px'; + canvas.width = viewport.width * window.devicePixelRatio; + canvas.height = viewport.height * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + state.pdf.renderTask = page.render({ + canvasContext: ctx, + viewport: viewport + }); + await state.pdf.renderTask.promise; + page.cleanup(); + state.pdf.renderTask = null; + document.getElementById('pageInput').value = pageNum; + document.getElementById('prevPage').disabled = pageNum <= 1; + document.getElementById('nextPage').disabled = pageNum >= state.pdf.doc.numPages; + state.pdf.currentPage = pageNum; + saveToStorage(); + } catch (err) { + if (err.name === 'RenderingCancelledException') { + console.log('Rendering cancelled normally'); + return; + } + console.warn('Render error, reloading PDF:', err); + await loadPDF(); + handleError(err, 'renderPage'); + } +} +export async function searchPDF() { + const query = document.getElementById('pdfSearchInput').value.trim(); + if (!query || !state.pdf.doc) return; + const resultsSpan = document.getElementById('pdfSearchResults'); + const searchBtn = document.getElementById('pdfSearchBtn'); + resultsSpan.textContent = 'Searching...'; + searchBtn.disabled = true; + searchBtn.textContent = 'Searching...'; + clearSearchHighlights(); + currentSearch = { + query: query, + results: [], + currentResult: -1, + highlights: [] + }; + try { + for (let pageNum = 1; pageNum <= state.pdf.doc.numPages; pageNum++) { + const page = await state.pdf.doc.getPage(pageNum) + const textContent = await page.getTextContent(); + const text = textContent.items.map(item => item.str).join(' '); + const regex = new RegExp(query.replace(/[.*+?^{}()|[\]\\]/g, '\\$&'), 'gi'); + let match; + while ((match = regex.exec(text)) !== null) { + currentSearch.results.push({ + page: pageNum, + index: match.index, + text: match[0] + }); + } + } + if (currentSearch.results.length > 0) { + resultsSpan.textContent = `Found ${currentSearch.results.length} results`; + document.getElementById('clearSearchBtn').style.display = 'inline-block'; + if (currentSearch.results.length > 1) { + document.getElementById('prevSearchResult').style.display = 'inline-block'; + document.getElementById('nextSearchResult').style.display = 'inline-block'; + } + navigateToSearchResult(0); + } else { + resultsSpan.textContent = 'No results found'; + document.getElementById('clearSearchBtn').style.display = 'none'; + document.getElementById('prevSearchResult').style.display = 'none'; + document.getElementById('nextSearchResult').style.display = 'none'; + } + } catch (err) { + handleError(err, 'searchPDF'); + resultsSpan.textContent = 'Search failed'; + document.getElementById('clearSearchBtn').style.display = 'none'; + document.getElementById('prevSearchResult').style.display = 'none'; + document.getElementById('nextSearchResult').style.display = 'none'; + } finally { + searchBtn.disabled = false; + searchBtn.textContent = 'Search'; + } +} +export function clearSearch() { + document.getElementById('pdfSearchInput').value = ''; + document.getElementById('pdfSearchResults').textContent = ''; + document.getElementById('clearSearchBtn').style.display = 'none'; + document.getElementById('prevSearchResult').style.display = 'none'; + document.getElementById('nextSearchResult').style.display = 'none'; + clearSearchHighlights(); + currentSearch = { + query: '', + results: [], + currentResult: -1, + highlights: [] + }; +} +export async function navigateToSearchResult(index) { + try { + if (!currentSearch.results.length || index < 0 || index >= currentSearch.results.length) return; + if (state.pdf.renderTask) { + await state.pdf.renderTask.cancel(); + state.pdf.renderTask = null; + } + currentSearch.currentResult = index; + const result = currentSearch.results[index]; + state.pdf.currentPage = result.page; + await renderPage(result.page); + document.getElementById('pdfSearchResults').textContent = + `Result ${index + 1} of ${currentSearch.results.length}`; + } catch (err) { + handleError(err, 'navigateToSearchResult'); + } +} +function clearSearchHighlights() { + currentSearch.highlights.forEach(highlight => { + if (highlight.animation) { + currentSearch.highlights = []; + } + }); + if (state.pdf.currentPage) { + renderPage(state.pdf.currentPage); + } +} \ No newline at end of file diff --git a/modules/settings.js b/modules/settings.js new file mode 100644 index 0000000..32d73b3 --- /dev/null +++ b/modules/settings.js @@ -0,0 +1,274 @@ +import { + applyColorTheme, + applyTheme, + clearError, + handleError, + showLoading +} from '../main.js' +import { loadPassage } from './passage.js' +import { + deletePDFFromIndexedDB, + openDB, + STORE_NAME, + updateCustomPdfInfo +} from './pdf.js' +import { + APP_VERSION, + BOOK_ORDER, + readingPlan, + saveToCookies, + saveToStorage, + state, + updateBibleGatewayVersion +} from './state.js' +import { + restorePanelStates, + restoreSidebarState, + switchNotesView, + updateMarkdownPreview, + updateReferencePanel +} from './ui.js' +export function exportData() { + const payload = { + version: '2.0', + exportDate: new Date().toISOString(), + highlights: state.highlights, + notes: state.notes, + settings: { ...state.settings } + }; + if (payload.settings.customPdf && payload.settings.customPdf.data) { + const { data, ...meta } = payload.settings.customPdf; + payload.settings.customPdf = meta; + } + const blob = new Blob([JSON.stringify(payload, null, 2)], + { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bible-study-backup-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} +export function importData(ev) { + const file = ev.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = async e => { + try { + const incoming = JSON.parse(e.target.result); + if (!incoming.settings) { + throw new Error('Invalid backup format'); + } + if (!confirm('Import will overwrite all current data (except any uploaded PDF). Continue?')) { + return; + } + Object.assign(state.settings, incoming.settings); + if (incoming.highlights) { + state.highlights = incoming.highlights; + } + if (incoming.notes) { + state.notes = incoming.notes; + } + saveToStorage(); + applyTheme(); + applyColorTheme(); + restoreSidebarState(); + restorePanelStates(); + updateCustomPdfInfo(); + switchNotesView(state.settings.notesView || 'text'); + await loadPassage(); + document.getElementById('notesInput').value = state.notes; + updateMarkdownPreview(); + alert('Backup imported successfully!'); + } catch (err) { + console.error('Import error:', err); + alert('Failed to import backup – see console for details.'); + } + }; + reader.readAsText(file); + ev.target.value = ''; +} +export function resumeReadingPlan() { + const curBook = state.settings.manualBook; + const curChapter = state.settings.manualChapter; + const idx = findReadingPlanIndex(curBook, curChapter); + if (idx !== -1) { + state.settings.currentPassageIndex = idx; + } + state.settings.readingMode = 'readingPlan'; + loadPassage(); +} +function findReadingPlanIndex(book, chapter) { + for (let i = 0; i < readingPlan.length; i++) { + const p = readingPlan[i]; + if (p.book === book && p.chapter === chapter) { + return i; + } + } + return -1; +} +export function openSettings() { + document.getElementById('bibleTranslationSetting').value = + state.settings.bibleTranslation; + document.getElementById('referenceVersionSetting').value = + state.settings.referenceVersion; + document.getElementById('readingPlanId').value = + state.settings.readingPlanId || 'default'; + document.querySelectorAll('.color-theme-option') + .forEach(o => o.classList.toggle('selected', + o.dataset.theme === state.settings.colorTheme)); + document.getElementById('settingsModal').classList.add('active'); + document.getElementById('settingsOverlay').classList.add('active'); + document.getElementById('appVersion').textContent = APP_VERSION; +} +export function closeSettings() { + document.getElementById('settingsModal').classList.remove('active'); + document.getElementById('settingsOverlay').classList.remove('active'); +} +export async function saveSettings() { + try { + const newPlanId = document.getElementById('readingPlanId').value; + const newTranslation = document.getElementById('bibleTranslationSetting').value; + const newReferenceVersion = document.getElementById('referenceVersionSetting').value; + const oldPlanId = state.settings.readingPlanId; + state.settings.bibleTranslation = newTranslation; + state.settings.referenceVersion = newReferenceVersion; + state.settings.readingPlanId = newPlanId; + if (newReferenceVersion === 'BSB' && state.settings.referenceSource === 'biblegateway') { + state.settings.referenceSource = 'biblehub'; + document.getElementById('referenceSource').value = 'biblehub'; + } else if (newReferenceVersion === 'NASB1995' && state.settings.referenceSource === 'biblehub') { + state.settings.referenceSource = 'biblegateway'; + document.getElementById('referenceSource').value = 'biblegateway'; + } + if (oldPlanId !== newPlanId) { + state.settings.currentPassageIndex = 0; + state.settings.readingMode = 'readingPlan'; + } + const selectedTheme = document.querySelector('.color-theme-option.selected'); + if (selectedTheme) { + state.settings.colorTheme = selectedTheme.dataset.theme; + applyColorTheme(); + } + document.getElementById('referenceTranslation').value = state.settings.referenceVersion; + updateBibleGatewayVersion(); + saveToStorage(); + saveToCookies(); + closeSettings(); + await loadPassage(); + if (state.settings.referencePanelOpen) { + updateReferencePanel(); + } + alert('Settings saved!' + (oldPlanId !== newPlanId ? ' Starting from beginning of new reading plan.' : '')); + } catch (err) { + handleError(err, 'saveSettings'); + } +} +export function restartReadingPlan() { + if (confirm('Reset the reading plan to the very first passage? Highlights and notes will stay unchanged.')) { + state.settings.currentPassageIndex = 0; + state.settings.readingMode = 'readingPlan'; + saveToStorage(); + loadPassage(); + alert('Reading plan restarted – you are now at the beginning.'); + } +} +export async function clearCache() { + if (confirm('Clear all cached Bible data? This will remove offline access to previously viewed passages.')) { + try { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' }); + } + const db = await openDB(); + const tx = db.transaction([STORE_NAME], 'readwrite'); + const store = tx.objectStore(STORE_NAME); + await store.clear(); + alert('Cache cleared successfully'); + } catch (err) { + handleError(err, 'clearCache'); + alert('Error clearing cache: ' + err.message); + } + } +} +export async function deleteAllData() { + const confirmDelete = confirm('WARNING: This will delete ALL your data. Would you like to create a backup first?'); + if (confirmDelete) { + exportData(); + const proceed = confirm('Backup created. Proceed with deletion?'); + if (!proceed) return; + } + try { + showLoading(true); + localStorage.removeItem('bibleStudyState'); + document.cookie = 'bibleStudySettings=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + try { + await deletePDFFromIndexedDB(); + } catch (e) { + console.warn('Could not delete PDF from IndexedDB:', e); + } + const defaultState = { + currentVerse: null, + currentVerseData: null, + highlights: {}, + notes: '', + settings: { + bibleTranslation: 'BSB', + referenceVersion: 'NASB1995', + passageType: 'default', + readingMode: 'readingPlan', + manualBook: BOOK_ORDER[0], + manualChapter: 1, + lastUpdate: null, + currentPassageIndex: 0, + theme: 'light', + colorTheme: 'blue', + notesView: 'text', + hasSeenWelcome: true, + referencePanelOpen: false, + referenceSource: 'biblegateway', + collapsedSections: {}, + collapsedPanels: {}, + panelWidths: { + sidebar: 280, + referencePanel: 400, + scriptureSection: null, + notesSection: 400 + }, + customPdf: null, + pdfZoom: 1 + }, + currentPassageReference: '', + pdf: { + doc: null, + currentPage: 1, + renderTask: null, + zoomLevel: 1 + }, + welcomePdfFile: null + }; + Object.assign(state, defaultState); + document.getElementById('notesInput').value = ''; + updateMarkdownPreview(); + updateCustomPdfInfo(); + applyTheme(); + applyColorTheme(); + updateBibleGatewayVersion(); + document.getElementById('pageInput').value = 1; + document.getElementById('pageCount').textContent = '?'; + const zoomDisplay = document.getElementById('zoomLevel'); + if (zoomDisplay) { + zoomDisplay.textContent = '100%'; + } + await loadPassage(); + clearError(); + closeSettings(); + alert('All data has been deleted. The app has been reset to defaults.'); + } catch (err) { + handleError(err, 'deleteAllData'); + alert('Error deleting data. See console for details.'); + } finally { + showLoading(false); + } +} \ No newline at end of file diff --git a/modules/state.js b/modules/state.js new file mode 100644 index 0000000..847efc2 --- /dev/null +++ b/modules/state.js @@ -0,0 +1,399 @@ +import { handleError } from '../main.js' +import { loadPDFFromIndexedDB } from './pdf.js' +export const APP_VERSION = '1.0.2025.11.01'; +let saveTimeout = null; +const SAVE_DEBOUNCE_MS = 500; +export const BOOK_ORDER = [ + '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' +]; +export const CHAPTER_COUNTS = { + Genesis: 50, Exodus: 40, Leviticus: 27, Numbers: 36, Deuteronomy: 34, + Joshua: 24, Judges: 21, Ruth: 4, '1 Samuel': 31, '2 Samuel': 24, + '1 Kings': 22, '2 Kings': 25, '1 Chronicles': 29, '2 Chronicles': 36, + Ezra: 10, Nehemiah: 13, Esther: 10, + Job: 42, Psalms: 150, Proverbs: 31, Ecclesiastes: 12, 'Song of Solomon': 8, + Isaiah: 66, Jeremiah: 52, Lamentations: 5, Ezekiel: 48, Daniel: 12, + Hosea: 14, Joel: 3, Amos: 9, Obadiah: 1, Jonah: 4, Micah: 7, + Nahum: 3, Habakkuk: 3, Zephaniah: 3, Haggai: 2, Zechariah: 14, Malachi: 4, + Matthew: 28, Mark: 16, Luke: 24, John: 21, + Acts: 28, + Romans: 16, '1 Corinthians': 16, '2 Corinthians': 13, Galatians: 6, + Ephesians: 6, Philippians: 4, Colossians: 4, '1 Thessalonians': 5, + '2 Thessalonians': 3, '1 Timothy': 6, '2 Timothy': 4, Titus: 3, Philemon: 1, + Hebrews: 13, James: 5, '1 Peter': 5, '2 Peter': 3, '1 John': 5, + '2 John': 1, '3 John': 1, Jude: 1, + Revelation: 22 +}; +export const state = { + currentVerse: null, + currentVerseData: null, + highlights: {}, + notes: '', // User's study notes (plain text/markdown) + settings: { + bibleTranslation: 'BSB', + referenceVersion: 'NASB1995', + footnotes: {}, + passageType: 'default', + readingMode: 'readingPlan', // 'readingPlan' | 'manual' + manualBook: BOOK_ORDER[0], + manualChapter: 1, + lastUpdate: null, + currentPassageIndex: 0, + theme: 'light', // 'light' | 'dark' + colorTheme: 'blue', + notesView: 'text', // 'text' | 'markdown' + hasSeenWelcome: false, + referencePanelOpen: false, + referenceSource: 'biblegateway',// 'biblegateway' | 'biblehub' | 'pdf' + collapsedSections: {}, + collapsedPanels: {}, + panelWidths: { + sidebar: 280, + referencePanel: 400, + scriptureSection: null, + notesSection: 400 + }, + customPdf: null, + pdfZoom: 1 + }, + currentPassageReference: '', + pdf: { + doc: null, + currentPage: 1, + renderTask: null, + zoomLevel: 1 + }, + welcomePdfFile: null +}; +export function formatBookNameForSource(bookName, source) { + const book = bookName.toLowerCase(); + switch(source) { + case 'biblecom': + const bibleComCodes = { + 'genesis': 'GEN', 'exodus': 'EXO', 'leviticus': 'LEV', 'numbers': 'NUM', + 'deuteronomy': 'DEU', 'joshua': 'JOS', 'judges': 'JDG', 'ruth': 'RUT', + '1 samuel': '1SA', '2 samuel': '2SA', '1 kings': '1KI', '2 kings': '2KI', + '1 chronicles': '1CH', '2 chronicles': '2CH', 'ezra': 'EZR', 'nehemiah': 'NEH', + 'esther': 'EST', 'job': 'JOB', 'psalms': 'PSA', 'proverbs': 'PRO', + 'ecclesiastes': 'ECC', 'song of solomon': 'SNG', 'isaiah': 'ISA', 'jeremiah': 'JER', + 'lamentations': 'LAM', 'ezekiel': 'EZK', 'daniel': 'DAN', 'hosea': 'HOS', + 'joel': 'JOL', 'amos': 'AMO', 'obadiah': 'OBA', 'jonah': 'JON', + 'micah': 'MIC', 'nahum': 'NAM', 'habakkuk': 'HAB', 'zephaniah': 'ZEP', + 'haggai': 'HAG', 'zechariah': 'ZEC', 'malachi': 'MAL', 'matthew': 'MAT', + 'mark': 'MRK', 'luke': 'LUK', 'john': 'JHN', 'acts': 'ACT', + 'romans': 'ROM', '1 corinthians': '1CO', '2 corinthians': '2CO', 'galatians': 'GAL', + 'ephesians': 'EPH', 'philippians': 'PHP', 'colossians': 'COL', '1 thessalonians': '1TH', + '2 thessalonians': '2TH', '1 timothy': '1TI', '2 timothy': '2TI', 'titus': 'TIT', + 'philemon': 'PHM', 'hebrews': 'HEB', 'james': 'JAS', '1 peter': '1PE', + '2 peter': '2PE', '1 john': '1JN', '2 john': '2JN', '3 john': '3JN', + 'jude': 'JUD', 'revelation': 'REV' + }; + return bibleComCodes[book] || book.substring(0, 3).toUpperCase(); + case 'ebibleorg': + if (book === 'psalms') return 'PS1'; + return book.substring(0, 3).toUpperCase() + '1'; + default: + return book.replace(/\s+/g, '_'); + } +} +export function saveToStorage() { + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + try { + const cleanState = { + currentVerse: null, + currentVerseData: state.currentVerseData, + highlights: state.highlights, + notes: state.notes, + settings: { ...state.settings }, + currentPassageReference: state.currentPassageReference, + pdf: { + currentPage: state.pdf.currentPage, + zoomLevel: state.pdf.zoomLevel + }, + welcomePdfFile: null + }; + if (cleanState.settings.customPdf && cleanState.settings.customPdf.data) { + const { data, ...meta } = cleanState.settings.customPdf; + cleanState.settings.customPdf = { ...meta, storedInDB: true }; + } + localStorage.setItem('bibleStudyState', JSON.stringify(cleanState)); + saveToCookies(); + } catch (e) { + console.error('Storage error:', e); + } + }, SAVE_DEBOUNCE_MS); +} +export async function loadFromStorage() { + const raw = localStorage.getItem('bibleStudyState'); + if (!raw) return; + try { + const parsed = JSON.parse(raw); + Object.assign(state, parsed); + if (parsed.pdf) { + state.pdf.currentPage = parsed.pdf.currentPage || 1; + state.pdf.zoomLevel = parsed.pdf.zoomLevel || state.settings.pdfZoom; + } + if (state.settings.customPdf && state.settings.customPdf.storedInDB) { + const pdfMeta = await loadPDFFromIndexedDB(); + if (!pdfMeta) { + console.warn('PDF metadata present but DB entry missing'); + state.settings.customPdf = null; + } + } + document.getElementById('notesInput').value = state.notes; + } catch (e) { + handleError(err, 'loadFromStorage'); + } +} +export function saveToCookies() { + const exp = new Date(); + exp.setFullYear(exp.getFullYear() + 10); + const cookieVal = encodeURIComponent(JSON.stringify({ + ...state.settings, + customPdf: undefined + })); + document.cookie = `bibleStudySettings=${cookieVal}; expires=${exp.toUTCString()}; path=/; SameSite=Strict`; +} +export function loadFromCookies() { + const pairs = document.cookie.split(';'); + for (let pair of pairs) { + const [k, v] = pair.trim().split('='); + if (k === 'bibleStudySettings') { + try { + const settings = JSON.parse(decodeURIComponent(v)); + Object.assign(state.settings, settings); + } catch (e) { + console.error('Cookie parse error:', e); + } + } + } +} +export const readingPlan = [ + { book: 'Genesis', chapter: 1, startVerse: 1, endVerse: 31, displayRef: 'Genesis 1' }, + { book: 'Genesis', chapter: 2, startVerse: 1, endVerse: 25, displayRef: 'Genesis 2' }, + { book: 'Genesis', chapter: 3, startVerse: 1, endVerse: 24, displayRef: 'Genesis 3' }, + { book: 'Genesis', chapter: 6, startVerse: 5, endVerse: 22, displayRef: 'Genesis 6:5-22' }, + { book: 'Genesis', chapter: 12, startVerse: 1, endVerse: 9, displayRef: 'Genesis 12:1-9' }, + { book: 'Genesis', chapter: 22, startVerse: 1, endVerse: 19, displayRef: 'Genesis 22:1-19' }, + { book: 'Exodus', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Exodus 3' }, + { book: 'Exodus', chapter: 20, startVerse: 1, endVerse: 21, displayRef: 'Exodus 20:1-21' }, + { book: 'Leviticus', chapter: 19, startVerse: 1, endVerse: 18, displayRef: 'Leviticus 19:1-18' }, + { book: 'Numbers', chapter: 14, startVerse: 1, endVerse: 38, displayRef: 'Numbers 14:1-38' }, + { book: 'Deuteronomy', chapter: 6, startVerse: 1, endVerse: 25, displayRef: 'Deuteronomy 6' }, + { book: 'Deuteronomy', chapter: 30, startVerse: 1, endVerse: 20, displayRef: 'Deuteronomy 30' }, + { book: 'Joshua', chapter: 1, startVerse: 1, endVerse: 9, displayRef: 'Joshua 1:1-9' }, + { book: 'Judges', chapter: 2, startVerse: 6, endVerse: 23, displayRef: 'Judges 2:6-23' }, + { book: 'Ruth', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Ruth 1' }, + { book: '1 Samuel', chapter: 16, startVerse: 1, endVerse: 13, displayRef: '1 Samuel 16:1-13' }, + { book: '2 Samuel', chapter: 7, startVerse: 1, endVerse: 29, displayRef: '2 Samuel 7' }, + { book: '1 Kings', chapter: 18, startVerse: 1, endVerse: 46, displayRef: '1 Kings 18' }, + { book: '2 Kings', chapter: 22, startVerse: 1, endVerse: 20, displayRef: '2 Kings 22' }, + { book: 'Ezra', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Ezra 1' }, + { book: 'Nehemiah', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Nehemiah 1' }, + { book: 'Esther', chapter: 4, startVerse: 1, endVerse: 17, displayRef: 'Esther 4' }, + { book: 'Job', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Job 1' }, + { book: 'Psalms', chapter: 1, startVerse: 1, endVerse: 6, displayRef: 'Psalm 1' }, + { book: 'Psalms', chapter: 19, startVerse: 1, endVerse: 14, displayRef: 'Psalm 19' }, + { book: 'Psalms', chapter: 23, startVerse: 1, endVerse: 6, displayRef: 'Psalm 23' }, + { book: 'Psalms', chapter: 51, startVerse: 1, endVerse: 19, displayRef: 'Psalm 51' }, + { book: 'Psalms', chapter: 103, startVerse: 1, endVerse: 22, displayRef: 'Psalm 103' }, + { book: 'Psalms', chapter: 119, startVerse: 1, endVerse: 16, displayRef: 'Psalm 119:1-16' }, + { book: 'Proverbs', chapter: 3, startVerse: 1, endVerse: 12, displayRef: 'Proverbs 3:1-12' }, + { book: 'Ecclesiastes', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Ecclesiastes 3' }, + { book: 'Song of Solomon', chapter: 2, startVerse: 1, endVerse: 17, displayRef: 'Song of Solomon 2' }, + { book: 'Isaiah', chapter: 6, startVerse: 1, endVerse: 13, displayRef: 'Isaiah 6' }, + { book: 'Isaiah', chapter: 9, startVerse: 1, endVerse: 7, displayRef: 'Isaiah 9:1-7' }, + { book: 'Isaiah', chapter: 40, startVerse: 1, endVerse: 31, displayRef: 'Isaiah 40' }, + { book: 'Isaiah', chapter: 53, startVerse: 1, endVerse: 12, displayRef: 'Isaiah 53' }, + { book: 'Jeremiah', chapter: 29, startVerse: 1, endVerse: 14, displayRef: 'Jeremiah 29:1-14' }, + { book: 'Lamentations', chapter: 3, startVerse: 1, endVerse: 33, displayRef: 'Lamentations 3:1-33' }, + { book: 'Ezekiel', chapter: 36, startVerse: 22, endVerse: 38, displayRef: 'Ezekiel 36:22-38' }, + { book: 'Daniel', chapter: 3, startVerse: 1, endVerse: 30, displayRef: 'Daniel 3' }, + { book: 'Daniel', chapter: 6, startVerse: 1, endVerse: 28, displayRef: 'Daniel 6' }, + { book: 'Hosea', chapter: 6, startVerse: 1, endVerse: 11, displayRef: 'Hosea 6' }, + { book: 'Joel', chapter: 2, startVerse: 12, endVerse: 32, displayRef: 'Joel 2:12-32' }, + { book: 'Jonah', chapter: 1, startVerse: 1, endVerse: 17, displayRef: 'Jonah 1' }, + { book: 'Micah', chapter: 6, startVerse: 1, endVerse: 16, displayRef: 'Micah 6' }, + { book: 'Habakkuk', chapter: 3, startVerse: 1, endVerse: 19, displayRef: 'Habakkuk 3' }, + { book: 'Malachi', chapter: 3, startVerse: 1, endVerse: 18, displayRef: 'Malachi 3' }, + { book: 'Matthew', chapter: 5, startVerse: 1, endVerse: 30, displayRef: 'Matthew 5:1-30' }, + { book: 'Matthew', chapter: 6, startVerse: 1, endVerse: 34, displayRef: 'Matthew 6' }, + { book: 'Matthew', chapter: 7, startVerse: 1, endVerse: 29, displayRef: 'Matthew 7' }, + { book: 'Mark', chapter: 10, startVerse: 17, endVerse: 45, displayRef: 'Mark 10:17-45' }, + { book: 'Luke', chapter: 15, startVerse: 1, endVerse: 32, displayRef: 'Luke 15' }, + { book: 'John', chapter: 1, startVerse: 1, endVerse: 51, displayRef: 'John 1:1-51' }, + { book: 'John', chapter: 3, startVerse: 1, endVerse: 36, displayRef: 'John 3:1-36' }, + { book: 'John', chapter: 6, startVerse:30, endVerse: 66, displayRef: 'John 6:30-66' }, + { book: 'John', chapter: 14, startVerse: 1, endVerse: 31, displayRef: 'John 14' }, + { book: 'Acts', chapter: 2, startVerse: 1, endVerse: 47, displayRef: 'Acts 2' }, + { book: 'Romans', chapter: 1, startVerse: 1, endVerse: 32, displayRef: 'Romans 1' }, + { book: 'Romans', chapter: 3, startVerse: 1, endVerse: 31, displayRef: 'Romans 3' }, + { book: 'Romans', chapter: 8, startVerse: 1, endVerse: 39, displayRef: 'Romans 8' }, + { book: 'Romans', chapter: 12, startVerse: 1, endVerse: 21, displayRef: 'Romans 12' }, + { book: '1 Corinthians', chapter: 13, startVerse: 1, endVerse: 13, displayRef: '1 Corinthians 13' }, + { book: '2 Corinthians', chapter: 5, startVerse: 1, endVerse: 21, displayRef: '2 Corinthians 5' }, + { book: 'Galatians', chapter: 5, startVerse: 16, endVerse: 26, displayRef: 'Galatians 5:16-26' }, + { book: 'Ephesians', chapter: 2, startVerse: 1, endVerse: 22, displayRef: 'Ephesians 2' }, + { book: 'Philippians', chapter: 2, startVerse: 1, endVerse: 18, displayRef: 'Philippians 2:1-18' }, + { book: 'Colossians', chapter: 1, startVerse: 1, endVerse: 29, displayRef: 'Colossians 1' }, + { book: '1 Thessalonians', chapter: 4, startVerse: 1, endVerse: 18, displayRef: '1 Thessalonians 4' }, + { book: '1 Timothy', chapter: 3, startVerse: 1, endVerse: 16, displayRef: '1 Timothy 3' }, + { book: 'Hebrews', chapter: 11, startVerse: 1, endVerse: 40, displayRef: 'Hebrews 11' }, + { book: 'James', chapter: 1, startVerse: 1, endVerse: 27, displayRef: 'James 1' }, + { book: '1 Peter', chapter: 1, startVerse: 1, endVerse: 25, displayRef: '1 Peter 1' }, + { book: '1 John', chapter: 4, startVerse: 1, endVerse: 21, displayRef: '1 John 4' }, + { book: 'Revelation', chapter: 1, startVerse: 1, endVerse: 20, displayRef: 'Revelation 1' }, + { book: 'Revelation', chapter: 21, startVerse: 1, endVerse: 27, displayRef: 'Revelation 21' }, + { book: 'Revelation', chapter: 22, startVerse: 1, endVerse: 21, displayRef: 'Revelation 22' } +]; +function buildFullBookPlan(bookName) { + const maxChapters = CHAPTER_COUNTS[bookName]; + if (!maxChapters) { + console.warn(`No chapter count for "${bookName}" – skipping plan`); + return []; + } + const plan = []; + for (let ch = 1; ch <= maxChapters; ch++) { + plan.push({ + book: bookName, + chapter: ch, + startVerse: 1, + endVerse: 999, + displayRef: `${bookName} ${ch}` + }); + } + return plan; +} +const READING_PLANS = { + default: readingPlan, + genesis: buildFullBookPlan('Genesis'), + psalms: buildFullBookPlan('Psalms'), + proverbs: buildFullBookPlan('Proverbs'), + ecclesiastes: buildFullBookPlan('Ecclesiastes'), + romans: buildFullBookPlan('Romans'), + revelation: buildFullBookPlan('Revelation') +}; +export function getActivePlan() { + const id = state.settings.readingPlanId || 'default'; + return READING_PLANS[id] || READING_PLANS['default']; +} +const PLAN_LABELS = { + default: '90‑Day Sequential', + genesis: 'Genesis', + psalms: 'Psalms', + proverbs: 'Proverbs', + ecclesiastes: 'Ecclesiastes', + romans: 'Romans', + revelation: 'Revelation' +}; +export function getCurrentPlanLabel() { + const id = state.settings.readingPlanId || 'default'; + return PLAN_LABELS[id] || id; +} +export const bookNameMapping = { + Genesis: 'GEN', Exodus: 'EXO', Leviticus: 'LEV', Numbers: 'NUM', Deuteronomy: 'DEU', + Joshua: 'JOS', Judges: 'JDG', Ruth: 'RUT', '1 Samuel': '1SA', '2 Samuel': '2SA', + '1 Kings': '1KI', '2 Kings': '2KI', '1 Chronicles': '1CH', '2 Chronicles': '2CH', + Ezra: 'EZR', Nehemiah: 'NEH', Esther: 'EST', Job: 'JOB', Psalms: 'PSA', + Proverbs: 'PRO', Ecclesiastes: 'ECC', 'Song of Solomon': 'SNG', Isaiah: 'ISA', Jeremiah: 'JER', + Lamentations: 'LAM', Ezekiel: 'EZK', Daniel: 'DAN', Hosea: 'HOS', Joel: 'JOL', + Amos: 'AMO', Obadiah: 'OBA', Jonah: 'JON', Micah: 'MIC', Nahum: 'NAM', + Habakkuk: 'HAB', Zephaniah: 'ZEP', Haggai: 'HAG', Zechariah: 'ZEC', Malachi: 'MAL', + Matthew: 'MAT', Mark: 'MRK', Luke: 'LUK', John: 'JHN', Acts: 'ACT', + Romans: 'ROM', '1 Corinthians': '1CO', '2 Corinthians': '2CO', Galatians: 'GAL', + Ephesians: 'EPH', Philippians: 'PHP', Colossians: 'COL', '1 Thessalonians': '1TH', + '2 Thessalonians': '2TH', '1 Timothy': '1TI', '2 Timothy': '2TI', Titus: 'TIT', + Philemon: 'PHM', Hebrews: 'HEB', James: 'JAS', '1 Peter': '1PE', '2 Peter': '2PE', + '1 John': '1JN', '2 John': '2JN', '3 John': '3JN', Jude: 'JUD', Revelation: 'REV' +}; +export const bibleHubUrlMap = { + 'NASB1995': 'nasb', // Bible Hub uses 'nasb' for NASB 1995 + 'NASB': 'nasb_', // Bible Hub uses 'nasb_' for NASB 2020 + 'ASV': 'asv', + 'ESV': 'esv', + 'KJV': 'kjv', + 'NKJV': 'nkjv', + 'BSB': 'bsb', + 'CSB': 'csb', + 'NET': 'net', + 'NIV': 'niv', + 'NLT': 'nlt' +}; +export const bibleComUrlMap = { + 'NASB1995': '100', + 'NASB': '2692', + 'ASV': '12', + 'ESV': '59', + 'KJV': '1', + 'GNV': '2163', + 'NKJV': '114', + 'BSB': '3034', + 'CSB': '1713', + 'NET': '107', + 'NIV': '111', + 'NLT': '116' +}; +export const ebibleOrgUrlMap = { + 'NASB1995': 'local:engnasb', + 'ASV': 'local:eng-asv', + 'KJV': 'local:eng-kjv2006', + 'GNV': 'local:enggnv', + 'BSB': 'local:engbsb', + 'NET': 'local:engnet' +}; +export const stepBibleUrlMap = { + 'NASB1995': 'NASB1995', + 'NASB': 'NASB2020', + 'ASV': 'ASV', + 'ESV': 'ESV', + 'KJV': 'KJV', + 'GNV': 'Gen', + 'BSB': 'BSB', + 'NET': 'NET2full', + 'NIV': 'NIV' +}; +export function getTranslationShorthand() { + return state.settings.bibleTranslation || 'BSB'; +} +function getBibleGatewayVersionCode(appTranslation) { + const versionMap = { + 'NASB1995': 'NASB1995', + 'NASB': 'NASB', + 'ASV': 'ASV', + 'ESV': 'ESV', + 'KJV': 'KJV', + 'GNV': 'GNV', + 'NKJV': 'NKJV', + 'BSB': 'BSB', + 'CSB': 'CSB', + 'NET': 'NET', + 'NIV': 'NIV', + 'NLT': 'NLT' + }; + return versionMap[appTranslation] || 'NASB1995'; +} +export function updateBibleGatewayVersion() { + const versionCode = getBibleGatewayVersionCode(state.settings.referenceVersion); + const versionInput = document.getElementById('bgVersion'); + if (versionCode === 'BSB') { + versionInput.value = 'NASB1995'; + } else { + versionInput.value = versionCode; + } +} \ No newline at end of file diff --git a/modules/strongs.js b/modules/strongs.js new file mode 100644 index 0000000..5a3d7c0 --- /dev/null +++ b/modules/strongs.js @@ -0,0 +1,254 @@ +import { state } from './state.js' +import { getStepBibleUrl } from './ui.js' +export function showStrongsReference(verseEl) { + const ref = verseEl.dataset.verse; + state.currentVerseElement = verseEl; + const textSpan = verseEl.querySelector('.verse-text'); + let verseText = ''; + if (textSpan) { + verseText = textSpan.innerHTML; + if (!verseText || verseText.trim() === '') { + verseText = textSpan.textContent || ''; + if (!verseText || verseText.trim() === '') { + verseText = verseEl.dataset.verseText || ''; + } + } + } else { + verseText = verseEl.dataset.verseText || ''; + } + if (!verseText || verseText.trim() === '') { + verseText = 'Verse text not available'; + } + state.currentVerseData = { reference: ref, text: verseText }; + const content = document.getElementById('strongsContent'); + const m = ref.match(/^([\w\s]+)\s+(\d+):(\d+)/); + let book = '', chapter = '', verse = ''; + if (m) { + book = m[1].trim().replace(/\s+/g, '_').toLowerCase(); + chapter = m[2]; + verse = m[3]; + } + const greekUrl = `https://biblehub.com/interlinear/${book}/${chapter}-${verse}.htm`; + const currentTranslation = state.settings.referenceVersion; + const stepUrl = getStepBibleUrl(ref, currentTranslation); + content.innerHTML = ` +
+
+ + ${ref} + +
+ +
+
+ ${verseText} +
+ +
+
+
+
+

BibleHub Interlinear

+
+ +
+
+ +
+
+
+

STEP Bible Analysis

+
+ +
+
+ +
+
+

+ These resources provide detailed word-by-word analysis. + Use the "Pop Out" button to open them in a new tab. +

+ + `; + populateStrongsFootnotes(ref); + document.getElementById('strongsPopup').classList.add('active'); + document.getElementById('popupOverlay').classList.add('active'); + setTimeout(() => { + const copyBtn = document.getElementById('copyVerseBtn'); + if (copyBtn) { + copyBtn.addEventListener('click', copyVerseText); + } + const prevBtn = document.getElementById('prevVerseBtn'); + const nextBtn = document.getElementById('nextVerseBtn'); + if (prevBtn) { + prevBtn.addEventListener('click', navigateToPreviousVerse); + } + if (nextBtn) { + nextBtn.addEventListener('click', navigateToNextVerse); + } + document.querySelectorAll('.resource-frame-btn').forEach(btn => { + btn.addEventListener('click', function() { + const url = this.dataset.url; + const title = this.dataset.title; + popOutResource(url, title); + }); + }); + setupStrongsFootnoteHandlers(); + }, 0); +} +function navigateToPreviousVerse() { + const currentVerseEl = state.currentVerseElement; + if (!currentVerseEl) return; + const allVerses = Array.from(document.querySelectorAll('.verse')); + const currentIndex = allVerses.indexOf(currentVerseEl); + if (currentIndex > 0) { + const prevVerseEl = allVerses[currentIndex - 1]; + showStrongsReference(prevVerseEl); + } +} +function navigateToNextVerse() { + const currentVerseEl = state.currentVerseElement; + if (!currentVerseEl) return; + const allVerses = Array.from(document.querySelectorAll('.verse')); + const currentIndex = allVerses.indexOf(currentVerseEl); + if (currentIndex < allVerses.length - 1) { + const nextVerseEl = allVerses[currentIndex + 1]; + showStrongsReference(nextVerseEl); + } +} +export function closeStrongsPopup() { + document.getElementById('strongsPopup').classList.remove('active'); + document.getElementById('popupOverlay').classList.remove('active'); + state.currentVerseData = null; +} +function copyVerseText() { + if (!state.currentVerseData) return; + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = state.currentVerseData.text; + let plainText = tempDiv.textContent || tempDiv.innerText || ''; + plainText = plainText + .replace(/\s+/g, ' ') + .replace(/\s([.,;:!?])/g, '$1') + .replace(/([.,;:!?])(?=\w)/g, '$1 ') + .trim(); + const txt = `${state.currentVerseData.reference} – ${plainText}`; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(txt) + .then(() => { + const btn = document.getElementById('copyVerseBtn'); + const original = btn.innerHTML; + btn.innerHTML = ' Copied!'; + btn.classList.add('copied'); + setTimeout(() => { + btn.innerHTML = original; + btn.classList.remove('copied'); + }, 2000); + }) + .catch(err => { + console.error('Copy failed:', err); + copyVerseFallback(txt); + }); + } else { + copyVerseFallback(txt); + } +} +function populateStrongsFootnotes(verseRef) { + const container = document.getElementById('strongsFootnotesContainer'); + if (!container) return; + container.innerHTML = ''; + const verseFootnotes = state.footnotes[verseRef]; + if (!verseFootnotes || verseFootnotes.length === 0) { + container.innerHTML = '

No footnotes available for this verse

'; + return; + } + console.log('Found stored footnotes:', verseFootnotes); + container.innerHTML = ` +
+

Footnotes

+ `; + verseFootnotes.forEach(fn => { + const footnoteDiv = document.createElement('div'); + footnoteDiv.className = 'footnote'; + footnoteDiv.innerHTML = ` + ${fn.number} + ${fn.content} + `; + container.appendChild(footnoteDiv); + }); + container.style.display = 'block'; +} +function setupStrongsFootnoteHandlers() { + document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => { + const newRef = ref.cloneNode(true); + ref.parentNode.replaceChild(newRef, ref); + }); + document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => { + ref.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + const footnoteNumber = this.dataset.footnoteNumber; + const footnoteElement = document.querySelector(`#strongsFootnotesContainer .footnote[data-footnote-number="${footnoteNumber}"]`); + if (footnoteElement) { + footnoteElement.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + footnoteElement.style.backgroundColor = 'var(--verse-hover)'; + setTimeout(() => { + footnoteElement.style.backgroundColor = ''; + }, 2000); + } + }); + }); +} +function copyVerseFallback(text) { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + const btn = document.getElementById('copyVerseBtn'); + const original = btn.textContent; + btn.textContent = '✓ Copied!'; + btn.classList.add('copied'); + setTimeout(() => { + btn.textContent = original; + btn.classList.remove('copied'); + }, 2000); + } catch (err) { + console.error('Copy fallback failed:', err); + alert('Could not copy verse text.'); + } finally { + document.body.removeChild(textarea); + } +} +function popOutResource(url, title) { + window.open(url, title, + 'width=800,height=600,menubar=no,toolbar=no,location=no'); +} \ No newline at end of file diff --git a/modules/ui.js b/modules/ui.js new file mode 100644 index 0000000..1fa2216 --- /dev/null +++ b/modules/ui.js @@ -0,0 +1,407 @@ +import { handleError } from '../main.js' +import { + loadSelectedChapter, + populateBookDropdown, + populateChapterDropdown +} from './navigation.js' +import { loadPDF } from './pdf.js' +import { + BOOK_ORDER, + CHAPTER_COUNTS, + bibleComUrlMap, + bibleHubUrlMap, + ebibleOrgUrlMap, + formatBookNameForSource, + getActivePlan, + saveToStorage, + state, + stepBibleUrlMap +} from './state.js' +export function switchNotesView(view) { + state.settings.notesView = view; + const txtBtn = document.getElementById('textViewBtn'); + const mdBtn = document.getElementById('markdownViewBtn'); + const input = document.getElementById('notesInput'); + const display = document.getElementById('notesDisplay'); + if (view === 'text') { + txtBtn.classList.add('active'); + mdBtn.classList.remove('active'); + input.style.display = 'block'; + display.style.display = 'none'; + } else { + txtBtn.classList.remove('active'); + mdBtn.classList.add('active'); + input.style.display = 'none'; + display.style.display = 'block'; + updateMarkdownPreview(); + } + saveToStorage(); +} +export function updateMarkdownPreview() { + if (state.settings.notesView !== 'markdown' || typeof marked === 'undefined') return; + const out = document.getElementById('notesDisplay'); + try { + out.innerHTML = marked.parse(state.notes); + } catch (e) { + console.error('Markdown error:', e); + out.innerHTML = '

Error rendering markdown

'; + } +} +export function insertMarkdown(type) { + const ta = document.getElementById('notesInput'); + const start = ta.selectionStart; + const end = ta.selectionEnd; + const sel = ta.value.substring(start, end); + let repl = ''; + let cursorAdj = 0; + switch (type) { + case 'bold': repl = `**${sel || 'bold text'}**`; cursorAdj = sel ? 0 : -2; break; + case 'italic': repl = `*${sel || 'italic text'}*`; cursorAdj = sel ? 0 : -1; break; + case 'h1': repl = `# ${sel || 'Heading 1'}`; cursorAdj = sel ? 0 : -10; break; + case 'h2': repl = `## ${sel || 'Heading 2'}`; cursorAdj = sel ? 0 : -10; break; + case 'h3': repl = `### ${sel || 'Heading 3'}`; cursorAdj = sel ? 0 : -10; break; + case 'ul': repl = `- ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break; + case 'ol': repl = `1. ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break; + case 'quote': repl = `> ${sel || 'Quote'}`; cursorAdj = sel ? 0 : -6; break; + case 'code': repl = `\`${sel || 'code'}\``; cursorAdj = sel ? 0 : -1; break; + case 'link': repl = `[${sel || 'link text'}](url)`; cursorAdj = sel ? -4 : -14; break; + } + ta.value = ta.value.slice(0, start) + repl + ta.value.slice(end); + const newPos = start + repl.length + cursorAdj; + ta.setSelectionRange(newPos, newPos); + ta.focus(); + state.notes = ta.value; + saveToStorage(); + updateMarkdownPreview(); +} +export function exportNotes(ext) { + if (!state.notes || state.notes.trim() === '') { + alert('No notes to export!'); + return; + } + const blob = new Blob([state.notes], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `study-notes-${new Date().toISOString().split('T')[0]}.${ext}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} +export function toggleNotes() { + document.getElementById('notesSection').classList.toggle('hidden'); +} +export function togglePanelCollapse(panelId) { + const panel = document.getElementById(panelId); + const collapsed = panel.classList.contains('panel-collapsed'); + if (collapsed) { + panel.classList.remove('panel-collapsed'); + if (state.settings.panelWidths[panelId]) { + panel.style.width = state.settings.panelWidths[panelId] + 'px'; + } + state.settings.collapsedPanels[panelId] = false; + } else { + panel.classList.add('panel-collapsed'); + state.settings.collapsedPanels[panelId] = true; + } + saveToStorage(); +} +export function toggleSection(sectionId) { + const content = document.getElementById(`content-${sectionId}`); + const header = document.querySelector(`[data-section="${sectionId}"]`); + const toggle = header.querySelector('.section-toggle'); + const nowCollapsed = content.classList.contains('collapsed'); + content.classList.toggle('collapsed'); + toggle.classList.toggle('collapsed'); + state.settings.collapsedSections[sectionId] = !nowCollapsed; + saveToStorage(); +} +export function toggleReferencePanel() { + const panel = document.getElementById('referencePanel'); + const nowOpen = panel.classList.contains('active'); + if (nowOpen) { + panel.classList.remove('active'); + state.settings.referencePanelOpen = false; + } else { + panel.classList.add('active'); + state.settings.referencePanelOpen = true; + updateReferencePanel(); + } + saveToStorage(); +} +export async function updateReferencePanel() { + try { + const sourceSelect = document.getElementById('referenceSource'); + const source = sourceSelect.value; + const iframe = document.getElementById('referenceIframe'); + const pdfViewer = document.getElementById('pdfViewer'); + const transSel = document.getElementById('referenceTranslation'); + state.settings.referenceSource = source; + state.settings.referenceVersion = transSel.value; + transSel.style.display = source === 'pdf' ? 'none' : 'block'; + filterTranslationOptions(source, transSel); + const actualSource = document.getElementById('referenceSource').value; + const translation = transSel.value; + let passage; + if (state.settings.readingMode === 'readingPlan') { + passage = getActivePlan()[state.settings.currentPassageIndex]; + } else { + passage = { + book: state.settings.manualBook, + chapter: state.settings.manualChapter, + displayRef: `${state.settings.manualBook} ${state.settings.manualChapter}` + }; + } + const bookName = passage.book.toLowerCase().replace(/\s+/g, '_'); + const bookAbbr = bookName.substring(0, 3).toUpperCase(); + const chapter = passage.chapter; + if (actualSource === 'pdf') { + if (!state.settings.customPdf) { + alert('No PDF uploaded. Please upload one in Settings first.'); + document.getElementById('referenceSource').value = 'biblegateway'; + transSel.style.display = 'block'; + filterTranslationOptions('biblegateway', transSel); + return; + } + iframe.style.display = 'none'; + pdfViewer.classList.add('active'); + document.getElementById('zoomLevel').textContent = + Math.round(state.settings.pdfZoom * 100) + '%'; + await loadPDF(); + } else if (actualSource === 'biblehub') { + const bibleHubCode = bibleHubUrlMap[translation] || translation.toLowerCase(); + const url = `https://biblehub.com/${bibleHubCode}/${bookName}/${chapter}.htm`; + iframe.src = url; + } else if (actualSource === 'biblecom') { + const bibleComCode = bibleComUrlMap[translation]; + if (!bibleComCode) { + alert(`Bible.com doesn't support ${translation}. Please choose another translation.`); + return; + } + const formattedBook = formatBookNameForSource(passage.book, 'biblecom'); + const urlFormats = [ + `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}?interface=embed`, + `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}`, + `https://www.bible.com/bible/${bibleComCode}/${chapter}.${translation}?${formattedBook}=${chapter}` + ]; + let currentUrlIndex = 0; + function tryNextUrl() { + if (currentUrlIndex >= urlFormats.length) { + alert('Could not load Bible.com. Please try another reference source.'); + return; + } + iframe.src = urlFormats[currentUrlIndex]; + currentUrlIndex++; + } + iframe.onload = function() { + console.log('Bible.com loaded successfully with format', currentUrlIndex); + }; + iframe.onerror = function() { + console.log('Trying next Bible.com URL format...'); + tryNextUrl(); + }; + tryNextUrl(); + } else if (actualSource === 'ebibleorg') { + const ebibleOrgCode = ebibleOrgUrlMap[translation]; + if (!ebibleOrgCode) { + alert(`eBible.org doesn't support ${translation}. Please choose another translation.`); + return; + } + const bookRef = bookName === 'psalms' ? 'PS1' : `${bookAbbr}1`; + const url = `https://ebible.org/study/?w1=bible&t1=${encodeURIComponent(ebibleOrgCode)}&v1=${bookRef}_${chapter}`; + iframe.src = url; + } else if (actualSource === 'stepbible') { + const stepBibleCode = stepBibleUrlMap[translation]; + if (!stepBibleCode) { + alert(`STEP Bible doesn't support ${translation}. Please choose another translation.`); + return; + } + const url = getStepBibleUrl(passage.displayRef, translation); + iframe.src = url; + } else { + const query = passage.displayRef.replace(/\s+/g, '+'); + let version = translation; + if (translation === 'GNV') version = 'GNV'; + const url = `https://www.biblegateway.com/passage/?search=${query}&version=${version}&interface=print`; + iframe.src = url; + } + saveToStorage(); + } catch (err) { + handleError(err, 'updateReferencePanel'); + } +} +function filterTranslationOptions(source, selectElement) { + const unsupportedTranslations = { + biblecom: [], + biblehub: ['GNV'], + biblegateway: ['BSB'], + stepbible: ['NKJV', 'CSB', 'NLT'], + ebibleorg: ['NASB', 'ASV', 'ESV', 'NKJV', 'CSB', 'NIV', 'NLT'], + pdf: ['NASB1995', 'NASB', 'ASV', 'ESV', 'KJV', 'GNV', 'NKJV', 'BSB', 'CSB', 'NET', 'NIV', 'NLT'] + }; + const allOptions = selectElement.querySelectorAll('option'); + const currentValue = selectElement.value; + let needsNewSelection = false; + let needsSourceChange = false; + allOptions.forEach(option => { + const value = option.value; + const isUnsupported = unsupportedTranslations[source]?.includes(value); + if (isUnsupported) { + option.style.display = 'none'; + option.disabled = true; + if (value === currentValue) { + needsNewSelection = true; + if (value === 'BSB' && source === 'biblegateway') { + needsSourceChange = true; + } + } + } else { + option.style.display = 'block'; + option.disabled = false; + } + }); + if (needsSourceChange) { + document.getElementById('referenceSource').value = 'biblehub'; + state.settings.referenceSource = 'biblehub'; + const sourceSelect = document.getElementById('referenceSource'); + sourceSelect.value = 'biblehub'; + selectElement.value = 'BSB'; + state.settings.referenceVersion = 'BSB'; + } + else if (needsNewSelection) { + let fallbackValue = 'NASB1995'; + if (source === 'biblehub') { + fallbackValue = 'NASB'; + } + selectElement.value = fallbackValue; + state.settings.referenceVersion = fallbackValue; + } + if (needsSourceChange || needsNewSelection) { + saveToStorage(); + } +} +export function restoreBookChapterUI() { + populateBookDropdown(); + const bookSel = document.getElementById('bookSelect'); + const chapterSel = document.getElementById('chapterSelect'); + const savedBook = state.settings.manualBook || BOOK_ORDER[0]; + const bookIdx = BOOK_ORDER.indexOf(savedBook); + const book = bookIdx >= 0 ? BOOK_ORDER[bookIdx] : BOOK_ORDER[0]; + populateBookDropdown(); + bookSel.value = book; + populateChapterDropdown(book); + const savedChap = Number(state.settings.manualChapter) || 1; + const maxChap = CHAPTER_COUNTS[book]; + const chapter = Math.min(savedChap, maxChap); + chapterSel.value = String(chapter); + state.settings.manualBook = book; + state.settings.manualChapter = chapter; + loadSelectedChapter(book, chapter); +} +export function initResizeHandles() { + const handles = document.querySelectorAll('.resize-handle'); + const SPEED_FACTOR = 1.8; + const limits = { + sidebar: { min: 150, max: 600 }, + referencePanel: { min: 250, max: 800 }, + scriptureSection: { min: 300, max: 1200 }, + notesSection: { min: 250, max: 800 } + }; + let resizing = false, + startX = 0, + startW = 0, + panel = null, + invert = false, + pendingRAF = false; + handles.forEach(handle => { + handle.addEventListener('mousedown', e => { + resizing = true; + startX = e.clientX; + const panelId = handle.dataset.panel; + panel = document.getElementById(panelId); + startW = panel.offsetWidth; + invert = (panel.id === 'notesSection'); + document.body.style.cursor = 'ew-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + }); + document.addEventListener('mousemove', e => { + if (!resizing || !panel) return; + let delta = (invert ? startX - e.clientX : e.clientX - startX) * SPEED_FACTOR; + let newW = startW + delta; + const { min, max } = limits[panel.id] || { min: 150, max: 1200 }; + if (newW < min) newW = min; + if (newW > max) newW = max; + if (!pendingRAF) { + pendingRAF = true; + requestAnimationFrame(() => { + panel.style.width = newW + 'px'; + state.settings.panelWidths[panel.id] = newW; + pendingRAF = false; + }); + } + }); + document.addEventListener('mouseup', () => { + if (resizing) { + resizing = false; + panel = null; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + saveToStorage(); + } + }); +} +export function popOutResource(url, title) { + window.open(url, title, + 'width=800,height=600,menubar=no,toolbar=no,location=no'); +} +export function getStepBibleUrl(reference, translation) { + const stepBibleCode = stepBibleUrlMap[translation] || translation; + return `https://www.stepbible.org/?q=version=${stepBibleCode}@reference=${encodeURIComponent(reference)}&options=HNVUG`; +} +export function makeToggleSticky() { + const sidebar = document.getElementById('sidebar'); + const toggle = sidebar.querySelector('.collapse-toggle'); + if (!toggle) return; + toggle.style.position = 'sticky'; + toggle.style.top = '10px'; + toggle.style.zIndex = '1000'; + toggle.style.marginLeft = 'auto'; + toggle.style.marginRight = '10px'; +} +export function restoreSidebarState() { + Object.entries(state.settings.collapsedSections || {}) + .forEach(([sec, collapsed]) => { + if (collapsed) { + const content = document.getElementById(`content-${sec}`); + const header = document.querySelector(`[data-section="${sec}"]`); + const toggle = header?.querySelector('.section-toggle'); + if (content && toggle) { + content.classList.add('collapsed'); + toggle.classList.add('collapsed'); + } + } + }); +} +export function restorePanelStates() { + Object.entries(state.settings.panelWidths || {}) + .forEach(([id, w]) => { + const el = document.getElementById(id); + if (el && w) el.style.width = w + 'px'; + }); + Object.entries(state.settings.collapsedPanels || {}) + .forEach(([id, collapsed]) => { + const el = document.getElementById(id); + if (el && collapsed) el.classList.add('panel-collapsed'); + }); + if (state.settings.referencePanelOpen) { + document.getElementById('referencePanel').classList.add('active'); + document.getElementById('referenceSource').value = + state.settings.referenceSource || 'biblegateway'; + document.getElementById('referenceTranslation').value = + state.settings.referenceVersion || 'NASB1995'; + updateReferencePanel(); + } +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..8d5580b --- /dev/null +++ b/styles.css @@ -0,0 +1,363 @@ +:root{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db;--background:#ecf0f1;--card-background:#ffffff;--toolbar-background:#f8f9fa;--notes-background:#f8f9fa;--sidebar-background:#2c3e50;--header-background:#ffffff;--popup-background:#ffffff;--text-color:#2c3e50;--sidebar-text:#ecf0f1;--border-color:#bdc3c7;--verse-hover:#f0f0f0;--shadow:rgba(0,0,0,0.1);--shadow-strong:rgba(0,0,0,0.3);--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--transition-fast:150ms ease;--transition-normal:300ms ease;--transition-slow:500ms ease} +[data-color-theme="blue"]{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db} +[data-color-theme="green"]{--primary-color:#1b5e20;--secondary-color:#2e7d32;--accent-color:#4caf50} +[data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#6a1b9a;--accent-color:#9c27b0} +[data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#c62828;--accent-color:#f44336} +[data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#ef6c00;--accent-color:#ff9800} +[data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00695c;--accent-color:#009688} +[data-theme="dark"]{--background:#0d1117;--card-background:#161b22;--toolbar-background:#161b22;--notes-background:#0d1117;--sidebar-background:#0d1117;--header-background:#161b22;--popup-background:#161b22;--text-color:#e1e1e1;--sidebar-text:#c9d1d9;--border-color:#444;--verse-hover:#21262d;--shadow:rgba(0,0,0,0.4);--shadow-strong:rgba(0,0,0,0.6)} +[data-theme="dark"][data-color-theme="blue"]{--primary-color:#1a3a52;--secondary-color:#2c5f7f;--accent-color:#5dade2} +[data-theme="dark"][data-color-theme="green"]{--primary-color:#1b4d20;--secondary-color:#2e6d32;--accent-color:#66bb6a} +[data-theme="dark"][data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#7b1fa2;--accent-color:#ab47bc} +[data-theme="dark"][data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#d32f2f;--accent-color:#ef5350} +[data-theme="dark"][data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#f57c00;--accent-color:#ffa726} +[data-theme="dark"][data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00796b;--accent-color:#26a69a} + +*,*::before,*::after{margin:0;padding:0;box-sizing:border-box} +@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important;scroll-behavior:auto!important} +} +:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px} +@media(prefers-contrast:high){:root{--border-color:#000000;--shadow:rgba(0,0,0,0.8);--shadow-strong:rgba(0,0,0,0.9)} +.verse:hover{outline:2px solid var(--accent-color)} +} + +body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background-color:var(--background);color:var(--text-color);line-height:1.6;transition:background-color 0.3s,color 0.3s} +.container{display:flex;height:100vh;overflow:hidden} +.panel-collapsed{min-width:50px!important;max-width:50px!important} +.panel-collapsed>*:not(.collapse-toggle):not(.resize-handle){display:none!important} +.collapse-toggle{will-change:transform;position:absolute;top:55%;right:5px;transform:translateY(-50%);background:var(--accent-color);color:white;border:none;width:40px;height:40px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;z-index:100;transition:transform 0.3s ease,background-color 0.3s ease;box-shadow:0 2px 8px rgba(0,0,0,0.2)} +.collapse-toggle:hover{transform:translateY(-50%)scale(1.1);box-shadow:0 4px 12px rgba(0,0,0,0.3)} +.collapse-toggle:focus{outline:2px solid white;outline-offset:2px} +.panel-collapsed .collapse-toggle{right:5px} +.collapse-toggle::before{content:'\25C0';transition:transform 0.3s ease} +.panel-collapsed .collapse-toggle::before{content:'\25B6'} +#scriptureSection .collapse-toggle::before{content:'\25B6'} +#scriptureSection.panel-collapsed .collapse-toggle::before{content:'\25C0'} +#notesSection .collapse-toggle::before{content:'\25B6'} +#notesSection.panel-collapsed .collapse-toggle::before{content:'\25C0'} +.scripture-section{scroll-behavior:smooth;scroll-padding-top:20px} +.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0} +.content-area{flex:1;display:flex;overflow:hidden} + +.welcome-screen{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,var(--primary-color),var(--accent-color));z-index:5000;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto} +.welcome-screen.hidden{display:none} +.welcome-content{background:var(--card-background);border-radius:20px;padding:40px;max-width:900px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,0.3);margin:auto;max-height:90vh;overflow-y:auto} +.welcome-content h1{color:var(--primary-color);text-align:center;margin-bottom:15px;font-size:2.5em} +.welcome-content>p{color:var(--text-color);text-align:center;margin-bottom:30px;font-size:1.2em;opacity:0.8} +.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin:30px 0} +.feature-card{padding:20px;background:var(--background);border-radius:10px;border:2px solid var(--border-color);transition:all 0.3s} +.feature-card:hover{transform:translateY(-5px);box-shadow:0 5px 20px var(--shadow);border-color:var(--accent-color)} +.feature-icon{font-size:2.5em;margin-bottom:10px} +.feature-card h3{color:var(--primary-color);margin-bottom:10px} +.feature-card p{color:var(--text-color);opacity:0.8;font-size:0.95em} +.offline-setup{margin:30px 0;padding:25px;background:var(--background);border-radius:10px;border:2px solid var(--accent-color)} +.offline-setup h2{color:var(--primary-color);margin-bottom:15px;display:flex;align-items:center;gap:10px} +.pdf-upload-area{border:2px dashed var(--border-color);border-radius:8px;padding:30px;text-align:center;cursor:pointer;transition:all 0.3s;background:var(--card-background);margin:15px 0} +.pdf-upload-area:hover{border-color:var(--accent-color);background-color:rgba(52,152,219,0.05)} +.pdf-upload-area.has-file{border-color:var(--accent-color);background-color:rgba(52,152,219,0.1)} +.pdf-download-options{margin-top:15px;padding:15px;background-color:var(--card-background);border-radius:5px} +.pdf-download-options h4{margin-bottom:10px;color:var(--text-color)} +.pdf-download-link{display:block;padding:8px 12px;margin-bottom:8px;background-color:var(--background);border:1px solid var(--border-color);border-radius:4px;color:var(--accent-color);text-decoration:none;transition:all 0.2s} +.pdf-download-link:hover{background-color:var(--accent-color);color:white} +.welcome-actions{display:flex;gap:15px;margin-top:30px;justify-content:center} +.welcome-actions button{padding:15px 40px;border:none;border-radius:8px;font-size:16px;font-weight:bold;cursor:pointer;transition:all 0.3s} + +.sidebar{width:280px;background-color:var(--sidebar-background);color:var(--sidebar-text);padding:20px;overflow-y:auto;flex-shrink:0;border-right:1px solid var(--border-color);position:relative;min-width:50px;transition:all 0.3s ease} +.sidebar h2{margin-bottom:20px;font-size:1.3em;border-bottom:2px solid var(--accent-color);padding-bottom:10px;color:var(--sidebar-text)} +.sidebar-section{margin-bottom:10px} +.sidebar-section-header{display:flex;justify-content:space-between;align-items:center;cursor:pointer;padding:10px;margin-top:15px;background-color:rgba(255,255,255,0.05);border-radius:5px;transition:background-color 0.2s} +.sidebar-section-header:hover{background-color:rgba(255,255,255,0.1)} +.sidebar-section-header:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.sidebar-section-header h3{margin:0;font-size:1.1em;color:var(--accent-color)} +.section-toggle{font-size:0.9em;transition:transform 0.3s} +.section-toggle.collapsed{transform:rotate(-90deg)} +.sidebar-section-content{max-height:1000px;overflow:hidden;transition:max-height 0.3s ease-out,opacity 0.3s ease-out;opacity:1} +.sidebar-section-content.collapsed{max-height:0;opacity:0} +.sidebar-links{list-style:none;margin-top:10px} +.sidebar-links li{margin-bottom:8px} +.sidebar-links a{color:var(--sidebar-text);text-decoration:none;display:block;padding:8px 12px;border-radius:5px;transition:background-color 0.3s;opacity:0.9} +.sidebar-links a:hover{background-color:var(--secondary-color);opacity:1} +.sidebar-links a:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.reference-panel-toggle{margin-top:20px;padding:12px;background:var(--accent-color);color:white;border:none;border-radius:5px;cursor:pointer;width:100%;font-size:14px;font-weight:bold;transition:opacity 0.3s} +.reference-panel-toggle:hover{opacity:0.8} +.reference-panel-toggle:focus{outline:2px solid white;outline-offset:2px} + +.reference-panel{display:none;width:400px;background-color:var(--card-background);border-right:1px solid var(--border-color);position:relative;flex-shrink:0;min-width:50px;transition:all 0.3s ease} +.reference-panel.active{display:flex;flex-direction:column} +.reference-panel-header{padding:15px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px} +.reference-panel-header h3{color:var(--text-color);margin:0} +.reference-panel-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap} +.reference-panel-controls select{padding:5px 10px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px} +.reference-panel-controls select:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.reference-panel-close{background:none;border:none;color:var(--text-color);cursor:pointer;font-size:20px;padding:5px 10px;border-radius:4px;transition:background-color 0.2s;margin-left:10px} +.reference-panel-close:hover{background-color:var(--verse-hover)} +.reference-panel-close:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.reference-panel-content{flex:1;overflow:hidden;position:relative} +.reference-panel-iframe{width:100%;height:100%;border:none} +.pdf-viewer{width:100%;height:100%;display:none;flex-direction:column;background-color:var(--card-background)} +.pdf-viewer.active{display:flex} +.pdf-controls{padding:10px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap} +.pdf-controls button{padding:5px 15px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px} +.pdf-controls button:hover{opacity:0.8} +.pdf-controls button:focus{outline:2px solid white;outline-offset:2px} +.pdf-controls button:disabled{opacity:0.5;cursor:not-allowed} +.pdf-controls span{color:var(--text-color);font-size:13px} +.pdf-controls input[type="number"]{width:60px;padding:5px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px} +.pdf-controls input[type="number"]:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.pdf-canvas-container{flex:1;overflow:auto;display:flex;justify-content:center;align-items:flex-start;padding:20px;background-color:var(--background)} +#pdfCanvas{box-shadow:0 2px 10px var(--shadow)} + +.resize-handle{position:absolute;top:0;bottom:0;width:10px;cursor:ew-resize;background-color:transparent;transition:background-color 0.2s;z-index:10;display:flex;align-items:center;justify-content:center} +.resize-handle::after{content:'\22EE\22EE';color:var(--border-color);font-size:18px;opacity:0;transition:opacity 0.2s} +.resize-handle:hover{background-color:rgba(52,152,219,0.1)} +.resize-handle:hover::after{opacity:1;color:var(--accent-color)} +.resize-handle-right{right:0} +.resize-handle-left{left:0} + +.header{background-color:var(--header-background);padding:20px 30px;box-shadow:0 2px 5px var(--shadow);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)} +.header h1{color:var(--text-color);font-size:1.8em} +.header-controls{display:flex;gap:10px;align-items:center} +.theme-toggle{background:none;border:2px solid var(--border-color);color:var(--text-color);width:44px;height:44px;border-radius:50%;cursor:pointer;font-size:20px;display:flex;align-items:center;justify-content:center;transition:all 0.3s} +.theme-toggle:hover{background-color:var(--accent-color);border-color:var(--accent-color);color:white;transform:rotate(180deg)} +.theme-toggle:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.btn{position:relative;padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-size:14px;transition:all 0.3s;background-color:var(--accent-color);color:white} +.btn:hover{opacity:0.8;transform:translateY(-1px)} +.btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.btn:focus::after{content:'';position:absolute;top:-4px;left:-4px;right:-4px;bottom:-4px;border:2px solid var(--accent-color);border-radius:7px;pointer-events:none} +.btn-secondary{background-color:var(--secondary-color)} +.btn:disabled{opacity:0.5;cursor:not-allowed;transform:none} +.btn-primary{background:var(--accent-color);color:white} +.btn-primary:hover{opacity:0.9;transform:translateY(-2px);box-shadow:0 5px 15px var(--shadow)} +.btn-primary:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.btn-danger{background-color:#f44336!important;color:white} +.btn-danger:hover{background-color:#d32f2f!important;opacity:0.9} +.toolbar{background-color:var(--toolbar-background);padding:15px 30px;border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;flex-wrap:wrap} +.toolbar button{padding:8px 15px;font-size:13px} +.toolbar-info{margin-left:auto;color:var(--text-color);opacity:0.7;font-size:14px} +.toolbar-divider{width:1px;height:24px;margin:0 12px;background-color:var(--border-color);align-self:center;opacity:0.6} + +.scripture-section{flex:1;padding:30px;overflow-y:auto;background-color:var(--card-background);margin:var(--spacing-xl);padding-inline:var(--spacing-xl);padding-block:var(--spacing-lg);border-radius:10px;box-shadow:0 2px 10px var(--shadow);border:1px solid var(--border-color);position:relative;min-width:300px;transition:all 0.3s ease;contain:layout style} +.scripture-content{font-size:1.1em;line-height:1.8;text-rendering:optimizeLegibility;font-feature-settings:"kern" 1,"liga" 1,"calt" 1} +.passage-header{background:linear-gradient(135deg,var(--primary-color),var(--primary-color));color:white;padding:20px;border-radius:8px;margin-bottom:20px} +.passage-header h2{font-size:1.5em;margin-bottom:5px;color:white;border:none} +.passage-header .date{opacity:0.9;font-size:0.9em} +.passage-reference{font-weight:bold;font-size:1.2em;margin-bottom:15px;color:var(--accent-color)} +.plan-label{font-size:1.1rem;font-weight:500;color:var(--accent-color);margin-top:8px;margin-bottom:12px} +.verse{margin-bottom:8px;padding:var(--spacing-sm);border-radius:var(--radius-sm);cursor:pointer;transition:background-color var(--transition-fast);position:relative;contain:content} +.verse:hover,.verse:active{background-color:var(--verse-hover);will-change:background-color} +.verse:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.verse-number{font-weight:bold;color:var(--accent-color);margin-right:8px;font-size:0.85em;vertical-align:super} +.verse-text{display:inline} +.highlight-yellow{background-color:#fff59d;color:#000} +.highlight-green{background-color:#a5d6a7;color:#000} +.highlight-blue{background-color:#90caf9;color:#000} +.highlight-pink{background-color:#f48fb1;color:#000} +.highlight-orange{background-color:#ffcc80;color:#000} +.highlight-purple{background-color:#ce93d8;color:#000} +.footnote-ref{position:relative;color:var(--accent-color);font-size:0.7em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease} +.footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)} +.footnote-ref::before{content:'['} +.footnote-ref::after{content:']'} +.footnotes-container{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:6px;border-left:3px solid var(--accent-color);font-size:0.9em;overflow-y:auto;max-height:300px;position:relative;z-index:100} +.footnotes-separator{margin:30px 0 15px 0;border:none;border-top:1px solid var(--border-color);opacity:0.5} +.footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600} +.footnote{cursor:pointer;transition:all 0.2s ease;padding:8px 12px;border-radius:4px;margin-bottom:8px} +.footnote:hover{background-color:var(--verse-hover);transform:translateX(2px)} +.footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;background-color:rgba(0,0,0,0.08);padding:2px 6px;border-radius:3px;transition:all 0.2s ease} +.footnote:hover .footnote-number{background-color:var(--accent-color);color:white} +.footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95} +.footnote.highlighted{background-color:var(--verse-hover);border-left-color:var(--accent-color)} +.strongs-footnotes-container{display:block!important;max-height:200px;overflow-y:auto;padding:2px;background-color:var(--toolbar-background)} +.strongs-footnotes-container .footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600} +.strongs-footnotes-container .footnote{margin-bottom:12px;padding:10px;border-radius:4px;transition:all 0.3s ease} +.strongs-footnotes-container .footnote:hover{background-color:rgba(0,0,0,0.03)} +.strongs-footnotes-container .footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;font-size:0.85em;background-color:rgba(0,0,0,0.08);padding:3px 6px;border-radius:3px} +.strongs-footnotes-container .footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95} +.strongs-footnotes-container .footnote-ref{color:var(--accent-color);font-size:0.75em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease} +.strongs-footnotes-container .footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)} +#strongsFootnotesContainer .footnote{margin-bottom:12px;padding:10px;background-color:var(--toolbar-background);border-radius:4px} +#strongsFootnotesContainer .footnote-number{font-weight:bold;color:var(--accent-color);margin-right:8px} +#strongsFootnotesContainer .footnote-content{display:inline} +#strongsPopup .footnote{cursor:pointer;transition:all 0.2s ease} +#strongsPopup .footnote:hover{background-color:var(--verse-hover)} +#strongsPopup .footnote:hover .footnote-number{background-color:var(--accent-color);color:white} +.verse-navigation{display:flex;align-items:center;gap:10px;margin-right:auto} +.nav-btn{background:var(--button-bg);border:1px solid var(--border-color);border-radius:4px;padding:6px 10px;cursor:pointer;font-size:12px;color:var(--text-color);transition:all 0.2s ease} +.nav-btn:hover{background:var(--button-hover-bg);border-color:var(--accent-color)} +.nav-btn:disabled{opacity:0.5;cursor:not-allowed} + +.color-picker{transform:translateZ(0);display:none;position:absolute;background:var(--popup-background);border:1px solid var(--border-color);border-radius:5px;padding:10px;box-shadow:0 4px 15px var(--shadow-strong);z-index:1000} +.color-picker.active{display:block} +.color-options{display:grid;grid-template-columns:repeat(3,1fr);gap:8px} +.color-option{width:40px;height:40px;border-radius:5px;cursor:pointer;border:2px solid transparent;transition:all 0.2s} +.color-option:hover{border-color:var(--accent-color);transform:scale(1.1)} +.color-option:focus{outline:2px solid var(--accent-color);outline-offset:2px} + +.strongs-popup{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:25px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:90vw;width:90%;max-height:90vh;overflow-y:auto;border:1px solid var(--border-color)} +.strongs-popup.active{display:block} +.popup-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1999} +.popup-overlay.active{display:block} +.popup-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;border-bottom:2px solid var(--accent-color);padding-bottom:10px} +.popup-header h2{color:var(--text-color)} +.popup-close{cursor:pointer;font-size:24px;color:var(--text-color);background:none;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color 0.2s} +.popup-close:hover{background-color:var(--verse-hover)} +.popup-close:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.strongs-content{line-height:1.8;color:var(--text-color)} +.verse-reference-display{font-size:1.2em;font-weight:bold;color:var(--accent-color);margin-bottom:15px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px} +.verse-text-display{padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px;margin-bottom:20px;font-size:1.1em;line-height:1.6} +.copy-verse-btn{padding:6px 12px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px;transition:all 0.2s} +.copy-verse-btn:hover{opacity:0.8} +.copy-verse-btn:focus{outline:2px solid white;outline-offset:2px} +.copy-verse-btn.copied{background-color:#4caf50} +.embedded-resources{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:20px} +.resource-frame{border:1px solid var(--border-color);border-radius:5px;overflow:hidden;background:var(--card-background)} +.resource-frame-header{background:var(--toolbar-background);padding:10px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)} +.resource-frame-header h4{margin:0;font-size:0.9em;color:var(--text-color)} +.resource-frame-actions{display:flex;gap:5px} +.resource-frame-btn{padding:4px 8px;font-size:11px;background:var(--accent-color);color:white;border:none;border-radius:3px;cursor:pointer;transition:opacity 0.2s} +.resource-frame-btn:hover{opacity:0.8} +.resource-frame-btn:focus{outline:2px solid white;outline-offset:2px} +.resource-frame iframe{width:100%;height:400px;border:none} +.strongs-definition{margin-top:15px;padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px} +.strongs-definition h3{color:var(--text-color);margin-bottom:10px} +.api-attribution{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:5px;border:1px solid var(--border-color);font-size:0.85em;opacity:0.8} +.api-attribution a{color:var(--accent-color);text-decoration:none} +.api-attribution a:hover{text-decoration:underline} +.api-attribution a:focus{outline:2px solid var(--accent-color);outline-offset:2px} + +.notes-section{width:400px;padding:20px;background-color:var(--notes-background);border-left:1px solid var(--border-color);display:flex;flex-direction:column;overflow:hidden;position:relative;min-width:50px;transition:all 0.3s ease} +.notes-section.hidden{display:none} +.notes-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px} +.notes-header h3{color:var(--text-color)} +.notes-controls{display:flex;gap:5px} +.notes-controls button{padding:5px 10px;font-size:12px} +.notes-view-toggle{display:flex;gap:5px;padding:5px;background-color:var(--toolbar-background);border-radius:5px;margin-bottom:10px} +.view-toggle-btn{flex:1;padding:8px;border:none;border-radius:4px;cursor:pointer;font-size:13px;background-color:transparent;color:var(--text-color);transition:all 0.2s} +.view-toggle-btn.active{background-color:var(--accent-color);color:white} +.view-toggle-btn:hover:not(.active){background-color:var(--verse-hover)} +.view-toggle-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.markdown-toolbar{display:flex;gap:5px;padding:10px;background-color:var(--toolbar-background);border:1px solid var(--border-color);border-radius:5px 5px 0 0;flex-wrap:wrap} +.markdown-btn{padding:6px 12px;background-color:var(--card-background);border:1px solid var(--border-color);border-radius:4px;cursor:pointer;font-size:13px;color:var(--text-color);transition:all 0.2s;display:flex;align-items:center;gap:5px} +.markdown-btn:hover{background-color:var(--accent-color);color:white;border-color:var(--accent-color)} +.markdown-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.markdown-btn:active{transform:scale(0.95)} +#notesInput,#notesDisplay{flex:1;width:100%;border:1px solid var(--border-color);border-radius:0 0 5px 5px;padding:15px;margin-bottom:10px;background-color:var(--card-background);color:var(--text-color)} +#notesInput{font-family:'Courier New',monospace;font-size:14px;resize:none} +#notesInput:focus{outline:2px solid var(--accent-color);outline-offset:2px} +#notesDisplay{overflow-y:auto;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;font-size:14px} +#notesDisplay h1,#notesDisplay h2,#notesDisplay h3{margin-top:20px;margin-bottom:10px;color:var(--text-color)} +#notesDisplay h1{font-size:2em;border-bottom:2px solid var(--border-color);padding-bottom:10px} +#notesDisplay h2{font-size:1.5em} +#notesDisplay h3{font-size:1.2em} +#notesDisplay code{background-color:var(--toolbar-background);padding:2px 6px;border-radius:3px;font-family:'Courier New',monospace;border:1px solid var(--border-color)} +#notesDisplay pre{background-color:var(--toolbar-background);padding:15px;border-radius:5px;overflow-x:auto;border:1px solid var(--border-color)} +#notesDisplay pre code{background:none;border:none;padding:0} +#notesDisplay blockquote{border-left:4px solid var(--accent-color);padding-left:15px;margin:15px 0;opacity:0.8} +#notesDisplay ul,#notesDisplay ol{margin-left:25px;margin-bottom:15px} +#notesDisplay a{color:var(--accent-color)} +#notesDisplay a:focus{outline:2px solid var(--accent-color);outline-offset:2px} +#notesDisplay p{margin-bottom:10px} +#notesDisplay table{border-collapse:collapse;width:100%;margin:15px 0} +#notesDisplay table th,#notesDisplay table td{border:1px solid var(--border-color);padding:8px;text-align:left} +#notesDisplay table th{background-color:var(--toolbar-background);font-weight:bold} + +.settings-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:30px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:600px;width:90%;border:1px solid var(--border-color);max-height:80vh;overflow-y:auto} +.settings-modal.active{display:block} +.settings-group{margin-bottom:20px} +.settings-group label{display:block;margin-bottom:8px;font-weight:bold;color:var(--text-color)} +.settings-group input,.settings-group select{width:100%;padding:10px;border:1px solid var(--border-color);border-radius:5px;font-size:14px;background-color:var(--card-background);color:var(--text-color)} +.settings-group input:focus,.settings-group select:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.settings-group small{display:block;margin-top:5px;opacity:0.7;color:var(--text-color)} +.settings-group small a{color:var(--accent-color)} +.settings-group small a:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.color-theme-options{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px} +.color-theme-option{padding:15px;border:2px solid var(--border-color);border-radius:8px;cursor:pointer;text-align:center;transition:all 0.3s;background:var(--card-background)} +.color-theme-option:hover{transform:scale(1.05)} +.color-theme-option:focus{outline:2px solid var(--accent-color);outline-offset:2px} +.color-theme-option.selected{border-color:var(--accent-color);background-color:var(--accent-color);color:white} +.color-theme-preview{width:100%;height:40px;border-radius:5px;margin-bottom:8px} +.settings-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:25px} +.settings-section{margin-bottom:25px;padding-bottom:20px;border-bottom:1px solid var(--border-color)} +.about-content{line-height:1.6} +.about-creator{margin-bottom:12px;font-size:.9em} +.about-description{margin-bottom:5px;padding:5px;background-color:var(--bg-secondary);border-radius:5px} +.about-description p{margin:0;color:var(--text-color)} +.attribution-links{margin-top:20px;padding-top:15px;border-top:1px solid var(--border-color)} +.attribution-links h4{margin-bottom:12px;color:var(--text-color);font-size:1em;font-weight:600} +.attribution-link{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-bottom:8px;background-color:var(--bg-secondary);border-radius:6px;text-decoration:none;color:var(--text-color);transition:all 0.2s ease;border:1px solid transparent} +.attribution-link:hover{background-color:var(--button-hover-bg);border-color:var(--accent-color);transform:translateY(-1px)} +.attribution-link i{font-size:1.2em;width:20px;text-align:center} +.attribution-link.gab{color:#4cf278} +.attribution-link.gab:hover{background-color:rgba(151,246,202,0.1)} +.attribution-link.lumo{color:#8c67cd} +.attribution-link.lumo:hover{background-color:rgba(197,151,246,0.1)} +.link-description{font-size:0.85em;opacity:0.8;margin-top:4px} +.version-info{font-family:monospace;padding:2px 4px;background-color:var(--bg-secondary);border-radius:6px;display:inline-block} + +.loading-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);display:none;justify-content:center;align-items:center;z-index:3000} +.loading-overlay.active{display:flex} +.loading-spinner{width:60px;height:60px;border:6px solid rgba(255,255,255,0.3);border-top-color:var(--accent-color);border-radius:50%;animation:spin 1s linear infinite} +@keyframes spin{to{transform:rotate(360deg)} +} + +.error-message{background-color:#ff5252;color:white;padding:15px;border-radius:5px;margin-bottom:20px} + +@media(max-width:1024px){.sidebar{width:250px} +.notes-section{width:350px} +.reference-panel{width:350px} +.feature-grid{grid-template-columns:1fr} +} +@media(max-width:768px){.container{flex-direction:column} +.sidebar{width:100%;height:auto;max-height:200px} +.content-area{flex-direction:column} +.notes-section{width:100%;border-left:none;border-top:1px solid var(--border-color)} +.reference-panel{width:100%;border-left:none;border-top:1px solid var(--border-color)} +.footnote{padding:6px;margin-bottom:8px} +.toolbar-info{display:none} +.embedded-resources{grid-template-columns:1fr} +.welcome-content h1{font-size:2em} +.welcome-actions{flex-direction:column} +} +@container(max-width:768px){.feature-grid{grid-template-columns:1fr} +.toolbar{flex-direction:column;gap:15px} +} +.toggle-notes{display:none} +@media(max-width:768px){.toggle-notes{display:inline-block} +} +.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} +@media print{.sidebar,.header-controls,.toolbar,.notes-section,.reference-panel-toggle{display:none!important} +.scripture-section{margin:0;box-shadow:none;border:none} +body{background:white;color:black} +} +html{font-size:16px} +@media(max-width:768px){html{font-size:14px} +} +@media(hover:none)and(pointer:coarse){.verse:hover{background-color:transparent} +.sidebar-links a{padding:var(--spacing-md)var(--spacing-lg)} +.btn{min-height:44px;min-width:44px} +} +@media(prefers-contrast:high){.verse-number{font-weight:900} +.passage-header{border:2px solid var(--text-color)} +} +@keyframes spin{0%{transform:rotate(0deg)} +100%{transform:rotate(360deg)} +} +.loading-spinner{animation:spin 1s linear infinite;transform:translateZ(0)} +.settings-modal:focus{outline:none} +.settings-modal *:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px} +@media print{.verse{page-break-inside:avoid} +.passage-header{break-after:avoid} +body{background:white!important;color:black!important} +.verse{cursor:default!important} +.highlight-yellow{background-color:#fff59d!important} +} + +::-webkit-scrollbar{width:10px;height:10px} +::-webkit-scrollbar-track{background:var(--background)} +::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:5px} +::-webkit-scrollbar-thumb:hover{background:var(--secondary-color)} +[data-theme="dark"]::-webkit-scrollbar-thumb{background:var(--secondary-color)} +[data-theme="dark"]::-webkit-scrollbar-thumb:hover{background:var(--accent-color)} +*{scrollbar-width:thin;scrollbar-color:var(--border-color)var(--background)} +[data-theme="dark"]*{scrollbar-color:var(--secondary-color)var(--background)} + diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..67c6375 --- /dev/null +++ b/sw.js @@ -0,0 +1,249 @@ +import { APP_VERSION } from "./modules/state"; +const CACHE_VERSION = { + static: 'v2', + api: 'v1', + pdf: 'v1' +}; +const CACHE_NAME = `provinent-scripture-${APP_VERSION}-${CACHE_VERSION.static}`; +const API_CACHE_NAME = `provinent-api-cache-${CACHE_VERSION.api}`; +const OFFLINE_PDF_CACHE = `provinent-pdf-cache-${CACHE_VERSION.pdf}`; +const MAX_CACHE_AGE = 24 * 60 * 60 * 1000; +const PDF_SIZE_LIMIT = 10 * 1024 * 1024; +const PRECACHE_URLS = [ + '/', + '/index.html', + '/api.js', + '/main.js', + '/navigation.js', + '/passage.js', + '/pdf.js', + '/settings.js', + '/state.js', + '/strongs.js', + '/ui.js', + '/styles.css', + '/manifest.json' +]; +self.addEventListener('install', (event) => { + console.log('[Service Worker] Installing and pre-caching static assets'); + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + console.log('[Service Worker] Caching static assets:', PRECACHE_URLS); + return cache.addAll(PRECACHE_URLS); + }) + .then(() => { + console.log('[Service Worker] Pre-caching complete, skipping waiting'); + return self.skipWaiting(); + }) + .catch((error) => { + console.error('[Service Worker] Pre-caching failed:', error); + }) + ); +}); +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys() + .then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME && + cacheName !== API_CACHE_NAME && + cacheName !== OFFLINE_PDF_CACHE) { + console.log('[Service Worker] Deleting old cache:', cacheName); + return caches.delete(cacheName) + .catch((error) => { + console.warn('[Service Worker] Failed to delete cache:', cacheName, error); + }); + } + }) + ); + }) + .then(() => { + return Promise.all([ + manageCacheSize(API_CACHE_NAME, 100), + manageCacheSize(OFFLINE_PDF_CACHE, 5) + ]); + }) + .then(() => { + console.log('[Service Worker] Cache cleanup complete, claiming clients'); + return self.clients.claim(); + }) + ); +}); +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + if (url.origin === 'https://bible.helloao.org') { + event.respondWith(handleApiRequest(event.request)); + } + else if (url.hostname.includes('biblehub.com') || + url.hostname.includes('biblegateway.com') || + url.hostname.includes('bible.com')) { + event.respondWith(handleExternalBibleResource(event.request)); + } + else if (url.pathname.endsWith('.pdf')) { + event.respondWith(handlePdfRequest(event.request)); + } + else if (PRECACHE_URLS.includes(url.pathname) || + PRECACHE_URLS.includes(url.pathname + '/')) { + event.respondWith(handleStaticAssetRequest(event.request)); + } +}); +async function handleApiRequest(request) { + const cache = await caches.open(API_CACHE_NAME); + const cachedResponse = await cache.match(request); + if (cachedResponse) { + const cacheTime = new Date(cachedResponse.headers.get('sw-cache-time')); + if (Date.now() - cacheTime.getTime() < MAX_CACHE_AGE) { + console.log('[Service Worker] Serving fresh cached API response'); + return cachedResponse; + } + } + try { + console.log('[Service Worker] Fetching fresh API response from network'); + const networkResponse = await fetch(request); + if (networkResponse.ok) { + const headers = new Headers(networkResponse.headers); + headers.set('sw-cache-time', new Date().toISOString()); + const responseToCache = new Response( + await networkResponse.clone().blob(), + { + status: networkResponse.status, + statusText: networkResponse.statusText, + headers: headers + } + ); + await cache.put(request, responseToCache); + console.log('[Service Worker] Cached fresh API response'); + await manageCacheSize(API_CACHE_NAME, 100); + } + return networkResponse; + } + catch (error) { + console.error('[Service Worker] API request failed, returning offline response'); + return new Response( + JSON.stringify({ + error: 'Offline mode - Bible data not available', + message: 'Please connect to the internet to access Scripture data' + }), + { + status: 503, + headers: { 'Content-Type': 'application/json' } + } + ); + } +} +async function handleExternalBibleResource(request) { + try { + console.log('[Service Worker] Fetching external Bible resource:', request.url); + return await fetch(request); + } + catch (error) { + console.warn('[Service Worker] External resource unavailable offline:', request.url); + return new Response( + ` + +

Offline Mode

+

External Bible resources from ${new URL(request.url).hostname} + are not available offline.

+

Please connect to the internet to access this resource.

+ + `, + { + headers: { 'Content-Type': 'text/html' } + } + ); + } +} +async function handlePdfRequest(request) { + const cache = await caches.open(OFFLINE_PDF_CACHE); + const cachedPdf = await cache.match(request); + if (cachedPdf) { + console.log('[Service Worker] Serving cached PDF:', request.url); + return cachedPdf; + } + try { + console.log('[Service Worker] Fetching PDF from network:', request.url); + const response = await fetch(request); + if (response.status === 200) { + const contentLength = response.headers.get('content-length'); + if (!contentLength || parseInt(contentLength) <= PDF_SIZE_LIMIT) { + await cache.put(request, response.clone()); + console.log('[Service Worker] Cached PDF:', request.url); + await manageCacheSize(OFFLINE_PDF_CACHE, 5); + } + } + return response; + } + catch (error) { + console.warn('[Service Worker] PDF unavailable offline:', request.url); + return new Response( + 'PDF not available offline. Please connect to the internet to access this resource.', + { + status: 503, + headers: { 'Content-Type': 'text/plain' } + } + ); + } +} +async function handleStaticAssetRequest(request) { + try { + const cachedResponse = await caches.match(request); + if (cachedResponse) { + console.log('[Service Worker] Serving cached static asset:', request.url); + return cachedResponse; + } + console.log('[Service Worker] Fetching static asset from network:', request.url); + return await fetch(request); + } + catch (error) { + console.error('[Service Worker] Static asset unavailable:', request.url); + if (event.request.destination === 'document') { + return new Response( + ` + +

Offline

+

Provident Scripture Study is currently offline.

+

Some features may be limited without an internet connection.

+ + `, + { + headers: { 'Content-Type': 'text/html' } + } + ); + } + throw error; + } +} +self.addEventListener('message', (event) => { + switch (event.data.type) { + case 'CLEAR_CACHE': + console.log('[Service Worker] Clearing caches per client request'); + caches.delete(CACHE_NAME).catch(console.warn); + caches.delete(API_CACHE_NAME).catch(console.warn); + break; + case 'CACHE_PDF': + console.log('[Service Worker] Caching PDF from client data:', event.data.url); + caches.open(OFFLINE_PDF_CACHE) + .then(cache => cache.put(event.data.url, new Response(event.data.data))) + .catch(console.error); + break; + default: + console.log('[Service Worker] Received unknown message:', event.data.type); + } +}); +async function manageCacheSize(cacheName, maxSize = 50) { + try { + const cache = await caches.open(cacheName); + const requests = await cache.keys(); + if (requests.length > maxSize) { + const excessCount = requests.length - maxSize; + const excessRequests = requests.slice(0, excessCount); + await Promise.all(excessRequests.map(request => cache.delete(request))); + console.log(`[Service Worker] Trimmed ${excessCount} entries from ${cacheName}`); + } + } + catch (error) { + console.error(`[Service Worker] Failed to manage cache size for ${cacheName}:`, error); + } +} \ No newline at end of file From 11de5256e6d26f1c915142f871df1607793dc48e Mon Sep 17 00:00:00 2001 From: jd-code76 Date: Sat, 1 Nov 2025 21:37:34 -0400 Subject: [PATCH 4/4] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..3ec04b3 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +provinent.org \ No newline at end of file