feat: add MTR Next Train application with multi-page navigation and real-time arrival info
This commit is contained in:
@@ -1,23 +1,40 @@
|
||||
set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_wifi esp_psram esp_lvgl_port)
|
||||
file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.cpp" "ui/**/*.cpp" "ui/**/*.c")
|
||||
# Explicitly list all source files to ensure build system picks them up
|
||||
# set(SRCS
|
||||
# "main.cpp"
|
||||
# "display/display.cpp"
|
||||
# "display/eink_display_handler.cpp"
|
||||
# "info/info.cpp"
|
||||
# "io/nvs_handler.cpp"
|
||||
# "network/http_handler.cpp"
|
||||
# "network/network.cpp"
|
||||
# "network/udp_client.cpp"
|
||||
# "network/wifi_handler.cpp"
|
||||
# "ui/page_stack.cpp"
|
||||
# "ui/root_layout.cpp"
|
||||
# "ui/ui_handler.cpp"
|
||||
# "ui/apps/demo_app.cpp"
|
||||
# "ui/apps/discord_app.cpp"
|
||||
# "ui/apps/shutdown_app.cpp"
|
||||
# )
|
||||
file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.cpp" "ui/**/*.cpp" "ui/**/*.c" "external/**/*.cpp" "external/**/*.c")
|
||||
|
||||
|
||||
# Path to the source JSON in this component
|
||||
set(ASSETS_SRC_DIR ${CMAKE_CURRENT_LIST_DIR}/../assets)
|
||||
set(ASSETS_BINARY_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/assets)
|
||||
set(MTR_JSON_SRC ${ASSETS_SRC_DIR}/MTR_LINE_STATION.json)
|
||||
set(MTR_JSON_HEADER ${ASSETS_BINARY_OUTPUT_DIR}/MTR_LINE_STATION.h)
|
||||
set(CUSTOM_CMAKE_MODULES_DIR ${CMAKE_CURRENT_LIST_DIR}/cmake)
|
||||
|
||||
## Generate a minified header at configure time using Python
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
file(MAKE_DIRECTORY ${ASSETS_BINARY_OUTPUT_DIR})
|
||||
if (Python3_Interpreter_FOUND)
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} -c "import json,sys,io; sys.stdout.write(json.dumps(json.load(open(sys.argv[1], 'r', encoding='utf-8')),separators=(',',':')))"
|
||||
"${MTR_JSON_SRC}"
|
||||
RESULT_VARIABLE _mtr_json_minify_result
|
||||
OUTPUT_VARIABLE MTR_JSON_MINIFIED
|
||||
ERROR_VARIABLE _mtr_json_minify_error
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
if (_mtr_json_minify_result)
|
||||
message(WARNING "Python minify failed (code=${_mtr_json_minify_result}): ${_mtr_json_minify_error}\nEmbedding original ${MTR_JSON_SRC} instead.")
|
||||
file(READ ${MTR_JSON_SRC} MTR_JSON_MINIFIED)
|
||||
elseif (NOT MTR_JSON_MINIFIED)
|
||||
message(WARNING "Python minified output empty; embedding original ${MTR_JSON_SRC} instead.")
|
||||
file(READ ${MTR_JSON_SRC} MTR_JSON_MINIFIED)
|
||||
endif()
|
||||
else()
|
||||
message(WARNING "Python3 not found; embedding original JSON without minification.")
|
||||
file(READ ${MTR_JSON_SRC} MTR_JSON_MINIFIED)
|
||||
endif()
|
||||
|
||||
file(WRITE ${MTR_JSON_HEADER} "#pragma once\nstatic const char MTR_LINE_STATION_JSON[] = R\"json(${MTR_JSON_MINIFIED})json\";\n")
|
||||
|
||||
|
||||
idf_component_register(SRCS ${SRCS}
|
||||
PRIV_REQUIRES ${requires}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "ui/apps/demo_app.h"
|
||||
#include "ui/apps/shutdown_app.h"
|
||||
#include "ui/apps/discord_app.h"
|
||||
#include "ui/apps/mtr_app.h"
|
||||
#include <tick/lv_tick.h>
|
||||
#include "esp_lvgl_port.h"
|
||||
#include "lvgl.h"
|
||||
@@ -127,6 +128,14 @@ void app_main(void) {
|
||||
DemoAppDescriptor* demo_descriptor = new DemoAppDescriptor();
|
||||
ShutdownAppDescriptor* shutdown_descriptor = new ShutdownAppDescriptor();
|
||||
DiscordAppDescriptor::instance(); // Use singleton pattern for Discord app
|
||||
MtrAppDescriptor* mtr_descriptor = new MtrAppDescriptor();
|
||||
|
||||
// Pass network handler to MtrApp so it can fetch arrival data
|
||||
MtrApp* mtr_app = dynamic_cast<MtrApp*>(mtr_descriptor->get_app_instance());
|
||||
if (mtr_app) {
|
||||
mtr_app->set_network_handler(network_handler);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Apps registered with AppRegistry\n");
|
||||
|
||||
// Initialize UI Handler (will render app icons from registry)
|
||||
@@ -162,6 +171,7 @@ void app_main(void) {
|
||||
ui_handler.deinit();
|
||||
delete demo_descriptor;
|
||||
delete shutdown_descriptor;
|
||||
delete mtr_descriptor;
|
||||
delete display_handler;
|
||||
vSemaphoreDelete(lvgl_mutex);
|
||||
vEventGroupDelete(system_event_group);
|
||||
|
||||
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);
|
||||
}
|
||||
71
main/ui/apps/mtr_app.h
Normal file
71
main/ui/apps/mtr_app.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/ui_app.h"
|
||||
#include "ui/app_registry.h"
|
||||
#include "ui/page_stack.h"
|
||||
#include "external/mtr/mtr.h"
|
||||
#include "external/mtr/line_info.h"
|
||||
#include "external/mtr/station_info.h"
|
||||
#include "network/network.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* @brief MTR Next Train application
|
||||
*
|
||||
* Provides multi-page navigation for:
|
||||
* 1. Line selection - choose MTR line
|
||||
* 2. Station selection - choose station within selected line
|
||||
* 3. Arrival display - show real-time train arrival information
|
||||
*/
|
||||
class MtrApp : public UIApp {
|
||||
public:
|
||||
MtrApp();
|
||||
virtual ~MtrApp() = default;
|
||||
|
||||
esp_err_t init(lv_obj_t* container) override;
|
||||
esp_err_t deinit(void) override;
|
||||
std::string get_name(void) const override;
|
||||
bool on_back_button_pressed(void) override;
|
||||
void handle_event(uint32_t event_type, void* event_data) override;
|
||||
|
||||
// Set network handler (must be called before using app)
|
||||
void set_network_handler(NetworkHandler* handler) { _network_handler = handler; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<MTRNextTrainHandler> _mtr_handler;
|
||||
std::unique_ptr<PageStack> _page_stack;
|
||||
NetworkHandler* _network_handler = nullptr;
|
||||
|
||||
// Current selection state
|
||||
std::string _selected_line_code;
|
||||
std::string _selected_station_code;
|
||||
LineInfo* _selected_line_info = nullptr;
|
||||
std::vector<LineInfo> _all_lines;
|
||||
|
||||
// Page builders
|
||||
void build_line_selection_page(lv_obj_t* page_container);
|
||||
void build_station_selection_page(lv_obj_t* page_container);
|
||||
void build_arrival_page(lv_obj_t* page_container);
|
||||
|
||||
// Event handlers
|
||||
static void line_button_event_cb(lv_event_t* e);
|
||||
static void station_button_event_cb(lv_event_t* e);
|
||||
static void refresh_button_event_cb(lv_event_t* e);
|
||||
|
||||
// Helper functions
|
||||
void load_arrival_data(lv_obj_t* page_container);
|
||||
uint32_t parse_color_hex(const char* hex_str);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief AppDescriptor for MtrApp
|
||||
*/
|
||||
class MtrAppDescriptor : public AppDescriptor {
|
||||
public:
|
||||
MtrAppDescriptor();
|
||||
void draw_icon(lv_obj_t* parent) override;
|
||||
|
||||
private:
|
||||
static MtrApp* _app_instance;
|
||||
};
|
||||
Reference in New Issue
Block a user