Load layouts and optional features from staged asset caches
This commit is contained in:
@@ -1666,7 +1666,10 @@ function applyAppearance() {
|
||||
root.dataset.density = s.density || 'comfortable';
|
||||
root.dataset.perf = s.perfMode ? 'on' : 'off';
|
||||
root.dataset.motion = s.reduceMotion ? 'reduced' : 'full';
|
||||
root.dataset.layout = s.layout || 'classic';
|
||||
const desiredLayout=s.layout || 'classic';
|
||||
if(root.dataset.layout!==desiredLayout && !Lazy.loaded('layout:'+desiredLayout)) {
|
||||
Lazy.load('layout:'+desiredLayout).then(()=>{if((data.settings.layout||'classic')===desiredLayout)applyAppearance();}).catch(error=>toast(error.message));
|
||||
} else root.dataset.layout=desiredLayout;
|
||||
}
|
||||
function fmtBytes(n) {
|
||||
if (!n) return '0 B';
|
||||
@@ -2056,7 +2059,8 @@ async function deleteCustomVideo(id) {
|
||||
// Open the editor modal for a source video. `duration` seconds is needed to
|
||||
// compute keep segments; we take it from the live player when the video is
|
||||
// currently playing, else from the card metadata.
|
||||
function openVideoEditor(source) {
|
||||
async function openVideoEditor(source) {
|
||||
try { await Lazy.load('feature:video-edit'); } catch (error) { toast(error.message); return; }
|
||||
if (!(WEB && window.OPFS && window.OPFS.isSupported())) {
|
||||
toast('⚠ Editing needs offline storage, which this browser doesn’t support');
|
||||
return;
|
||||
@@ -3135,6 +3139,7 @@ async function renderAvailability(id) {
|
||||
const el = $('npAvail');
|
||||
if (!el) return;
|
||||
const off = data.settings.p2pShare === false && data.settings.p2pReceive === false;
|
||||
if (WEB && !off && navigator.onLine !== false && id && !window.P2PCore) { try { await Lazy.load('feature:p2p'); } catch {} }
|
||||
if (!WEB || !window.P2PCore || off || navigator.onLine === false || String(id).startsWith('edit_')) {
|
||||
el.classList.add('hidden');
|
||||
return;
|
||||
@@ -3161,6 +3166,7 @@ async function renderAvailability(id) {
|
||||
// Fetch a verified copy from another device (docs/p2p-architecture.md flow 6).
|
||||
// Download-then-play: on success the file is saved offline like any save.
|
||||
async function getFromPeers(videoObj, c, peers, onProgress) {
|
||||
try { await Lazy.load('feature:direct'); } catch (error) { toast(error.message); return false; }
|
||||
if (window.DirectMedia && !DirectMedia.enabled()) { toast('Direct device transfer is off. Enable it in Settings or use a server copy.'); return false; }
|
||||
if (window.DirectMedia?.enabled()) { const ok = await DirectMedia.obtain(videoObj.id); if (!ok) toast('Pair the source device through Remote or use the same profile to receive its saved copy.'); return !!ok; }
|
||||
const id = videoObj.id;
|
||||
@@ -3186,6 +3192,7 @@ async function getFromPeers(videoObj, c, peers, onProgress) {
|
||||
|
||||
// Offered when a video won't load: only if some device holding it is online now.
|
||||
async function offerPeerDownload(videoObj) {
|
||||
if (WEB && data.settings.p2pReceive !== false) { try { await Lazy.load('feature:p2p'); } catch { return; } }
|
||||
if (!WEB || !window.P2PTransfer || !window.P2PCore || data.settings.p2pReceive === false) return;
|
||||
const id = videoObj && videoObj.id;
|
||||
if (!id || videoObj.custom || cachedIds.has(id)) return;
|
||||
@@ -5035,7 +5042,8 @@ const Remote = (() => {
|
||||
body.querySelector('[data-act="unpair"]').classList.toggle('hidden', !live);
|
||||
}
|
||||
|
||||
function openHostModal() {
|
||||
async function openHostModal() {
|
||||
try { await Lazy.load('feature:direct'); } catch(error) { toast(error.message); return; }
|
||||
const body = document.createElement('div');
|
||||
body.className = 'remote-host';
|
||||
body.innerHTML = `
|
||||
@@ -5306,7 +5314,8 @@ const Remote = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
function openRemoteView({ code = '' } = {}) {
|
||||
async function openRemoteView({ code = '' } = {}) {
|
||||
try { await Lazy.load('feature:direct'); } catch(error) { toast(error.message); return; }
|
||||
rc.open = true;
|
||||
rc.seekTimer = VisibleTimer.setIntervalWhenVisible(renderSeek, 500);
|
||||
// This phone is the remote now — don't also play here.
|
||||
@@ -5918,7 +5927,8 @@ const StatsTrack = (() => {
|
||||
}
|
||||
function tick(cur) {
|
||||
const meta = current && current.meta;
|
||||
if (!meta || !meta.id || !window.StatsCore) return;
|
||||
if (!meta || !meta.id) return;
|
||||
if (!window.StatsCore) { Lazy.load('feature:stats').catch(() => {}); return; }
|
||||
if (meta.id !== st.id) {
|
||||
commit(false);
|
||||
st.id = meta.id;
|
||||
@@ -5949,7 +5959,9 @@ const StatsTrack = (() => {
|
||||
// ============================================================================
|
||||
// Stats view
|
||||
// ============================================================================
|
||||
function renderStats() {
|
||||
async function renderStats() {
|
||||
try { await Lazy.load('feature:stats'); } catch (error) { toast(error.message); return; }
|
||||
if (view.type !== 'stats') return;
|
||||
StatsTrack.flush();
|
||||
els.listTitle.textContent = 'Stats';
|
||||
resetListHeaderExtras();
|
||||
@@ -6679,8 +6691,8 @@ const EqRender = (() => {
|
||||
const g = EQ.gains();
|
||||
const lufs = Level.enabled() ? Level.remembered(id) : null;
|
||||
const levelDb = lufs == null ? 0 : Math.round(Level.gainFor(lufs) * 2) / 2;
|
||||
const vocal = VocalReducer.amount(data.settings.vocalLevel ?? 100);
|
||||
const width = EqCore.width(data.settings.stereoWidth);
|
||||
const vocal = EqParameters.amount(data.settings.vocalLevel ?? 100);
|
||||
const width = EqParameters.width(data.settings.stereoWidth);
|
||||
if (g.every((v) => v === 0) && Math.abs(levelDb) < 0.5 && vocal === 100 && width === 0) return null;
|
||||
return { gains: g, levelDb, vocal, width, sig: g.join(',') + '|' + levelDb + '|vocal:' + vocal + '|width:' + width };
|
||||
}
|
||||
@@ -6719,7 +6731,7 @@ const EqRender = (() => {
|
||||
|
||||
function writeViaWorker(name, buffer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const w = new Worker('/opfs-worker.js');
|
||||
const w = Lazy.worker('/opfs-worker.js');
|
||||
w.onmessage = (e) => { w.terminate(); (e.data && e.data.type === 'written') ? resolve() : reject(new Error((e.data && e.data.error) || 'write failed')); };
|
||||
w.onerror = () => { w.terminate(); reject(new Error('worker failed')); };
|
||||
w.postMessage({ op: 'write', dir: DIR, name, buffer }, [buffer]);
|
||||
@@ -6731,6 +6743,7 @@ const EqRender = (() => {
|
||||
const w = wanted(id);
|
||||
if (!w) { await drop(id); return; }
|
||||
if (readMap()[id] === w.sig) return;
|
||||
await loadSoundFeatures();
|
||||
const file = await window.OPFS.getFileObject(id);
|
||||
if (!file) return;
|
||||
const buf = await file.arrayBuffer();
|
||||
@@ -6834,8 +6847,9 @@ const PiP = (() => {
|
||||
// is chosen (createMediaElementSource can't be undone; afterwards "Flat"
|
||||
// simply zeroes the gains). Disabled on iOS: Safari suspends Web Audio when
|
||||
// the screen locks, which would silence background playback.
|
||||
const EQ_BANDS = EqCore.bands;
|
||||
const EQ_PRESETS = EqCore.presets;
|
||||
const loadSoundFeatures=()=>Promise.all([Lazy.load('feature:eq-core'),Lazy.load('feature:vocal-reducer')]);
|
||||
const EQ_BANDS = EqParameters.bands;
|
||||
const EQ_PRESETS = EqParameters.presets;
|
||||
const EQ = (() => {
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
const AC = window.AudioContext || window.webkitAudioContext;
|
||||
@@ -6850,12 +6864,16 @@ const EQ = (() => {
|
||||
const rendered = () => !!(current && current.eqAudioUrl);
|
||||
const gains = () => {
|
||||
const g = data.settings.eq && Array.isArray(data.settings.eq.g) ? data.settings.eq.g : EQ_PRESETS.flat.g;
|
||||
return EqCore.migrate(g);
|
||||
return EqParameters.migrate(g);
|
||||
};
|
||||
const isFlat = () => gains().every((v) => v === 0);
|
||||
|
||||
function build() {
|
||||
if (st.ctx) return true;
|
||||
if (!Lazy.loaded('feature:eq-core') || !Lazy.loaded('feature:vocal-reducer')) {
|
||||
loadSoundFeatures().then(()=>{apply(); Level.apply();}).catch(error=>toast(error.message));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
st.ctx = new AC();
|
||||
st.filters = EQ_BANDS.map((b) => {
|
||||
@@ -6893,7 +6911,7 @@ const EQ = (() => {
|
||||
// Apply the stored curve; wires the graph up the first time it's non-flat.
|
||||
function apply() {
|
||||
if (!available()) return;
|
||||
if (!st.ctx && isFlat() && VocalReducer.amount(data.settings.vocalLevel ?? 100) === 100 && EqCore.width(data.settings.stereoWidth) === 0) return;
|
||||
if (!st.ctx && isFlat() && EqParameters.amount(data.settings.vocalLevel ?? 100) === 100 && EqParameters.width(data.settings.stereoWidth) === 0) return;
|
||||
if (!build()) return;
|
||||
attach(els.video);
|
||||
attach(els.audio);
|
||||
@@ -6921,7 +6939,8 @@ const EQ = (() => {
|
||||
if (typeof EqRender !== 'undefined') EqRender.scheduleAll();
|
||||
}
|
||||
|
||||
function open() {
|
||||
async function open() {
|
||||
try { await loadSoundFeatures(); } catch(error) { toast(error.message); return; }
|
||||
const body = document.createElement('div');
|
||||
body.className = 'eq-panel';
|
||||
if (!AC) {
|
||||
@@ -7795,7 +7814,7 @@ const Party = (() => {
|
||||
function setStatus(t) { const el = $('ptStatus'); if (el) el.textContent = t; }
|
||||
function sysMsg(text) { st.chat.push({ sys: true, text, at: Date.now() }); renderChat(); }
|
||||
function link() { return `${location.origin}/?party=${st.code}`; }
|
||||
function showPanel() { $('partyPanel').classList.remove('hidden'); st.minimized = false; $('partyPanel').classList.remove('min'); render(); }
|
||||
function showPanel() { Lazy.load('feature:party-dj').then(render).catch(error=>toast(error.message)); Lazy.load('feature:direct').catch(error=>toast(error.message)); $('partyPanel').classList.remove('hidden'); st.minimized = false; $('partyPanel').classList.remove('min'); render(); }
|
||||
|
||||
function render() {
|
||||
const panel = $('partyPanel');
|
||||
@@ -7836,7 +7855,7 @@ const Party = (() => {
|
||||
<form class="pt-send"><input class="pt-msg" maxlength="500" placeholder="Message…" autocomplete="off" /><button class="np-btn" type="submit">Send</button></form>
|
||||
<button class="pt-leave" type="button" data-act="leave">${isHost() ? 'End the party' : 'Leave the party'}</button>`;
|
||||
const dj = document.createElement('div'); panel.querySelector('.pt-body').append(dj);
|
||||
PartyDJ.render(dj, { state: st.dj, host: isHost(), pid, send, search: async q => { const result = await API.search(q); return result.results || result.videos || []; } });
|
||||
if (Lazy.loaded('feature:party-dj')) PartyDJ.render(dj, { state: st.dj, host: isHost(), pid, send, search: async q => { const result = await API.search(q); return result.results || result.videos || []; } });
|
||||
renderChat();
|
||||
}
|
||||
function renderChat() {
|
||||
@@ -9104,6 +9123,7 @@ function addBrowseCta(empty, label) {
|
||||
// through the server. Buttons appear only when the file is ready so each one
|
||||
// runs inside its own tap (the share sheet needs a fresh user gesture).
|
||||
async function exportToDevice(video) {
|
||||
try { await Lazy.load('feature:export'); } catch(error) { toast(error.message); return; }
|
||||
if (!video || !video.id || !window.DeviceExport) return;
|
||||
const id = video.id;
|
||||
const title = video.title || videoTitleById(id) || id;
|
||||
@@ -9869,7 +9889,12 @@ async function renderSettings() {
|
||||
|
||||
// ---- Wire appearance / accessibility ----
|
||||
$('setTheme').addEventListener('change', (e) => { data.settings.theme = e.target.value; applyAppearance(); persist(); });
|
||||
$('setLayout').addEventListener('change', (e) => { data.settings.layout = e.target.value; applyAppearance(); persist(); });
|
||||
$('setLayout').addEventListener('change', async (e) => {
|
||||
const select=e.target, next=select.value, previous=data.settings.layout; select.disabled=true;
|
||||
try { await Lazy.load('layout:'+next); data.settings.layout=next; Lazy.layout=next; if(WEB)saveDataToStorage(JSON.stringify(data)); applyAppearance(); persist(); Lazy.reportLayout?.(); }
|
||||
catch(error) { select.value=previous; toast(error.message); }
|
||||
finally { select.disabled=false; }
|
||||
});
|
||||
$('setFont').addEventListener('change', (e) => { data.settings.fontScale = e.target.value; applyAppearance(); persist(); });
|
||||
$('setParallelSaves').addEventListener('change', (e) => { data.settings.parallelSaves = Number(e.target.value) || 4; persist(); SaveSlots.pump(); });
|
||||
$('setDensity').addEventListener('change', (e) => { data.settings.density = e.target.value; applyAppearance(); persist(); });
|
||||
@@ -11118,7 +11143,7 @@ function wireUI() {
|
||||
|
||||
// ---- Empty-home playlist grid ----
|
||||
$('homePlNewBtn').addEventListener('click', () => newPlaylist(null));
|
||||
const importSetlist = document.createElement('button'); importSetlist.type = 'button'; importSetlist.className = 'icon-btn'; importSetlist.textContent = '⇥'; importSetlist.title = 'Import setlist'; importSetlist.setAttribute('aria-label', 'Import setlist'); importSetlist.onclick = () => SetlistImport.open(); els.newPlaylistBtn.after(importSetlist);
|
||||
const importSetlist = document.createElement('button'); importSetlist.type = 'button'; importSetlist.className = 'icon-btn'; importSetlist.textContent = '⇥'; importSetlist.title = 'Import setlist'; importSetlist.setAttribute('aria-label', 'Import setlist'); importSetlist.onclick = () => SetlistImport.open(); els.newPlaylistBtn.after(importSetlist); Lazy.intent(importSetlist,'feature:setlist-import');
|
||||
$('homePlGrid').addEventListener('scroll', updateHomeGridScrollState, { passive: true });
|
||||
window.addEventListener('resize', updateHomeGridScrollState);
|
||||
|
||||
@@ -12267,7 +12292,7 @@ async function applyUpdate(target, { quiet = false } = {}) {
|
||||
const reg = _swReg || (('serviceWorker' in navigator) ? await navigator.serviceWorker.getRegistration() : null);
|
||||
const SU = window.SwUpdate;
|
||||
const reload = () => window.location.reload();
|
||||
if (!SU) { reload(); return; } // helper failed to load (flaky link): a reload is the best we can do
|
||||
if (!SU) throw new Error('Update helper unavailable; try Refresh UI again when paused');
|
||||
await SU.applyUpdate({
|
||||
reg,
|
||||
container: navigator.serviceWorker,
|
||||
@@ -12342,6 +12367,7 @@ async function registerServiceWorker() {
|
||||
// cached copy of sw.js — the server always returns it fresh (no-store).
|
||||
const reg = await navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' });
|
||||
_swReg = reg;
|
||||
Lazy.reportLayout(reg);
|
||||
|
||||
// A waiting worker only means "update pending" when this page is already
|
||||
// controlled by a previous SW. On a FIRST install (fresh visit, or right
|
||||
@@ -12588,6 +12614,7 @@ async function boot() {
|
||||
// and re-render only if something actually changed.
|
||||
syncOnLaunch();
|
||||
startPlaylistInboxWatch();
|
||||
if(LowerThird.active()) await Lazy.load('feature:lower-third');
|
||||
Presenter.boot();
|
||||
if (LowerThird.active()) return;
|
||||
SectionRail.boot();
|
||||
@@ -12617,7 +12644,7 @@ async function boot() {
|
||||
PiP.render();
|
||||
LyricsWindow.configure({ prepare: () => { if (!isServiceModeOpen()) Notes.stageLyrics.set(true); }, view: () => isServiceModeOpen() && data.settings.serviceLyrics ? $('serviceLyrics') : $('stageLyrics'), line: () => $('serviceLyricsList')?.querySelector('.active')?.textContent || $('stageLyricsList')?.querySelector('.active')?.textContent || current?.meta?.title || 'Nothing playing', toggle: () => { Player.toggle(); updatePlayBtn(); }, next: playNext, refresh: () => { Notes.stageLyrics.render(true); Notes.serviceRefresh(); }, toast });
|
||||
EQ.renderButton();
|
||||
if (data.settings.eq?.g?.length === 5) { data.settings.eq.g = EqCore.migrate(data.settings.eq.g); persist(); }
|
||||
if (data.settings.eq?.g?.length === 5) { data.settings.eq.g = EqParameters.migrate(data.settings.eq.g); persist(); }
|
||||
EQ.apply();
|
||||
EqCore.register({ settings: () => data.settings, persist, open: EQ.open, apply: () => { EQ.apply(); EqRender.scheduleAll(); } });
|
||||
VocalReducer.register({ settings: () => data.settings, persist, apply: () => { EQ.apply(); EqRender.scheduleAll(); } });
|
||||
@@ -12630,6 +12657,8 @@ async function boot() {
|
||||
else if (action === 'panic') MidiPedals.fade({ volume: Player.soundEl.volume, setVolume: v => { Player.soundEl.volume = v; }, pause: () => { Player.pause(); updatePlayBtn(); }, restore: () => Player.applyVolume() });
|
||||
} });
|
||||
applyAppearance();
|
||||
Lazy.load('layout:'+data.settings.layout).then(()=>{ if(window.BentoHub)BentoHub.playlists=()=>data.playlists||[]; renderSidebar(); }).catch(error=>toast(error.message));
|
||||
if(data.settings.midiEnabled)Lazy.load('feature:midi').catch(error=>toast(error.message));
|
||||
CarMode.configure({ input: els.searchInput, search: () => els.searchForm.requestSubmit(), toast, state: () => ({ title: current?.meta?.title || '', paused: Player.master?.paused !== false }), action: action => { if (action === 'toggle') { Player.toggle(); updatePlayBtn(); } else if (action === 'next') playNext(); else if (action === 'prev') playPrev(); else if (action === 'back') Player.seek(Math.max(0, (Player.master.currentTime || 0) - 15)); } });
|
||||
updateProfileStatus();
|
||||
updateLoopRepeatButtons();
|
||||
@@ -12643,10 +12672,12 @@ async function boot() {
|
||||
await refreshCachedIds();
|
||||
startCacheResyncWatch();
|
||||
// Peer-to-peer: report what this device holds (on by default; settings.p2pShare).
|
||||
if (WEB && window.P2PClient) {
|
||||
window.P2PClient.start({ getSettings: () => data.settings, getProfile: () => (data.profile && data.profile.name) || '', getProfileSecret: () => ProfileSecret.getFor(data.profile?.name) });
|
||||
if (window.P2PTransfer) window.P2PTransfer.start({ canShare: () => false }); // New scoped invitations replace the unconfirmed legacy sender.
|
||||
}
|
||||
Lazy.onLoad('feature:p2p', () => { if (WEB) {
|
||||
window.P2PClient.start({ getSettings: () => data.settings, getProfile: () => (data.profile && data.profile.name) || '', getProfileSecret: () => ProfileSecret.getFor(data.profile?.name), initialDelay: 0 });
|
||||
if (window.P2PTransfer) window.P2PTransfer.start({ canShare: () => false }); // Scoped invitations preserve receiver confirmation.
|
||||
} });
|
||||
// Preserve the existing eight-second sharing startup without parsing transport at boot.
|
||||
if (WEB) setTimeout(() => Lazy.load('feature:direct').catch(error=>console.warn(error.message)),8000);
|
||||
data.playlists.forEach(preloadPlaylist);
|
||||
preloadPinnedPlaylists();
|
||||
updateBottomChrome();
|
||||
@@ -12675,6 +12706,11 @@ async function boot() {
|
||||
if (WEB) checkUpdateOutcome();
|
||||
registerServiceWorker();
|
||||
if (WEB) pollBuildTag();
|
||||
window.addEventListener('ytp-reload-required',()=>checkBuildTag());
|
||||
window.addEventListener('ytp-lazy-error',event=>{ toast(event.detail?.error?.message||'Feature unavailable; try Refresh UI'); checkBuildTag(); });
|
||||
Lazy.intent($('navPartyBtn'),'feature:party-dj'); Lazy.intent($('navRemoteBtn'),'feature:direct');
|
||||
Lazy.intent($('remoteChip'),'feature:direct'); Lazy.intent($('eqBtn'),'feature:eq-core');
|
||||
Lazy.reportLayout?.(); Lazy.warm?.();
|
||||
if (WEB) {
|
||||
fetch('/api/user/sync', {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user