feat: add 2m, 10m presets and custom duration for slideshow interval

Presets are now [1, 2, 5, 10, 15, 30, 60] minutes. A 'Custom' button
reveals a number input clamped to 1–60 minutes. Backend also clamps
the received value with constrain(v, 1, 60) for safety.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 21:14:19 -04:00
parent 103f8000f3
commit 21b3e40992
2 changed files with 21 additions and 4 deletions

View File

@@ -86,14 +86,20 @@ async function renderAlbums(el) {
async function renderSlideshow(el) {
const settings = await api('/api/settings');
const intervals = [1, 5, 15, 30, 60];
const presets = [1, 2, 5, 10, 15, 30, 60];
const isCustom = !presets.includes(settings.interval_min);
const modes = ['Random', 'Chronological', 'Reverse Chrono', 'Favorites Weighted',
'Weighted Recent', 'Weighted Oldest'];
el.innerHTML = `
<div class="card">
<h2>Interval</h2>
<div class="preset-btns">
${intervals.map(i => `<button class="${settings.interval_min===i?'active':''}" data-interval="${i}">${i}m</button>`).join('')}
${presets.map(i => `<button class="${!isCustom && settings.interval_min===i?'active':''}" data-interval="${i}">${i}m</button>`).join('')}
<button class="${isCustom?'active':''}" data-interval="custom">Custom</button>
</div>
<div class="field" id="customIntervalRow" style="margin-top:8px;display:${isCustom?'flex':'none'}">
<label>Minutes (1–60):</label>
<input type="number" id="customInterval" min="1" max="60" value="${isCustom?settings.interval_min:5}" style="width:60px;margin-left:8px">
</div>
</div>
<div class="card">
@@ -109,10 +115,18 @@ async function renderSlideshow(el) {
btn.onclick = () => {
el.querySelectorAll('[data-interval]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('customIntervalRow').style.display =
btn.dataset.interval === 'custom' ? 'flex' : 'none';
};
});
document.getElementById('saveSlideshow').onclick = async () => {
const interval = parseInt(el.querySelector('[data-interval].active')?.dataset.interval || '5');
const activeBtn = el.querySelector('[data-interval].active');
let interval;
if (activeBtn && activeBtn.dataset.interval === 'custom') {
interval = Math.min(60, Math.max(1, parseInt(document.getElementById('customInterval').value) || 5));
} else {
interval = parseInt(activeBtn?.dataset.interval || '5');
}
const mode = parseInt(document.getElementById('cycleMode').value);
await api('/api/settings', { method: 'POST', body: JSON.stringify({ interval_min: interval, cycle_mode: mode }) });
toast('Slideshow settings saved');

View File

@@ -310,7 +310,10 @@ void AppWebServer::handlePostSettings(AsyncWebServerRequest* request,
Settings s = _settings->get();
if (!doc["interval_min"].isNull()) s.interval_min = doc["interval_min"];
if (!doc["interval_min"].isNull()) {
uint8_t v = doc["interval_min"];
s.interval_min = constrain(v, 1, 60);
}
if (!doc["cycle_mode"].isNull()) s.cycle_mode = static_cast<CycleMode>((uint8_t)doc["cycle_mode"]);
if (!doc["img_quality"].isNull()) s.img_quality = static_cast<ImageQuality>((uint8_t)doc["img_quality"]);
if (!doc["meta_flags"].isNull()) s.meta_flags = doc["meta_flags"];