Kiosk: apply schedule engine in playback and add System Settings toggle

ScrollerPage now resolves the active Block from ScheduleManager (reusing
the existing stream-switch path), honoring dynamicAudio true/false and
falling back to local selection for gaps, no schedule, or toggle off.
Adds a 'Schedule' On/Off entry (default On) in System Settings persisted
in config.yaml, and config plumbing (proclamation.schedule_enabled). All
on-device schedule times remain 12h AM/PM.
This commit is contained in:
NeilClack
2026-09-29 10:34:45 -05:00
parent 509c925480
commit da0b6ffd5e
15 changed files with 959 additions and 41 deletions
+39 -26
View File
@@ -214,15 +214,32 @@ void ScrollerPage::buildStreams(AppState &state) {
streamIndex_ = 0;
scheduleChangePending_ = false;
// Schedule state: only an active schedule with a currently-matching block
// changes the play list. The block boundary is handled in advanceStream(),
// so rebuilding mid-playback never truncates the current proclamation.
// Schedule state: resolve the active Block from the schedule manager (which
// owns the fetched/cached payload and the local on/off toggle). The block
// boundary is handled in advanceStream(), so rebuilding mid-playback never
// truncates the current proclamation.
const radiantwave::config::AppConfig cfg = state.config.get();
scheduleActive_ = cfg.proclamation.schedule_active;
timezone_ = cfg.timezone;
scheduleBlocks_ = scheduleActive_ ? state.db.getScheduleBlocks()
: std::vector<radiantwave::db::ScheduleBlockRecord>{};
activeBlockIndex_ = currentScheduleBlockIndex();
// Keep the engine's local toggle in step with config (it may have been
// changed in System Settings or hot-reloaded).
state.scheduleManager.setEnabled(cfg.proclamation.schedule_enabled);
const int now = radiantwave::timezone::currentLocalMinutes(timezone_);
const radiantwave::schedule::Weekday weekday =
radiantwave::schedule::weekdayFromIndex(
radiantwave::timezone::currentLocalWeekday(timezone_));
const radiantwave::schedule::Decision decision =
state.scheduleManager.activeBlock(now, weekday);
scheduleActive_ = state.scheduleManager.enabled();
if (decision.hasBlock) {
activeBlockProclamationIds_ = decision.block.proclamation_ids;
activeBlockDynamicAudio_ = decision.block.dynamic_audio;
activeBlockIndex_ = 1; // any non-negative marker: a block is active
} else {
activeBlockProclamationIds_.clear();
activeBlockDynamicAudio_ = false;
activeBlockIndex_ = -1;
}
auto proclamations = state.db.getActiveProclamations();
if (proclamations.empty()) {
@@ -264,9 +281,8 @@ void ScrollerPage::buildStreams(AppState &state) {
const std::vector<std::string>* scheduledIds = nullptr;
std::vector<std::string> scheduledOrder;
if (activeBlockIndex_ >= 0 &&
activeBlockIndex_ < static_cast<int>(scheduleBlocks_.size())) {
scheduledOrder = scheduleBlocks_[activeBlockIndex_].proclamation_ids;
if (activeBlockIndex_ >= 0 && !activeBlockProclamationIds_.empty()) {
scheduledOrder = activeBlockProclamationIds_;
scheduledIds = &scheduledOrder;
}
@@ -275,26 +291,25 @@ void ScrollerPage::buildStreams(AppState &state) {
DEFAULT_PROPLEAMATION_ID, scheduledIds);
}
int ScrollerPage::currentScheduleBlockIndex() const {
if (!scheduleActive_ || scheduleBlocks_.empty()) {
return -1;
bool ScrollerPage::scheduleBlockActiveNow() const {
if (!state_) {
return false;
}
const int now = radiantwave::timezone::currentLocalMinutes(timezone_);
for (size_t i = 0; i < scheduleBlocks_.size(); ++i) {
const radiantwave::db::ScheduleBlockRecord &block = scheduleBlocks_[i];
if (radiantwave::scroller::minuteInScheduleBlock(
now, block.start_minutes, block.end_minutes, block.overnight)) {
return static_cast<int>(i);
}
}
return -1;
const radiantwave::schedule::Weekday weekday =
radiantwave::schedule::weekdayFromIndex(
radiantwave::timezone::currentLocalWeekday(timezone_));
return state_->scheduleManager.activeBlock(now, weekday).hasBlock;
}
void ScrollerPage::pollScheduleChange() {
if (!scheduleActive_) {
// The manager owns the local toggle; when off, there is never an active block.
if (!state_) {
return;
}
if (currentScheduleBlockIndex() != activeBlockIndex_) {
const bool activeNow = scheduleBlockActiveNow();
const bool hadBlock = activeBlockIndex_ >= 0;
if (activeNow != hadBlock) {
scheduleChangePending_ = true;
}
}
@@ -303,9 +318,7 @@ bool ScrollerPage::effectiveDynamicAudio() const {
// dynamicAudio=true on the active block forces dynamic audio, overriding
// whatever the device selected. false means "don't override": the device's
// own selection (dynamic or a fixed track) applies as usual.
if (scheduleActive_ && activeBlockIndex_ >= 0 &&
activeBlockIndex_ < static_cast<int>(scheduleBlocks_.size()) &&
scheduleBlocks_[activeBlockIndex_].dynamic_audio) {
if (scheduleActive_ && activeBlockIndex_ >= 0 && activeBlockDynamicAudio_) {
return true;
}
return state_ ? state_->config.get().audio.dynamic_audio : false;