if ('serviceWorker' in navigator) { const currentHost = window.location.hostname; if (currentHost === 'creative-encounters.com' || currentHost === 'www.creative-encounters.com') { navigator.serviceWorker.addEventListener('message', function(event) { if (event.data && event.data.type === 'SW_UPDATED') { window.location.reload(); } }); navigator.serviceWorker.register('/sw.js'); } else { navigator.serviceWorker.getRegistrations().then(function(registrations) { for (let registration of registrations) { registration.unregister(); } }); } } let modal; let playerBook = ''; let playerVerse = ''; let ceIsAdmin = false; let currentNavFn = null; function loaded() { console.log('[Creative-Encounters] loaded') modal = new bootstrap.Modal(document.getElementById('modal')) checkAuth(); currentNavFn = biLinkPress; setActiveNav('nav-bible'); bibleInjectTocBadges(); get("/bible", "rightColumn").then(() => { bibleInjectTocBadges(); readInjectTocProgress(); }); historyUpdateNav(); favUpdateNav(); // Audio position tracking (saves every ~5 s) const audio = document.getElementById('player-bar-audio'); let _posTimer = null; audio.addEventListener('timeupdate', function() { if (!playerCurrentArchive || this.currentTime < 5) return; if (_posTimer) return; const arch = playerCurrentArchive, time = this.currentTime; _posTimer = setTimeout(() => { posSave(arch, time); _posTimer = null; }, 5000); }); window.addEventListener('scroll', () => { const btn = document.getElementById('bibleBackToTop'); if (!btn) return; btn.classList.toggle('visible', window.scrollY > 200); }, { passive: true }); } function closeNavbar() { /* no-op - collapsible navbar removed */ } function dualColumn() { /* no-op - layout is now always two-column */ } function singleColumn() { /* no-op - layout is now always two-column */ } // --- Auth --- async function checkAuth() { try { const resp = await fetch('/authcheck'); ceIsAdmin = resp.ok; } catch (_) { ceIsAdmin = false; } } function setActiveNav(id) { document.querySelectorAll('.left-nav-item').forEach(el => el.classList.remove('active')); if (id) { const el = document.getElementById(id); if (el) el.classList.add('active'); } } function scriptureLink(book, verse) { document.getElementById('modal-title').innerText = `${book} ${verse}`; document.getElementById('modal-body').innerHTML = ` `; document.getElementById('modal-footer').innerHTML = ''; modal.show(); } function stLinkPress() { if (!confirmLeaveStudy()) return; currentNavFn = stLinkPress; setActiveNav('nav-st'); get("/studies", "rightColumn"); clearLeftColumn(); } function studiesLinkPress(id) { get(`/study/${id}`, "rightColumn"); } function biLinkPress() { if (!confirmLeaveStudy()) return; currentNavFn = biLinkPress; setActiveNav('nav-bible'); get("/bible", "rightColumn").then(() => bibleInjectTocBadges()); clearLeftColumn(); } function abLinkPress() { if (!confirmLeaveStudy()) return; setActiveNav(null); get("/about", "rightColumn"); clearLeftColumn(); } function wnLinkPress() { if (!confirmLeaveStudy()) return; currentNavFn = wnLinkPress; setActiveNav('nav-wn'); get("/whatsnew", "rightColumn"); clearLeftColumn(); } function wwLinkPress() { if (!confirmLeaveStudy()) return; currentNavFn = wwLinkPress; setActiveNav('nav-ww'); get("/wow", "rightColumn"); clearLeftColumn(); } function rsLinkPress() { if (!confirmLeaveStudy()) return; currentNavFn = rsLinkPress; setActiveNav('nav-rs'); get("/renewal", "rightColumn"); clearLeftColumn(); } function wnBookLinkPress(testament, book) { get(`/wnbook/${testament}/${book}`, "rightColumn"); } function wnTopicLinkPress(section, topic) { get(`/wntopic/${section}/${topic}`, "rightColumn"); } function bibleBookLinkPress(book) { bibleCancelSelection(); get(`/bible/${book.replace(/\s+/g, '')}`, "rightColumn").then(() => { bibleLoadHighlights(book); readLoadBook(book); bibleUpdateHlBtn(book); }); } // --- Bible Highlights --- function bibleHlKey(book) { return `ce-hl-${book}`; } function bibleHlTxtKey(book) { return `ce-hl-txt-${book}`; } // --- Verse / Passage Selection --- let bibleSelectionStart = null; // { book, ch, v } let bibleSelectionEnd = null; // { book, ch, v } function bibleVersePress(el) { const book = el.dataset.book; const ch = el.dataset.ch; const v = parseInt(el.dataset.v, 10); if (!bibleSelectionStart) { // Nothing selected yet — set start bibleSelectionStart = { book, ch, v }; el.classList.add('verse-selected'); bibleColorBarShow(); } else if (bibleSelectionStart.book === book && bibleSelectionStart.ch === ch) { if (bibleSelectionStart.v === v && !bibleSelectionEnd) { // Tap same verse again — cancel bibleCancelSelection(); return; } // Same chapter — set/update end, show preview bibleSelectionEnd = { book, ch, v }; const minV = Math.min(bibleSelectionStart.v, v); const maxV = Math.max(bibleSelectionStart.v, v); document.querySelectorAll('.verse-preview').forEach(e => e.classList.remove('verse-preview')); for (let i = minV; i <= maxV; i++) { const e = document.querySelector(`.bible-verse[data-book="${book}"][data-ch="${ch}"][data-v="${i}"]`); if (e) e.classList.add('verse-preview'); } bibleColorBarShow(); } else { // Different chapter — reset to this verse as new start bibleCancelSelection(); bibleSelectionStart = { book, ch, v }; el.classList.add('verse-selected'); bibleColorBarShow(); } } function bibleColorBarShow() { const { book, ch, v: sv } = bibleSelectionStart; const ev = bibleSelectionEnd ? bibleSelectionEnd.v : null; const minV = ev !== null ? Math.min(sv, ev) : sv; const maxV = ev !== null ? Math.max(sv, ev) : sv; const label = minV === maxV ? `${book} ${ch}:${minV}` : `${book} ${ch}:${minV}\u2013${maxV}`; document.getElementById('bible-color-bar-label').textContent = label; const hint = document.getElementById('bible-color-bar-hint'); if (hint) hint.style.display = ev !== null ? 'none' : ''; document.getElementById('bible-color-bar').style.display = ''; } function bibleApplyColor(colorIdx) { if (!bibleSelectionStart) return; const { book, ch } = bibleSelectionStart; const minV = Math.min(bibleSelectionStart.v, bibleSelectionEnd ? bibleSelectionEnd.v : bibleSelectionStart.v); const maxV = Math.max(bibleSelectionStart.v, bibleSelectionEnd ? bibleSelectionEnd.v : bibleSelectionStart.v); const arr = bibleGetHighlights(book); const texts = bibleGetHlTexts(book); const colors = bibleGetHlColors(book); for (let i = minV; i <= maxV; i++) { const key = `${ch}:${i}`; const verseEl = document.querySelector(`.bible-verse[data-book="${book}"][data-ch="${ch}"][data-v="${i}"]`); if (colorIdx === -1) { const idx = arr.indexOf(key); if (idx !== -1) arr.splice(idx, 1); delete texts[key]; delete colors[key]; if (verseEl) { verseEl.classList.remove('highlighted'); verseEl.removeAttribute('data-hl'); } } else { if (!arr.includes(key)) arr.push(key); if (verseEl) texts[key] = verseEl.textContent.trim(); colors[key] = colorIdx; if (verseEl) { verseEl.classList.add('highlighted'); verseEl.dataset.hl = String(colorIdx); } } } if (Object.keys(texts).length) localStorage.setItem(bibleHlTxtKey(book), JSON.stringify(texts)); else localStorage.removeItem(bibleHlTxtKey(book)); if (Object.keys(colors).length) localStorage.setItem(bibleHlColorKey(book), JSON.stringify(colors)); else localStorage.removeItem(bibleHlColorKey(book)); bibleSaveHighlights(book, arr); bibleUpdateHlBtn(book); bibleInjectTocBadges(); bibleCancelSelection(); } function bibleCancelSelection() { document.querySelectorAll('.verse-selected').forEach(e => e.classList.remove('verse-selected')); document.querySelectorAll('.verse-preview').forEach(e => e.classList.remove('verse-preview')); bibleSelectionStart = null; bibleSelectionEnd = null; const bar = document.getElementById('bible-color-bar'); if (bar) bar.style.display = 'none'; } function bibleGetHighlights(book) { try { return JSON.parse(localStorage.getItem(bibleHlKey(book)) || '[]'); } catch { return []; } } function bibleGetHlTexts(book) { try { return JSON.parse(localStorage.getItem(bibleHlTxtKey(book)) || '{}'); } catch { return {}; } } function bibleSaveHighlights(book, arr) { if (arr.length === 0) { localStorage.removeItem(bibleHlKey(book)); localStorage.removeItem(bibleHlTxtKey(book)); } else localStorage.setItem(bibleHlKey(book), JSON.stringify(arr)); } // Count distinct passages (runs of consecutive verses in the same chapter) function bibleCountPassages(arr) { // Group verse numbers by chapter const byChapter = {}; arr.forEach(key => { const [ch, v] = key.split(':'); (byChapter[ch] = byChapter[ch] || []).push(parseInt(v, 10)); }); let passages = 0; Object.values(byChapter).forEach(verses => { verses.sort((a, b) => a - b); passages++; // first verse always starts a passage for (let i = 1; i < verses.length; i++) { if (verses[i] !== verses[i - 1] + 1) passages++; // gap = new passage } }); return passages; } // Group a verse key array into passage run objects for rendering function bibleGroupPassagesForBook(arr, book) { const byChapter = {}; arr.forEach(key => { const [ch, v] = key.split(':'); (byChapter[ch] = byChapter[ch] || []).push(parseInt(v, 10)); }); const colors = bibleGetHlColors(book); const texts = bibleGetHlTexts(book); const runs = []; Object.keys(byChapter).sort((a, b) => parseInt(a) - parseInt(b)).forEach(ch => { const verses = byChapter[ch].slice().sort((a, b) => a - b); let runStart = verses[0], runEnd = verses[0]; for (let i = 1; i <= verses.length; i++) { if (i < verses.length && verses[i] === runEnd + 1) { runEnd = verses[i]; continue; } // Emit run const colorIdx = colors[`${ch}:${runStart}`] || 0; const allText = []; for (let v = runStart; v <= runEnd; v++) { const key = `${ch}:${v}`; const el = document.querySelector(`.bible-verse[data-book="${book}"][data-ch="${ch}"][data-v="${v}"]`); const t = (el ? el.textContent.trim() : null) || texts[key] || ''; if (t) allText.push(t); } const firstText = allText[0] || ''; const fullText = allText.join(' '); const isRange = runStart !== runEnd; const label = isRange ? `${ch}:​${runStart}–${runEnd}` : `${ch}:${runStart}`; const displayText = isRange ? firstText + '…' : firstText; const note = bibleGetNote(book, ch, runStart); const noteId = `${book.replace(/\s+/g, '_')}-${ch}-${runStart}`; runs.push({ ch, startV: runStart, endV: runEnd, colorIdx, displayText, fullText, note, noteId, label, isRange }); if (i < verses.length) { runStart = verses[i]; runEnd = verses[i]; } } }); return runs; } function bibleHlCopyPassage(book, ch, startV, endV, fullText) { const ref = startV === endV ? `${book} ${ch}:${startV}` : `${book} ${ch}:${startV}\u2013${endV}`; navigator.clipboard.writeText(`\u201C${fullText}\u201D \u2014 ${ref}`).then(() => { const el = document.querySelector(`.bible-verse[data-book="${book}"][data-ch="${ch}"][data-v="${startV}"]`); if (el) { el.classList.add('bible-verse-copied'); setTimeout(() => el.classList.remove('bible-verse-copied'), 800); } }).catch(() => {}); } function bibleUpdateHlBtn(book) { const btn = document.querySelector('.bible-hl-btn'); if (!btn) return; const arr = bibleGetHighlights(book); const count = bibleCountPassages(arr); btn.style.display = count ? 'inline-block' : 'none'; } function bibleVerseKey(el) { return `${el.dataset.ch}:${el.dataset.v}`; } function bibleLoadHighlights(book) { const arr = bibleGetHighlights(book); if (!arr.length) return; const set = new Set(arr); const texts = bibleGetHlTexts(book); const colors = bibleGetHlColors(book); let textsDirty = false; document.querySelectorAll('.bible-verse[data-book]').forEach(el => { if (el.dataset.book === book && set.has(bibleVerseKey(el))) { el.classList.add('highlighted'); const key = bibleVerseKey(el); el.dataset.hl = String(colors[key] || 0); if (!texts[key]) { texts[key] = el.textContent.trim(); textsDirty = true; } } }); if (textsDirty) localStorage.setItem(bibleHlTxtKey(book), JSON.stringify(texts)); bibleLoadNotes(book); } function bibleInjectTocBadges() { // Collect all highlighted book counts from localStorage const counts = {}; let total = 0; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (!k.startsWith('ce-hl-') || k.startsWith('ce-hl-txt-') || k.startsWith('ce-hl-color-')) continue; const book = k.slice(6); try { const arr = JSON.parse(localStorage.getItem(k) || '[]'); if (Array.isArray(arr) && arr.length) { const n = bibleCountPassages(arr); counts[book] = n; total += n; } } catch {} } // Inject/update badges on any visible TOC items document.querySelectorAll('.bible-book-item[id^="bible-book-"]').forEach(el => { const book = el.id.slice(11); let badge = el.querySelector('.bible-hl-badge'); if (counts[book]) { if (!badge) { badge = document.createElement('span'); badge.className = 'bible-hl-badge'; el.appendChild(badge); } badge.textContent = counts[book]; } else if (badge) { badge.remove(); } }); // Update global highlights nav item const globalNav = document.getElementById('nav-hl'); const globalBadge = document.getElementById('global-hl-badge'); if (globalNav) globalNav.style.display = total ? '' : 'none'; if (globalBadge) globalBadge.textContent = total; readInjectTocProgress(); } function bibleHlSearch(input) { const q = input.value.trim().toLowerCase(); // Filter items document.querySelectorAll('#hl-list .bible-hl-item').forEach(el => { el.style.display = (!q || el.dataset.text.includes(q)) ? '' : 'none'; }); // Hide groups where all items are hidden document.querySelectorAll('#hl-list .bible-hl-group').forEach(group => { const visible = group.querySelectorAll('.bible-hl-item:not([style*="display: none"])'); group.style.display = visible.length ? '' : 'none'; }); // Hide book headings where all following groups are hidden (global modal) document.querySelectorAll('#hl-list .bible-hl-book-heading').forEach(heading => { let next = heading.nextElementSibling; let anyVisible = false; while (next && !next.classList.contains('bible-hl-book-heading')) { if (next.style.display !== 'none') { anyVisible = true; break; } next = next.nextElementSibling; } heading.style.display = anyVisible ? '' : 'none'; }); } function bibleHlPassageRow(book, run, onClickFn, extraClass) { const { ch, startV, endV, colorIdx, displayText, fullText, note, noteId, label, isRange } = run; const safeBook = book.replace(/'/g, "\\'"); const safeFullText = fullText.replace(/'/g, "\\'"); let html = `
]/g, '')}"> `; html += `
`; html += ``; html += `
${label} ${displayText}`; if (note) html += `
📝 ${note}
`; else html += ``; html += `
`; html += `
`; html += ``; html += ``; html += `
`; html += `
`; return html; } function bibleViewAllHighlights() { const titleEl = document.getElementById('modal-title'); const bodyEl = document.getElementById('modal-body'); const footerEl = document.getElementById('modal-footer'); titleEl.textContent = 'All Highlights'; footerEl.innerHTML = ''; // Collect all books with highlights (skip txt/color sub-keys) const books = {}; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (!k.startsWith('ce-hl-') || k.startsWith('ce-hl-txt-') || k.startsWith('ce-hl-color-')) continue; const book = k.slice(6); try { const arr = JSON.parse(localStorage.getItem(k) || '[]'); if (Array.isArray(arr) && arr.length) books[book] = arr; } catch {} } if (!Object.keys(books).length) { bodyEl.innerHTML = '

No highlighted verses yet.

'; modal.show(); return; } let html = ''; Object.keys(books).sort().forEach(book => { const runs = bibleGroupPassagesForBook(books[book], book); if (!runs.length) return; html += `
${book}
`; let lastCh = null; runs.forEach(run => { if (run.ch !== lastCh) { if (lastCh !== null) html += ''; html += `
Chapter ${run.ch}
`; lastCh = run.ch; } const safeBook = book.replace(/'/g, "\\'"); html += bibleHlPassageRow(book, run, `bibleGoToVerseFromGlobal('${safeBook}','${run.ch}',${run.startV})`, ''); }); if (lastCh !== null) html += '
'; }); bodyEl.innerHTML = '
' + html + '
'; modal.show(); document.getElementById('hl-search').focus(); } function bibleGoToVerseFromGlobal(book, ch, v) { modal.hide(); // If the book is already loaded, jump directly const existing = document.querySelector(`.bible-verse[data-book="${book}"]`); if (existing) { bibleGoToVerse(book, ch, v); return; } // Otherwise load the book first, then jump get(`/bible/${book.replace(/\s+/g, '')}`, 'rightColumn').then(() => { bibleLoadHighlights(book); readLoadBook(book); bibleUpdateHlBtn(book); bibleGoToVerse(book, ch, v); }); } function bibleViewHighlights(book) { const arr = bibleGetHighlights(book); const titleEl = document.getElementById('modal-title'); const bodyEl = document.getElementById('modal-body'); const footerEl = document.getElementById('modal-footer'); titleEl.textContent = `${book} — Highlights`; footerEl.innerHTML = ''; if (!arr.length) { bodyEl.innerHTML = '

No highlighted verses yet. Tap any verse to highlight it.

'; } else { const runs = bibleGroupPassagesForBook(arr, book); let html = ''; let lastCh = null; runs.forEach(run => { if (run.ch !== lastCh) { if (lastCh !== null) html += ''; html += `
Chapter ${run.ch}
`; lastCh = run.ch; } const safeBook = book.replace(/'/g, "\'"); html += bibleHlPassageRow(book, run, `bibleGoToVerse('${safeBook}','${run.ch}',${run.startV})`, ''); }); if (lastCh !== null) html += '
'; footerEl.innerHTML = ``; bodyEl.innerHTML = '
' + html + '
'; document.getElementById('hl-search').focus(); } modal.show(); } function bibleClearHighlights(book) { localStorage.removeItem(bibleHlKey(book)); localStorage.removeItem(bibleHlTxtKey(book)); localStorage.removeItem(bibleHlColorKey(book)); document.querySelectorAll('.bible-verse.highlighted').forEach(el => { el.classList.remove('highlighted'); el.removeAttribute('data-hl'); }); bibleUpdateHlBtn(book); bibleInjectTocBadges(); modal.hide(); } function bibleGoToVerse(book, ch, v) { modal.hide(); const el = document.querySelector(`.bible-verse[data-book="${book}"][data-ch="${ch}"][data-v="${v}"]`); if (!el) return; el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.classList.add('bible-verse-flash'); setTimeout(() => el.classList.remove('bible-verse-flash'), 1500); } let currentOpenArchive = null; async function programLinkPress(title, verse, id, archive) { const panel = document.getElementById('prog-expand-' + archive); const chevron = document.getElementById('prog-chevron-' + archive); // Toggle closed if already open if (currentOpenArchive === archive) { panel.innerHTML = ''; panel.classList.remove('open'); if (chevron) chevron.classList.remove('open'); currentOpenArchive = null; return; } // Close previously open panel if (currentOpenArchive) { const prevPanel = document.getElementById('prog-expand-' + currentOpenArchive); const prevChevron = document.getElementById('prog-chevron-' + currentOpenArchive); if (prevPanel) { prevPanel.innerHTML = ''; prevPanel.classList.remove('open'); } if (prevChevron) prevChevron.classList.remove('open'); } currentOpenArchive = archive; await get(`/program/${id}/${archive}`, 'prog-expand-' + archive); panel.classList.add('open'); if (chevron) chevron.classList.add('open'); } function closeProgramList() { const pl = document.getElementById('programList'); if (pl) { pl.remove(); lastArchive = undefined; } } async function loadStudy(id) { await get(`/study/${id}`, "rightColumn"); const spans = document.getElementsByClassName('study-scripture'); for (let i = 0; i < spans.length; i++) { await getScriptureStudy(spans[i].id, spans[i].dataset.book, spans[i].dataset.verse); } } let studyDirty = false; function markStudyDirty() { studyDirty = true; } function clearStudyDirty() { studyDirty = false; } async function loadNewStudy() { if (!confirmLeaveStudy()) return; await get("/studynew", "rightColumn"); document.getElementById('studyID').value = randomGuid(); clearStudyDirty(); document.getElementById('studyTitle').addEventListener('input', markStudyDirty); } async function loadEditStudy(id) { if (!confirmLeaveStudy()) return; await get("/studynew", "rightColumn"); clearStudyDirty(); document.getElementById('studyTitle').addEventListener('input', markStudyDirty); fetch(`/studydata/${id}`) .then(response => response.json()) .then(data => { document.getElementById('studyID').value = data.id; document.getElementById('studyTitle').value = data.title; data.data.forEach(item => { if (item.type === 'text') { addStudyText(item.content); } else if (item.type === 'scripture') { addStudyScripture(item.book, item.verse); } }); }); } function confirmLeaveStudy() { if (studyDirty) { return confirm('You have unsaved changes. Leave anyway?'); } return true; } function deleteStudy(id) { if (confirm('Are you sure you want to delete this study? This action cannot be undone.')) { fetch(getAuth(`/studydelete/${id}`), { method: 'DELETE' }) .then(response => { if (response.ok) { stLinkPress(); } else { alert('Error deleting study.'); } }) .catch(error => { console.error('Error:', error); alert('Error deleting study.'); }); } } function saveNewStudy() { const id = document.getElementById('studyID').value.trim(); const title = document.getElementById('studyTitle').value.trim(); if (!title) { alert('Please enter a title for the study.'); return; } const data = { id: id, title: title, data: [] }; document.querySelectorAll('.study-data').forEach(ta => { if (ta.tagName.toLowerCase() === 'textarea') { data.data.push({ type: 'text', content: ta.value.trim() }); } else if (ta.tagName.toLowerCase() === 'span') { const book = ta.querySelector('input[name="book"]').value.trim(); const verse = ta.querySelector('input[name="verse"]').value.trim(); if (book && verse) { data.data.push({ type: 'scripture', book: book, verse: verse }); } } }); if (data.data.length === 0) { alert('Please add at least one text or scripture to the study.'); return; } fetch('/studysave', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => { if (response.ok) { clearStudyDirty(); loadStudy(data.id); } else { alert('Error saving study.'); } }) .catch(error => { console.error('Error:', error); alert('Error saving study.'); }); } function addStudyText(value) { const container = document.getElementById('extraInputs'); const div = document.createElement('div'); div.className = 'study-block mb-2'; const ta = document.createElement('textarea'); ta.className = 'form-control study-data'; ta.rows = 1; ta.value = value || ''; ta.placeholder = 'Text...'; ta.addEventListener('input', function() { markStudyDirty(); this.style.height = 'auto'; this.style.height = this.scrollHeight + 'px'; }); // Set initial height for pre-filled content setTimeout(() => { ta.style.height = ta.scrollHeight + 'px'; }, 0); div.appendChild(blockControls(div)); div.appendChild(ta); container.appendChild(div); ta.focus(); } function addStudyScripture(book, verse) { const container = document.getElementById('extraInputs'); const div = document.createElement('div'); div.className = 'study-block mb-2'; const inner = document.createElement('span'); inner.className = 'd-flex gap-2 align-items-center study-data'; const bookInput = document.createElement('input'); bookInput.type = 'text'; bookInput.className = 'form-control'; bookInput.name = 'book'; bookInput.placeholder = 'Book'; bookInput.setAttribute('list', 'bible-books'); bookInput.style.maxWidth = '200px'; bookInput.value = book || ''; bookInput.addEventListener('input', markStudyDirty); const verseInput = document.createElement('input'); verseInput.type = 'text'; verseInput.className = 'form-control'; verseInput.name = 'verse'; verseInput.placeholder = 'e.g. 3:16'; verseInput.style.maxWidth = '160px'; verseInput.value = verse || ''; verseInput.addEventListener('input', markStudyDirty); inner.appendChild(bookInput); inner.appendChild(verseInput); div.appendChild(blockControls(div)); div.appendChild(inner); container.appendChild(div); bookInput.focus(); } function blockControls(div) { const bar = document.createElement('div'); bar.className = 'study-block-controls'; const up = document.createElement('button'); up.type = 'button'; up.className = 'study-block-btn'; up.title = 'Move up'; up.innerHTML = '↑'; up.onclick = () => { const prev = div.previousElementSibling; if (prev) div.parentNode.insertBefore(div, prev); }; const down = document.createElement('button'); down.type = 'button'; down.className = 'study-block-btn'; down.title = 'Move down'; down.innerHTML = '↓'; down.onclick = () => { const next = div.nextElementSibling; if (next) div.parentNode.insertBefore(next, div); }; const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'study-block-btn study-block-btn-remove'; remove.title = 'Remove'; remove.innerHTML = '×'; remove.onclick = () => { div.remove(); markStudyDirty(); }; bar.appendChild(up); bar.appendChild(down); bar.appendChild(remove); return bar; } function showPersistentPlayer(title, sub, archiveSrc, hasScripture) { const audio = document.getElementById('player-bar-audio'); audio.src = `https://s3.amazonaws.com/creative-encounters/${archiveSrc}.mp3`; document.getElementById('player-bar-title').textContent = title; document.getElementById('player-bar-sub').textContent = sub || ''; const scriptureBtn = document.getElementById('player-scripture-btn'); hasScripture ? scriptureBtn.classList.remove('d-none') : scriptureBtn.classList.add('d-none'); document.getElementById('persistent-player').classList.remove('d-none'); document.body.classList.add('player-active'); playerCurrentArchive = archiveSrc; favUpdatePlayerBtn(archiveSrc); // Restore saved position if available const savedPos = posGet(archiveSrc); if (savedPos > 5) { const handler = function() { audio.currentTime = savedPos; }; audio.addEventListener('canplay', handler, { once: true }); } audio.play().catch(() => {}); } function closePlayer() { const audio = document.getElementById('player-bar-audio'); audio.pause(); audio.src = ''; document.getElementById('persistent-player').classList.add('d-none'); document.body.classList.remove('player-active'); playerCurrentArchive = null; } function loadSong(url, title) { showPersistentPlayer(title, '', url, false); } function loadProgram(title, verse, day, archive) { modal.hide(); playerBook = title; playerVerse = verse; historyRecord(title, `${verse} (${day})`, archive); showPersistentPlayer(title, `${verse} (${day})`, archive, true); } function loadTopicProgram(archive, title) { showPersistentPlayer(title, '', archive, false); } // --- Play History --- const HISTORY_KEY = 'ce-play-history'; function historyGet() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); } catch { return []; } } function historyRecord(title, sub, archive) { const arr = historyGet(); // Remove existing entry for same archive so it bubbles to top const idx = arr.findIndex(e => e.archive === archive); if (idx !== -1) arr.splice(idx, 1); arr.unshift({ title, sub, archive, ts: Date.now() }); // Keep at most 100 entries if (arr.length > 100) arr.length = 100; localStorage.setItem(HISTORY_KEY, JSON.stringify(arr)); historyUpdateNav(); } function historyUpdateNav() { const arr = historyGet(); const nav = document.getElementById('nav-history'); const badge = document.getElementById('history-badge'); if (nav) nav.style.display = arr.length ? '' : 'none'; if (badge) badge.textContent = arr.length; } function showPlayHistory() { const arr = historyGet(); const titleEl = document.getElementById('modal-title'); const bodyEl = document.getElementById('modal-body'); const footerEl = document.getElementById('modal-footer'); titleEl.textContent = 'Play History'; footerEl.innerHTML = arr.length ? `` : ''; if (!arr.length) { bodyEl.innerHTML = '

