feat: add MTR Next Train application with multi-page navigation and real-time arrival info
This commit is contained in:
399
main/ui/apps/mtr_app.cpp
Normal file
399
main/ui/apps/mtr_app.cpp
Normal file
@@ -0,0 +1,399 @@
|
||||
#include "apps/mtr_app.h"
|
||||
#include "external/mtr/arrival.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define TAG "MtrApp"
|
||||
|
||||
// Event type for network ready
|
||||
#define EVENT_NETWORK_READY 1
|
||||
|
||||
MtrApp::MtrApp() {
|
||||
_mtr_handler = std::make_unique<MTRNextTrainHandler>();
|
||||
}
|
||||
|
||||
esp_err_t MtrApp::init(lv_obj_t* container) {
|
||||
if (!container) {
|
||||
ESP_LOGE(TAG, "Container is null");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
_container = container;
|
||||
ESP_LOGI(TAG, "Initializing MTR app...");
|
||||
|
||||
// Create page stack
|
||||
_page_stack = std::make_unique<PageStack>(container);
|
||||
|
||||
// Load all lines
|
||||
_all_lines = _mtr_handler->get_lines();
|
||||
ESP_LOGI(TAG, "Loaded %zu MTR lines", _all_lines.size());
|
||||
|
||||
// Build initial line selection page
|
||||
_page_stack->push([this](lv_obj_t* page) {
|
||||
this->build_line_selection_page(page);
|
||||
});
|
||||
|
||||
ESP_LOGI(TAG, "MTR app initialized successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t MtrApp::deinit(void) {
|
||||
ESP_LOGI(TAG, "Deinitializing MTR app");
|
||||
|
||||
// Clear page stack
|
||||
if (_page_stack) {
|
||||
_page_stack->clear();
|
||||
_page_stack.reset();
|
||||
}
|
||||
|
||||
// Clear state
|
||||
_selected_line_code.clear();
|
||||
_selected_station_code.clear();
|
||||
_selected_line_info = nullptr;
|
||||
_all_lines.clear();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
std::string MtrApp::get_name(void) const {
|
||||
return "MTR";
|
||||
}
|
||||
|
||||
bool MtrApp::on_back_button_pressed(void) {
|
||||
if (_page_stack && _page_stack->depth() > 1) {
|
||||
_page_stack->pop();
|
||||
return true; // Handled
|
||||
}
|
||||
return false; // Not handled, go back to main menu
|
||||
}
|
||||
|
||||
void MtrApp::handle_event(uint32_t event_type, void* event_data) {
|
||||
if (event_type == EVENT_NETWORK_READY) {
|
||||
ESP_LOGI(TAG, "Network ready event received");
|
||||
}
|
||||
}
|
||||
|
||||
void MtrApp::build_line_selection_page(lv_obj_t* page_container) {
|
||||
ESP_LOGI(TAG, "Building line selection page");
|
||||
|
||||
// Title
|
||||
lv_obj_t* title = lv_label_create(page_container);
|
||||
lv_label_set_text(title, "選擇路綫 Select Line");
|
||||
lv_obj_set_style_text_color(title, lv_color_black(), 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
|
||||
|
||||
// Scrollable container for line buttons
|
||||
lv_obj_t* scroll_container = lv_obj_create(page_container);
|
||||
lv_obj_set_size(scroll_container, lv_pct(95), lv_pct(85));
|
||||
lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 40);
|
||||
lv_obj_set_flex_flow(scroll_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(scroll_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(scroll_container, 5, 0);
|
||||
lv_obj_set_style_pad_row(scroll_container, 8, 0);
|
||||
|
||||
// Create button for each line
|
||||
for (size_t i = 0; i < _all_lines.size(); i++) {
|
||||
LineInfo* line = &_all_lines[i];
|
||||
|
||||
lv_obj_t* btn = lv_btn_create(scroll_container);
|
||||
lv_obj_set_size(btn, lv_pct(95), 60);
|
||||
|
||||
// Set button color based on line color
|
||||
uint32_t color = parse_color_hex(line->color());
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(color), 0);
|
||||
|
||||
// Button label
|
||||
lv_obj_t* label = lv_label_create(btn);
|
||||
lv_label_set_text_fmt(label, "%s", line->code());
|
||||
lv_obj_set_style_text_color(label, lv_color_white(), 0);
|
||||
lv_obj_center(label);
|
||||
|
||||
// Store line pointer in user data
|
||||
lv_obj_add_event_cb(btn, line_button_event_cb, LV_EVENT_CLICKED, this);
|
||||
lv_obj_set_user_data(btn, (void*)line);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Created %zu line buttons", _all_lines.size());
|
||||
}
|
||||
|
||||
void MtrApp::build_station_selection_page(lv_obj_t* page_container) {
|
||||
ESP_LOGI(TAG, "Building station selection page for line: %s", _selected_line_code.c_str());
|
||||
|
||||
if (!_selected_line_info) {
|
||||
ESP_LOGE(TAG, "No line info selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Title with line code
|
||||
lv_obj_t* title = lv_label_create(page_container);
|
||||
lv_label_set_text_fmt(title, "%s 路綫車站", _selected_line_code.c_str());
|
||||
lv_obj_set_style_text_color(title, lv_color_black(), 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
|
||||
|
||||
// Scrollable container for station buttons
|
||||
lv_obj_t* scroll_container = lv_obj_create(page_container);
|
||||
lv_obj_set_size(scroll_container, lv_pct(95), lv_pct(85));
|
||||
lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 40);
|
||||
lv_obj_set_flex_flow(scroll_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(scroll_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(scroll_container, 5, 0);
|
||||
lv_obj_set_style_pad_row(scroll_container, 6, 0);
|
||||
|
||||
// Create button for each station
|
||||
const std::vector<StationInfo>* stations = _selected_line_info->stations();
|
||||
for (size_t i = 0; i < stations->size(); i++) {
|
||||
const StationInfo* station = &(*stations)[i];
|
||||
|
||||
lv_obj_t* btn = lv_btn_create(scroll_container);
|
||||
lv_obj_set_size(btn, lv_pct(95), 50);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(0x4CAF50), 0);
|
||||
|
||||
// Button label with station name and code
|
||||
lv_obj_t* label = lv_label_create(btn);
|
||||
lv_label_set_text_fmt(label, "%s (%s)", station->name(), station->code());
|
||||
lv_obj_set_style_text_color(label, lv_color_white(), 0);
|
||||
lv_obj_center(label);
|
||||
|
||||
// Store station pointer in user data
|
||||
lv_obj_add_event_cb(btn, station_button_event_cb, LV_EVENT_CLICKED, this);
|
||||
lv_obj_set_user_data(btn, (void*)station);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Created %zu station buttons", stations->size());
|
||||
}
|
||||
|
||||
void MtrApp::build_arrival_page(lv_obj_t* page_container) {
|
||||
ESP_LOGI(TAG, "Building arrival page");
|
||||
|
||||
// Title
|
||||
lv_obj_t* title = lv_label_create(page_container);
|
||||
lv_label_set_text_fmt(title, "%s - %s", _selected_line_code.c_str(), _selected_station_code.c_str());
|
||||
lv_obj_set_style_text_color(title, lv_color_black(), 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
|
||||
|
||||
// Loading message
|
||||
lv_obj_t* loading_label = lv_label_create(page_container);
|
||||
lv_label_set_text(loading_label, "載入中... Loading...");
|
||||
lv_obj_set_style_text_color(loading_label, lv_color_black(), 0);
|
||||
lv_obj_center(loading_label);
|
||||
|
||||
// Refresh button
|
||||
lv_obj_t* refresh_btn = lv_btn_create(page_container);
|
||||
lv_obj_set_size(refresh_btn, 120, 50);
|
||||
lv_obj_align(refresh_btn, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_add_event_cb(refresh_btn, refresh_button_event_cb, LV_EVENT_CLICKED, this);
|
||||
|
||||
lv_obj_t* refresh_label = lv_label_create(refresh_btn);
|
||||
lv_label_set_text(refresh_label, LV_SYMBOL_REFRESH " 重新整理");
|
||||
lv_obj_set_style_text_color(refresh_label, lv_color_white(), 0);
|
||||
lv_obj_center(refresh_label);
|
||||
|
||||
// Load arrival data asynchronously
|
||||
load_arrival_data(page_container);
|
||||
}
|
||||
|
||||
void MtrApp::load_arrival_data(lv_obj_t* page_container) {
|
||||
if (!_network_handler) {
|
||||
ESP_LOGW(TAG, "Network handler not set, cannot fetch arrival data");
|
||||
// Update UI to show error
|
||||
lv_obj_t* error_label = lv_label_create(page_container);
|
||||
lv_label_set_text(error_label, "網絡未就緒\nNetwork not ready");
|
||||
lv_obj_set_style_text_color(error_label, lv_color_black(), 0);
|
||||
lv_obj_align(error_label, LV_ALIGN_CENTER, 0, -30);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Fetching arrival data for %s/%s", _selected_line_code.c_str(), _selected_station_code.c_str());
|
||||
|
||||
StationArrivalInfo* arrival_info = nullptr;
|
||||
MtrArrivalErrorCode error_code = _mtr_handler->get_next_arrival_info(
|
||||
_network_handler,
|
||||
_selected_line_code,
|
||||
_selected_station_code,
|
||||
arrival_info,
|
||||
Language::TC
|
||||
);
|
||||
|
||||
// Clear loading message
|
||||
lv_obj_clean(page_container);
|
||||
|
||||
// Recreate title
|
||||
lv_obj_t* title = lv_label_create(page_container);
|
||||
lv_label_set_text_fmt(title, "%s - %s", _selected_line_code.c_str(), _selected_station_code.c_str());
|
||||
lv_obj_set_style_text_color(title, lv_color_black(), 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
|
||||
|
||||
if (error_code != MtrArrivalErrorCode::NONE || !arrival_info) {
|
||||
ESP_LOGE(TAG, "Failed to fetch arrival info, error code: %d", (int)error_code);
|
||||
|
||||
lv_obj_t* error_label = lv_label_create(page_container);
|
||||
lv_label_set_text(error_label, "無法取得班次資料\nFailed to fetch arrival data");
|
||||
lv_obj_set_style_text_color(error_label, lv_color_black(), 0);
|
||||
lv_obj_center(error_label);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create scrollable container for arrivals
|
||||
lv_obj_t* scroll_container = lv_obj_create(page_container);
|
||||
lv_obj_set_size(scroll_container, lv_pct(95), lv_pct(75));
|
||||
lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 45);
|
||||
lv_obj_set_style_pad_all(scroll_container, 10, 0);
|
||||
|
||||
int y_offset = 0;
|
||||
|
||||
// Display UP direction trains
|
||||
lv_obj_t* up_header = lv_label_create(scroll_container);
|
||||
lv_label_set_text(up_header, "上行 UP:");
|
||||
lv_obj_set_style_text_color(up_header, lv_color_black(), 0);
|
||||
lv_obj_set_pos(up_header, 0, y_offset);
|
||||
y_offset += 30;
|
||||
|
||||
const std::vector<ArrivalInfo>* up_arrivals = arrival_info->up_arrivals();
|
||||
if (up_arrivals->empty()) {
|
||||
lv_obj_t* no_train = lv_label_create(scroll_container);
|
||||
lv_label_set_text(no_train, " 暫無班次 No trains");
|
||||
lv_obj_set_style_text_color(no_train, lv_color_hex(0x666666), 0);
|
||||
lv_obj_set_pos(no_train, 10, y_offset);
|
||||
y_offset += 25;
|
||||
} else {
|
||||
for (const auto& arrival : *up_arrivals) {
|
||||
lv_obj_t* arrival_label = lv_label_create(scroll_container);
|
||||
lv_label_set_text_fmt(arrival_label, " %s → %s", arrival.arrival_time(), arrival.destination());
|
||||
lv_obj_set_style_text_color(arrival_label, lv_color_black(), 0);
|
||||
lv_obj_set_pos(arrival_label, 10, y_offset);
|
||||
y_offset += 25;
|
||||
}
|
||||
}
|
||||
|
||||
y_offset += 10;
|
||||
|
||||
// Display DOWN direction trains
|
||||
lv_obj_t* down_header = lv_label_create(scroll_container);
|
||||
lv_label_set_text(down_header, "下行 DOWN:");
|
||||
lv_obj_set_style_text_color(down_header, lv_color_black(), 0);
|
||||
lv_obj_set_pos(down_header, 0, y_offset);
|
||||
y_offset += 30;
|
||||
|
||||
const std::vector<ArrivalInfo>* down_arrivals = arrival_info->down_arrivals();
|
||||
if (down_arrivals->empty()) {
|
||||
lv_obj_t* no_train = lv_label_create(scroll_container);
|
||||
lv_label_set_text(no_train, " 暫無班次 No trains");
|
||||
lv_obj_set_style_text_color(no_train, lv_color_hex(0x666666), 0);
|
||||
lv_obj_set_pos(no_train, 10, y_offset);
|
||||
y_offset += 25;
|
||||
} else {
|
||||
for (const auto& arrival : *down_arrivals) {
|
||||
lv_obj_t* arrival_label = lv_label_create(scroll_container);
|
||||
lv_label_set_text_fmt(arrival_label, " %s → %s", arrival.arrival_time(), arrival.destination());
|
||||
lv_obj_set_style_text_color(arrival_label, lv_color_black(), 0);
|
||||
lv_obj_set_pos(arrival_label, 10, y_offset);
|
||||
y_offset += 25;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
if (arrival_info != nullptr) {
|
||||
delete arrival_info;
|
||||
}
|
||||
|
||||
// Refresh button
|
||||
lv_obj_t* refresh_btn = lv_btn_create(page_container);
|
||||
lv_obj_set_size(refresh_btn, 120, 50);
|
||||
lv_obj_align(refresh_btn, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_add_event_cb(refresh_btn, refresh_button_event_cb, LV_EVENT_CLICKED, this);
|
||||
|
||||
lv_obj_t* refresh_label = lv_label_create(refresh_btn);
|
||||
lv_label_set_text(refresh_label, LV_SYMBOL_REFRESH " 重新整理");
|
||||
lv_obj_set_style_text_color(refresh_label, lv_color_white(), 0);
|
||||
lv_obj_center(refresh_label);
|
||||
|
||||
ESP_LOGI(TAG, "Arrival data displayed successfully");
|
||||
}
|
||||
|
||||
uint32_t MtrApp::parse_color_hex(const char* hex_str) {
|
||||
if (!hex_str || hex_str[0] != '#') {
|
||||
return 0x808080; // Default gray
|
||||
}
|
||||
|
||||
// Skip the '#' character
|
||||
hex_str++;
|
||||
|
||||
uint32_t color = 0;
|
||||
sscanf(hex_str, "%" SCNx32, &color);
|
||||
return color;
|
||||
}
|
||||
|
||||
void MtrApp::line_button_event_cb(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
MtrApp* app = (MtrApp*)lv_event_get_user_data(e);
|
||||
lv_obj_t* btn = (lv_obj_t*)lv_event_get_target(e);
|
||||
LineInfo* line = (LineInfo*)lv_obj_get_user_data(btn);
|
||||
|
||||
if (app && line) {
|
||||
ESP_LOGI(TAG, "Line selected: %s", line->code());
|
||||
app->_selected_line_code = line->code();
|
||||
app->_selected_line_info = line;
|
||||
|
||||
// Push station selection page
|
||||
app->_page_stack->push([app](lv_obj_t* page) {
|
||||
app->build_station_selection_page(page);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MtrApp::station_button_event_cb(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
MtrApp* app = (MtrApp*)lv_event_get_user_data(e);
|
||||
lv_obj_t* btn = (lv_obj_t*)lv_event_get_target(e);
|
||||
const StationInfo* station = (const StationInfo*)lv_obj_get_user_data(btn);
|
||||
|
||||
if (app && station) {
|
||||
ESP_LOGI(TAG, "Station selected: %s (%s)", station->name(), station->code());
|
||||
app->_selected_station_code = station->code();
|
||||
|
||||
// Push arrival page
|
||||
app->_page_stack->push([app](lv_obj_t* page) {
|
||||
app->build_arrival_page(page);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MtrApp::refresh_button_event_cb(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_CLICKED) {
|
||||
MtrApp* app = (MtrApp*)lv_event_get_user_data(e);
|
||||
if (app && app->_page_stack && app->_page_stack->current_page()) {
|
||||
ESP_LOGI(TAG, "Refresh button clicked");
|
||||
app->load_arrival_data(app->_page_stack->current_page());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MtrAppDescriptor implementation
|
||||
MtrApp* MtrAppDescriptor::_app_instance = nullptr;
|
||||
|
||||
MtrAppDescriptor::MtrAppDescriptor()
|
||||
: AppDescriptor("MTR", []() -> UIApp* {
|
||||
if (!MtrAppDescriptor::_app_instance) {
|
||||
MtrAppDescriptor::_app_instance = new MtrApp();
|
||||
}
|
||||
return MtrAppDescriptor::_app_instance;
|
||||
}()) {
|
||||
// Register with AppRegistry
|
||||
AppRegistry::instance().register_app(this);
|
||||
ESP_LOGI(TAG, "MtrApp registered with AppRegistry");
|
||||
}
|
||||
|
||||
void MtrAppDescriptor::draw_icon(lv_obj_t* parent) {
|
||||
// Create MTR icon with train symbol
|
||||
lv_obj_t* icon_label = lv_label_create(parent);
|
||||
lv_label_set_text(icon_label, LV_SYMBOL_GPS "\nMTR");
|
||||
lv_obj_set_style_text_color(icon_label, lv_color_white(), 0);
|
||||
lv_obj_set_style_text_align(icon_label, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_center(icon_label);
|
||||
}
|
||||
Reference in New Issue
Block a user