mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-24 11:39:49 +00:00
247 lines
11 KiB
C++
247 lines
11 KiB
C++
#include "LocationShareScreen.h"
|
|
#include "Theme.h"
|
|
|
|
#ifdef ARDUINO
|
|
#include "../LVGL/LVGLLock.h"
|
|
#include "../LVGL/LVGLInit.h"
|
|
#include <cstring>
|
|
|
|
namespace UI {
|
|
namespace LXMF {
|
|
|
|
// NO DIRECT SCHEDULER ACCESS CONTRACT: this screen may only publish fixed UI
|
|
// commands. It never owns or mutates LocationShareScheduler, persistence, I/O,
|
|
// or router state.
|
|
namespace {
|
|
lv_obj_t* makeButton(lv_obj_t* parent, const char* text, lv_coord_t width) {
|
|
lv_obj_t* button = lv_btn_create(parent);
|
|
lv_obj_set_width(button, width);
|
|
lv_obj_t* label = lv_label_create(button);
|
|
lv_label_set_text(label, text);
|
|
lv_obj_center(label);
|
|
return button;
|
|
}
|
|
}
|
|
|
|
LocationShareScreen::LocationShareScreen()
|
|
: screen_(nullptr), back_button_(nullptr), state_label_(nullptr), error_label_(nullptr),
|
|
duration_dropdown_(nullptr), cadence_dropdown_(nullptr),
|
|
precision_dropdown_(nullptr), start_button_(nullptr), stop_button_(nullptr),
|
|
confirmation_dialog_(nullptr) {
|
|
LVGL_LOCK();
|
|
screen_ = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(screen_, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_set_style_bg_color(screen_, Theme::surface(), 0);
|
|
lv_obj_set_style_pad_all(screen_, 6, 0);
|
|
lv_obj_set_flex_flow(screen_, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_gap(screen_, 4, 0);
|
|
|
|
lv_obj_t* header = lv_obj_create(screen_);
|
|
lv_obj_set_size(header, LV_PCT(100), 34);
|
|
lv_obj_set_flex_flow(header, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_style_pad_all(header, 2, 0);
|
|
back_button_ = makeButton(header, LV_SYMBOL_LEFT, 44);
|
|
lv_obj_add_event_cb(back_button_, on_back, LV_EVENT_CLICKED, this);
|
|
lv_obj_t* title = lv_label_create(header);
|
|
lv_label_set_text(title, LV_SYMBOL_GPS " Location sharing");
|
|
lv_obj_set_style_text_color(title, Theme::textPrimary(), 0);
|
|
lv_obj_set_flex_grow(title, 1);
|
|
|
|
state_label_ = lv_label_create(screen_);
|
|
lv_obj_set_width(state_label_, LV_PCT(100));
|
|
lv_label_set_long_mode(state_label_, LV_LABEL_LONG_WRAP);
|
|
|
|
duration_dropdown_ = lv_dropdown_create(screen_);
|
|
lv_dropdown_set_options(duration_dropdown_, "15 min\n1 hour\n4 hours");
|
|
lv_obj_set_width(duration_dropdown_, LV_PCT(100));
|
|
lv_obj_add_event_cb(duration_dropdown_, on_selection, LV_EVENT_VALUE_CHANGED, this);
|
|
cadence_dropdown_ = lv_dropdown_create(screen_);
|
|
lv_dropdown_set_options(cadence_dropdown_, "1 min\n5 min\n15 min");
|
|
lv_obj_set_width(cadence_dropdown_, LV_PCT(100));
|
|
lv_obj_add_event_cb(cadence_dropdown_, on_selection, LV_EVENT_VALUE_CHANGED, this);
|
|
precision_dropdown_ = lv_dropdown_create(screen_);
|
|
lv_dropdown_set_options(precision_dropdown_, "Exact (approximation off)\n100 m\n1 km\n10 km");
|
|
lv_obj_set_width(precision_dropdown_, LV_PCT(100));
|
|
lv_obj_add_event_cb(precision_dropdown_, on_selection, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
lv_obj_t* actions = lv_obj_create(screen_);
|
|
lv_obj_set_size(actions, LV_PCT(100), 42);
|
|
lv_obj_set_flex_flow(actions, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_style_pad_all(actions, 2, 0);
|
|
start_button_ = makeButton(actions, "Start / Update", 180);
|
|
lv_obj_add_event_cb(start_button_, on_start, LV_EVENT_CLICKED, this);
|
|
stop_button_ = makeButton(actions, "Stop", 100);
|
|
lv_obj_add_event_cb(stop_button_, on_stop, LV_EVENT_CLICKED, this);
|
|
|
|
error_label_ = lv_label_create(screen_);
|
|
lv_obj_set_width(error_label_, LV_PCT(100));
|
|
lv_label_set_long_mode(error_label_, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_style_text_color(error_label_, Theme::error(), 0);
|
|
hide();
|
|
}
|
|
|
|
LocationShareScreen::~LocationShareScreen() {
|
|
hide();
|
|
LVGL_LOCK();
|
|
if (screen_) lv_obj_del(screen_);
|
|
}
|
|
|
|
bool LocationShareScreen::open_for_peer(const uint8_t* peer, std::size_t peer_size,
|
|
const Telemetry::ShareSession* session) {
|
|
if (!model_.openForPeer(peer, peer_size)) { refresh(); return false; }
|
|
if (session) {
|
|
if (session->cease_pending) model_.markStopping();
|
|
else model_.applyActive(session->expires_at_millis, session->cadence_millis,
|
|
session->has_approx_radius, session->approx_radius_meters);
|
|
}
|
|
refresh();
|
|
return true;
|
|
}
|
|
|
|
bool LocationShareScreen::matches_peer(const uint8_t* peer, std::size_t peer_size) const {
|
|
return peer != nullptr && peer_size == 16 && model_.hasPeer() &&
|
|
std::memcmp(model_.peer(), peer, 16) == 0;
|
|
}
|
|
|
|
void LocationShareScreen::apply_result(Telemetry::LocationConsentResult result,
|
|
const Telemetry::ShareSession* session) {
|
|
switch (result) {
|
|
case Telemetry::LocationConsentResult::STARTED:
|
|
case Telemetry::LocationConsentResult::UPDATED:
|
|
if (session) model_.applyActive(session->expires_at_millis, session->cadence_millis,
|
|
session->has_approx_radius, session->approx_radius_meters);
|
|
else model_.applyError(LocationShareControlError::INVALID);
|
|
break;
|
|
case Telemetry::LocationConsentResult::STOPPING: model_.markStopping(); break;
|
|
case Telemetry::LocationConsentResult::NOT_FOUND: model_.applyOff(); break;
|
|
case Telemetry::LocationConsentResult::CLOCK_UNAVAILABLE:
|
|
model_.applyError(LocationShareControlError::CLOCK_UNAVAILABLE); break;
|
|
case Telemetry::LocationConsentResult::STORAGE_FAILURE:
|
|
case Telemetry::LocationConsentResult::NOT_READY:
|
|
model_.applyError(LocationShareControlError::STORAGE_FAILURE); break;
|
|
case Telemetry::LocationConsentResult::CAPACITY:
|
|
model_.applyError(LocationShareControlError::CAPACITY); break;
|
|
case Telemetry::LocationConsentResult::BUSY:
|
|
model_.applyError(LocationShareControlError::BUSY); break;
|
|
case Telemetry::LocationConsentResult::INVALID_ARGUMENT:
|
|
model_.applyError(LocationShareControlError::INVALID); break;
|
|
}
|
|
refresh();
|
|
}
|
|
|
|
void LocationShareScreen::refresh() {
|
|
char state[160] = {};
|
|
model_.formatState(state, sizeof(state));
|
|
lv_label_set_text(state_label_, state);
|
|
lv_label_set_text(error_label_, model_.errorText());
|
|
const bool canStop = model_.status() == LocationShareControlStatus::ACTIVE;
|
|
if (canStop) lv_obj_clear_state(stop_button_, LV_STATE_DISABLED);
|
|
else lv_obj_add_state(stop_button_, LV_STATE_DISABLED);
|
|
}
|
|
|
|
void LocationShareScreen::show_confirmation() {
|
|
if (!model_.requestConfirmation()) { refresh(); return; }
|
|
char text[192] = {};
|
|
if (!model_.formatConfirmation(text, sizeof(text))) {
|
|
model_.applyError(LocationShareControlError::INVALID); refresh(); return;
|
|
}
|
|
static const char* buttons[] = {"Confirm", "Cancel", ""};
|
|
confirmation_dialog_ = lv_msgbox_create(nullptr, "Confirm location sharing", text,
|
|
buttons, false);
|
|
lv_obj_center(confirmation_dialog_);
|
|
lv_obj_add_event_cb(confirmation_dialog_, on_confirmation,
|
|
LV_EVENT_VALUE_CHANGED, this);
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_obj_t* buttons_object = lv_msgbox_get_btns(confirmation_dialog_);
|
|
lv_group_add_obj(group, buttons_object);
|
|
lv_group_focus_obj(buttons_object);
|
|
}
|
|
}
|
|
|
|
void LocationShareScreen::show() {
|
|
LVGL_LOCK();
|
|
lv_obj_clear_flag(screen_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(screen_);
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_add_obj(group, back_button_);
|
|
lv_group_add_obj(group, duration_dropdown_);
|
|
lv_group_add_obj(group, cadence_dropdown_);
|
|
lv_group_add_obj(group, precision_dropdown_);
|
|
lv_group_add_obj(group, start_button_);
|
|
lv_group_add_obj(group, stop_button_);
|
|
lv_group_focus_obj(back_button_);
|
|
}
|
|
}
|
|
void LocationShareScreen::hide() {
|
|
LVGL_LOCK();
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_remove_obj(back_button_);
|
|
lv_group_remove_obj(duration_dropdown_);
|
|
lv_group_remove_obj(cadence_dropdown_);
|
|
lv_group_remove_obj(precision_dropdown_);
|
|
lv_group_remove_obj(start_button_);
|
|
lv_group_remove_obj(stop_button_);
|
|
if (confirmation_dialog_) {
|
|
lv_group_remove_obj(lv_msgbox_get_btns(confirmation_dialog_));
|
|
}
|
|
}
|
|
if (confirmation_dialog_) { lv_msgbox_close(confirmation_dialog_); confirmation_dialog_ = nullptr; }
|
|
model_.cancelConfirmation();
|
|
lv_obj_add_flag(screen_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
void LocationShareScreen::on_back(lv_event_t* event) {
|
|
LocationShareScreen* self = static_cast<LocationShareScreen*>(lv_event_get_user_data(event));
|
|
if (self->back_callback_) self->back_callback_();
|
|
}
|
|
void LocationShareScreen::on_start(lv_event_t* event) {
|
|
static_cast<LocationShareScreen*>(lv_event_get_user_data(event))->show_confirmation();
|
|
}
|
|
void LocationShareScreen::on_stop(lv_event_t* event) {
|
|
LocationShareScreen* self = static_cast<LocationShareScreen*>(lv_event_get_user_data(event));
|
|
if (!self->model_.hasPeer() || !self->stop_callback_ ||
|
|
!self->stop_callback_(self->model_.peer(), 16)) {
|
|
self->model_.applyError(LocationShareControlError::BUSY);
|
|
} else {
|
|
self->model_.markStopping();
|
|
}
|
|
self->refresh();
|
|
}
|
|
void LocationShareScreen::on_selection(lv_event_t* event) {
|
|
LocationShareScreen* self = static_cast<LocationShareScreen*>(lv_event_get_user_data(event));
|
|
lv_obj_t* target = lv_event_get_target(event);
|
|
const uint16_t selected = lv_dropdown_get_selected(target);
|
|
if (target == self->duration_dropdown_) self->model_.selectDuration(static_cast<LocationShareDuration>(selected));
|
|
else if (target == self->cadence_dropdown_) self->model_.selectCadence(static_cast<LocationShareCadence>(selected));
|
|
else if (target == self->precision_dropdown_) self->model_.selectPrecision(static_cast<LocationSharePrecision>(selected));
|
|
}
|
|
void LocationShareScreen::on_confirmation(lv_event_t* event) {
|
|
LocationShareScreen* self = static_cast<LocationShareScreen*>(lv_event_get_user_data(event));
|
|
lv_obj_t* dialog = lv_event_get_current_target(event);
|
|
const uint16_t selected = lv_msgbox_get_active_btn(dialog);
|
|
if (selected == 0 && self->start_callback_) {
|
|
const bool queued = self->start_callback_(
|
|
self->model_.peer(), 16, static_cast<uint8_t>(self->model_.duration()),
|
|
self->model_.cadenceMillis(), self->model_.hasApproximation(),
|
|
self->model_.approximationMeters());
|
|
if (!queued) self->model_.applyError(LocationShareControlError::BUSY);
|
|
} else {
|
|
self->model_.cancelConfirmation();
|
|
}
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_remove_obj(lv_msgbox_get_btns(dialog));
|
|
lv_group_focus_obj(self->start_button_);
|
|
}
|
|
self->confirmation_dialog_ = nullptr;
|
|
lv_msgbox_close(dialog);
|
|
self->refresh();
|
|
}
|
|
|
|
} // namespace LXMF
|
|
} // namespace UI
|
|
#endif
|