diff --git a/.env.example b/.env.example index 0991883..33b5c07 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,4 @@ AUDIOBOOKS_URL=https://audiobooks.necloud.us # First-time passkey enrolment. If unset, a random code is printed in the server log while no passkey exists. HUB_SETUP_CODE= VERSE_URL=https://versebyversewithnate.us/admin -PODCAST_URL=http://172.16.16.106:8383/ +PODCAST_URL=https://podcast.necloud.us/ diff --git a/docker-compose.yml b/docker-compose.yml index e05d1b9..7f67957 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: ONLYOFFICE_URL: ${ONLYOFFICE_URL:-https://onlyoffice.necloud.us} AUDIOBOOKS_URL: ${AUDIOBOOKS_URL:-https://audiobooks.necloud.us} VERSE_URL: ${VERSE_URL:-https://versebyversewithnate.us/admin} - PODCAST_URL: ${PODCAST_URL:-http://172.16.16.106:8383/} + PODCAST_URL: ${PODCAST_URL:-https://podcast.necloud.us/} ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-} # same value as the OnlyOffice server's JWT secret PUBLIC_URL: ${PUBLIC_URL:-} # e.g. https://hub.necloud.us (OnlyOffice posts saved edits here) HUB_SETUP_CODE: ${HUB_SETUP_CODE:-} # first passkey; if empty, a code is printed in the log diff --git a/public/app.js b/public/app.js index 618a83d..8962141 100644 --- a/public/app.js +++ b/public/app.js @@ -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')} @@ -639,13 +639,25 @@ function viewVerse() {
${icon('book')}Open admin
`; } +// 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 = `

Podcast

${esc(u.host)}

-

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.

-
`; +
Open in new tab
+ ${canFrame ? '

If the page below stays blank, the app does not allow being shown inside another app. Use Open in new tab.

' + : '

This is a plain http address, which a secure page cannot show inside itself. It opens in its own tab, and works at home or on your VPN.

'}`; + if (canFrame) showFrame('podcast', url, 'Podcast', 'autoplay; fullscreen; picture-in-picture; clipboard-write'); } function viewAudio() { @@ -656,14 +668,7 @@ function viewAudio() { ${c.u ? `` : ''} ${c.p ? `` : ''} ${c.u || c.p ? '' : 'Save sign-in'}`; - // 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'); diff --git a/public/style.css b/public/style.css index 9d52707..5d50c96 100644 --- a/public/style.css +++ b/public/style.css @@ -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; } diff --git a/server.js b/server.js index 753c8dd..4a2ffd2 100644 --- a/server.js +++ b/server.js @@ -12,7 +12,7 @@ const AUDIOBOOKS_URL = (process.env.AUDIOBOOKS_URL || 'https://audiobooks.neclou const auth = require('./auth'); const cal = require('./cal'); const VERSE_URL = process.env.VERSE_URL || 'https://versebyversewithnate.us/admin'; -const PODCAST_URL = process.env.PODCAST_URL || 'http://172.16.16.106:8383/'; +const PODCAST_URL = process.env.PODCAST_URL || 'https://podcast.necloud.us/'; const PUBLIC = path.join(__dirname, 'public'); const TYPES = {