Add TODAY window mode with configurable start hour
New 'TODAY' button in the window picker shows birds since a
configurable start hour (default 6 AM). The window grows through
the day and resets each morning.
Start hour is adjustable in the settings panel ('Today starts at'
slider) and persists in localStorage (bird:todayStart).
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
+56
-9
@@ -160,15 +160,29 @@
|
|||||||
applySiteTitle(siteTitle);
|
applySiteTitle(siteTitle);
|
||||||
|
|
||||||
var winBtns = [].slice.call(winPick.querySelectorAll('button'));
|
var winBtns = [].slice.call(winPick.querySelectorAll('button'));
|
||||||
var currentHours = +readLS('bird:window', '24') || 24;
|
var START_HOUR = +(readLS('bird:todayStart', '6')) || 6;
|
||||||
|
var windowMode = readLS('bird:window', '24');
|
||||||
|
var currentHours = windowMode === 'today' ? todayHours() : (+windowMode || 24);
|
||||||
|
function todayHours() {
|
||||||
|
var now = new Date();
|
||||||
|
var start = new Date(now); start.setHours(START_HOUR, 0, 0, 0);
|
||||||
|
if (now < start) start.setDate(start.getDate() - 1);
|
||||||
|
return Math.max(1, (now - start) / 3600000);
|
||||||
|
}
|
||||||
|
function effectiveHours() {
|
||||||
|
if (windowMode === 'today') { currentHours = todayHours(); }
|
||||||
|
return currentHours;
|
||||||
|
}
|
||||||
winBtns.forEach(function (b) {
|
winBtns.forEach(function (b) {
|
||||||
b.setAttribute('aria-current', (+b.dataset.h === currentHours) ? 'true' : 'false');
|
var match = windowMode === 'today' ? b.dataset.h === 'today' : +b.dataset.h === currentHours;
|
||||||
|
b.setAttribute('aria-current', match ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
winBtns.forEach(function (b) {
|
winBtns.forEach(function (b) {
|
||||||
b.addEventListener('click', function () {
|
b.addEventListener('click', function () {
|
||||||
winBtns.forEach(function (x) { x.setAttribute('aria-current', x === b ? 'true' : 'false'); });
|
winBtns.forEach(function (x) { x.setAttribute('aria-current', x === b ? 'true' : 'false'); });
|
||||||
currentHours = +b.dataset.h;
|
windowMode = b.dataset.h;
|
||||||
writeLS('bird:window', String(currentHours));
|
currentHours = windowMode === 'today' ? todayHours() : +windowMode;
|
||||||
|
writeLS('bird:window', windowMode);
|
||||||
syncPill(winPick);
|
syncPill(winPick);
|
||||||
// Actual data refresh is wired below via refreshRecent().
|
// Actual data refresh is wired below via refreshRecent().
|
||||||
});
|
});
|
||||||
@@ -850,9 +864,10 @@
|
|||||||
// a bare "window" with the span it actually covers. Thresholds match
|
// a bare "window" with the span it actually covers. Thresholds match
|
||||||
// the winPick buttons (1H / 12H / 24H / 7D / ALL).
|
// the winPick buttons (1H / 12H / 24H / 7D / ALL).
|
||||||
function windowLabel(h) {
|
function windowLabel(h) {
|
||||||
|
if (windowMode === 'today') return 'today';
|
||||||
if (h <= 1) return 'this hour';
|
if (h <= 1) return 'this hour';
|
||||||
if (h <= 12) return 'past 12h';
|
if (h <= 12) return 'past 12h';
|
||||||
if (h <= 24) return 'today';
|
if (h <= 24) return 'past 24h';
|
||||||
if (h <= 168) return 'this week';
|
if (h <= 168) return 'this week';
|
||||||
return 'all time';
|
return 'all time';
|
||||||
}
|
}
|
||||||
@@ -1381,8 +1396,8 @@
|
|||||||
// changes the picker again before it resolves - or a slower poll
|
// changes the picker again before it resolves - or a slower poll
|
||||||
// lands later - we discard the stale response so the collage
|
// lands later - we discard the stale response so the collage
|
||||||
// never reverts to a different window.
|
// never reverts to a different window.
|
||||||
var forHours = currentHours;
|
var forHours = effectiveHours();
|
||||||
return fetchJson('./avian/api/birdnet-api.php?action=recent&hours=' + forHours)
|
return fetchJson('./avian/api/birdnet-api.php?action=recent&hours=' + Math.round(forHours))
|
||||||
.then(function (j) {
|
.then(function (j) {
|
||||||
if (forHours !== currentHours) return; // window changed mid-flight
|
if (forHours !== currentHours) return; // window changed mid-flight
|
||||||
DATA.recent = j; renderWindowDependent(animate);
|
DATA.recent = j; renderWindowDependent(animate);
|
||||||
@@ -1390,13 +1405,13 @@
|
|||||||
.catch(function (e) { console.warn('recent fetch failed', e); });
|
.catch(function (e) { console.warn('recent fetch failed', e); });
|
||||||
}
|
}
|
||||||
function refreshAll(animate) {
|
function refreshAll(animate) {
|
||||||
var forHours = currentHours;
|
var forHours = effectiveHours();
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
fetchJson('./avian/api/birdnet-api.php?action=stats').catch(function () { return null; }),
|
fetchJson('./avian/api/birdnet-api.php?action=stats').catch(function () { return null; }),
|
||||||
fetchJson('./avian/api/birdnet-api.php?action=lifelist').catch(function () { return null; }),
|
fetchJson('./avian/api/birdnet-api.php?action=lifelist').catch(function () { return null; }),
|
||||||
fetchJson('./avian/api/birdnet-api.php?action=timeseries&days=30').catch(function () { return null; }),
|
fetchJson('./avian/api/birdnet-api.php?action=timeseries&days=30').catch(function () { return null; }),
|
||||||
fetchJson('./avian/api/birdnet-api.php?action=firstseen&limit=10').catch(function () { return null; }),
|
fetchJson('./avian/api/birdnet-api.php?action=firstseen&limit=10').catch(function () { return null; }),
|
||||||
fetchJson('./avian/api/birdnet-api.php?action=recent&hours=' + forHours).catch(function () { return null; }),
|
fetchJson('./avian/api/birdnet-api.php?action=recent&hours=' + Math.round(forHours)).catch(function () { return null; }),
|
||||||
]).then(function (parts) {
|
]).then(function (parts) {
|
||||||
DATA.stats = parts[0];
|
DATA.stats = parts[0];
|
||||||
DATA.lifelist = parts[1];
|
DATA.lifelist = parts[1];
|
||||||
@@ -1817,8 +1832,39 @@
|
|||||||
+ ' <input type="text" class="title-input" data-title-input maxlength="40" value="' + adminEsc(siteTitle) + '" placeholder="your birds">'
|
+ ' <input type="text" class="title-input" data-title-input maxlength="40" value="' + adminEsc(siteTitle) + '" placeholder="your birds">'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
}
|
}
|
||||||
|
function todayStartRow() {
|
||||||
|
var h = START_HOUR;
|
||||||
|
var label = (h === 0 ? '12' : h > 12 ? '' + (h - 12) : '' + h) + ':00 ' + (h < 12 ? 'AM' : 'PM');
|
||||||
|
return ''
|
||||||
|
+ '<div class="slider-row">'
|
||||||
|
+ ' <div class="head">'
|
||||||
|
+ ' <div class="label-block">'
|
||||||
|
+ ' <span class="label">Today starts at</span>'
|
||||||
|
+ ' <span class="hint">when the TODAY window resets</span>'
|
||||||
|
+ ' </div>'
|
||||||
|
+ ' <span class="value" data-value-for="todayStart">' + label + '</span>'
|
||||||
|
+ ' </div>'
|
||||||
|
+ ' <div class="slider-track">'
|
||||||
|
+ ' <input type="range" min="0" max="12" step="1" value="' + h + '" data-today-start>'
|
||||||
|
+ ' </div>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
function wireSettingsControls(scope) {
|
function wireSettingsControls(scope) {
|
||||||
scope = scope || document;
|
scope = scope || document;
|
||||||
|
var todaySlider = scope.querySelector('[data-today-start]');
|
||||||
|
if (todaySlider) {
|
||||||
|
todaySlider.addEventListener('input', function () {
|
||||||
|
var h = +todaySlider.value;
|
||||||
|
var label = (h === 0 ? '12' : h > 12 ? '' + (h - 12) : '' + h) + ':00 ' + (h < 12 ? 'AM' : 'PM');
|
||||||
|
var valEl = scope.querySelector('[data-value-for="todayStart"]');
|
||||||
|
if (valEl) valEl.textContent = label;
|
||||||
|
});
|
||||||
|
todaySlider.addEventListener('change', function () {
|
||||||
|
START_HOUR = +todaySlider.value;
|
||||||
|
writeLS('bird:todayStart', String(START_HOUR));
|
||||||
|
if (windowMode === 'today') { currentHours = todayHours(); refreshRecent(true); }
|
||||||
|
});
|
||||||
|
}
|
||||||
scope.querySelectorAll('.switch').forEach(function (sw) {
|
scope.querySelectorAll('.switch').forEach(function (sw) {
|
||||||
sw.addEventListener('click', function () {
|
sw.addEventListener('click', function () {
|
||||||
var on = sw.getAttribute('aria-checked') !== 'true';
|
var on = sw.getAttribute('aria-checked') !== 'true';
|
||||||
@@ -2389,6 +2435,7 @@
|
|||||||
'<div class="admin-settings">'
|
'<div class="admin-settings">'
|
||||||
+ themeRow()
|
+ themeRow()
|
||||||
+ titleRow()
|
+ titleRow()
|
||||||
|
+ todayStartRow()
|
||||||
+ settingsToggle('preserve', 'Preserve all recordings', "don't auto-delete", preserve)
|
+ settingsToggle('preserve', 'Preserve all recordings', "don't auto-delete", preserve)
|
||||||
+ settingsSlider('CONFIDENCE', 'Confidence threshold', 'min score to log a detection', v.CONFIDENCE, 0.1, 0.95, 0.05, 2)
|
+ settingsSlider('CONFIDENCE', 'Confidence threshold', 'min score to log a detection', v.CONFIDENCE, 0.1, 0.95, 0.05, 2)
|
||||||
+ settingsSlider('SENSITIVITY', 'Sensitivity', 'analyzer sensitivity', v.SENSITIVITY, 0.5, 1.5, 0.05, 2)
|
+ settingsSlider('SENSITIVITY', 'Sensitivity', 'analyzer sensitivity', v.SENSITIVITY, 0.5, 1.5, 0.05, 2)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<div class="window-pick" id="winPick" role="tablist">
|
<div class="window-pick" id="winPick" role="tablist">
|
||||||
<i class="seg-pill" aria-hidden="true"></i>
|
<i class="seg-pill" aria-hidden="true"></i>
|
||||||
<button data-h="1" type="button">1H</button>
|
<button data-h="1" type="button">1H</button>
|
||||||
|
<button data-h="today" type="button">TODAY</button>
|
||||||
<button data-h="12" type="button">12H</button>
|
<button data-h="12" type="button">12H</button>
|
||||||
<button data-h="24" type="button" aria-current="true">24H</button>
|
<button data-h="24" type="button" aria-current="true">24H</button>
|
||||||
<button data-h="168" type="button">7D</button>
|
<button data-h="168" type="button">7D</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user