139 lines
5.0 KiB
JavaScript
139 lines
5.0 KiB
JavaScript
window.App = window.App || {};
|
|
App.version = App.version || {};
|
|
|
|
(function() {
|
|
const VERSION_URL = '/api/version';
|
|
const POLL_INTERVAL_MS = 60000;
|
|
|
|
// Baseline manifest captured on startup: { version, files: { rel: hash } }.
|
|
let baseline = null;
|
|
let timer = null;
|
|
// Set once a JS/HTML change is detected; the page reloads at a safe moment.
|
|
let reloadPending = false;
|
|
let checking = false;
|
|
|
|
async function fetchVersion() {
|
|
const resp = await fetch(VERSION_URL, { cache: 'no-store' });
|
|
if (!resp.ok) throw new Error('version fetch failed: ' + resp.status);
|
|
return resp.json();
|
|
}
|
|
|
|
// Swap a stylesheet <link> in place using a cache-busted href so updated CSS
|
|
// applies instantly. The old link is removed only after the new one loads to
|
|
// avoid a flash of unstyled content.
|
|
function hotReloadCss(relPath, hash) {
|
|
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
|
|
const match = links.find((l) => {
|
|
const href = (l.getAttribute('href') || '').split('?')[0];
|
|
return href.endsWith(relPath) || href.endsWith('/' + relPath);
|
|
});
|
|
if (!match) return false;
|
|
const base = (match.getAttribute('href') || '').split('?')[0];
|
|
const fresh = match.cloneNode(false);
|
|
fresh.setAttribute('href', base + '?v=' + hash);
|
|
fresh.addEventListener('load', () => { if (match.parentNode) match.remove(); });
|
|
fresh.addEventListener('error', () => { if (fresh.parentNode) fresh.remove(); });
|
|
match.parentNode.insertBefore(fresh, match.nextSibling);
|
|
return true;
|
|
}
|
|
|
|
function diffFiles(oldFiles, newFiles) {
|
|
const changed = [];
|
|
const keys = new Set([
|
|
...Object.keys(oldFiles || {}),
|
|
...Object.keys(newFiles || {})
|
|
]);
|
|
keys.forEach((k) => {
|
|
if ((oldFiles || {})[k] !== (newFiles || {})[k]) changed.push(k);
|
|
});
|
|
return changed;
|
|
}
|
|
|
|
// A reload is "safe" when the user isn't mid-playback: no open video modal,
|
|
// no active reels feed, and no playing <video>. App state survives a reload
|
|
// because it is restored from localStorage on boot.
|
|
function isSafeToReload() {
|
|
if (App.state && App.state.feedOpen) return false;
|
|
const modal = document.getElementById('video-modal');
|
|
if (modal && modal.style.display && modal.style.display !== 'none') return false;
|
|
const player = document.getElementById('player');
|
|
if (player && !player.paused && !player.ended) return false;
|
|
return true;
|
|
}
|
|
|
|
function showUpdateBanner() {
|
|
const banner = document.getElementById('update-banner');
|
|
if (!banner) return;
|
|
banner.classList.add('show');
|
|
const btn = document.getElementById('update-banner-btn');
|
|
if (btn) btn.onclick = () => window.location.reload();
|
|
}
|
|
|
|
function tryReloadWhenSafe() {
|
|
if (!reloadPending) return;
|
|
if (isSafeToReload()) {
|
|
window.location.reload();
|
|
} else {
|
|
showUpdateBanner();
|
|
}
|
|
}
|
|
|
|
function apply(latest) {
|
|
const changed = diffFiles(baseline.files, latest.files);
|
|
if (!changed.length) return;
|
|
|
|
let needsReload = false;
|
|
changed.forEach((file) => {
|
|
if (file.endsWith('.css') && hotReloadCss(file, latest.files[file])) {
|
|
return; // hot-swapped without reload
|
|
}
|
|
// JS and HTML can't be safely live-patched; they require a reload.
|
|
needsReload = true;
|
|
});
|
|
|
|
// Adopt the new manifest so we don't re-trigger on the same change.
|
|
baseline = latest;
|
|
|
|
if (needsReload) {
|
|
reloadPending = true;
|
|
tryReloadWhenSafe();
|
|
}
|
|
}
|
|
|
|
async function check() {
|
|
if (checking || !baseline) return;
|
|
checking = true;
|
|
try {
|
|
const latest = await fetchVersion();
|
|
apply(latest);
|
|
} catch (e) {
|
|
// Network blips are non-fatal; we retry on the next tick.
|
|
} finally {
|
|
checking = false;
|
|
}
|
|
}
|
|
|
|
App.version.start = async function() {
|
|
try {
|
|
baseline = await fetchVersion();
|
|
} catch (e) {
|
|
return; // endpoint unavailable; skip version checking entirely
|
|
}
|
|
timer = setInterval(check, POLL_INTERVAL_MS);
|
|
// Check promptly when the user returns to the tab so updates land while
|
|
// they were away, and retry a pending reload once playback stops.
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
tryReloadWhenSafe();
|
|
check();
|
|
}
|
|
});
|
|
// Re-attempt a deferred reload whenever a video finishes/pauses.
|
|
const player = document.getElementById('player');
|
|
if (player) {
|
|
player.addEventListener('pause', tryReloadWhenSafe);
|
|
player.addEventListener('ended', tryReloadWhenSafe);
|
|
}
|
|
};
|
|
})();
|