From e2eb24074ad6549412cfcf5912966ea1b8efe072 Mon Sep 17 00:00:00 2001 From: liu weikai Date: Sun, 8 Nov 2026 22:37:11 +0800 Subject: [PATCH] fix(ui): qualify Cardputer Zero media and tracker rehearsal flows --- CHANGELOG.md | 6 ++ .../linux_cardputer_zero_widgets_test.cpp | 80 +++++++++++++++++++ cmake/TrailMateLinuxSources.cmake | 9 +-- .../ui/screens/tracker/tracker_state.h | 1 + .../ui_shared/src/ui/app_catalog_builder.cpp | 2 +- .../ui/screens/music/music_page_layout.cpp | 33 ++++---- .../tracker/tracker_page_components.cpp | 14 +++- tests/hostlink/test_image_source_ui.cpp | 5 ++ 8 files changed, 128 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c7369d..97bd996f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ target-specific; see the linked acceptance records below. - Cardputer Zero ARM64 Debian package with APPLaunch integration, framebuffer rendering, physical keyboard navigation, hardware permissions, Cap LoRa/GNSS detection, heading integration and the shared launcher/widget screens. +- Cardputer Zero access to the shared music player, including its compact + now-playing layout and launcher-widget controls. - T-Display-CM0 Raspbian ARMHF application and matching native 1seg backend packages, preserving the target's GCC 14 C++ ABI and ARMv7 receive contract. CM0 also gains the LR2021 FM/1seg views, OFDM learning workbench and receiver @@ -37,6 +39,10 @@ target-specific; see the linked acceptance records below. ### Fixed +- Tracker file cards refresh automatically when storage availability changes, + including completion of the initial asynchronous Linux storage probe. +- Shared route-photo preview state preserves verified fullscreen-cache readiness + instead of falling back to original JPEG files and showing corrupted images. - Linux application launchers display the product name `TrailMate`, including the Cardputer Zero window title and notification source. - TMAP area searches defer page reconstruction until textarea input events have diff --git a/apps/linux_cardputer_zero/tests/linux_cardputer_zero_widgets_test.cpp b/apps/linux_cardputer_zero/tests/linux_cardputer_zero_widgets_test.cpp index 48eb3b03..f1e41a49 100644 --- a/apps/linux_cardputer_zero/tests/linux_cardputer_zero_widgets_test.cpp +++ b/apps/linux_cardputer_zero/tests/linux_cardputer_zero_widgets_test.cpp @@ -1,14 +1,20 @@ +#include "../../../modules/ui_shared/src/ui/screens/music/music_state.h" #include "app/input_event.h" #include "platform/ui/orientation_runtime.h" #include "ui/app_runtime.h" #include "ui/menu/dashboard/dashboard_state.h" #include "ui/menu/menu_dashboard.h" +#include "ui/menu/menu_layout.h" +#include "ui/screens/tracker/tracker_state.h" #include "ui/shell_ui_runner.h" +#include "ui/widgets/route_image_strip.h" #include #include #include +#include #include +#include #include #include @@ -84,6 +90,80 @@ int main() assert(ui::menu::dashboard::isFeedOpen()); key(InputKey::Home); assert(!ui::menu::dashboard::isFeedOpen()); + assert(ui::menu_layout::launchAppByStableId("music")); + tick(100ms); + auto* music = ui::music_page::state(); + assert(music && music->service && music->root); + ui::music_page::navigate(ui::music_page::View::Playing); + lv_obj_update_layout(music->root); + lv_area_t album, progress, elapsed; + lv_obj_get_coords(music->album, &album); + lv_obj_get_coords(music->progress, &progress); + lv_obj_get_coords(music->elapsed, &elapsed); + assert(album.y2 + 3 < progress.y1); + assert(progress.y2 < elapsed.y1 && elapsed.y2 < 146); + key(InputKey::Home); + const auto storage = root / "sd"; + const auto retained = root / "sd-retained"; + std::filesystem::create_directories(storage / "tracks"); + std::ofstream(storage / "tracks/storage-ready.gpx") + << "Storage ready"; + assert(ui::menu_layout::launchAppByStableId("tracker")); + tick(1600ms); + auto& tracker = ::tracker::ui::g_tracker_state; + assert(tracker.card_counter && !std::strcmp(lv_label_get_text(tracker.card_counter), "1 / 1")); + // Exercise the real asynchronous storage probe: recover the open page + // after its root becomes unavailable, without leaving/reentering it. + std::filesystem::rename(storage, retained); + std::ofstream(storage) << "temporarily unavailable volume"; + tick(1600ms); + assert(!std::strcmp(lv_label_get_text(tracker.card_name), "No SD Card")); + std::filesystem::remove(storage); + std::filesystem::rename(retained, storage); + tick(1600ms); + assert(!std::strcmp(lv_label_get_text(tracker.card_counter), "1 / 1")); + key(InputKey::Home); + std::vector bitmap(54 + 320 * 180 * 3, 0); + const auto integer = [&](size_t offset, uint32_t value) + { for (unsigned byte=0;byte<4;++byte) bitmap[offset+byte]=uint8_t(value>>(byte*8)); }; + bitmap[0] = 'B'; + bitmap[1] = 'M'; + bitmap[26] = 1; + bitmap[28] = 24; + integer(2, bitmap.size()); + integer(10, 54); + integer(14, 40); + integer(18, 320); + integer(22, 180); + for (unsigned y = 0; y < 180; ++y) + for (unsigned x = 0; x < 320; ++x) bitmap[54 + ((179 - y) * 320 + x) * 3 + (y < 60 ? 2 : y < 120 ? 1 + : 0)] = 255; + const auto image_path = (root / "fullscreen.bmp").string(); + std::ofstream(image_path, std::ios::binary).write(reinterpret_cast(bitmap.data()), bitmap.size()); + ui::widgets::route_image_strip::Widget photos; + ui::widgets::route_image_strip::create(lv_screen_active(), photos); + ui::widgets::route_image_strip::Item photo; + photo.downloaded = true; + photo.local_path = image_path; + photo.preview_path = image_path; + photo.view_path = image_path; + ui::widgets::route_image_strip::set_items(photos, &photo, 1); + ui::widgets::route_image_strip::set_hidden(photos, false); + assert(ui::widgets::route_image_strip::handle_key(photos, LV_KEY_ENTER)); + tick(400ms); + lv_obj_invalidate(photos.fullscreen_root); + tick(400ms); + for (unsigned y : {30U, 85U, 150U}) + { + for (unsigned x : {80U, 240U}) + { + const auto& pixel = host.canvas().pixel(x, y); + if (y == 30) assert(pixel.r > 240 && pixel.g < 8 && pixel.b < 8); + else if (y == 85) assert(pixel.r < 8 && pixel.g > 240 && pixel.b < 8); + else assert(pixel.r < 8 && pixel.g < 8 && pixel.b > 240); + } + } + ui::widgets::route_image_strip::destroy(photos); } std::filesystem::remove_all(root); } diff --git a/cmake/TrailMateLinuxSources.cmake b/cmake/TrailMateLinuxSources.cmake index 2912f57f..8ae813ff 100644 --- a/cmake/TrailMateLinuxSources.cmake +++ b/cmake/TrailMateLinuxSources.cmake @@ -948,15 +948,14 @@ function(trailmate_add_linux_ui_shell target_name common_target) "${TRAIL_MATE_LINUX_COMMON_SRC_ROOT}/platform/linux/cardputer_cap.cpp") elseif(UI_UX_PACK STREQUAL "t_display_p4_touch") target_compile_definitions(${target_name} PRIVATE TRAIL_MATE_UI_P4_TOUCH=1) - target_sources(${target_name} PRIVATE - "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_page_layout.cpp" - "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_page_input.cpp" - "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_state.cpp" - "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_page_shell.cpp") else() message(FATAL_ERROR "Unsupported Linux LVGL UX pack: ${UI_UX_PACK}") endif() target_sources(${target_name} PRIVATE + "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_page_layout.cpp" + "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_page_input.cpp" + "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_state.cpp" + "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/music/music_page_shell.cpp" "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/assets/calc.c" "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/assets/geocaching.c" "${TRAIL_MATE_UI_SHARED_SRC_ROOT}/ui/screens/calculator/calculator_engine.cpp" diff --git a/modules/ui_shared/include/ui/screens/tracker/tracker_state.h b/modules/ui_shared/include/ui/screens/tracker/tracker_state.h index 4884428a..2d0efd50 100644 --- a/modules/ui_shared/include/ui/screens/tracker/tracker_state.h +++ b/modules/ui_shared/include/ui/screens/tracker/tracker_state.h @@ -41,6 +41,7 @@ struct TrackerPageState lv_obj_t* card_distance = nullptr; ::ui::support::CardLocation* card_location_model = nullptr; lv_timer_t* card_timer = nullptr; + bool card_storage_ready = false; lv_obj_t* card_counter = nullptr; lv_obj_t* card_previous = nullptr; lv_obj_t* card_next = nullptr; diff --git a/modules/ui_shared/src/ui/app_catalog_builder.cpp b/modules/ui_shared/src/ui/app_catalog_builder.cpp index f17ee8b8..d100fc3d 100644 --- a/modules/ui_shared/src/ui/app_catalog_builder.cpp +++ b/modules/ui_shared/src/ui/app_catalog_builder.cpp @@ -23,7 +23,7 @@ #define TRAIL_MATE_ENABLE_MUSIC_APP 1 #elif defined(TRAIL_MATE_CARDPUTER_ZERO_LINUX) #define TRAIL_MATE_ENABLE_GEOCACHING_APP 1 -#define TRAIL_MATE_ENABLE_MUSIC_APP 0 +#define TRAIL_MATE_ENABLE_MUSIC_APP 1 #else #define TRAIL_MATE_ENABLE_GEOCACHING_APP 0 #define TRAIL_MATE_ENABLE_MUSIC_APP 0 diff --git a/modules/ui_shared/src/ui/screens/music/music_page_layout.cpp b/modules/ui_shared/src/ui/screens/music/music_page_layout.cpp index 7b355cd3..8ececf9e 100644 --- a/modules/ui_shared/src/ui/screens/music/music_page_layout.cpp +++ b/modules/ui_shared/src/ui/screens/music/music_page_layout.cpp @@ -126,16 +126,21 @@ void cover(lv_obj_t* parent, lv_coord_t x, lv_coord_t y, lv_coord_t edge) void now_playing(lv_coord_t width, lv_coord_t height) { auto& self = *state(); + const bool short_screen = !self.wide && height < 150; const lv_coord_t margin = self.wide ? std::max(24, (width - 1040) / 2) : 8; const lv_coord_t gap = 20; const lv_coord_t w = self.wide ? (width - 2 * margin - gap) / 2 : width - 2 * margin; const lv_coord_t h = self.wide ? std::min(400, height - 40) : height - 8; const lv_coord_t y = (height - h) / 2; auto* panel = card(self.body, margin, y, w, h); - lv_coord_t title_y = 10, text_x = 14; + lv_coord_t title_y = short_screen ? 4 : 10, text_x = 14; + const lv_coord_t controls_y = h - (short_screen ? 28 : 44); + const lv_coord_t progress_y = controls_y - (short_screen ? 26 : 34); // Include the slider knob's painted extent, not just its five-pixel track. - const lv_coord_t edge = self.wide ? std::min(200, h - 210) : std::min(96, h - 96); - cover(panel, self.wide ? (w - edge) / 2 : 12, self.wide ? 16 : 10, edge); + const lv_coord_t edge = self.wide ? std::min(200, h - 210) + : short_screen ? std::max(16, std::min(48, progress_y - 8)) + : std::min(96, h - 96); + cover(panel, self.wide ? (w - edge) / 2 : 12, self.wide ? 16 : title_y, edge); if (self.wide) { title_y = edge + 32; @@ -143,21 +148,20 @@ void now_playing(lv_coord_t width, lv_coord_t height) else text_x = edge + 24; const lv_coord_t text_w = w - text_x - 14; self.title = content_label(panel, self.snapshot.current.title[0] ? self.snapshot.current.title : ui::i18n::tr("Choose a song"), - text_x, title_y, text_w, &lv_font_montserrat_16); + text_x, title_y, text_w, short_screen ? &lv_font_montserrat_14 : &lv_font_montserrat_16); lv_label_set_long_mode(self.title, LV_LABEL_LONG_DOT); - lv_obj_set_height(self.title, 20); - self.artist = content_label(panel, self.snapshot.current.artist, text_x, title_y + 26, text_w, &lv_font_montserrat_14, true); + lv_obj_set_height(self.title, short_screen ? 18 : 20); + self.artist = content_label(panel, self.snapshot.current.artist, text_x, title_y + (short_screen ? 18 : 26), text_w, + short_screen ? &lv_font_montserrat_12 : &lv_font_montserrat_14, true); lv_label_set_long_mode(self.artist, LV_LABEL_LONG_DOT); - self.album = content_label(panel, self.snapshot.current.album, text_x, title_y + 46, text_w - 44, &lv_font_montserrat_12, true); + self.album = content_label(panel, self.snapshot.current.album, text_x, title_y + (short_screen ? 34 : 46), text_w - 44, &lv_font_montserrat_12, true); lv_label_set_long_mode(self.album, LV_LABEL_LONG_DOT); char volume[16]; std::snprintf(volume, sizeof(volume), "%u%%", self.snapshot.volume); - self.volume = label(panel, volume, w - 54, title_y + 46, 40, &lv_font_montserrat_12, true); + self.volume = label(panel, volume, w - 54, title_y + (short_screen ? 34 : 46), 40, &lv_font_montserrat_12, true); self.playback_status = label(panel, "", text_x, title_y + 64, text_w, &lv_font_montserrat_12, true); lv_label_set_long_mode(self.playback_status, LV_LABEL_LONG_DOT); lv_obj_set_height(self.playback_status, lv_font_get_line_height(&lv_font_montserrat_12)); - const lv_coord_t controls_y = h - 44; - const lv_coord_t progress_y = controls_y - 34; self.progress = lv_slider_create(panel); lv_obj_set_pos(self.progress, 14, progress_y); lv_obj_set_size(self.progress, w - 28, 5); @@ -168,14 +172,15 @@ void now_playing(lv_coord_t width, lv_coord_t height) lv_obj_set_style_pad_all(self.progress, 3, LV_PART_KNOB); lv_obj_add_event_cb(self.progress, action_event, LV_EVENT_RELEASED, reinterpret_cast(99)); lv_obj_add_event_cb(self.progress, key_event, LV_EVENT_KEY, nullptr); - self.elapsed = label(panel, "0:00", 14, progress_y + 12, 70, &lv_font_montserrat_12, true); - self.duration = label(panel, "--:--", w - 84, progress_y + 12, 70, &lv_font_montserrat_12, true); + const lv_coord_t time_y = progress_y + (short_screen ? 8 : 12); + self.elapsed = label(panel, "0:00", 14, time_y, 70, &lv_font_montserrat_12, true); + self.duration = label(panel, "--:--", w - 84, time_y, 70, &lv_font_montserrat_12, true); lv_obj_set_style_text_align(self.duration, LV_TEXT_ALIGN_RIGHT, 0); if (!self.wide) { // Status shares the time row on compact screens, leaving the cover, // metadata and the slider's painted knob area independent. - lv_obj_set_pos(self.playback_status, 92, progress_y + 12); + lv_obj_set_pos(self.playback_status, 92, time_y); lv_obj_set_width(self.playback_status, w - 184); lv_obj_set_style_text_align(self.playback_status, LV_TEXT_ALIGN_CENTER, 0); } @@ -189,7 +194,7 @@ void now_playing(lv_coord_t width, lv_coord_t height) lv_obj_set_pos(previous, (w - 256) / 2 + 104, controls_y); lv_obj_set_pos(play, (w - 256) / 2 + 148, controls_y); lv_obj_set_pos(next, (w - 256) / 2 + 220, controls_y); - for (auto* control : {mode, previous, play, next}) lv_obj_set_height(control, 30); + for (auto* control : {mode, previous, play, next}) lv_obj_set_height(control, short_screen ? 24 : 30); lv_obj_set_style_bg_color(play, ui::theme::accent(), 0); lv_group_focus_obj(play); if (self.wide) diff --git a/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp b/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp index 80df29ad..86ad7a0c 100644 --- a/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp @@ -187,6 +187,7 @@ lv_coord_t modal_button_width(); lv_coord_t action_menu_button_height(); lv_coord_t action_menu_row_gap(); void update_record_status(); +void refresh_record_list(); void refresh_route_list(); void update_start_stop_button(); void update_record_page(); @@ -898,7 +899,6 @@ void refresh_preview_image_storage_state(bool include_cache_state) { ++saved_count; } - image.view_ready = false; } s_preview_saved_image_count_hint = saved_count; s_preview_image_saved_state_known = true; @@ -3297,8 +3297,18 @@ void update_item_card() state.card_next = view.next; state.card_actions = view.actions; ::ui::i18n::set_label_text(lv_obj_get_child(view.actions, 0), "Actions"); + state.card_storage_ready = platform::ui::device::sd_ready(); state.card_timer = lv_timer_create([](lv_timer_t*) - { update_card_location(); }, + { + auto& current = g_tracker_state; + const bool ready = platform::ui::device::sd_ready(); + if (!is_any_modal_open() && current.card_storage_ready != ready) + { + current.card_storage_ready = ready; + refresh_record_list(); + refresh_route_list(); + } + update_card_location(); }, 500, nullptr); lv_obj_add_event_cb(card, on_list_item_clicked, LV_EVENT_CLICKED, nullptr); lv_obj_add_event_cb( diff --git a/tests/hostlink/test_image_source_ui.cpp b/tests/hostlink/test_image_source_ui.cpp index bde81a94..a593c0a6 100644 --- a/tests/hostlink/test_image_source_ui.cpp +++ b/tests/hostlink/test_image_source_ui.cpp @@ -305,6 +305,11 @@ int main() assert(first.downloaded && first.preview_ready && first.view_ready); assert(preview::s_preview_saved_image_count_hint == 1); assert(preview::s_preview_images[0].downloaded && preview::s_preview_images[0].preview_ready && !preview::s_preview_images[1].downloaded); + assert(preview::s_preview_images[0].view_ready); + preview::refresh_preview_image_storage_state(false); + assert(preview::s_preview_images[0].downloaded && !preview::s_preview_images[0].preview_ready && !preview::s_preview_images[0].view_ready); + preview::refresh_preview_image_storage_state(true); + assert(preview::s_preview_images[0].preview_ready && preview::s_preview_images[0].view_ready); platform::ui::route_storage::RouteImageDownloadStatus status; status.asset_id = "test"; status.busy = true;