No programs played yet.

'; modal.show(); return; } let html = '
'; arr.forEach(e => { const date = new Date(e.ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); const searchText = `${e.title} ${e.sub}`.toLowerCase(); html += `
`; html += `
${e.title}
${e.sub}
${date}
`; html += ``; html += '
'; }); html += '
'; bodyEl.innerHTML = html; modal.show(); document.getElementById('history-search').focus(); } function historySearch(input) { const q = input.value.trim().toLowerCase(); document.querySelectorAll('#history-list .history-item').forEach(el => { el.style.display = (!q || el.dataset.text.includes(q)) ? '' : 'none'; }); } function historyReplay(archive, title, sub) { modal.hide(); showPersistentPlayer(title, sub, archive, true); } function historyClear() { localStorage.removeItem(HISTORY_KEY); historyUpdateNav(); modal.hide(); } function getAuth(path) { return path; // auth cookie is sent automatically } async function get(path, id) { return fetch(getAuth(path)) .then(function (response) { return response.text(); }) .then(function (data) { document.getElementById(id).innerHTML = data; scrollToTop(); return data; }) .catch(function (error) { console.log(error); }); }; function clearLeftColumn() { document.getElementById('leftColumn').innerHTML = ''; } function hideScripture() { // no-op — scripture is now shown in the modal } function getScripture(data, verse, labels) { let text, stc, stv, enc, env, chps; const parts = verse.split(" - "); [stc, stv] = startChapterVerse(parts[0], data); if (parts.length === 2) { [enc, env] = endChapterVerse(parts[1], data); } if (parts.length === 1) { if (parts[0].includes(":")) { if (parts[0].includes("-")) { [enc, env] = [stc, parts[0].split("-")[1]]; } else { [enc, env] = [stc, stv]; } } else { [enc, env] = [stc, data[stc-1].verses.length]; } } text = ''; chps = enc - stc + 1; if (chps === 1) { if (labels) { text += formatChapter(stc); } text += formatVerse(stv, env, data[stc-1].verses, labels); } else { if (labels) { text += formatChapter(stc); } text += formatVerse(stv, data[stc-1].verses.length, data[stc-1].verses, labels); if (chps >2) { for (let i = stc; i < enc-1; i++) { text += "

"; if (labels) { text += formatChapter(i+1); } text += formatVerse(1, data[i].verses.length, data[i].verses, labels); } } text += "

"; if (labels) { text += formatChapter(enc); } text += formatVerse(1, env, data[enc-1].verses, labels); } return text; } async function getScriptureStudy(id, book, verse) { const container = document.getElementById(id); if (!container) return; // Render a clickable reference link immediately — no need to prefetch container.innerHTML = ` 📖 ${book} ${verse} `; } function getScriptureProgram(book, verse) { fetch("/scripture/" + book.replace(/\s+/g, '')) .then(response => response.text()) .then(data => { data = JSON.parse(data); document.getElementById('modal-title').innerText = `${book} ${verse}`; document.getElementById('modal-body').innerHTML = `
${getScripture(data, verse, true)}
`; document.getElementById('modal-footer').innerHTML = ''; modal.show(); }) .catch(error => console.error(error)); } function formatVerse(start, end, section, labels) { let text = ''; for (let i = start; i <= end; i++) { if (labels) { text += `${i}`; } text += section[i-1]; } return text; } function formatChapter(num) { return `
Chapter ${num}
`; } function startChapterVerse(verse) { const parts = verse.split(":"); return [ parseInt(parts[0]), (parts.length === 2 ? parseInt(parts[1]) : 1) ]; } function endChapterVerse(verse, data) { const parts = verse.split(":"); return [ parseInt(parts[0]), (parts.length === 2 ? parseInt(parts[1]) : data[parts[0]-1].verses.length) ]; } function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } function randomGuid() { // RFC4122 version 4 compliant UUID return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } window.onload = loaded; const bibleAppBooks = { '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', 'Psalm': 'PSA', 'Proverbs': 'PRO', 'Ecclesiastes': 'ECC', 'Song of Songs': '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' } // ============================================================= // Color-coded Highlights (helpers) // ============================================================= function bibleHlColorKey(book) { return `ce-hl-color-${book}`; } function bibleGetHlColors(book) { try { return JSON.parse(localStorage.getItem(bibleHlColorKey(book)) || '{}'); } catch { return {}; } } // ============================================================= // Verse Notes // ============================================================= function bibleNotesKey(book) { return `ce-notes-${book}`; } function bibleGetNotes(book) { try { return JSON.parse(localStorage.getItem(bibleNotesKey(book)) || '{}'); } catch { return {}; } } function bibleGetNote(book, ch, v) { return bibleGetNotes(book)[`${ch}:${v}`] || ''; } function bibleSaveNote(book, ch, v, text) { const notes = bibleGetNotes(book); const key = `${ch}:${v}`; if (text) notes[key] = text; else delete notes[key]; if (Object.keys(notes).length) localStorage.setItem(bibleNotesKey(book), JSON.stringify(notes)); else localStorage.removeItem(bibleNotesKey(book)); const dot = document.querySelector(`.bible-verse-num[data-book="${book}"][data-ch="${ch}"][data-v="${v}"] .note-dot`); if (dot) dot.classList.toggle('visible', !!text); } function bibleLoadNotes(book) { const notes = bibleGetNotes(book); Object.entries(notes).forEach(([key, text]) => { if (!text) return; const [ch, v] = key.split(':'); const dot = document.querySelector(`.bible-verse-num[data-book="${book}"][data-ch="${ch}"][data-v="${v}"] .note-dot`); if (dot) dot.classList.add('visible'); }); } let currentNoteVerse = null; function bibleVerseNumPress(el) { const book = el.dataset.book; const ch = el.dataset.ch; const v = el.dataset.v; const editor = document.getElementById('verse-note-editor'); if (currentNoteVerse && currentNoteVerse.book === book && currentNoteVerse.ch === ch && currentNoteVerse.v === v) { bibleNoteCancel(); return; } bibleNoteCancel(); currentNoteVerse = { book, ch, v }; document.getElementById('verse-note-label').textContent = `${book} ${ch}:${v}`; document.getElementById('verse-note-text').value = bibleGetNote(book, ch, v); const p = el.closest('.bible-chapter-text'); if (p) p.insertAdjacentElement('afterend', editor); else document.getElementById('rightColumn').appendChild(editor); editor.style.display = ''; document.getElementById('verse-note-text').focus(); } function bibleNoteCancel() { const editor = document.getElementById('verse-note-editor'); editor.style.display = 'none'; document.body.appendChild(editor); currentNoteVerse = null; } function bibleNoteSave() { if (!currentNoteVerse) return; const { book, ch, v } = currentNoteVerse; const text = document.getElementById('verse-note-text').value.trim(); bibleSaveNote(book, ch, v, text); bibleNoteCancel(); } function bibleNoteDelete() { if (!currentNoteVerse) return; const { book, ch, v } = currentNoteVerse; bibleSaveNote(book, ch, v, ''); bibleNoteCancel(); } function bibleHlCopyVerse(book, ch, v) { const texts = bibleGetHlTexts(book); const verseEl = document.querySelector(`.bible-verse[data-book="${book}"][data-ch="${ch}"][data-v="${v}"]`); const text = (verseEl ? verseEl.textContent.trim() : null) || texts[`${ch}:${v}`] || `${book} ${ch}:${v}`; navigator.clipboard.writeText(`"${text}" — ${book} ${ch}:${v}`).then(() => { if (verseEl) { verseEl.classList.add('bible-verse-copied'); setTimeout(() => verseEl.classList.remove('bible-verse-copied'), 800); } }).catch(() => {}); } function bibleHlToggleNote(noteId) { const ed = document.getElementById(`hl-note-ed-${noteId}`); if (!ed) return; const isOpen = ed.style.display !== 'none'; document.querySelectorAll('[id^="hl-note-ed-"]').forEach(e => { e.style.display = 'none'; }); if (!isOpen) { ed.style.display = ''; const ta = document.getElementById(`hl-note-txt-${noteId}`); if (ta) ta.focus(); } } function bibleHlNoteSave(book, ch, v, noteId) { const ta = document.getElementById(`hl-note-txt-${noteId}`); if (!ta) return; const text = ta.value.trim(); bibleSaveNote(book, ch, v, text); const prev = document.getElementById(`hl-note-prev-${noteId}`); if (prev) { prev.textContent = text ? `\uD83D\uDCDD ${text}` : ''; prev.style.display = text ? '' : 'none'; } const ed = document.getElementById(`hl-note-ed-${noteId}`); if (ed) ed.style.display = 'none'; } function bibleHlNoteCancel(noteId) { const ed = document.getElementById(`hl-note-ed-${noteId}`); if (ed) ed.style.display = 'none'; } // ============================================================= // Reading Plan // ============================================================= const BIBLE_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 }; function readKey(book) { return `ce-read-${book}`; } function readGetChapters(book) { try { return new Set(JSON.parse(localStorage.getItem(readKey(book)) || '[]')); } catch { return new Set(); } } function readSaveChapters(book, set) { if (!set.size) localStorage.removeItem(readKey(book)); else localStorage.setItem(readKey(book), JSON.stringify([...set])); } function toggleChapterRead(btn) { const book = btn.dataset.book; const ch = parseInt(btn.dataset.ch, 10); const set = readGetChapters(book); if (set.has(ch)) { set.delete(ch); btn.classList.remove('read'); } else { set.add(ch); btn.classList.add('read'); } readSaveChapters(book, set); readInjectTocProgress(); } function readLoadBook(book) { const set = readGetChapters(book); document.querySelectorAll(`.chapter-read-btn[data-book="${book}"]`).forEach(btn => { btn.classList.toggle('read', set.has(parseInt(btn.dataset.ch, 10))); }); } function readInjectTocProgress() { document.querySelectorAll('.bible-book-item[id^="bible-book-"]').forEach(el => { const book = el.id.slice(11); const total = BIBLE_CHAPTER_COUNTS[book]; if (!total) return; const read = readGetChapters(book).size; let prog = el.querySelector('.bible-read-prog'); if (!prog) { prog = document.createElement('span'); prog.className = 'bible-read-prog'; el.appendChild(prog); } prog.textContent = read > 0 ? (read === total ? '\u2713' : `${read}/${total}`) : ''; }); } // ============================================================= // Favorites // ============================================================= const FAV_KEY = 'ce-favorites'; function favGet() { try { return JSON.parse(localStorage.getItem(FAV_KEY) || '[]'); } catch { return []; } } function favIsFavorite(archive) { return favGet().some(e => e.archive === archive); } function favToggle(archive, title, sub) { const arr = favGet(); const idx = arr.findIndex(e => e.archive === archive); if (idx !== -1) arr.splice(idx, 1); else { arr.unshift({ title, sub, archive, ts: Date.now() }); if (arr.length > 200) arr.length = 200; } localStorage.setItem(FAV_KEY, JSON.stringify(arr)); favUpdateNav(); favUpdatePlayerBtn(archive); } function favUpdateNav() { const arr = favGet(); const nav = document.getElementById('nav-fav'); const badge = document.getElementById('fav-badge'); if (nav) nav.style.display = arr.length ? '' : 'none'; if (badge) badge.textContent = arr.length; } function favUpdatePlayerBtn(archive) { const btn = document.getElementById('player-fav-btn'); if (!btn) return; const isFav = !!(archive && favIsFavorite(archive)); btn.textContent = isFav ? '\u2665' : '\u2661'; btn.title = isFav ? 'Remove from favorites' : 'Add to favorites'; btn.dataset.archive = archive || ''; } function togglePlayerFavorite() { const btn = document.getElementById('player-fav-btn'); const archive = btn ? btn.dataset.archive : ''; if (!archive) return; const title = document.getElementById('player-bar-title').textContent; const sub = document.getElementById('player-bar-sub').textContent; favToggle(archive, title, sub); } function showFavorites() { const arr = favGet(); const titleEl = document.getElementById('modal-title'); const bodyEl = document.getElementById('modal-body'); const footerEl = document.getElementById('modal-footer'); titleEl.textContent = 'Favorites'; footerEl.innerHTML = arr.length ? `` : ''; if (!arr.length) { bodyEl.innerHTML = '

No favorites yet. Tap \u2661 while a program is playing.

'; modal.show(); return; } let html = '
'; arr.forEach(e => { const safeTitle = e.title.replace(/'/g, "\\'"); const safeSub = (e.sub || '').replace(/'/g, "\\'"); const searchText = `${e.title} ${e.sub || ''}`.toLowerCase().replace(/"/g, ''); html += `
`; html += `
${e.title}
${e.sub || ''}
`; html += ``; html += ``; html += '
'; }); html += '
'; bodyEl.innerHTML = html; modal.show(); document.getElementById('fav-search').focus(); } function favSearch(input) { const q = input.value.trim().toLowerCase(); document.querySelectorAll('#fav-list .history-item').forEach(el => { el.style.display = (!q || el.dataset.text.includes(q)) ? '' : 'none'; }); } function favRemoveItem(archive) { const arr = favGet().filter(e => e.archive !== archive); if (arr.length) localStorage.setItem(FAV_KEY, JSON.stringify(arr)); else localStorage.removeItem(FAV_KEY); favUpdateNav(); favUpdatePlayerBtn(archive); showFavorites(); } function favClear() { localStorage.removeItem(FAV_KEY); favUpdateNav(); const btn = document.getElementById('player-fav-btn'); if (btn) btn.textContent = '\u2661'; modal.hide(); } // ============================================================= // Resume Position // ============================================================= let playerCurrentArchive = null; let posSaveTimer = null; function posKey(archive) { return `ce-pos-${archive}`; } function posSave(archive, seconds) { if (seconds > 5) localStorage.setItem(posKey(archive), String(Math.floor(seconds))); else localStorage.removeItem(posKey(archive)); } function posGet(archive) { const v = localStorage.getItem(posKey(archive)); return v ? parseInt(v, 10) : 0; }