Split app.js into one script per feature; versioning covers every public file; README for the new features
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c1f4a220f9
commit
1564941ea5
20 files changed
+2297
-2275
No files matched your search
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
// ---- Audiobooks: embedded site. Sign-in details saved in Settings (this browser only) can be copied with one tap,
|
||||
// because a page can't type into another site's login form.
|
||||
const abCreds = () => store.get('ab_creds', { u: '', p: '' });
|
||||
// Podcast Admin (Verse by Verse): the site refuses to be framed (X-Frame-Options), so this is a launcher.
|
||||
function viewVerse() {
|
||||
const url = cfg.verse, host = new URL(url).host;
|
||||
$('#view').innerHTML = `<div class="wrap"><h1>Podcast Admin</h1><p class="sub">${esc(host + new URL(url).pathname)}</p>
|
||||
<div class="card" style="max-width:none"><p style="margin-top:0">This site doesn't allow being shown inside another app, so it opens in its own tab.</p>
|
||||
<div class="bar"><a class="btn" href="${esc(url)}" target="_blank" rel="noopener">${icon('book')}Open admin</a></div></div></div>`;
|
||||
}
|
||||
|
||||
// Frames that must keep running (audio) live outside #view: created once, then only shown or hidden.
|
||||
function showFrame(key, url, title, allow) {
|
||||
const wrap = $('#audiowrap');
|
||||
let f = wrap.querySelector(`iframe[data-k="${key}"]`);
|
||||
if (!f) { f = document.createElement('iframe'); f.dataset.k = key; f.src = url; f.title = title; f.allow = allow; wrap.appendChild(f); }
|
||||
wrap.querySelectorAll('iframe').forEach(x => x.classList.toggle('act', x === f));
|
||||
wrap.className = 'on'; document.body.classList.add('onaudio');
|
||||
}
|
||||
|
||||
// Podcast: opens in a frame when it has a secure (https) address; a plain-http address cannot be framed by a secure page.
|
||||
// Podcast: a plain-http address on the home network. A secure page cannot frame http content, and the address is
|
||||
// private to the LAN, so this opens it directly in the browser (which works on the home network or over a VPN).
|
||||
function viewPodcast() {
|
||||
const url = cfg.podcast, u = new URL(url), canFrame = u.protocol === 'https:';
|
||||
$('#view').innerHTML = `<div class="wrap"><h1>Podcast</h1><p class="sub">${esc(u.host)}</p>
|
||||
<div class="bar" style="margin-bottom:0"><a class="btn" href="${esc(url)}" target="_blank" rel="noopener">Open in new tab</a></div>
|
||||
${canFrame ? '<p class="sub" style="margin:8px 0 0">If the page below stays blank, the app does not allow being shown inside another app. Use Open in new tab.</p>'
|
||||
: '<div class="card" style="max-width:none;margin-top:12px"><p style="margin:0">This is a plain <b>http</b> address, which a secure page cannot show inside itself. It opens in its own tab, and works at home or on your VPN.</p></div>'}</div>`;
|
||||
if (canFrame) showFrame('podcast', url, 'Podcast', 'autoplay; fullscreen; picture-in-picture; clipboard-write');
|
||||
}
|
||||
|
||||
// The frame is created once and kept alive, so playback carries on while you use other tabs.
|
||||
// It loads /api/audiobooks/enter, where the server adds the site's secret sign-in link itself (the key never reaches this page).
|
||||
const ENTER = '/api/audiobooks/enter';
|
||||
function viewAudio() {
|
||||
const c = abCreds(), old = $('#audiowrap iframe[data-k="audio"]');
|
||||
$('#view').innerHTML = `<div class="abbar"><span class="grow"><b>Audiobooks</b><small id="abst">${old ? '' : 'Opening…'}</small></span>
|
||||
${c.u ? `<button class="ibtn" id="cu" aria-label="Copy username" title="Copy username">${icon('copy')}</button>` : ''}
|
||||
<button class="ibtn" id="ar" aria-label="Reload" title="Reload">${icon('refresh')}</button>
|
||||
<button class="ibtn" id="af" aria-label="Full screen" title="Full screen">${icon('full')}</button>
|
||||
<a class="ibtn" href="${ENTER}" target="_blank" rel="noopener" aria-label="Open in new tab" title="Open in new tab">${icon('ext')}</a></div>`;
|
||||
const setSt = t => { const e = $('#abst'); if (e) e.textContent = t; };
|
||||
const open = fresh => {
|
||||
setSt('Opening…');
|
||||
let f = $('#audiowrap iframe[data-k="audio"]'); if (f) f.remove();
|
||||
showFrame('audio', ENTER + (fresh ? '?t=' + Date.now() : ''), 'Audiobooks', 'autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media');
|
||||
f = $('#audiowrap iframe[data-k="audio"]'); f.referrerPolicy = 'no-referrer';
|
||||
let done = false;
|
||||
f.addEventListener('load', () => { done = true; setSt(''); });
|
||||
setTimeout(() => { if (!done) setSt('Still loading… if it stays blank, tap the arrow to open it in its own tab'); }, 10000);
|
||||
};
|
||||
if (!old) open(false); else { showFrame('audio', '', 'Audiobooks', ''); setSt(''); }
|
||||
$('#ar').onclick = () => open(true);
|
||||
$('#af').onclick = () => { const f = $('#audiowrap iframe[data-k="audio"]'); if (f && f.requestFullscreen) f.requestFullscreen().catch(() => toast('Full screen is not available here')); else toast('Full screen is not available here'); };
|
||||
const cu = $('#cu'); if (cu) cu.onclick = async () => { try { await navigator.clipboard.writeText(c.u); toast('Username copied. Password is in Settings.'); } catch { toast('Could not copy'); } };
|
||||
}
|
||||
Reference in new issue
Block a user