fix(ui): qualify Cardputer Zero media and tracker rehearsal flows

This commit is contained in:
liu weikai committed 2026-11-08 22:37:11 +08:00
1 parent a2125d7da3
commit e2eb24074a
8 files changed
+128 -22

No files matched your search

+6
View File
@@ -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
@@ -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 <cassert>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <thread>
#include <unistd.h>
@@ -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")
<< "<gpx><trk><name>Storage ready</name><trkseg><trkpt lat=\"51.5\" lon=\"-0.17\"/></trkseg></trk></gpx>";
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<uint8_t> 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<const char*>(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);
}
+4 -5
View File
@@ -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"
@@ -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;
@@ -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
@@ -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<lv_coord_t>(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<lv_coord_t>(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<lv_coord_t>(200, h - 210) : std::min<lv_coord_t>(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<lv_coord_t>(200, h - 210)
: short_screen ? std::max<lv_coord_t>(16, std::min<lv_coord_t>(48, progress_y - 8))
: std::min<lv_coord_t>(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<void*>(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)
@@ -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(
+5
View File
@@ -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;