Podcast tab: show podcast.necloud.us inside the app
build-container / build (push) Successful in 1m44s

Audio and Podcast now share kept-alive frames that stay loaded while hidden.
A plain-http address still falls back to a launcher (a secure page cannot
frame http content).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-20 15:18:42 -04:00
1 parent 5466506ffb
commit ee2678d9ed
5 files changed
+24 -16

No files matched your search

+18 -13
View File
@@ -63,7 +63,7 @@ function mailUrl(m, kind) {
return base; // iCloud has no sign-in hint
}
let cfg = { podcast: 'http://172.16.16.106:8383/', verse: 'https://versebyversewithnate.us/admin', audiobooks: 'https://audiobooks.necloud.us', seafile: 'https://seafile.necloud.us', onlyoffice: 'https://onlyoffice.necloud.us' };
let cfg = { podcast: 'https://podcast.necloud.us/', verse: 'https://versebyversewithnate.us/admin', audiobooks: 'https://audiobooks.necloud.us', seafile: 'https://seafile.necloud.us', onlyoffice: 'https://onlyoffice.necloud.us' };
// ---- Seafile client (via same-origin proxy)
const sf = {
@@ -159,7 +159,7 @@ function viewHome() {
${tile('#/calendar', '#d9453a', 'cal', 'Calendar', 'Agenda')}
${tile('#/office', '#e0742b', 'doc', 'Office', 'OnlyOffice')}
${tile('#/audio', '#8a5cf0', 'audio', 'Audio', 'Audiobooks')}
${tile(cfg.podcast, '#c2417a', 'mic', 'Podcast', 'Home network', true)}
${tile('#/podcast', '#c2417a', 'mic', 'Podcast', 'Podcasts')}
${tile(cfg.verse, '#0f9d9a', 'book', 'Podcast Admin', 'Opens in a new tab', true)}
${tile('#/mail', '#4f7cff', 'mail', 'Mail', '4 accounts')}
</div>
@@ -639,13 +639,25 @@ function viewVerse() {
<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);
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="card" style="max-width:none"><p style="margin-top:0">Opens in its own tab. This address is on your home network, so it works when you are at home or connected to your VPN.</p>
<div class="bar"><a class="btn" href="${esc(url)}" target="_blank" rel="noopener">${icon('mic')}Open podcast</a></div></div></div>`;
<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');
}
function viewAudio() {
@@ -656,14 +668,7 @@ function viewAudio() {
${c.u ? `<button class="btn ghost" id="cu">${icon('copy')}Username</button>` : ''}
${c.p ? `<button class="btn ghost" id="cp">${icon('copy')}Password</button>` : ''}
${c.u || c.p ? '' : '<a class="btn ghost" href="#/settings">Save sign-in</a>'}</div></div>`;
// Create the frame once and keep it: it is only shown/hidden, never rebuilt, so playback carries on across tabs.
const wrap = $('#audiowrap');
if (!wrap.firstChild) {
const f = document.createElement('iframe');
f.src = url; f.title = 'Audiobooks'; f.allow = 'autoplay; fullscreen; picture-in-picture; clipboard-write';
wrap.appendChild(f);
}
wrap.className = 'on'; document.body.classList.add('onaudio');
showFrame('audio', url, 'Audiobooks', 'autoplay; fullscreen; picture-in-picture; clipboard-write'); // created once, so playback carries on across tabs
const copy = async (v, what) => { try { await navigator.clipboard.writeText(v); toast(what + ' copied. Paste it into the sign-in box.'); } catch { toast('Could not copy'); } };
const cu = $('#cu'), cp = $('#cp');
if (cu) cu.onclick = () => copy(c.u, 'Username');
+3
View File
@@ -229,3 +229,6 @@ textarea { padding: 12px 14px; resize: vertical; font-family: inherit; }
.chk { display: flex; align-items: center; gap: 10px; color: var(--text); font-size: 16px; margin: 14px 0 0; }
.chk input { width: 22px; height: 22px; }
.row input[type=checkbox] { width: 22px; height: 22px; }
/* Several kept-alive frames share #audiowrap; only the active one is shown (the others stay loaded, out of sight) */
#audiowrap iframe:not(.act) { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; border: 0; }