diff --git a/propresenter_notes/static/app.js b/propresenter_notes/static/app.js index e9995de..36fc9f3 100644 --- a/propresenter_notes/static/app.js +++ b/propresenter_notes/static/app.js @@ -1,6 +1,7 @@ const el = { connection: document.querySelector('#connection'), refresh: document.querySelector('#refresh'), + librarySelect: document.querySelector('#librarySelect'), presentationSelect: document.querySelector('#presentationSelect'), previous: document.querySelector('#previous'), next: document.querySelector('#next'), @@ -14,6 +15,7 @@ const el = { let presentations = []; let selected = null; +let selectedLibraryId = ''; let selectedSlideIndex = 0; let selectedCache = null; let changeTimer = null; @@ -62,10 +64,31 @@ function selectedPresentationId() { return selected ? idValue(selected.presentation) : ''; } -function labelFor(item) { - const lib = item.library?.name || item.library?.uuid || 'Library'; - const pres = item.presentation?.name || item.presentation?.uuid || `Presentation ${item.presentation?.index ?? ''}`; - return `${lib} — ${pres}`; +function libraryIdFor(item) { + return item?.libraryId || idValue(item?.library); +} + +function selectedLibraryValue() { + return selected ? libraryIdFor(selected) : selectedLibraryId; +} + +function libraryLabelFor(item) { + return item.library?.name || item.library?.uuid || item.libraryId || 'Library'; +} + +function presentationLabelFor(item) { + return item.presentation?.name || item.presentation?.uuid || `Presentation ${item.presentation?.index ?? ''}`; +} + +function uniqueLibraries() { + const byId = new Map(); + presentations.forEach((item) => { + const libraryId = libraryIdFor(item); + if (libraryId && !byId.has(libraryId)) { + byId.set(libraryId, { id: libraryId, label: libraryLabelFor(item) }); + } + }); + return [...byId.values()]; } function setConnection(ok, message) { @@ -96,33 +119,101 @@ async function checkHealth() { } } -async function loadPresentations(keepCurrent = false) { - const storedSelection = readStoredSelection(); - const previousId = keepCurrent ? selectedPresentationId() : storedSelection.presentationId || ''; - presentations = await request('/api/presentations'); +function renderLibraryOptions(preferredLibraryId = '') { + const libraries = uniqueLibraries(); + el.librarySelect.innerHTML = ''; + + if (!libraries.length) { + const option = document.createElement('option'); + option.value = ''; + option.textContent = 'No libraries found'; + el.librarySelect.appendChild(option); + el.librarySelect.disabled = true; + selectedLibraryId = ''; + return ''; + } + + libraries.forEach((library) => { + const option = document.createElement('option'); + option.value = library.id; + option.textContent = library.label; + el.librarySelect.appendChild(option); + }); + + const nextLibraryId = libraries.some((library) => library.id === preferredLibraryId) + ? preferredLibraryId + : libraries[0].id; + el.librarySelect.disabled = libraries.length === 1; + el.librarySelect.value = nextLibraryId; + selectedLibraryId = nextLibraryId; + return nextLibraryId; +} + +function renderPresentationOptions(libraryId, preferredPresentationId = '') { + const libraryPresentations = presentations + .map((item, index) => ({ item, index })) + .filter(({ item }) => libraryIdFor(item) === libraryId); el.presentationSelect.innerHTML = ''; - if (!presentations.length) { + if (!libraryPresentations.length) { const option = document.createElement('option'); option.value = ''; - option.textContent = 'No presentations found'; + option.textContent = 'No presentations found in this library'; el.presentationSelect.appendChild(option); + el.presentationSelect.disabled = true; selected = null; return; } - presentations.forEach((item, index) => { + libraryPresentations.forEach(({ item, index }) => { const option = document.createElement('option'); option.value = String(index); - option.textContent = labelFor(item); + option.textContent = presentationLabelFor(item); el.presentationSelect.appendChild(option); }); - const previousIndex = presentations.findIndex((item) => idValue(item.presentation) === previousId); - const nextIndex = previousIndex >= 0 ? previousIndex : 0; - selected = presentations[nextIndex]; - el.presentationSelect.value = String(nextIndex); - writeStoredSelection({ presentationId: selectedPresentationId() }); + const preferred = libraryPresentations.find( + ({ item }) => idValue(item.presentation) === preferredPresentationId + ); + const next = preferred || libraryPresentations[0]; + selected = next.item; + el.presentationSelect.disabled = false; + el.presentationSelect.value = String(next.index); +} + +async function loadPresentations(keepCurrent = false) { + const storedSelection = readStoredSelection(); + const previousId = keepCurrent ? selectedPresentationId() : storedSelection.presentationId || ''; + const previousLibraryId = keepCurrent ? selectedLibraryValue() : storedSelection.libraryId || ''; + presentations = await request('/api/presentations'); + + if (!presentations.length) { + el.librarySelect.innerHTML = ''; + el.presentationSelect.innerHTML = ''; + + const libraryOption = document.createElement('option'); + libraryOption.value = ''; + libraryOption.textContent = 'No libraries found'; + el.librarySelect.appendChild(libraryOption); + + const presentationOption = document.createElement('option'); + presentationOption.value = ''; + presentationOption.textContent = 'No presentations found'; + el.presentationSelect.appendChild(presentationOption); + + el.librarySelect.disabled = true; + el.presentationSelect.disabled = true; + selected = null; + selectedLibraryId = ''; + return; + } + + const presentationLibraryId = presentations.find( + (item) => idValue(item.presentation) === previousId + ); + const nextLibraryId = renderLibraryOptions(previousLibraryId || libraryIdFor(presentationLibraryId)); + renderPresentationOptions(nextLibraryId, previousId); + writeStoredSelection({ libraryId: selectedLibraryValue(), presentationId: selectedPresentationId() }); } function slideNotes(slide) { @@ -148,6 +239,7 @@ function showSlide(index) { const clampedIndex = Math.max(0, Math.min(index, selectedCache.slides.length - 1)); selectedSlideIndex = clampedIndex; writeStoredSelection({ + libraryId: selectedLibraryValue(), presentationId: selectedPresentationId(), slideIndex: clampedIndex }); @@ -303,10 +395,26 @@ el.refresh.addEventListener('click', async () => { await loadSelectedPresentationCache(selectedSlideIndex); }); +el.librarySelect.addEventListener('change', async () => { + selectedLibraryId = el.librarySelect.value; + renderPresentationOptions(selectedLibraryId); + writeStoredSelection({ + libraryId: selectedLibraryValue(), + presentationId: selectedPresentationId(), + slideIndex: 0 + }); + await loadSelectedPresentationCache(0); +}); + el.presentationSelect.addEventListener('change', async () => { selected = presentations[Number(el.presentationSelect.value)] || null; if (selected) { - writeStoredSelection({ presentationId: selectedPresentationId(), slideIndex: 0 }); + selectedLibraryId = libraryIdFor(selected); + writeStoredSelection({ + libraryId: selectedLibraryValue(), + presentationId: selectedPresentationId(), + slideIndex: 0 + }); } await loadSelectedPresentationCache(0); }); diff --git a/propresenter_notes/static/style.css b/propresenter_notes/static/style.css index ced76fc..06f4538 100644 --- a/propresenter_notes/static/style.css +++ b/propresenter_notes/static/style.css @@ -38,11 +38,18 @@ button, select, input { font: inherit; } gap: 16px; } +.selectGroup { + flex: 1 1 620px; + display: grid; + grid-template-columns: minmax(180px, 0.8fr) minmax(260px, 1.2fr); + gap: 14px; +} + .topbar, .controls { justify-content: space-between; flex-wrap: wrap; } h1 { margin: 0; font-size: clamp(1.7rem, 3vw, 2.6rem); } p { margin: 6px 0 0; color: #cbd5e1; } -label { display: grid; gap: 8px; min-width: min(520px, 100%); color: #cbd5e1; } +label { display: grid; gap: 8px; min-width: 0; color: #cbd5e1; } select, button, input { min-height: var(--touch-target); border: 1px solid rgba(255,255,255,0.14); @@ -244,12 +251,18 @@ input { } .topbar > div:first-child, + .selectGroup, .controls label, .controls select { min-width: 0; width: 100%; } + .selectGroup { + grid-template-columns: minmax(0, 1fr); + gap: 12px; + } + h1 { font-size: clamp(1.45rem, 8vw, 2rem); line-height: 1.08; diff --git a/propresenter_notes/templates/index.html b/propresenter_notes/templates/index.html index ed69f59..a209e3c 100644 --- a/propresenter_notes/templates/index.html +++ b/propresenter_notes/templates/index.html @@ -24,12 +24,21 @@