From 3cfd0cfd3a4f576b81709ce9c83f7928171d44b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9?= <1131761436@qq.com> Date: Tue, 11 Aug 2026 19:12:18 +0800 Subject: [PATCH 1/9] fix(applaunch): stabilize input callbacks and volume controls Serialize background Wi-Fi callbacks with LVGL, avoid synthetic Shift lock hints, and make Settings/shortcuts share live PipeWire volume in rounded 10 percent steps. --- .../main/ui/launcher_media_controls.cpp | 8 ++++-- .../main/ui/launcher_media_controls.h | 2 ++ .../main/ui/model/global_hint_policy.cpp | 3 +-- .../main/ui/model/setup_value_policy.cpp | 14 +++++----- .../main/ui/model/setup_value_policy.hpp | 1 + .../main/ui/page_app/setting/basic.cpp | 3 ++- .../main/ui/page_app/setting/wifi.cpp | 12 +++++++-- projects/APPLaunch/main/ui/ui_global_hint.cpp | 18 +++++++------ projects/APPLaunch/tests/run_tests.sh | 1 + .../tests/test_global_hint_policy.cpp | 6 ++--- .../tests/test_launcher_media_controls.cpp | 27 ++++++++++++------- .../tests/test_setup_value_policy.cpp | 19 ++++++++++--- .../test_wifi_view_lifecycle_contract.py | 14 ++++++++++ 13 files changed, 90 insertions(+), 38 deletions(-) diff --git a/projects/APPLaunch/main/ui/launcher_media_controls.cpp b/projects/APPLaunch/main/ui/launcher_media_controls.cpp index 413238ee..21938817 100644 --- a/projects/APPLaunch/main/ui/launcher_media_controls.cpp +++ b/projects/APPLaunch/main/ui/launcher_media_controls.cpp @@ -8,6 +8,7 @@ #include "hal_lvgl_bsp.h" #include "model/launcher_media_model.hpp" +#include "model/setup_value_policy.hpp" #include @@ -145,8 +146,11 @@ int adjust_volume(int delta_percent) { if (read_mute()) (void)toggle_mute(); - const int current = model.has_volume() ? model.volume_or(0) : read_volume(); - return write_volume(current, current + delta_percent); + const int current = read_volume(); + const int base = setup_values::round_volume_percent(current); + const int delta = delta_percent > 0 ? VOLUME_STEP_PERCENT + : delta_percent < 0 ? -VOLUME_STEP_PERCENT : 0; + return write_volume(current, base + delta); } int adjust_brightness(int delta_percent) diff --git a/projects/APPLaunch/main/ui/launcher_media_controls.h b/projects/APPLaunch/main/ui/launcher_media_controls.h index b1257e1d..2967b41d 100644 --- a/projects/APPLaunch/main/ui/launcher_media_controls.h +++ b/projects/APPLaunch/main/ui/launcher_media_controls.h @@ -8,6 +8,8 @@ namespace launcher_media_controls { +constexpr int VOLUME_STEP_PERCENT = 10; + int adjust_volume(int delta_percent); int adjust_brightness(int delta_percent); bool toggle_mute(); diff --git a/projects/APPLaunch/main/ui/model/global_hint_policy.cpp b/projects/APPLaunch/main/ui/model/global_hint_policy.cpp index 9647c5a3..2a56a88a 100644 --- a/projects/APPLaunch/main/ui/model/global_hint_policy.cpp +++ b/projects/APPLaunch/main/ui/model/global_hint_policy.cpp @@ -85,8 +85,7 @@ GlobalHintAction GlobalHintPolicy::action_for(const GlobalHintKeyInput &input) c if (input.key_code == KEY_FN) return GlobalHintAction::NONE; - if (input.key_code == KEY_LEFTSHIFT || input.key_code == KEY_RIGHTSHIFT || - input.key_code == KEY_COMPOSE || is_symbol_key_name(input.symbol_name)) { + if (input.key_code == KEY_COMPOSE || is_symbol_key_name(input.symbol_name)) { return GlobalHintAction::SHOW_LOCK_HINT; } diff --git a/projects/APPLaunch/main/ui/model/setup_value_policy.cpp b/projects/APPLaunch/main/ui/model/setup_value_policy.cpp index f2aedda4..039bd72a 100644 --- a/projects/APPLaunch/main/ui/model/setup_value_policy.cpp +++ b/projects/APPLaunch/main/ui/model/setup_value_policy.cpp @@ -9,7 +9,7 @@ namespace { constexpr int kBrightnessPercentages[] = {100, 75, 50, 25}; constexpr int kDarkTimes[] = {0, 10, 30, 60, 300}; -constexpr int kVolumePercentages[] = {100, 75, 50, 25, 0}; +constexpr int kVolumePercentages[] = {100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0}; constexpr CameraResolution kCameraResolutions[] = {{1280, 720}, {640, 480}}; template @@ -70,13 +70,15 @@ int dark_time_seconds(int index) return kDarkTimes[clamp_index(index, array_size(kDarkTimes))]; } +int round_volume_percent(int percent) +{ + const int clamped = std::clamp(percent, 0, 100); + return std::min(100, ((clamped + 5) / 10) * 10); +} + int volume_index(int percent) { - if (percent >= 87) return 0; - if (percent >= 62) return 1; - if (percent >= 37) return 2; - if (percent >= 12) return 3; - return 4; + return (100 - round_volume_percent(percent)) / 10; } int volume_percent(int index) diff --git a/projects/APPLaunch/main/ui/model/setup_value_policy.hpp b/projects/APPLaunch/main/ui/model/setup_value_policy.hpp index bd7e4150..c401c17c 100644 --- a/projects/APPLaunch/main/ui/model/setup_value_policy.hpp +++ b/projects/APPLaunch/main/ui/model/setup_value_policy.hpp @@ -16,6 +16,7 @@ bool parse_nonnegative_int(std::string_view text, int &value); int dark_time_index(int seconds); int dark_time_seconds(int index); +int round_volume_percent(int percent); int volume_index(int percent); int volume_percent(int index); bool volume_value_valid(int percent); diff --git a/projects/APPLaunch/main/ui/page_app/setting/basic.cpp b/projects/APPLaunch/main/ui/page_app/setting/basic.cpp index eea80fe1..47d52749 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/basic.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/basic.cpp @@ -262,7 +262,8 @@ void Speaker::enter_volume_adjust(UISetupPage &page) vol_val_ = read_config_int_strict("volume", 100); vol_val_ = std::clamp(vol_val_, 0, 100); preview_active_ = true; - access.enter_value("Volume", {"100%", "75%", "50%", "25%", "0%"}, + access.enter_value("Volume", {"100%", "90%", "80%", "70%", "60%", "50%", + "40%", "30%", "20%", "10%", "0%"}, setup_values::volume_index(vol_val_)); } diff --git a/projects/APPLaunch/main/ui/page_app/setting/wifi.cpp b/projects/APPLaunch/main/ui/page_app/setting/wifi.cpp index 9c70f07f..2bb8eec4 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/wifi.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/wifi.cpp @@ -40,6 +40,14 @@ namespace { constexpr auto kWifiScanPeriod = std::chrono::seconds(8); +lv_result_t queue_lvgl_async(lv_async_cb_t callback, void *user_data) +{ + lv_lock(); + const lv_result_t result = lv_async_call(callback, user_data); + lv_unlock(); + return result; +} + const char *wifi_error_message(int result) { switch (result) { @@ -168,7 +176,7 @@ void WiFi::start_scan(UISetupPage &page) } const bool failed = result->count < 0; ScanResult *raw = result.release(); - if (lv_async_call(scan_result_cb, raw) != LV_RESULT_OK) + if (queue_lvgl_async(scan_result_cb, raw) != LV_RESULT_OK) delete raw; std::unique_lock lock(state->mutex); @@ -377,7 +385,7 @@ bool WiFi::start_connection(UISetupPage &page, std::string ssid, return; } ConnectionResult *queued = result.release(); - if (lv_async_call(connection_result_cb, queued) != LV_RESULT_OK) { + if (queue_lvgl_async(connection_result_cb, queued) != LV_RESULT_OK) { queued->token.complete(); delete queued; } diff --git a/projects/APPLaunch/main/ui/ui_global_hint.cpp b/projects/APPLaunch/main/ui/ui_global_hint.cpp index 7353edaa..53a29e94 100644 --- a/projects/APPLaunch/main/ui/ui_global_hint.cpp +++ b/projects/APPLaunch/main/ui/ui_global_hint.cpp @@ -14,9 +14,10 @@ * "Hold ESC 3s to return home". Short taps (released * before 0.5s) show nothing, so a quick "back" press inside * an app no longer flashes the return-home toast. - * (b) Single press of SHIFT (Aa / KEY_LEFTSHIFT) or SYM (physical - * "SYM" key on the M5 CardputerZero; currently best-effort mapped) - * -> show "Double-tap to lock" for ~1.5s. + * (b) Single press of SYM (physical "SYM" key on the M5 + * CardputerZero; currently best-effort mapped) -> show + * "Double-tap to lock" for ~1.5s. Synthetic Shift events are + * ignored because they do not identify the physical key gesture. * * Fn key is intentionally NOT hinted (no lock feature yet). * @@ -45,7 +46,7 @@ #include #include -#define MEDIA_OSD_STEP 5 +#define BRIGHTNESS_OSD_STEP 5 static void show_hint(const char *text) { @@ -109,16 +110,17 @@ void on_key(const struct key_item *elm) noexcept switch (action) { case GlobalHintAction::BRIGHTNESS_UP: case GlobalHintAction::BRIGHTNESS_DOWN: { - const int delta = action == GlobalHintAction::BRIGHTNESS_UP ? MEDIA_OSD_STEP - : -MEDIA_OSD_STEP; + const int delta = action == GlobalHintAction::BRIGHTNESS_UP ? BRIGHTNESS_OSD_STEP + : -BRIGHTNESS_OSD_STEP; const int pct = launcher_media_controls::adjust_brightness(delta); launcher_media_osd().show_level("Brightness", LV_SYMBOL_TINT, pct); return; } case GlobalHintAction::VOLUME_UP: case GlobalHintAction::VOLUME_DOWN: { - const int delta = action == GlobalHintAction::VOLUME_UP ? MEDIA_OSD_STEP - : -MEDIA_OSD_STEP; + const int delta = action == GlobalHintAction::VOLUME_UP + ? launcher_media_controls::VOLUME_STEP_PERCENT + : -launcher_media_controls::VOLUME_STEP_PERCENT; const int pct = launcher_media_controls::adjust_volume(delta); launcher_media_osd().show_level( "Volume", pct == 0 ? LV_SYMBOL_MUTE : LV_SYMBOL_VOLUME_MAX, pct); diff --git a/projects/APPLaunch/tests/run_tests.sh b/projects/APPLaunch/tests/run_tests.sh index 87645088..181e41e0 100755 --- a/projects/APPLaunch/tests/run_tests.sh +++ b/projects/APPLaunch/tests/run_tests.sh @@ -199,6 +199,7 @@ ${CXX:-g++} -std=c++17 -Wall -Wextra -Werror \ "$(dirname "$0")/test_launcher_media_controls.cpp" \ "$(dirname "$0")/../main/ui/launcher_media_controls.cpp" \ "$(dirname "$0")/../main/ui/model/launcher_media_model.cpp" \ + "$(dirname "$0")/../main/ui/model/setup_value_policy.cpp" \ -o "$build_dir/test_launcher_media_controls" "$build_dir/test_launcher_media_controls" diff --git a/projects/APPLaunch/tests/test_global_hint_policy.cpp b/projects/APPLaunch/tests/test_global_hint_policy.cpp index 26c4c2ff..1ef42ff5 100644 --- a/projects/APPLaunch/tests/test_global_hint_policy.cpp +++ b/projects/APPLaunch/tests/test_global_hint_policy.cpp @@ -53,10 +53,8 @@ int main() GlobalHintAction::TAKE_SCREENSHOT); assert(policy.action_for({KEY_SYSRQ, "Print", false}) == GlobalHintAction::NONE); - assert(policy.action_for({KEY_LEFTSHIFT, nullptr, true}) == - GlobalHintAction::SHOW_LOCK_HINT); - assert(policy.action_for({KEY_RIGHTSHIFT, nullptr, true}) == - GlobalHintAction::SHOW_LOCK_HINT); + assert(policy.action_for({KEY_LEFTSHIFT, nullptr, true}) == GlobalHintAction::NONE); + assert(policy.action_for({KEY_RIGHTSHIFT, nullptr, true}) == GlobalHintAction::NONE); assert(policy.action_for({KEY_COMPOSE, nullptr, true}) == GlobalHintAction::SHOW_LOCK_HINT); assert(policy.action_for({999, "Multi_key", true}) == GlobalHintAction::SHOW_LOCK_HINT); diff --git a/projects/APPLaunch/tests/test_launcher_media_controls.cpp b/projects/APPLaunch/tests/test_launcher_media_controls.cpp index b0777aab..8c5e3781 100644 --- a/projects/APPLaunch/tests/test_launcher_media_controls.cpp +++ b/projects/APPLaunch/tests/test_launcher_media_controls.cpp @@ -18,7 +18,7 @@ eventpp::CallbackList, std::function= 4); assert(audio_commands[0] == "MuteRead"); @@ -89,16 +90,24 @@ int main() assert(audio_commands[3] == "VolumeWrite"); sink_muted = true; - assert(launcher_media_controls::adjust_volume(5) == 80); + sink_volume = 63; + assert(launcher_media_controls::adjust_volume(10) == 70); + assert(!sink_muted); + assert(sink_volume == 70); + assert(configured_volume == 70); + assert(mute_toggles == 2); + + sink_volume = 71; + assert(launcher_media_controls::adjust_volume(10) == 80); assert(!sink_muted); assert(sink_volume == 80); assert(configured_volume == 80); assert(mute_toggles == 2); - assert(launcher_media_controls::adjust_volume(-5) == 75); - assert(!sink_muted); - assert(sink_volume == 75); - assert(configured_volume == 75); + sink_volume = 82; + assert(launcher_media_controls::adjust_volume(-10) == 70); + assert(sink_volume == 70); + assert(configured_volume == 70); assert(mute_toggles == 2); return 0; } diff --git a/projects/APPLaunch/tests/test_setup_value_policy.cpp b/projects/APPLaunch/tests/test_setup_value_policy.cpp index b25d46f5..50b8b5c5 100644 --- a/projects/APPLaunch/tests/test_setup_value_policy.cpp +++ b/projects/APPLaunch/tests/test_setup_value_policy.cpp @@ -33,13 +33,24 @@ int main() assert(setup_values::dark_time_seconds(-1) == 0); assert(setup_values::dark_time_seconds(99) == 300); + assert(setup_values::round_volume_percent(-1) == 0); + assert(setup_values::round_volume_percent(4) == 0); + assert(setup_values::round_volume_percent(5) == 10); + assert(setup_values::round_volume_percent(63) == 60); + assert(setup_values::round_volume_percent(65) == 70); + assert(setup_values::round_volume_percent(71) == 70); + assert(setup_values::round_volume_percent(82) == 80); + assert(setup_values::round_volume_percent(96) == 100); + assert(setup_values::round_volume_percent(101) == 100); assert(setup_values::volume_index(100) == 0); - assert(setup_values::volume_index(86) == 1); - assert(setup_values::volume_index(61) == 2); - assert(setup_values::volume_index(36) == 3); - assert(setup_values::volume_index(11) == 4); + assert(setup_values::volume_index(96) == 0); + assert(setup_values::volume_index(82) == 2); + assert(setup_values::volume_index(63) == 4); + assert(setup_values::volume_index(5) == 9); + assert(setup_values::volume_index(4) == 10); assert(setup_values::volume_percent(-1) == 100); assert(setup_values::volume_percent(99) == 0); + assert(setup_values::volume_percent(4) == 60); assert(setup_values::volume_value_valid(0)); assert(setup_values::volume_value_valid(100)); assert(!setup_values::volume_value_valid(-1)); diff --git a/projects/APPLaunch/tests/test_wifi_view_lifecycle_contract.py b/projects/APPLaunch/tests/test_wifi_view_lifecycle_contract.py index b1e3150e..03eb6783 100644 --- a/projects/APPLaunch/tests/test_wifi_view_lifecycle_contract.py +++ b/projects/APPLaunch/tests/test_wifi_view_lifecycle_contract.py @@ -27,6 +27,19 @@ def test_wifi_controller_unmounts_list_while_view_is_alive(): assert body.index("list_view_.unmount();") < body.index("clear_password_view();") +def test_wifi_background_callbacks_lock_lvgl_async_queue(): + start = CONTROLLER.index("lv_result_t queue_lvgl_async") + end = CONTROLLER.index("const char *wifi_error_message", start) + body = CONTROLLER[start:end] + assert body.index("lv_lock();") < body.index("lv_async_call(") + assert body.index("lv_async_call(") < body.index("lv_unlock();") + + assert "queue_lvgl_async(scan_result_cb, raw)" in CONTROLLER + assert "queue_lvgl_async(connection_result_cb, queued)" in CONTROLLER + assert "lv_async_call(scan_result_cb, raw)" not in CONTROLLER + assert "lv_async_call(connection_result_cb, queued)" not in CONTROLLER + + def test_scan_respects_user_disabled_radio(): hidden_start = CONTROLLER.index("void WiFi::enter_hidden_wifi(UISetupPage &page)") hidden_end = CONTROLLER.index("void WiFi::enter_scan", hidden_start) @@ -66,4 +79,5 @@ def test_scan_respects_user_disabled_radio(): if __name__ == "__main__": test_owned_wifi_views_unmount_before_member_destruction() test_wifi_controller_unmounts_list_while_view_is_alive() + test_wifi_background_callbacks_lock_lvgl_async_queue() test_scan_respects_user_disabled_radio() From de1f34f219feb111eba5f9704fdf69f9310fb5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9?= <1131761436@qq.com> Date: Tue, 11 Aug 2026 19:12:18 +0800 Subject: [PATCH 2/9] fix(zclaw): use a legible 14px semibold mono font Prefer packaged JetBrains Mono SemiBold and render both ZClaw text roles at 14px, retaining the existing fallback chain. --- projects/ZClaw/main/ui/zclaw_fonts.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/projects/ZClaw/main/ui/zclaw_fonts.cpp b/projects/ZClaw/main/ui/zclaw_fonts.cpp index d30b0695..63228652 100644 --- a/projects/ZClaw/main/ui/zclaw_fonts.cpp +++ b/projects/ZClaw/main/ui/zclaw_fonts.cpp @@ -18,6 +18,8 @@ namespace { #if LV_USE_FREETYPE +constexpr uint32_t kZClawFontSize = 14; + bool file_exists(const std::string &path) { if (path.empty()) return false; @@ -29,6 +31,8 @@ std::string runtime_font_path() { const char *env_path = std::getenv("ZCLAW_FONT"); const std::vector candidates = { + "/usr/share/fonts/truetype/jetbrains-mono/JetBrainsMono-SemiBold.ttf", + "/usr/share/fonts/truetype/jetbrains-mono/JetBrainsMono-Bold.ttf", "/usr/share/fonts/truetype/jetbrains-mono/JetBrainsMono-Regular.ttf", "/usr/share/APPLaunch/share/font/LiberationMono-Regular.ttf", cp0_file_path("AlibabaPuHuiTi-3-55-Regular.ttf"), @@ -107,19 +111,19 @@ void FontManager::init() return; font_10_ = lv_freetype_font_create( - font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, 10, + font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); font_12_ = lv_freetype_font_create( - font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, 12, + font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); const std::string fallback_path = runtime_fallback_font_path(); if (!fallback_path.empty()) { fallback_font_10_ = lv_freetype_font_create( - fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, 10, + fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); fallback_font_12_ = lv_freetype_font_create( - fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, 12, + fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); } set_fallback_chain(font_10_, fallback_font_10_); @@ -156,7 +160,7 @@ const lv_font_t *FontManager::font_10() const #elif defined(LV_FONT_SOURCE_HAN_SANS_SC_16_CJK) && LV_FONT_SOURCE_HAN_SANS_SC_16_CJK return &lv_font_source_han_sans_sc_16_cjk; #endif - return &lv_font_montserrat_10; + return &lv_font_montserrat_14; } const lv_font_t *FontManager::font_12() const @@ -170,7 +174,7 @@ const lv_font_t *FontManager::font_12() const #elif defined(LV_FONT_SOURCE_HAN_SANS_SC_16_CJK) && LV_FONT_SOURCE_HAN_SANS_SC_16_CJK return &lv_font_source_han_sans_sc_16_cjk; #endif - return &lv_font_montserrat_12; + return &lv_font_montserrat_14; } } // namespace zclaw From ef682b46468583be0756f0b38e2f5a6ab2c58df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9?= <1131761436@qq.com> Date: Wed, 12 Aug 2026 19:18:00 +0800 Subject: [PATCH 3/9] feat(zclaw): polish compact UI feedback Add antialiased compact text layout, bounded settings navigation, persistent optional UISFX feedback, and glyph-height fixes without adding a service or resident process. --- .../APPLaunch/share/audio/LICENSE-UI-SFX.txt | 18 +++++++ .../share/audio/zclaw-scifi-error.mp3 | Bin 0 -> 4640 bytes .../share/audio/zclaw-scifi-receive.mp3 | Bin 0 -> 6313 bytes .../share/audio/zclaw-scifi-send.mp3 | Bin 0 -> 5686 bytes projects/ZClaw/main/SConstruct | 6 +-- projects/ZClaw/main/ui/zclaw_app.cpp | 6 ++- .../ZClaw/main/ui/zclaw_approval_dialog.cpp | 31 ++++++++--- .../ZClaw/main/ui/zclaw_chat_workflow.cpp | 16 ++++-- projects/ZClaw/main/ui/zclaw_chat_workflow.h | 6 ++- .../main/ui/zclaw_config_document_model.cpp | 11 +++- projects/ZClaw/main/ui/zclaw_fonts.cpp | 11 ++-- .../main/ui/zclaw_settings_coordinator.cpp | 40 +++++++++++--- .../ui/zclaw_settings_navigation_model.cpp | 2 + .../main/ui/zclaw_settings_navigation_model.h | 1 + .../ZClaw/main/ui/zclaw_settings_panel.cpp | 48 +++++++++++++---- .../main/ui/zclaw_settings_presentation.cpp | 1 + .../ZClaw/main/ui/zclaw_settings_workflow.cpp | 17 +++++- .../ZClaw/main/ui/zclaw_settings_workflow.h | 6 ++- .../main/ui/zclaw_setup_progress_view.cpp | 21 ++++---- projects/ZClaw/main/ui/zclaw_shell_view.cpp | 6 ++- .../ZClaw/main/ui/zclaw_sound_effects.cpp | 50 ++++++++++++++++++ projects/ZClaw/main/ui/zclaw_sound_effects.h | 34 ++++++++++++ projects/ZClaw/main/ui/zclaw_startup_view.cpp | 16 ++++-- projects/ZClaw/main/ui/zclaw_theme.h | 21 ++++---- projects/ZClaw/main/ui/zclaw_types.h | 1 + .../zclaw_config_document_model_test.cpp | 5 ++ projects/ZClaw/tests/zclaw_core_test.cpp | 28 +++++++++- .../ZClaw/tests/zclaw_settings_rules_test.cpp | 3 ++ 28 files changed, 332 insertions(+), 73 deletions(-) create mode 100644 projects/ZClaw/APPLaunch/share/audio/LICENSE-UI-SFX.txt create mode 100644 projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-error.mp3 create mode 100644 projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-receive.mp3 create mode 100644 projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-send.mp3 create mode 100644 projects/ZClaw/main/ui/zclaw_sound_effects.cpp create mode 100644 projects/ZClaw/main/ui/zclaw_sound_effects.h diff --git a/projects/ZClaw/APPLaunch/share/audio/LICENSE-UI-SFX.txt b/projects/ZClaw/APPLaunch/share/audio/LICENSE-UI-SFX.txt new file mode 100644 index 00000000..5df47c2e --- /dev/null +++ b/projects/ZClaw/APPLaunch/share/audio/LICENSE-UI-SFX.txt @@ -0,0 +1,18 @@ +UI SFX audio assets are dedicated to the public domain under the Creative +Commons CC0 1.0 Universal Public Domain Dedication. + +Source: https://github.com/romainsimon/uisfx +Package: uisfx 0.4.0 +Pack: scifi +Cues: send, receive, error + +To the extent possible under law, Yuki Capital has waived all copyright and +related or neighboring rights to the procedurally generated audio files in +packages/uisfx/sounds. + +You may copy, modify, distribute, and use these files, including for commercial +purposes, without asking permission. Attribution is appreciated but not +required. + +Legal code: https://creativecommons.org/publicdomain/zero/1.0/legalcode +SPDX identifier: CC0-1.0 diff --git a/projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-error.mp3 b/projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-error.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..632ee11cc928d125e38dc6429e8a3be1a8bf072c GIT binary patch literal 4640 zcmds)cTiK?y1;irq=e8SO^`#VQUWFzK$->!MnbO&h;*eF0XgbHLJDpe`c|3Q-*491Yv$7>oPq&#M9t02 zbZ9L*05Iq~T)(2EAb(08i^2Sw{QUy=j642I_jjo4eZ_|sanK$f0O0x#@X-*ZBTq+_ zjt&hII<|CN=>*USp%Y6djZPko3OY@6dg;8PF-_+)o$qu&8uTyIa&)5Qh{e)7|3nJn!CENz|N?=QySvUw$$1XC&iS?-p8SO$zQ0?H6IoGoI&NtQEw?t;@;fm z>W<4yvX|nKC#_CYr`K3{H%m?itgH^3wy1)_S2hgZf8c~(!j+CBT&?Yx=|Hw#4LmbS zu)xun0f3VKewE+vVAf{M6UKX|ZA0fcfq*SFzBd;BXlqNz6^}}sTnLbJx$JpThpBxj zdxb>S0xT`3&S!qTZa!ct)`$LaV<=3%x<_|0@F7-8A_ROr&iGlzNLwlj^V~DSyR^nb z--hzH1gHdw^aQK91l8=s28UH-uCV_JRFXcPXbCs`p%QstLuoGw{QLyuIgJ32!Jckh zEtx65cZjtiYIK{%7J^7&)F6*vig8w{O_N<$j&fZ z7W$+b$z5zUB&Vo&+Wkg9S!zL+55)$f%oLKSsR{{&e*{@@L$*VLK(MSs z1j2`ldmQ=ongyF|?eJk4$}v!;FT?aSg5$YxY}j z)eUPZ)=xgqQukVsNkfYm-?jRnpsX%_h$FH@+p#AZT(uicSetRgutHdEMe&e>n3^wi zAx41pdAN32iwD&-+NHL>FkZBpC;!}W(G6eYB0Zm($W6tGL(b*qaB=?vm!2Tk#T#mu zZ%0#hEiF7oT+Q6&j&TMdPn+FkiJ7!FFOq}^Akk3cK~n#qhtyYzly<(S!P=XOp#Yg> zYAX>LQl0Q8XXng47}wRBzGu3?@IXa1!78<+V&hg^)3~m}R7i;=zmO_U7hZX3$;<;f zFOvHt7F|j`HCIp&%1QbvGLmyS`>?egOk4q1fxyp@vX30Q*Z7}@_U4P*M01CC-lGe_ z!b0E%-+$i=8Etxe?rciY@XM-btraYf14ak6ugy&%9Dj&#YP4nx^VV4RTl;$Au&j}| zC9}0-eYV*}qisO8^zc4-`(VJ#+2U2Tw(9Mdy{hg>5!_)hg%;zw=G=u!bBtX@!&??e z<#0+TqfDL4Q&J2g?5N4Uc`Qod$P4Si3tY3|EPJ;N*%_4AeU>Z0TJNkE72&c6YIGqn zfMZQoz^aJM@&;9|e2NpQd^p)uO;{ZEzi#&)+?tLX8drbl(Kb{c^0f5(=*|yiS?XWY zcC4D3mc4eIAgq9{hde;HQl) z9MqFV&c{~-d}Aj{n?R_ra@%r6Wr>qt!m?Pl2(W$?&O6rJ^+!ylI-{c7@mta;bpOtii2F}(%FKR9YM&BwB||Z$sw88I24aD$ zKL73l^7W4V?i%696|o=4;{^WM4_9AI7RylXeyeT$S)qQVY$kKLY+1{cE+h=V5hhM$ z>fJlWoW&aTap~;JcSNQsjWA;2!6XR22kXze19&pmse=)mwxJ_7$D!e;lV)hz0C0v& zND@}P-{!UBCsFzCy6Bx-Z}hKy@|HWPC4etV(j)Dr2nF2~k33fhy_D3Y^FZtwi<~CY zc<}oLJ3H_|(S+d$@7??56tdKO;&&*lHyF?jB_cqWIjK@`?O<4aCt#>m32tCF!g@*DY_Q<@)thK6H1p`e0c6H04o< zXY$LR6`|ewY}a!6qzg$S*gnaN&G|4J10mIM7JibOg;K4(qB1e+QC&iTsC??!lBio@ z=}?jrR0`sARkeliP=#}3wz^J~AS*D4{@A}`zMq@1(Y4Wy@ndW7e=}nL%DvYsP|~_Q zU)}+}+dj370wCa*^0-_*fnd#=(4#Ap_D>{JT~PS1ajBiPpWmVBLP}(ooM5{ZeqTR> z#4B6%luEz)usxjpGm+OGBcfvI&!stj8nx@UZeK)1gmznGsR|ix!{tcMHvPHMX0i>J zV#eP~8^8e|v}(^e0Ydo>w{PfN9lYl!P3n;_MytnOI55nawr{+0AUM-43X3t2d7;p) z-cO+Ru|(b(JiL}Z6mtUi&Az)Vu|+R!8;orQH-g{o1ps)6iZ?ZnLwdTKwL+t^Fa)YD z+FMmoJgMt5byR6N`M$Bt?&ztBHe@2EBFA(@@eZe;*}B}yC|!sEu>1@jdN20V*Au=Z`>1sL55qAF(-~9@LQn3X))P6| zZuE9n^|p#TXsh8vyZfkipuMhz<$?hTk%9sBdD^6+1}Kd9yND#B!ZCKQwwW@E=9!AM zUAKKRkeDl6{ixY-qFDIp#v=fbt1HPlO_}cl9ih*> z)k*I?+Ucdm3(GM(*|~(oG10>bvCzOuul7UIp;TMMwq5I(g@0b@eA~kT?98f4s`lX% z830&sBoO#F7J5?T>Z7Lm!xy8jvPecLnJ5+z18>I#dF@#>`51j3dEh>=eNc9DH&5Ukk{Bl%TI9h zR;ouoZb}~0C0nli%Bz}ktLT;C)adm5blO`?IBSeWEm4^nh|)pk_JnI^nfsTp%No0H z62l^xp<$iOBi*Q^F65vJszyin+6kG3%zoEuN$()bid#3E+OIBweP8zKmRZCtyk_(LWbT;lx6Z}GzmfYPXJS%-v6d3TUZfVS_XnCy zBTmL@Utm$YG-#Cb5gu=qc&w}IXly}7ckWz1eTQ3`Yqnz1Tsy)QKA$ZH2T9sKC!odbBV_ zyl9c0r>|+C^Q)RH`QL;t^c-O7;bKG8+Czk6P!L#p`u`|_|C{{(B~`l6BEUQ=1hX%& w*EEeyE=W#q_?K1x&cJ_=P;Ph!04boA=qLcdkpTeq@8tiVy5`qE^#4idAJaJnKL7v# literal 0 HcmV?d00001 diff --git a/projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-receive.mp3 b/projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-receive.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0ff83b977b8904bef5b7a3cf86c3e046d42ce9ae GIT binary patch literal 6313 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 literal 0 HcmV?d00001 diff --git a/projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-send.mp3 b/projects/ZClaw/APPLaunch/share/audio/zclaw-scifi-send.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..faadad89d5cde7715ee981e44fa6e8e3f31bd5ad GIT binary patch literal 5686 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::FontManager fonts_; + zclaw::SoundEffects sounds_; zclaw::ShellView shell_view_; zclaw::InputDialog input_dialog_; zclaw::SettingsCoordinator settings_ui_{provider_manager_, config_manager_, @@ -64,9 +66,10 @@ class ZClawApp : public AppPageRoot zclaw::ChatView chat_view_; zclaw::SettingsWorkflow settings_workflow_{ provider_manager_, config_manager_, settings_ui_, input_dialog_, fonts_, - chat_view_, async_service_}; + chat_view_, async_service_, sounds_}; zclaw::ChatWorkflow chat_workflow_{ config_manager_, chat_view_, async_service_, approvals_, + sounds_, [this] { settings_workflow_.open_setup(shell_view_.content(), true); }}; zclaw::InputWorkflow input_workflow_{ input_dialog_, settings_workflow_, chat_workflow_}; @@ -87,6 +90,7 @@ class ZClawApp : public AppPageRoot const std::string sparkles_path = cp0_file_path("zclaw_sparkles_10.png"); const std::string send_button_path = cp0_file_path("zclaw_send_button_18.png"); load_configuration(); + sounds_.configure(config_manager_.config().ui_sounds_enabled); if (!shell_view_.create(root_screen_, &fonts_, avatar_path_, sparkles_path, send_button_path)) return; diff --git a/projects/ZClaw/main/ui/zclaw_approval_dialog.cpp b/projects/ZClaw/main/ui/zclaw_approval_dialog.cpp index 3e30f9a2..09b9dd90 100644 --- a/projects/ZClaw/main/ui/zclaw_approval_dialog.cpp +++ b/projects/ZClaw/main/ui/zclaw_approval_dialog.cpp @@ -6,6 +6,15 @@ namespace zclaw { +namespace { + +lv_coord_t centered_y(lv_coord_t container_height, lv_coord_t item_height) +{ + return (container_height - item_height) / 2; +} + +} // namespace + ApprovalDialog::~ApprovalDialog() { close(); @@ -27,23 +36,28 @@ void ApprovalDialog::show(const FontManager *fonts, lv_obj_move_foreground(dialog_); widgets::box(dialog_, 0, 0, 236, 20, theme::kPanel, 8); - widgets::label(dialog_, "Permission", 10, 5, 120, 12, fonts->font_12(), theme::kText); - widgets::label(dialog_, "Esc", 194, 6, 28, 10, fonts->font_10(), theme::kDim, + const lv_coord_t title_height = lv_font_get_line_height(fonts->font_12()); + const lv_coord_t text_height = lv_font_get_line_height(fonts->font_10()); + widgets::label(dialog_, "Permission", 10, centered_y(20, title_height), + 120, title_height, fonts->font_12(), theme::kText); + widgets::label(dialog_, "Esc", 194, centered_y(20, text_height), + 28, text_height, fonts->font_10(), theme::kDim, LV_TEXT_ALIGN_RIGHT); - widgets::label(dialog_, request.tool, 12, 27, 212, 14, + widgets::label(dialog_, request.tool, 12, 23, 212, title_height, fonts->font_12(), theme::kText); - widgets::label(dialog_, request.summary, 12, 45, 212, 30, + widgets::label(dialog_, request.summary, 12, 42, 212, text_height * 2, fonts->font_10(), theme::kMuted); - widgets::box(dialog_, 0, 78, 236, 1, theme::kPanelLine); + widgets::box(dialog_, 0, 80, 236, 1, theme::kPanelLine); static constexpr const char *labels[] = {"Yes", "Always", "No"}; static constexpr uint32_t colors[] = {theme::kOnline, theme::kPurple, theme::kDim}; for (int index = 0; index < 3; ++index) { - buttons_[index] = widgets::box(dialog_, 12 + index * 72, 86, 68, 17, + buttons_[index] = widgets::box(dialog_, 12 + index * 72, 85, 68, 20, theme::kPanel, 5); lv_obj_set_style_border_width(buttons_[index], 1, LV_PART_MAIN | LV_STATE_DEFAULT); - widgets::label(buttons_[index], labels[index], 0, 4, 68, 10, + widgets::label(buttons_[index], labels[index], 0, + centered_y(20, text_height), 68, text_height, fonts->font_10(), colors[index], LV_TEXT_ALIGN_CENTER); } update_selection(selected_index); @@ -79,7 +93,8 @@ void ApprovalDialog::update_selection(int selected_index) continue; const bool selected = index == selected_index; lv_obj_set_style_bg_color(buttons_[index], - lv_color_hex(selected ? 0x2B2B4A : theme::kPanel), + lv_color_hex(selected ? theme::kSelectedPanel + : theme::kPanel), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(buttons_[index], lv_color_hex(selected ? theme::kText : theme::kPanelLine), diff --git a/projects/ZClaw/main/ui/zclaw_chat_workflow.cpp b/projects/ZClaw/main/ui/zclaw_chat_workflow.cpp index ba7893bb..c1dd94aa 100644 --- a/projects/ZClaw/main/ui/zclaw_chat_workflow.cpp +++ b/projects/ZClaw/main/ui/zclaw_chat_workflow.cpp @@ -4,6 +4,7 @@ #include "zclaw_async_service.h" #include "zclaw_chat_view.h" #include "zclaw_runtime_state.h" +#include "zclaw_sound_effects.h" #include "zclaw_ui_config_manager.h" #include @@ -12,9 +13,10 @@ namespace zclaw { ChatWorkflow::ChatWorkflow(UiConfigManager &config, ChatView &chat, AsyncService &async_service, - ApprovalCoordinator &approvals, OpenSetup open_setup) + ApprovalCoordinator &approvals, SoundEffects &sounds, + OpenSetup open_setup) : config_(config), chat_(chat), async_service_(async_service), - approvals_(approvals), open_setup_(std::move(open_setup)), + approvals_(approvals), sounds_(sounds), open_setup_(std::move(open_setup)), lifetime_(std::make_shared>(this)) { } @@ -52,18 +54,22 @@ void ChatWorkflow::request_reply(const std::string &message) config_.config(), message, approvals_.handler(), [lifetime](OperationResult result) { lifetime->invoke([&result](ChatWorkflow &workflow) { - workflow.finish_request(std::move(result.text)); + workflow.finish_request(std::move(result)); }); })) { request_in_flight_ = false; chat_.append_assistant_message("Could not start webhook request."); + sounds_.play(SoundCue::Error); + return; } + sounds_.play(SoundCue::Send); } -void ChatWorkflow::finish_request(std::string text) +void ChatWorkflow::finish_request(OperationResult result) { request_in_flight_ = false; - chat_.append_assistant_message(text); + chat_.append_assistant_message(result.text); + sounds_.play(sound_cue_for_result(result.ok)); } } // namespace zclaw diff --git a/projects/ZClaw/main/ui/zclaw_chat_workflow.h b/projects/ZClaw/main/ui/zclaw_chat_workflow.h index 8e155781..aed7fb21 100644 --- a/projects/ZClaw/main/ui/zclaw_chat_workflow.h +++ b/projects/ZClaw/main/ui/zclaw_chat_workflow.h @@ -1,6 +1,7 @@ #pragma once #include "zclaw_callback_lifetime.h" +#include "zclaw_types.h" #include #include @@ -11,6 +12,7 @@ namespace zclaw { class ApprovalCoordinator; class AsyncService; class ChatView; +class SoundEffects; class UiConfigManager; class ChatWorkflow { @@ -19,6 +21,7 @@ class ChatWorkflow { ChatWorkflow(UiConfigManager &config, ChatView &chat, AsyncService &async_service, ApprovalCoordinator &approvals, + SoundEffects &sounds, OpenSetup open_setup); ~ChatWorkflow(); @@ -26,12 +29,13 @@ class ChatWorkflow { private: void request_reply(const std::string &message); - void finish_request(std::string text); + void finish_request(OperationResult result); UiConfigManager &config_; ChatView &chat_; AsyncService &async_service_; ApprovalCoordinator &approvals_; + SoundEffects &sounds_; OpenSetup open_setup_; bool request_in_flight_ = false; std::shared_ptr> lifetime_; diff --git a/projects/ZClaw/main/ui/zclaw_config_document_model.cpp b/projects/ZClaw/main/ui/zclaw_config_document_model.cpp index 4c0c5c40..34d26346 100644 --- a/projects/ZClaw/main/ui/zclaw_config_document_model.cpp +++ b/projects/ZClaw/main/ui/zclaw_config_document_model.cpp @@ -63,7 +63,7 @@ bool parse_ui_document(const std::string &document, UiConfig *config, const std::string key = decode_config_field(fields[0]); const bool known = key == "webhook_url" || key == "agent_alias" || key == "webhook_secret" || key == "bearer_token" || - key == "setup_complete"; + key == "setup_complete" || key == "ui_sounds_enabled"; if (!known) continue; if (strict && !is_valid_config_field_encoding(fields[1])) { @@ -94,6 +94,13 @@ bool parse_ui_document(const std::string &document, UiConfig *config, } config->setup_complete = value == "1"; recognized = true; + } else if (key == "ui_sounds_enabled") { + if (strict && value != "0" && value != "1") { + invalid = true; + continue; + } + config->ui_sounds_enabled = value != "0"; + recognized = true; } } return recognized && !invalid; @@ -156,6 +163,8 @@ std::string make_ui_config_document(const UiConfig &config) encode_config_field(config.bearer_token) + '\n'; document += encode_config_field("setup_complete") + '\t' + (config.setup_complete ? "1" : "0") + '\n'; + document += encode_config_field("ui_sounds_enabled") + '\t' + + (config.ui_sounds_enabled ? "1" : "0") + '\n'; return document; } diff --git a/projects/ZClaw/main/ui/zclaw_fonts.cpp b/projects/ZClaw/main/ui/zclaw_fonts.cpp index 63228652..7709a2ff 100644 --- a/projects/ZClaw/main/ui/zclaw_fonts.cpp +++ b/projects/ZClaw/main/ui/zclaw_fonts.cpp @@ -5,9 +5,6 @@ #if LV_USE_FREETYPE #include "lvgl/src/libs/freetype/lv_freetype.h" -#ifndef LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO -#define LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO ((lv_freetype_font_render_mode_t)2) -#endif #endif #include @@ -111,19 +108,19 @@ void FontManager::init() return; font_10_ = lv_freetype_font_create( - font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, + font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); font_12_ = lv_freetype_font_create( - font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, + font_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); const std::string fallback_path = runtime_fallback_font_path(); if (!fallback_path.empty()) { fallback_font_10_ = lv_freetype_font_create( - fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, + fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); fallback_font_12_ = lv_freetype_font_create( - fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP_MONO, kZClawFontSize, + fallback_path.c_str(), LV_FREETYPE_FONT_RENDER_MODE_BITMAP, kZClawFontSize, LV_FREETYPE_FONT_STYLE_NORMAL); } set_fallback_chain(font_10_, fallback_font_10_); diff --git a/projects/ZClaw/main/ui/zclaw_settings_coordinator.cpp b/projects/ZClaw/main/ui/zclaw_settings_coordinator.cpp index 444be2ec..ee1b795a 100644 --- a/projects/ZClaw/main/ui/zclaw_settings_coordinator.cpp +++ b/projects/ZClaw/main/ui/zclaw_settings_coordinator.cpp @@ -11,6 +11,15 @@ namespace zclaw { +namespace { + +lv_coord_t centered_y(lv_coord_t container_height, lv_coord_t item_height) +{ + return (container_height - item_height) / 2; +} + +} // namespace + SettingsCoordinator::SettingsCoordinator(ProviderManager &providers, UiConfigManager &config, InputDialog &input, FontManager &fonts) @@ -111,20 +120,25 @@ void SettingsCoordinator::show_setup_error(const std::string &error) lv_obj_set_style_border_color(setup_error_dialog_, lv_color_hex(theme::kPurple), LV_PART_MAIN); lv_obj_move_foreground(setup_error_dialog_); - widgets::label(setup_error_dialog_, "Download failed", 12, 9, 226, 14, + const lv_coord_t title_height = lv_font_get_line_height(fonts_.font_12()); + const lv_coord_t text_height = lv_font_get_line_height(fonts_.font_10()); + widgets::label(setup_error_dialog_, "Download failed", 12, 5, 226, + title_height, fonts_.font_12(), theme::kText); widgets::label(setup_error_dialog_, error.empty() ? "Network connection failed." : error, - 12, 30, 226, 36, fonts_.font_10(), theme::kMuted); + 12, 25, 226, text_height * 2, + fonts_.font_10(), theme::kMuted); static constexpr const char *labels[] = {"Retry", "Exit"}; for (int index = 0; index < 2; ++index) { setup_error_buttons_[index] = widgets::box( - setup_error_dialog_, 22 + index * 106, 74, 96, 18, + setup_error_dialog_, 22 + index * 106, 73, 96, 20, theme::kPanel, 5); lv_obj_set_style_border_width(setup_error_buttons_[index], 1, LV_PART_MAIN); - widgets::label(setup_error_buttons_[index], labels[index], 0, 4, - 96, 10, fonts_.font_10(), theme::kText, + widgets::label(setup_error_buttons_[index], labels[index], 0, + centered_y(20, text_height), 96, text_height, + fonts_.font_10(), theme::kText, LV_TEXT_ALIGN_CENTER); } update_setup_error_selection(0); @@ -147,7 +161,8 @@ void SettingsCoordinator::update_setup_error_selection(int selected_index) continue; const bool selected = index == selected_index; lv_obj_set_style_bg_color(setup_error_buttons_[index], - lv_color_hex(selected ? 0x252542 : theme::kPanel), + lv_color_hex(selected ? theme::kSelectedPanel + : theme::kPanel), LV_PART_MAIN); lv_obj_set_style_border_color(setup_error_buttons_[index], lv_color_hex(selected ? theme::kText @@ -206,18 +221,31 @@ void SettingsCoordinator::move_selection(int delta) if (!is_open()) return; if (state_.view() == SettingsView::SetupProviders) { + const PagedSelection before = state_.setup_provider_selection(); state_.move_setup_providers(static_cast(provider_preset_count()), SettingsPanel::kMaximumRows, delta); + const PagedSelection &after = state_.setup_provider_selection(); + if (after.selected_index == before.selected_index && + after.scroll_offset == before.scroll_offset) + return; render_setup_providers(); return; } if (state_.view() == SettingsView::Providers) { + const PagedSelection before = state_.provider_selection(); state_.move_providers(static_cast(providers_.providers().size()) + 1, SettingsPanel::kMaximumRows, delta); + const PagedSelection &after = state_.provider_selection(); + if (after.selected_index == before.selected_index && + after.scroll_offset == before.scroll_offset) + return; render_providers(); return; } + const int selected_before = state_.selected_row(); state_.move_rows(panel_.row_count(), delta); + if (state_.selected_row() == selected_before) + return; panel_.update_selection(state_.selected_row()); } diff --git a/projects/ZClaw/main/ui/zclaw_settings_navigation_model.cpp b/projects/ZClaw/main/ui/zclaw_settings_navigation_model.cpp index fe5e84fd..14d179df 100644 --- a/projects/ZClaw/main/ui/zclaw_settings_navigation_model.cpp +++ b/projects/ZClaw/main/ui/zclaw_settings_navigation_model.cpp @@ -50,6 +50,8 @@ SettingsActivation settings_activation(const SettingsActivationContext &context) return {SettingsActivationAction::ShowAuthorization}; if (context.selected_row == 2) return {SettingsActivationAction::ShowProviders}; + if (context.selected_row == 5) + return {SettingsActivationAction::ToggleUiSounds}; return {}; case SettingsView::Providers: { const int item = context.provider_scroll + context.selected_row; diff --git a/projects/ZClaw/main/ui/zclaw_settings_navigation_model.h b/projects/ZClaw/main/ui/zclaw_settings_navigation_model.h index 687abe92..8ffdcdfe 100644 --- a/projects/ZClaw/main/ui/zclaw_settings_navigation_model.h +++ b/projects/ZClaw/main/ui/zclaw_settings_navigation_model.h @@ -27,6 +27,7 @@ enum class SettingsActivationAction { ShowSetupProviders, ShowAuthorization, ShowProviders, + ToggleUiSounds, SelectSetupProvider, StartSetup, EditSetupField, diff --git a/projects/ZClaw/main/ui/zclaw_settings_panel.cpp b/projects/ZClaw/main/ui/zclaw_settings_panel.cpp index 5247ac13..e781faab 100644 --- a/projects/ZClaw/main/ui/zclaw_settings_panel.cpp +++ b/projects/ZClaw/main/ui/zclaw_settings_panel.cpp @@ -9,6 +9,12 @@ namespace { constexpr lv_coord_t kScreenWidth = 320; constexpr lv_coord_t kScreenHeight = 170; +constexpr lv_coord_t kHeaderHeight = 20; + +lv_coord_t centered_y(lv_coord_t container_height, lv_coord_t item_height) +{ + return (container_height - item_height) / 2; +} } // namespace SettingsPanel::~SettingsPanel() @@ -26,14 +32,28 @@ bool SettingsPanel::create(lv_obj_t *parent, const FontManager *fonts) lv_obj_add_event_cb(panel_, panel_deleted, LV_EVENT_DELETE, this); lv_obj_move_foreground(panel_); - lv_obj_t *bar = widgets::box(panel_, 0, 0, kScreenWidth, 20, theme::kBar); - header_label_ = widgets::label(bar, "ZClaw Settings", 12, 4, 160, 12, + lv_obj_t *bar = widgets::box(panel_, 0, 0, kScreenWidth, kHeaderHeight, + theme::kBar); + const lv_coord_t title_height = + lv_font_get_line_height(fonts_->font_12()); + const lv_coord_t hint_height = + lv_font_get_line_height(fonts_->font_10()); + header_label_ = widgets::label( + bar, "ZClaw Settings", 12, centered_y(kHeaderHeight, title_height), + 160, title_height, fonts_->font_12(), theme::kText); - hint_label_ = widgets::label(bar, "Tab / Esc", 214, 5, 94, 10, + hint_label_ = widgets::label(bar, "Tab / Esc", 214, + centered_y(kHeaderHeight, hint_height), + 94, hint_height, fonts_->font_10(), theme::kDim, LV_TEXT_ALIGN_RIGHT); - rows_container_ = widgets::box(panel_, 0, 20, kScreenWidth, 150, + widgets::box(bar, 0, kHeaderHeight - 1, kScreenWidth, 1, theme::kPanelLine); + rows_container_ = widgets::box(panel_, 0, kHeaderHeight, kScreenWidth, + kScreenHeight - kHeaderHeight, theme::kBackground); lv_obj_add_flag(rows_container_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(rows_container_, static_cast( + LV_OBJ_FLAG_SCROLL_ELASTIC | LV_OBJ_FLAG_SCROLL_MOMENTUM | + LV_OBJ_FLAG_SCROLL_CHAIN)); lv_obj_set_scroll_dir(rows_container_, LV_DIR_VER); lv_obj_set_scrollbar_mode(rows_container_, LV_SCROLLBAR_MODE_AUTO); lv_obj_set_style_width(rows_container_, 3, LV_PART_SCROLLBAR); @@ -99,16 +119,20 @@ bool SettingsPanel::add_row(const std::string &title, const std::string &value) return false; const int index = row_count_; const lv_coord_t y = 8 + index * 28; - lv_obj_t *row = widgets::box(rows_container_, 12, y, 296, 26, + constexpr lv_coord_t row_height = 26; + lv_obj_t *row = widgets::box(rows_container_, 12, y, 296, row_height, theme::kPanel, 8); lv_obj_set_style_border_width(row, 1, LV_PART_MAIN | LV_STATE_DEFAULT); - lv_obj_set_style_border_color(row, lv_color_hex(theme::kPanel), + lv_obj_set_style_border_color(row, lv_color_hex(theme::kPanelLine), LV_PART_MAIN | LV_STATE_DEFAULT); - widgets::label(row, title, 10, 5, 160, 14, fonts_->font_10(), theme::kText); - values_[index] = widgets::label(row, value, 168, 5, 118, 14, + const lv_coord_t text_height = lv_font_get_line_height(fonts_->font_10()); + const lv_coord_t text_y = centered_y(row_height, text_height); + widgets::label(row, title, 10, text_y, 160, text_height, + fonts_->font_10(), theme::kText); + values_[index] = widgets::label(row, value, 168, text_y, 118, text_height, fonts_->font_10(), theme::kMuted, LV_TEXT_ALIGN_RIGHT); - widgets::box(row, 8, 23, 280, 1, theme::kPanelLine); + widgets::box(row, 8, row_height - 3, 280, 1, theme::kPanelLine); rows_[index] = row; ++row_count_; return true; @@ -126,10 +150,12 @@ void SettingsPanel::update_selection(int selected_index) continue; const bool selected = index == selected_index; lv_obj_set_style_border_color(rows_[index], - lv_color_hex(selected ? theme::kPurple : theme::kPanel), + lv_color_hex(selected ? theme::kPurple + : theme::kPanelLine), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(rows_[index], - lv_color_hex(selected ? 0x252542 : theme::kPanel), + lv_color_hex(selected ? theme::kSelectedPanel + : theme::kPanel), LV_PART_MAIN | LV_STATE_DEFAULT); if (values_[index]) lv_obj_set_style_text_color(values_[index], diff --git a/projects/ZClaw/main/ui/zclaw_settings_presentation.cpp b/projects/ZClaw/main/ui/zclaw_settings_presentation.cpp index b4c452d5..4b41061a 100644 --- a/projects/ZClaw/main/ui/zclaw_settings_presentation.cpp +++ b/projects/ZClaw/main/ui/zclaw_settings_presentation.cpp @@ -17,6 +17,7 @@ SettingsPresentation present_settings_main(const UiConfig &config) {"Providers", "Manage"}, {"Agent", config.agent_alias}, {"Transport", config.bearer_token.empty() ? "Webhook" : "WS"}, + {"UI Sounds", config.ui_sounds_enabled ? "On" : "Off"}, }, }; } diff --git a/projects/ZClaw/main/ui/zclaw_settings_workflow.cpp b/projects/ZClaw/main/ui/zclaw_settings_workflow.cpp index 7489f2f7..86d5dde5 100644 --- a/projects/ZClaw/main/ui/zclaw_settings_workflow.cpp +++ b/projects/ZClaw/main/ui/zclaw_settings_workflow.cpp @@ -2,6 +2,8 @@ #include "zclaw_provider_manager.h" #include "zclaw_settings_coordinator.h" +#include "zclaw_sound_effects.h" +#include "zclaw_ui_config_manager.h" namespace zclaw { @@ -9,8 +11,9 @@ SettingsWorkflow::SettingsWorkflow(ProviderManager &providers, UiConfigManager &config, SettingsCoordinator &settings, InputDialog &input, FontManager &fonts, - ChatView &chat, AsyncService &async_service) - : providers_(providers), settings_(settings), + ChatView &chat, AsyncService &async_service, + SoundEffects &sounds) + : providers_(providers), config_(config), settings_(settings), sounds_(sounds), authorization_(config, settings, input, fonts, chat, async_service), provider_workflow_(providers, settings, input, fonts, chat), setup_(providers, config, settings, input, fonts, chat, async_service) @@ -100,6 +103,16 @@ void SettingsWorkflow::activate_selection() settings_.state().reset_providers(); settings_.render_providers(); break; + case SettingsActivationAction::ToggleUiSounds: { + UiConfig candidate = config_.config(); + candidate.ui_sounds_enabled = !candidate.ui_sounds_enabled; + std::string error; + if (!config_.update(candidate, &error)) + break; + sounds_.set_enabled(candidate.ui_sounds_enabled); + settings_.render_main(); + break; + } case SettingsActivationAction::SelectSetupProvider: setup_.select_provider( settings_.state().setup_provider_selection().selected_index); diff --git a/projects/ZClaw/main/ui/zclaw_settings_workflow.h b/projects/ZClaw/main/ui/zclaw_settings_workflow.h index 1581717a..fb279ad0 100644 --- a/projects/ZClaw/main/ui/zclaw_settings_workflow.h +++ b/projects/ZClaw/main/ui/zclaw_settings_workflow.h @@ -16,13 +16,15 @@ class FontManager; class InputDialog; class ProviderManager; class SettingsCoordinator; +class SoundEffects; class UiConfigManager; class SettingsWorkflow { public: SettingsWorkflow(ProviderManager &providers, UiConfigManager &config, SettingsCoordinator &settings, InputDialog &input, - FontManager &fonts, ChatView &chat, AsyncService &async_service); + FontManager &fonts, ChatView &chat, AsyncService &async_service, + SoundEffects &sounds); bool setup_in_flight() const; bool setup_retry_pending() const; @@ -41,7 +43,9 @@ class SettingsWorkflow { private: ProviderManager &providers_; + UiConfigManager &config_; SettingsCoordinator &settings_; + SoundEffects &sounds_; AuthorizationWorkflow authorization_; ProviderWorkflow provider_workflow_; SetupWorkflow setup_; diff --git a/projects/ZClaw/main/ui/zclaw_setup_progress_view.cpp b/projects/ZClaw/main/ui/zclaw_setup_progress_view.cpp index c96d7d93..26bd860c 100644 --- a/projects/ZClaw/main/ui/zclaw_setup_progress_view.cpp +++ b/projects/ZClaw/main/ui/zclaw_setup_progress_view.cpp @@ -57,17 +57,20 @@ void SetupProgressView::show(lv_obj_t *parent, const FontManager *fonts) lv_obj_set_style_arc_color(spinner_, lv_color_hex(theme::kPanelLine), LV_PART_MAIN); lv_obj_set_style_arc_color(spinner_, lv_color_hex(theme::kPurple), LV_PART_INDICATOR); - status_label_ = widgets::label(root_, "Preparing Quickstart", 50, 28, 252, 16, + const lv_coord_t title_height = lv_font_get_line_height(fonts->font_12()); + const lv_coord_t text_height = lv_font_get_line_height(fonts->font_10()); + status_label_ = widgets::label(root_, "Preparing Quickstart", 50, 26, + 252, title_height, fonts->font_12(), theme::kText); - detail_label_ = widgets::label(root_, "Starting...", 50, 45, 252, 14, + detail_label_ = widgets::label(root_, "Starting...", 50, 45, 252, text_height, fonts->font_10(), theme::kMuted); - url_prefix_label_ = widgets::label(root_, "URL :", 18, 64, 38, 14, + url_prefix_label_ = widgets::label(root_, "URL :", 18, 64, 38, text_height, fonts->font_10(), theme::kMuted); - url_label_ = widgets::label(root_, "", 56, 64, 246, 14, + url_label_ = widgets::label(root_, "", 56, 64, 246, text_height, fonts->font_10(), theme::kMuted); - path_prefix_label_ = widgets::label(root_, "SAVE :", 18, 79, 38, 14, + path_prefix_label_ = widgets::label(root_, "SAVE :", 18, 82, 38, text_height, fonts->font_10(), theme::kMuted); - path_label_ = widgets::label(root_, "", 56, 79, 246, 14, + path_label_ = widgets::label(root_, "", 56, 82, 246, text_height, fonts->font_10(), theme::kMuted); lv_label_set_long_mode(url_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); lv_label_set_long_mode(path_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); @@ -76,7 +79,7 @@ void SetupProgressView::show(lv_obj_t *parent, const FontManager *fonts) lv_obj_add_flag(url_prefix_label_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(path_prefix_label_, LV_OBJ_FLAG_HIDDEN); progress_bar_ = lv_bar_create(root_); - lv_obj_set_pos(progress_bar_, 18, 101); + lv_obj_set_pos(progress_bar_, 18, 104); lv_obj_set_size(progress_bar_, 284, 10); lv_bar_set_range(progress_bar_, 0, 100); lv_bar_set_value(progress_bar_, 0, LV_ANIM_OFF); @@ -84,9 +87,9 @@ void SetupProgressView::show(lv_obj_t *parent, const FontManager *fonts) lv_obj_set_style_radius(progress_bar_, 3, LV_PART_INDICATOR); lv_obj_set_style_bg_color(progress_bar_, lv_color_hex(theme::kPanel), LV_PART_MAIN); lv_obj_set_style_bg_color(progress_bar_, lv_color_hex(theme::kPurple), LV_PART_INDICATOR); - percent_label_ = widgets::label(root_, "0%", 18, 117, 70, 14, + percent_label_ = widgets::label(root_, "0%", 18, 117, 70, text_height, fonts->font_10(), theme::kText); - speed_label_ = widgets::label(root_, "", 92, 117, 210, 14, + speed_label_ = widgets::label(root_, "", 92, 117, 210, text_height, fonts->font_10(), theme::kMuted, LV_TEXT_ALIGN_RIGHT); } diff --git a/projects/ZClaw/main/ui/zclaw_shell_view.cpp b/projects/ZClaw/main/ui/zclaw_shell_view.cpp index d6a3fa58..f6f2be70 100644 --- a/projects/ZClaw/main/ui/zclaw_shell_view.cpp +++ b/projects/ZClaw/main/ui/zclaw_shell_view.cpp @@ -97,11 +97,13 @@ void ShellView::create_input_bar(const FontManager *fonts, { lv_obj_t *input_bar = widgets::box(root_, 0, 148, kScreenWidth, 22, theme::kBar); + const lv_coord_t text_height = lv_font_get_line_height(fonts->font_10()); widgets::box(input_bar, 0, 0, kScreenWidth, 1, theme::kPanelLine); - lv_obj_t *input_box = widgets::box(input_bar, 10, 3, 274, 16, + lv_obj_t *input_box = widgets::box(input_bar, 10, 2, 274, 18, theme::kPanel, 8); widgets::image(input_box, sparkles_path, 8, 3); - widgets::label(input_box, "Press Enter to ask", 26, 3, 180, 10, + widgets::label(input_box, "Press Enter to ask", 26, + centered_y(18, text_height), 180, text_height, fonts->font_10(), theme::kDim); widgets::image(input_bar, send_button_path, 292, 2); } diff --git a/projects/ZClaw/main/ui/zclaw_sound_effects.cpp b/projects/ZClaw/main/ui/zclaw_sound_effects.cpp new file mode 100644 index 00000000..c7a1aa33 --- /dev/null +++ b/projects/ZClaw/main/ui/zclaw_sound_effects.cpp @@ -0,0 +1,50 @@ +#include "zclaw_sound_effects.h" + +#include "cp0_lvgl_file.hpp" +#include "hal_lvgl_bsp.h" + +#include + +namespace zclaw { + +SoundEffects::~SoundEffects() +{ + if (configured_) + cp0_signal_audio_api({"SystemSoundEnable", "0"}, nullptr); +} + +void SoundEffects::configure(bool enabled) +{ + cp0_signal_audio_api( + {"SetSystemSoundNames", + cp0_file_path("share/audio/zclaw-scifi-send.mp3"), + cp0_file_path("share/audio/zclaw-scifi-receive.mp3"), + cp0_file_path("share/audio/zclaw-scifi-error.mp3")}, + nullptr); + configured_ = true; + set_enabled(enabled); +} + +void SoundEffects::set_enabled(bool enabled) +{ + enabled_ = enabled; + if (!configured_) + return; + cp0_signal_audio_api( + {"SystemSoundEnable", enabled ? "1" : "0"}, nullptr); +} + +bool SoundEffects::enabled() const +{ + return enabled_; +} + +void SoundEffects::play(SoundCue cue) const +{ + if (!configured_ || !enabled_) + return; + cp0_signal_audio_api( + {"SystemSoundPlay", std::to_string(static_cast(cue))}, nullptr); +} + +} // namespace zclaw diff --git a/projects/ZClaw/main/ui/zclaw_sound_effects.h b/projects/ZClaw/main/ui/zclaw_sound_effects.h new file mode 100644 index 00000000..ca4b8dc8 --- /dev/null +++ b/projects/ZClaw/main/ui/zclaw_sound_effects.h @@ -0,0 +1,34 @@ +#pragma once + +namespace zclaw { + +enum class SoundCue { + Send = 0, + Receive = 1, + Error = 2, +}; + +constexpr SoundCue sound_cue_for_result(bool ok) +{ + return ok ? SoundCue::Receive : SoundCue::Error; +} + +class SoundEffects { +public: + SoundEffects() = default; + ~SoundEffects(); + + SoundEffects(const SoundEffects &) = delete; + SoundEffects &operator=(const SoundEffects &) = delete; + + void configure(bool enabled); + void set_enabled(bool enabled); + bool enabled() const; + void play(SoundCue cue) const; + +private: + bool configured_ = false; + bool enabled_ = false; +}; + +} // namespace zclaw diff --git a/projects/ZClaw/main/ui/zclaw_startup_view.cpp b/projects/ZClaw/main/ui/zclaw_startup_view.cpp index f451912f..03630875 100644 --- a/projects/ZClaw/main/ui/zclaw_startup_view.cpp +++ b/projects/ZClaw/main/ui/zclaw_startup_view.cpp @@ -27,11 +27,15 @@ void StartupView::show_network_check(lv_obj_t *parent, const FontManager *fonts) overlay_ = widgets::box(parent, 0, 0, kScreenWidth, kScreenHeight, theme::kBackground); lv_obj_add_event_cb(overlay_, overlay_deleted, LV_EVENT_DELETE, this); lv_obj_move_foreground(overlay_); - widgets::label(overlay_, "ZCLAW", 16, 24, 288, 20, fonts_->font_12(), theme::kPurple, + const lv_coord_t title_height = lv_font_get_line_height(fonts_->font_12()); + const lv_coord_t text_height = lv_font_get_line_height(fonts_->font_10()); + widgets::label(overlay_, "ZCLAW", 16, 24, 288, title_height, + fonts_->font_12(), theme::kPurple, LV_TEXT_ALIGN_CENTER); - title_ = widgets::label(overlay_, "Checking network...", 16, 68, 288, 20, + title_ = widgets::label(overlay_, "Checking network...", 16, 68, 288, + title_height, fonts_->font_12(), theme::kText, LV_TEXT_ALIGN_CENTER); - detail_ = widgets::label(overlay_, "Please wait", 16, 96, 288, 16, + detail_ = widgets::label(overlay_, "Please wait", 16, 96, 288, text_height, fonts_->font_10(), theme::kMuted, LV_TEXT_ALIGN_CENTER); } @@ -44,9 +48,11 @@ void StartupView::show_offline(const std::string &error) const std::string detail = error.empty() ? "Check Wi-Fi and try again." : "Check Wi-Fi and try again.\n" + error; lv_label_set_text(detail_, detail.c_str()); - lv_obj_set_height(detail_, 34); + const lv_coord_t text_height = lv_font_get_line_height(fonts_->font_10()); + lv_obj_set_height(detail_, text_height * 2); if (!action_) - action_ = widgets::label(overlay_, "OK EXIT", 16, 142, 288, 14, + action_ = widgets::label(overlay_, "OK EXIT", 16, 140, 288, + text_height, fonts_->font_10(), theme::kPurple, LV_TEXT_ALIGN_CENTER); } diff --git a/projects/ZClaw/main/ui/zclaw_theme.h b/projects/ZClaw/main/ui/zclaw_theme.h index 8c6b9aeb..f8df7691 100644 --- a/projects/ZClaw/main/ui/zclaw_theme.h +++ b/projects/ZClaw/main/ui/zclaw_theme.h @@ -4,16 +4,17 @@ namespace zclaw::theme { -inline constexpr std::uint32_t kBackground = 0x0F0F1A; -inline constexpr std::uint32_t kBar = 0x16162A; -inline constexpr std::uint32_t kPanel = 0x1E1E35; -inline constexpr std::uint32_t kPanelLine = 0x2A2A45; -inline constexpr std::uint32_t kMuted = 0xA1A1AA; -inline constexpr std::uint32_t kDim = 0x52525B; -inline constexpr std::uint32_t kText = 0xF4F4F5; +inline constexpr std::uint32_t kBackground = 0x101014; +inline constexpr std::uint32_t kBar = 0x18181C; +inline constexpr std::uint32_t kPanel = 0x141414; +inline constexpr std::uint32_t kPanelLine = 0x3D3D3D; +inline constexpr std::uint32_t kSelectedPanel = 0x163837; +inline constexpr std::uint32_t kMuted = 0xA3A3A3; +inline constexpr std::uint32_t kDim = 0x616161; +inline constexpr std::uint32_t kText = 0xE6E6E6; inline constexpr std::uint32_t kWhite = 0xFFFFFF; -inline constexpr std::uint32_t kOnline = 0x10B981; -inline constexpr std::uint32_t kPurple = 0x8B5CF6; -inline constexpr std::uint32_t kIndigo = 0x6366F1; +inline constexpr std::uint32_t kOnline = 0x63E2B7; +inline constexpr std::uint32_t kPurple = 0x20A29D; +inline constexpr std::uint32_t kIndigo = 0x2080F0; } // namespace zclaw::theme diff --git a/projects/ZClaw/main/ui/zclaw_types.h b/projects/ZClaw/main/ui/zclaw_types.h index dcb7fddd..0bf65c2a 100644 --- a/projects/ZClaw/main/ui/zclaw_types.h +++ b/projects/ZClaw/main/ui/zclaw_types.h @@ -21,6 +21,7 @@ struct UiConfig { std::string webhook_secret; std::string bearer_token; bool setup_complete = false; + bool ui_sounds_enabled = true; }; struct ApprovalRequest { diff --git a/projects/ZClaw/tests/zclaw_config_document_model_test.cpp b/projects/ZClaw/tests/zclaw_config_document_model_test.cpp index ccde3caa..39374fdc 100644 --- a/projects/ZClaw/tests/zclaw_config_document_model_test.cpp +++ b/projects/ZClaw/tests/zclaw_config_document_model_test.cpp @@ -45,6 +45,7 @@ int main() config.webhook_secret = "hook\\secret"; config.bearer_token = "token"; config.setup_complete = true; + config.ui_sounds_enabled = false; const zclaw::UiConfig parsed_config = zclaw::parse_ui_config_document( "ignored\nunknown\tvalue\n" + zclaw::make_ui_config_document(config)); assert(parsed_config.webhook_url == config.webhook_url); @@ -52,11 +53,13 @@ int main() assert(parsed_config.webhook_secret == config.webhook_secret); assert(parsed_config.bearer_token == config.bearer_token); assert(parsed_config.setup_complete); + assert(!parsed_config.ui_sounds_enabled); const zclaw::UiConfig defaults = zclaw::parse_ui_config_document("agent_alias\t\nsetup_complete\ttrue\n"); assert(defaults.agent_alias == "zclaw"); assert(!defaults.setup_complete); + assert(defaults.ui_sounds_enabled); assert(!zclaw::parse_provider_config_document_checked( "alias\\q\tfamily\tmodel\turi\tkey\n", &checked_providers)); @@ -77,6 +80,8 @@ int main() "agent_alias\tfirst\nagent_alias\tsecond\n", &checked_config)); assert(!zclaw::parse_ui_config_document_checked( "agent_alias\tbad\\qvalue\n", &checked_config)); + assert(!zclaw::parse_ui_config_document_checked( + "agent_alias\tvalid\nui_sounds_enabled\tyes\n", &checked_config)); const zclaw::UiConfig lenient_config = zclaw::parse_ui_config_document( "agent_alias\tbad\\qvalue\nagent_alias\tsecond\n"); assert(lenient_config.agent_alias == "second"); diff --git a/projects/ZClaw/tests/zclaw_core_test.cpp b/projects/ZClaw/tests/zclaw_core_test.cpp index cec99ff0..6f176a27 100644 --- a/projects/ZClaw/tests/zclaw_core_test.cpp +++ b/projects/ZClaw/tests/zclaw_core_test.cpp @@ -1,4 +1,5 @@ #include "zclaw_provider_store.h" +#include "zclaw_sound_effects.h" #include "zclaw_provider_catalog.h" #include "zclaw_provider_manager.h" #include "zclaw_ui_config_store.h" @@ -120,6 +121,14 @@ int main() assert(settings_controller.provider_selection().selected_index == 5); assert(settings_controller.provider_selection().scroll_offset == 1); assert(settings_controller.selected_row() == 4); + settings_controller.move_providers(8, 5, 99); + const zclaw::PagedSelection provider_bottom = + settings_controller.provider_selection(); + settings_controller.move_providers(8, 5, 1); + assert(settings_controller.provider_selection().selected_index == + provider_bottom.selected_index); + assert(settings_controller.provider_selection().scroll_offset == + provider_bottom.scroll_offset); settings_controller.open_provider_detail(4); assert(settings_controller.provider_detail_index() == 4); assert(settings_controller.selected_row() == 0); @@ -134,6 +143,19 @@ int main() assert(settings_controller.selected_row() == 4); settings_controller.move_setup_providers(6, 5, -1); assert(settings_controller.setup_provider_selection().selected_index == 4); + settings_controller.move_setup_providers(6, 5, 99); + const zclaw::PagedSelection setup_provider_bottom = + settings_controller.setup_provider_selection(); + settings_controller.move_setup_providers(6, 5, 1); + assert(settings_controller.setup_provider_selection().selected_index == + setup_provider_bottom.selected_index); + assert(settings_controller.setup_provider_selection().scroll_offset == + setup_provider_bottom.scroll_offset); + settings_controller.reset_view(SettingsView::Main, 5); + settings_controller.move_rows(6, 1); + assert(settings_controller.selected_row() == 5); + settings_controller.move_rows(6, 1); + assert(settings_controller.selected_row() == 5); settings_controller.reset_view(SettingsView::Setup, 2); assert(settings_controller.activation(2, 3, false).action == SettingsActivationAction::StartSetup); @@ -148,15 +170,19 @@ int main() zclaw::SettingsPresentation settings_presentation = zclaw::present_settings_main(presentation_config); assert(settings_presentation.title == "ZClaw Settings"); - assert(settings_presentation.rows.size() == 5); + assert(settings_presentation.rows.size() == 6); assert(settings_presentation.rows[0].value == "Run"); assert(settings_presentation.rows[3].value == "demo-agent"); assert(settings_presentation.rows[4].value == "Webhook"); + assert(settings_presentation.rows[5].title == "UI Sounds"); + assert(settings_presentation.rows[5].value == "On"); presentation_config.setup_complete = true; presentation_config.bearer_token = "token"; settings_presentation = zclaw::present_authorization(presentation_config); assert(settings_presentation.rows[1].value == "Saved"); assert(settings_presentation.rows[3].value == "WS"); + assert(zclaw::sound_cue_for_result(true) == zclaw::SoundCue::Receive); + assert(zclaw::sound_cue_for_result(false) == zclaw::SoundCue::Error); settings_presentation = zclaw::present_setup(custom); assert(settings_presentation.rows.size() == 6); diff --git a/projects/ZClaw/tests/zclaw_settings_rules_test.cpp b/projects/ZClaw/tests/zclaw_settings_rules_test.cpp index bd314851..de306f7a 100644 --- a/projects/ZClaw/tests/zclaw_settings_rules_test.cpp +++ b/projects/ZClaw/tests/zclaw_settings_rules_test.cpp @@ -123,6 +123,9 @@ int main() SettingsActivationAction::ShowAuthorization); context.selected_row = 2; assert(zclaw::settings_activation(context).action == SettingsActivationAction::ShowProviders); + context.selected_row = 5; + assert(zclaw::settings_activation(context).action == + SettingsActivationAction::ToggleUiSounds); context.view = SettingsView::Providers; context.provider_count = 3; From 62f85964aa5309a779246bc7c8ce3ceb5522fc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9?= <1131761436@qq.com> Date: Wed, 12 Aug 2026 19:20:13 +0800 Subject: [PATCH 4/9] fix(applaunch): keep carousel borders consistent --- .../main/ui/ui_launch_page_carousel_view.cpp | 3 +-- projects/APPLaunch/tests/run_tests.sh | 1 + .../tests/test_carousel_border_contract.py | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 projects/APPLaunch/tests/test_carousel_border_contract.py diff --git a/projects/APPLaunch/main/ui/ui_launch_page_carousel_view.cpp b/projects/APPLaunch/main/ui/ui_launch_page_carousel_view.cpp index 2a646292..bb00618a 100644 --- a/projects/APPLaunch/main/ui/ui_launch_page_carousel_view.cpp +++ b/projects/APPLaunch/main/ui/ui_launch_page_carousel_view.cpp @@ -59,6 +59,7 @@ lv_obj_t *create_carousel_card(lv_obj_t *parent, const Slot &slot, lv_obj_set_style_bg_opa(card, LV_OPA_TRANSP, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_color(card, lv_color_hex(border_color), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_border_opa(card, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_border_post(card, true, LV_PART_MAIN | LV_STATE_DEFAULT); return card; } @@ -224,8 +225,6 @@ void UILaunchPage::create_app_container(lv_obj_t *parent) lv_obj_delete(container); return; } - lv_obj_set_style_border_width(elements[kCardCenter], 2, LV_PART_MAIN | LV_STATE_DEFAULT); - *carousel_alive_ = false; if (carousel_container_) lv_obj_delete(carousel_container_); carousel_alive_ = std::make_shared(true); diff --git a/projects/APPLaunch/tests/run_tests.sh b/projects/APPLaunch/tests/run_tests.sh index 181e41e0..eb273518 100755 --- a/projects/APPLaunch/tests/run_tests.sh +++ b/projects/APPLaunch/tests/run_tests.sh @@ -2,6 +2,7 @@ set -eu python3 "$(dirname "$0")/test_store_cache_sync.py" python3 "$(dirname "$0")/test_wifi_view_lifecycle_contract.py" +python3 "$(dirname "$0")/test_carousel_border_contract.py" python3 "$(dirname "$0")/test_bluetooth_power_contract.py" python3 "$(dirname "$0")/test_external_framebuffer_ownership.py" PYTHONPATH="$(dirname "$0")/..${PYTHONPATH:+:$PYTHONPATH}" \ diff --git a/projects/APPLaunch/tests/test_carousel_border_contract.py b/projects/APPLaunch/tests/test_carousel_border_contract.py new file mode 100644 index 00000000..4443ca40 --- /dev/null +++ b/projects/APPLaunch/tests/test_carousel_border_contract.py @@ -0,0 +1,20 @@ +from pathlib import Path + + +SOURCE = ( + Path(__file__).resolve().parents[1] + / "main/ui/ui_launch_page_carousel_view.cpp" +).read_text(encoding="utf-8") + + +def test_every_carousel_card_keeps_the_theme_border(): + start = SOURCE.index("lv_obj_t *create_carousel_card") + end = SOURCE.index("lv_obj_t *create_carousel_title", start) + factory = SOURCE[start:end] + assert "lv_obj_set_style_border_width(card," not in factory + assert "lv_obj_set_style_border_width(elements[kCardCenter]" not in SOURCE + assert "lv_obj_set_style_border_post(card, true," in factory + + +if __name__ == "__main__": + test_every_carousel_card_keeps_the_theme_border() From 3fa26d9a781676718838b107df9d33252ae3f8ba Mon Sep 17 00:00:00 2001 From: Merge Simulation Date: Fri, 14 Aug 2026 10:08:32 +0800 Subject: [PATCH 5/9] fix(applaunch): simplify launcher update flow --- .../main/ui/model/system_page_model.cpp | 23 ++----------- .../main/ui/model/system_page_model.hpp | 10 +----- .../main/ui/page_app/setting/system.cpp | 32 +++++-------------- .../main/ui/page_app/setting/system.hpp | 1 - .../main/ui/page_app/ui_app_setup.hpp | 1 - .../tests/test_system_page_model.cpp | 22 ++++--------- 6 files changed, 18 insertions(+), 71 deletions(-) diff --git a/projects/APPLaunch/main/ui/model/system_page_model.cpp b/projects/APPLaunch/main/ui/model/system_page_model.cpp index e970f592..573a0330 100644 --- a/projects/APPLaunch/main/ui/model/system_page_model.cpp +++ b/projects/APPLaunch/main/ui/model/system_page_model.cpp @@ -42,27 +42,10 @@ std::string build_label(const std::string &date, const std::string &channel, (commit.empty() ? std::string("unknown") : commit) + ")"; } -const char *update_request(UpdateAction action) -{ - switch (action) { - case UpdateAction::CheckSystem: - return "AptUpdateStart"; - case UpdateAction::UpdateLauncher: - return "UpdateLauncherStart"; - } - return ""; -} - -std::string update_job_label(UpdateAction action, int result_code, - const std::string &state) +std::string update_job_label(int result_code, const std::string &state) { if (state == "running") - return action == UpdateAction::CheckSystem - ? "Refreshing package lists..." - : "Checking launcher update...\nLauncher may restart"; - if (action == UpdateAction::CheckSystem) - return result_code == 0 && state.rfind("succeeded:", 0) == 0 - ? "Package lists refreshed" : "System check failed"; + return "Checking launcher update...\nLauncher may restart"; if (result_code == 0 && state.rfind("succeeded:", 0) == 0) return "Launcher updated"; @@ -86,7 +69,7 @@ std::string launcher_state_label(const std::string &state) if (state.empty()) return {}; if (state.rfind("succeeded:", 0) == 0) return "Launcher is up to date"; if (state.rfind("failed:", 0) == 0) - return update_job_label(UpdateAction::UpdateLauncher, -1, state); + return update_job_label(-1, state); return "Updating: " + state; } diff --git a/projects/APPLaunch/main/ui/model/system_page_model.hpp b/projects/APPLaunch/main/ui/model/system_page_model.hpp index 89ea6217..07a2e9f4 100644 --- a/projects/APPLaunch/main/ui/model/system_page_model.hpp +++ b/projects/APPLaunch/main/ui/model/system_page_model.hpp @@ -26,20 +26,12 @@ struct AccountInfo std::string hostname; }; -enum class UpdateAction -{ - CheckSystem, - UpdateLauncher, -}; - NetworkInfo parse_network_info(const std::string &payload); AccountInfo parse_account_info(const std::string &payload); std::string version_label(const std::string &version); std::string build_label(const std::string &date, const std::string &channel, const std::string &commit); -const char *update_request(UpdateAction action); -std::string update_job_label(UpdateAction action, int result_code, - const std::string &state); +std::string update_job_label(int result_code, const std::string &state); std::string launcher_state_label(const std::string &state); bool extport_toggle_value(bool previous, bool desired, bool gpio_succeeded); diff --git a/projects/APPLaunch/main/ui/page_app/setting/system.cpp b/projects/APPLaunch/main/ui/page_app/setting/system.cpp index b7d64853..1d4b2eec 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/system.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/system.cpp @@ -245,7 +245,6 @@ void Update::append(UISetupPage &page, std::vector &menu) MenuItem item; item.label = "Update"; item.sub_items = { - {"Check System", false, false, [page_ptr]() { Update::check_system_update(*page_ptr); }}, {"Update Launcher", false, false, [page_ptr]() { Update::update_launcher(*page_ptr); }}, {"Version: --", false, false, nullptr}, {"Build: --", false, false, nullptr}, @@ -257,26 +256,21 @@ void Update::append(UISetupPage &page, std::vector &menu) void Update::refresh_version_info(UISetupPage &page) { MenuItem *item = SetupPageAccess(page).find_menu("Update"); - if (item && item->sub_items.size() >= 4) { + if (item && item->sub_items.size() >= 3) { cp0_signal_osinfo_api({"UpdateLauncherState"}, [&](int code, std::string state) { if (code != 0) return; const std::string label = system_page::launcher_state_label(state); - if (!label.empty()) item->sub_items[1].label = label; + if (!label.empty()) item->sub_items[0].label = label; }); - item->sub_items[2].label = system_page::version_label(LAUNCHER_VERSION); - item->sub_items[3].label = system_page::build_label( + item->sub_items[1].label = system_page::version_label(LAUNCHER_VERSION); + item->sub_items[2].label = system_page::build_label( LAUNCHER_BUILD_DATE, LAUNCHER_CHANNEL, LAUNCHER_GIT_COMMIT); } } -void Update::check_system_update(UISetupPage &page) -{ - page.start_update_job("AptUpdateStart", 0); -} - void Update::update_launcher(UISetupPage &page) { - page.start_update_job("UpdateLauncherStart", 1); + page.start_update_job("UpdateLauncherStart", 0); } } // namespace setting @@ -289,7 +283,6 @@ void UISetupPage::stop_update_timer(bool cancel_job) if (cancel_job && !update_job_id_.empty()) cp0_signal_osinfo_api({"UpdateJobCancel", update_job_id_}, nullptr); update_job_id_.clear(); - update_item_index_ = -1; if (had_job) launcher_toast().hide(); } @@ -298,10 +291,7 @@ void UISetupPage::start_update_job(const char *command, int item_index) if (update_timer_ || !command) return; MenuItem *menu = setting::SetupPageAccess(*this).find_menu("Update"); if (!menu || item_index < 0 || item_index >= static_cast(menu->sub_items.size())) return; - const auto action = item_index == 0 - ? system_page::UpdateAction::CheckSystem - : system_page::UpdateAction::UpdateLauncher; - const std::string running_label = system_page::update_job_label(action, 0, "running"); + const std::string running_label = system_page::update_job_label(0, "running"); launcher_toast().show_persistent(running_label.c_str()); lv_refr_now(nullptr); try { @@ -312,11 +302,9 @@ void UISetupPage::start_update_job(const char *command, int item_index) update_job_id_.clear(); } if (update_job_id_.empty()) { - launcher_toast().show(item_index == 0 - ? "System check unavailable" : "Launcher update unavailable"); + launcher_toast().show("Launcher update unavailable"); return; } - update_item_index_ = item_index; update_timer_ = lv_timer_create(update_timer_cb, 500, this); if (!update_timer_) { stop_update_timer(); @@ -334,11 +322,7 @@ void UISetupPage::update_timer_cb(lv_timer_t *timer) noexcept cp0_signal_osinfo_api({"UpdateJobStatus", page->update_job_id_}, [&](int result, std::string payload) { code = result; state = std::move(payload); }); if (code == 0 && state == "running") return; - const auto action = page->update_item_index_ == 0 - ? system_page::UpdateAction::CheckSystem - : system_page::UpdateAction::UpdateLauncher; - const std::string result_label = - system_page::update_job_label(action, code, state); + const std::string result_label = system_page::update_job_label(code, state); page->stop_update_timer(false); launcher_toast().show(result_label.c_str()); } catch (...) { diff --git a/projects/APPLaunch/main/ui/page_app/setting/system.hpp b/projects/APPLaunch/main/ui/page_app/setting/system.hpp index ba72e221..1fdd4e25 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/system.hpp +++ b/projects/APPLaunch/main/ui/page_app/setting/system.hpp @@ -50,7 +50,6 @@ class Update public: static void append(UISetupPage &page, std::vector &menu); static void refresh_version_info(UISetupPage &page); - static void check_system_update(UISetupPage &page); static void update_launcher(UISetupPage &page); }; diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp index 975c196c..1caaf34a 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup.hpp @@ -125,7 +125,6 @@ class UISetupPage : public AppPage lv_timer_t *update_timer_ = nullptr; lv_timer_t *volume_preview_timer_ = nullptr; std::string update_job_id_; - int update_item_index_ = -1; static constexpr int SCREEN_W = 320; static constexpr int SCREEN_H = 150; diff --git a/projects/APPLaunch/tests/test_system_page_model.cpp b/projects/APPLaunch/tests/test_system_page_model.cpp index 5c2573c5..7c239668 100644 --- a/projects/APPLaunch/tests/test_system_page_model.cpp +++ b/projects/APPLaunch/tests/test_system_page_model.cpp @@ -35,23 +35,13 @@ int main() assert(version_label("") == "Version: --"); assert(build_label("2026-07-28", "stable", "abc123") == "Build: 2026-07-28 stable (abc123)"); - assert(std::string(update_request(UpdateAction::CheckSystem)) == "AptUpdateStart"); - assert(std::string(update_request(UpdateAction::UpdateLauncher)) == - "UpdateLauncherStart"); - assert(update_job_label(UpdateAction::CheckSystem, 0, "running") == - "Refreshing package lists..."); - assert(update_job_label(UpdateAction::UpdateLauncher, 0, "running") == + assert(update_job_label(0, "running") == "Checking launcher update...\nLauncher may restart"); - assert(update_job_label(UpdateAction::CheckSystem, 0, "succeeded:completed") == - "Package lists refreshed"); - assert(update_job_label(UpdateAction::CheckSystem, 1, "failed:apt-update:1") == - "System check failed"); - assert(update_job_label(UpdateAction::UpdateLauncher, 1, - "failed:incompatible:1") == "No compatible update"); - assert(update_job_label(UpdateAction::UpdateLauncher, 1, - "failed:version-not-newer:1") == "Launcher is up to date"); - assert(update_job_label(UpdateAction::UpdateLauncher, 1, - "failed:download-package:1") == "Update download failed"); + assert(update_job_label(1, "failed:incompatible:1") == "No compatible update"); + assert(update_job_label(1, "failed:version-not-newer:1") == + "Launcher is up to date"); + assert(update_job_label(1, "failed:download-package:1") == + "Update download failed"); assert(launcher_state_label("downloading") == "Updating: downloading"); assert(launcher_state_label("succeeded:0.6.32") == "Launcher is up to date"); assert(launcher_state_label("failed:incompatible") == "No compatible update"); From be9bbc7f7fdaf80650dd30590ce57fe2f7d21d1d Mon Sep 17 00:00:00 2001 From: Merge Simulation Date: Tue, 18 Aug 2026 09:14:57 +0800 Subject: [PATCH 6/9] fix(applaunch): complete launcher update flow --- .../main/ui/model/setup_page_model.cpp | 7 ++- .../main/ui/model/setup_page_model.hpp | 3 +- .../main/ui/page_app/setting/menu_types.hpp | 1 + .../main/ui/page_app/setting/system.cpp | 1 + .../main/ui/page_app/ui_app_setup_input.cpp | 2 +- .../APPLaunch/tests/test_setup_page_model.cpp | 4 ++ .../APPLaunch/tests/test_updater_packaging.py | 47 +++++++++++++++++-- scripts/debian_packager.py | 11 ++++- 8 files changed, 67 insertions(+), 9 deletions(-) diff --git a/projects/APPLaunch/main/ui/model/setup_page_model.cpp b/projects/APPLaunch/main/ui/model/setup_page_model.cpp index 11af6f0f..1863b5a1 100644 --- a/projects/APPLaunch/main/ui/model/setup_page_model.cpp +++ b/projects/APPLaunch/main/ui/model/setup_page_model.cpp @@ -66,10 +66,13 @@ bool SetupPageModel::move_main(int direction, int item_count) return true; } -void SetupPageModel::enter_sub(int item_count, int center_row) +void SetupPageModel::enter_sub(int item_count, int center_row, int preferred_index) { view = SetupViewState::SUB; - sub_selected_index = clamp_index(std::min(center_row, item_count - 1), item_count); + if (preferred_index >= 0) + sub_selected_index = clamp_index(preferred_index, item_count); + else + sub_selected_index = clamp_index(std::min(center_row, item_count - 1), item_count); } bool SetupPageModel::move_sub(int direction, int item_count) diff --git a/projects/APPLaunch/main/ui/model/setup_page_model.hpp b/projects/APPLaunch/main/ui/model/setup_page_model.hpp index ecd6e100..ca067284 100644 --- a/projects/APPLaunch/main/ui/model/setup_page_model.hpp +++ b/projects/APPLaunch/main/ui/model/setup_page_model.hpp @@ -84,7 +84,8 @@ class SetupPageModel std::string value_title; bool move_main(int direction, int item_count); - void enter_sub(int item_count, int center_row = DEFAULT_CENTER_ROW); + void enter_sub(int item_count, int center_row = DEFAULT_CENTER_ROW, + int preferred_index = -1); bool move_sub(int direction, int item_count); void select_sub(int index, int item_count); void enter_value(std::string title, std::vector options, int selected_index); diff --git a/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp b/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp index 35558d13..d6b27dd0 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp +++ b/projects/APPLaunch/main/ui/page_app/setting/menu_types.hpp @@ -21,6 +21,7 @@ struct MenuItem std::vector sub_items; std::function on_enter; std::function custom_key_handler; + int default_sub_selection = -1; }; } // namespace setting diff --git a/projects/APPLaunch/main/ui/page_app/setting/system.cpp b/projects/APPLaunch/main/ui/page_app/setting/system.cpp index 1d4b2eec..65fc663b 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/system.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/system.cpp @@ -249,6 +249,7 @@ void Update::append(UISetupPage &page, std::vector &menu) {"Version: --", false, false, nullptr}, {"Build: --", false, false, nullptr}, }; + item.default_sub_selection = 0; item.on_enter = [page_ptr]() { Update::refresh_version_info(*page_ptr); }; menu.push_back(item); } diff --git a/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp b/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp index 4c7ad7fd..ccac3fca 100644 --- a/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp +++ b/projects/APPLaunch/main/ui/page_app/ui_app_setup_input.cpp @@ -202,7 +202,7 @@ void UISetupPage::handle_main_key(uint32_t key) if (item.on_enter) item.on_enter(); if (!item.sub_items.empty()) { int sub_count = static_cast(item.sub_items.size()); - model_.enter_sub(sub_count, ROW_CENTER); + model_.enter_sub(sub_count, ROW_CENTER, item.default_sub_selection); build_sub_view(); } break; diff --git a/projects/APPLaunch/tests/test_setup_page_model.cpp b/projects/APPLaunch/tests/test_setup_page_model.cpp index ce644380..d53f1661 100644 --- a/projects/APPLaunch/tests/test_setup_page_model.cpp +++ b/projects/APPLaunch/tests/test_setup_page_model.cpp @@ -108,6 +108,10 @@ int main() assert(model.sub_selected_index == 0); assert(model.move_sub(-1, 2)); assert(model.sub_selected_index == 1); + model.enter_sub(2, SetupPageModel::DEFAULT_CENTER_ROW, 0); + assert(model.sub_selected_index == 0); + model.enter_sub(2, SetupPageModel::DEFAULT_CENTER_ROW, 99); + assert(model.sub_selected_index == 1); model.select_sub(0, 2); assert(model.sub_selected_index == 0); model.select_sub(99, 2); diff --git a/projects/APPLaunch/tests/test_updater_packaging.py b/projects/APPLaunch/tests/test_updater_packaging.py index 7e368e0d..e3d19c93 100755 --- a/projects/APPLaunch/tests/test_updater_packaging.py +++ b/projects/APPLaunch/tests/test_updater_packaging.py @@ -29,7 +29,12 @@ def run_update(fail_install: bool = False, unhealthy_process: bool = False, audit_required: bool = False, interrupted_candidate: bool = False, - compatible: bool = True) -> tuple[subprocess.CompletedProcess[str], str, str]: + compatible: bool = True, + candidate_version: str | None = None, + installed_version: str | None = None, + rollback_version: str | None = None, + after_version: str | None = None, + compare_exit: int = 0) -> tuple[subprocess.CompletedProcess[str], str, str]: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) fake_bin = root / "bin" @@ -60,6 +65,8 @@ def run_update(fail_install: bool = False, "1\n" if compatible else "0\n", encoding="ascii" ) if provide_rollback: + cache.mkdir() + (cache / "installed.deb").write_bytes(b"trusted rollback package") old_package = old_release / "applaunch_1.0_arm64.deb" old_package.write_bytes(b"trusted previous package") old_digest = hashlib.sha256(old_package.read_bytes()).hexdigest() @@ -73,17 +80,20 @@ def run_update(fail_install: bool = False, fake_bin / "dpkg-deb", 'case "$3" in Package) echo applaunch;; Architecture) echo arm64;; ' 'X-CardputerZero-Update-ABI) echo "${TEST_UPDATE_ABI:-1}";; ' - 'Version) case "$2" in *installed.deb|*rollback.deb) echo 1.0;; *) echo 2.0;; esac;; esac\n', + 'Version) case "$2" in *installed.deb|*rollback.deb) ' + 'echo "${TEST_ROLLBACK_VERSION:-1.0}";; *) ' + 'echo "${TEST_CANDIDATE_VERSION:-2.0}";; esac;; esac\n', ) executable( fake_bin / "dpkg-query", 'case "$2" in *Status*) echo installed;; *) ' - 'if [ -f "$TEST_MARKER" ]; then echo 2.0; else echo 1.0; fi;; esac\n', + 'if [ -f "$TEST_MARKER" ]; then echo "${TEST_AFTER_VERSION:-2.0}"; ' + 'else echo "${TEST_INSTALLED_VERSION:-1.0}"; fi;; esac\n', ) executable( fake_bin / "dpkg", 'echo "dpkg $*" >>"$TEST_LOG"\n' - 'case "$1" in --compare-versions) exit 0;; --audit) ' + 'case "$1" in --compare-versions) exit "${TEST_COMPARE_EXIT:-0}";; --audit) ' '[ ! -f "$TEST_AUDIT_MARKER" ] || echo "package needs configuration";; --configure) ' '[ "${TEST_FAIL_CONFIGURE:-0}" != 1 ] || exit 44; ' '[ "${TEST_KEEP_AUDIT_DIRTY:-0}" = 1 ] || rm -f "$TEST_AUDIT_MARKER";; -i) ' @@ -114,6 +124,11 @@ def run_update(fail_install: bool = False, "TEST_FAIL_CONFIGURE": "1" if fail_configure else "0", "TEST_KEEP_AUDIT_DIRTY": "1" if keep_audit_dirty else "0", "TEST_AUDIT_MARKER": str(root / "audit.marker"), + "TEST_CANDIDATE_VERSION": candidate_version or "", + "TEST_INSTALLED_VERSION": installed_version or "", + "TEST_ROLLBACK_VERSION": rollback_version or "", + "TEST_AFTER_VERSION": after_version or "", + "TEST_COMPARE_EXIT": str(compare_exit), "APPLAUNCH_UPDATER_REEXEC": "1", "APPLAUNCH_UPDATE_EXECUTABLE": str(app_executable), "APPLAUNCH_UPDATE_PROC_ROOT": str(proc), @@ -185,6 +200,30 @@ def run_update(fail_install: bool = False, assert missing_rollback.returncode != 0 assert status == "failed:rollback-unavailable" +local_to_cloud, status, commands = run_update( + candidate_version="2.0", + installed_version="3.0+local.test1", + rollback_version="3.0+local.test1", + after_version="2.0", + compare_exit=1, +) +assert local_to_cloud.returncode == 0, ( + local_to_cloud.stderr, local_to_cloud.stdout, status, commands +) +assert status == "succeeded:2.0" +assert "dpkg -i" in commands + +official_downgrade, status, commands = run_update( + candidate_version="2.0", + installed_version="3.0", + rollback_version="3.0", + after_version="2.0", + compare_exit=1, +) +assert official_downgrade.returncode != 0 +assert status == "failed:version-not-newer" +assert "dpkg -i" not in commands + incompatible, status, commands = run_update(compatible=False) assert incompatible.returncode != 0 assert status == "failed:incompatible" diff --git a/scripts/debian_packager.py b/scripts/debian_packager.py index d24b380f..f7d5c9db 100755 --- a/scripts/debian_packager.py +++ b/scripts/debian_packager.py @@ -551,7 +551,16 @@ def _updater_script_text() -> str: ;; esac fi -dpkg --compare-versions "$candidate" gt "$installed" || fail version-not-newer + +# Local verification builds use +local./+git. suffixes and can be newer than the +# current cloud release. Allow them to return to the official channel; regular +# official packages still cannot downgrade. +if ! dpkg --compare-versions "$candidate" ge "$installed"; then + case "$installed" in + *+local.*|*+git.*) ;; + *) fail version-not-newer ;; + esac +fi # Retain the last trusted package so a later upgrade can roll back after a # failed install or service health check. From 4debee03bc0e311624287641919ab2fba599e5d5 Mon Sep 17 00:00:00 2001 From: Merge Simulation Date: Thu, 20 Aug 2026 17:32:56 +0800 Subject: [PATCH 7/9] merge: sync launcher with origin master add ZClaw fix --- projects/ZClaw/main/ui/zclaw_app.cpp | 1 + projects/ZClaw/main/ui/zclaw_input_dialog.cpp | 5 +++-- projects/ZClaw/main/ui/zclaw_input_model.cpp | 5 ++++- projects/ZClaw/main/ui/zclaw_input_model.h | 2 ++ .../ZClaw/main/ui/zclaw_key_event_adapter.cpp | 1 + projects/ZClaw/main/ui/zclaw_key_router.cpp | 4 ++++ projects/ZClaw/main/ui/zclaw_key_router.h | 3 +++ .../ZClaw/main/ui/zclaw_provider_workflow.cpp | 17 +++++++---------- projects/ZClaw/main/ui/zclaw_setup_workflow.cpp | 17 +++++++---------- .../ZClaw/tests/zclaw_input_routing_test.cpp | 13 +++++++++++++ 10 files changed, 45 insertions(+), 23 deletions(-) diff --git a/projects/ZClaw/main/ui/zclaw_app.cpp b/projects/ZClaw/main/ui/zclaw_app.cpp index ab97e8a0..537a7dee 100644 --- a/projects/ZClaw/main/ui/zclaw_app.cpp +++ b/projects/ZClaw/main/ui/zclaw_app.cpp @@ -160,6 +160,7 @@ class ZClawApp : public AppPageRoot zclaw::KeyRouteContext context; context.startup = startup_workflow_.state(); context.input_open = input_dialog_.is_open(); + context.input_mode = input_dialog_.mode(); context.approval_pending = approvals_.pending(); context.setup_retry_pending = settings_workflow_.setup_retry_pending(); diff --git a/projects/ZClaw/main/ui/zclaw_input_dialog.cpp b/projects/ZClaw/main/ui/zclaw_input_dialog.cpp index 575005c6..d78480eb 100644 --- a/projects/ZClaw/main/ui/zclaw_input_dialog.cpp +++ b/projects/ZClaw/main/ui/zclaw_input_dialog.cpp @@ -7,7 +7,8 @@ namespace zclaw { namespace { -constexpr lv_coord_t kDialogHeight = 170 * 2 / 3; +constexpr lv_coord_t kDialogHeight = 104; +constexpr lv_coord_t kDialogBottomInset = 4; } // namespace @@ -144,7 +145,7 @@ void InputDialog::open(const FontManager *fonts) dialog_ = lv_msgbox_create(lv_layer_top()); lv_obj_add_event_cb(dialog_, dialog_deleted, LV_EVENT_DELETE, this); lv_obj_set_size(dialog_, 300, kDialogHeight); - lv_obj_align(dialog_, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_align(dialog_, LV_ALIGN_BOTTOM_MID, 0, -kDialogBottomInset); lv_obj_set_style_radius(dialog_, 8, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(dialog_, lv_color_hex(theme::kBar), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(dialog_, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT); diff --git a/projects/ZClaw/main/ui/zclaw_input_model.cpp b/projects/ZClaw/main/ui/zclaw_input_model.cpp index af916073..bfa3fae9 100644 --- a/projects/ZClaw/main/ui/zclaw_input_model.cpp +++ b/projects/ZClaw/main/ui/zclaw_input_model.cpp @@ -6,7 +6,8 @@ namespace zclaw { bool input_is_single_line(InputMode mode) { - return mode != InputMode::Chat; + return mode != InputMode::Chat && mode != InputMode::SetupUriEdit && + mode != InputMode::ProviderUriEdit; } InputSubmission input_submission(InputMode mode, std::string value) @@ -19,9 +20,11 @@ InputSubmission input_submission(InputMode mode, std::string value) : InputSubmissionAction::SendChat; break; case InputMode::SetupEdit: + case InputMode::SetupUriEdit: submission.action = InputSubmissionAction::ApplySetupEdit; break; case InputMode::ProviderEdit: + case InputMode::ProviderUriEdit: submission.action = InputSubmissionAction::ApplyProviderEdit; break; case InputMode::PairingCode: diff --git a/projects/ZClaw/main/ui/zclaw_input_model.h b/projects/ZClaw/main/ui/zclaw_input_model.h index a6f23491..f891facc 100644 --- a/projects/ZClaw/main/ui/zclaw_input_model.h +++ b/projects/ZClaw/main/ui/zclaw_input_model.h @@ -7,7 +7,9 @@ namespace zclaw { enum class InputMode { Chat, SetupEdit, + SetupUriEdit, ProviderEdit, + ProviderUriEdit, PairingCode, }; diff --git a/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp b/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp index 6d04fdd4..d1f5cfb0 100644 --- a/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp +++ b/projects/ZClaw/main/ui/zclaw_key_event_adapter.cpp @@ -49,6 +49,7 @@ KeyEvent adapt_key_event(std::uint32_t key_code, int key_state, event.phase = adapt_key_phase(key_state); event.key = adapt_key(key_code); event.shift = (modifiers & KBD_MOD_SHIFT) != 0; + event.fn = (modifiers & KBD_MOD_FN) != 0; if (utf8) event.text = utf8; return event; diff --git a/projects/ZClaw/main/ui/zclaw_key_router.cpp b/projects/ZClaw/main/ui/zclaw_key_router.cpp index b913567b..6738a6f9 100644 --- a/projects/ZClaw/main/ui/zclaw_key_router.cpp +++ b/projects/ZClaw/main/ui/zclaw_key_router.cpp @@ -62,6 +62,10 @@ KeyAction route_key(const KeyRouteContext &context, const KeyEvent &event) if (context.input_open) { if (event.key == Key::Escape) return {KeyActionType::InputClose, {}}; + if (event.key == Key::Tab && + (context.input_mode == InputMode::SetupUriEdit || + context.input_mode == InputMode::ProviderUriEdit)) + return {KeyActionType::InputInsertNewline, {}}; if (event.key == Key::Tab) return {KeyActionType::InputToggleSecretVisibility, {}}; if (event.key == Key::Enter && !event.shift) diff --git a/projects/ZClaw/main/ui/zclaw_key_router.h b/projects/ZClaw/main/ui/zclaw_key_router.h index 8bf2a525..7842183f 100644 --- a/projects/ZClaw/main/ui/zclaw_key_router.h +++ b/projects/ZClaw/main/ui/zclaw_key_router.h @@ -1,5 +1,6 @@ #pragma once +#include "zclaw_input_model.h" #include "zclaw_settings_navigation_model.h" #include "zclaw_startup_model.h" @@ -39,11 +40,13 @@ struct KeyEvent { Key key = Key::Other; bool shift = false; std::string text; + bool fn = false; }; struct KeyRouteContext { StartupState startup = StartupState::CheckingNetwork; bool input_open = false; + InputMode input_mode = InputMode::Chat; bool approval_pending = false; bool setup_retry_pending = false; bool setup_in_flight = false; diff --git a/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp b/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp index 0f0ebe7d..46a92162 100644 --- a/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp +++ b/projects/ZClaw/main/ui/zclaw_provider_workflow.cpp @@ -7,7 +7,6 @@ #include "zclaw_provider_manager.h" #include "zclaw_settings_coordinator.h" #include "zclaw_settings_panel.h" -#include "zclaw_secret_input_model.h" #include @@ -73,12 +72,13 @@ void ProviderWorkflow::edit_selected_field() const std::string *value = provider_field_value( &provider, settings_.state().provider_edit_field()); if (value) { - const bool secret = - settings_.state().provider_edit_field() == ProviderEditField::ApiKey; + const bool uri = + settings_.state().provider_edit_field() == ProviderEditField::Uri; input_.open_text(&fonts_, provider_field_name(settings_.state().provider_edit_field()), - secret ? secret_input_initial_text() : *value, - InputMode::ProviderEdit, secret); + *value, + uri ? InputMode::ProviderUriEdit : InputMode::ProviderEdit, + false); } } @@ -93,11 +93,8 @@ void ProviderWorkflow::apply_edit(const std::string &value) ProviderConfig provider = providers_.providers()[provider_index]; std::string *field = provider_field_value( &provider, settings_.state().provider_edit_field()); - if (field) { - *field = settings_.state().provider_edit_field() == ProviderEditField::ApiKey - ? apply_secret_input(*field, value) - : value; - } + if (field) + *field = value; settings_.state().set_provider_edit_field(ProviderEditField::None); std::string error; if (!providers_.replace(static_cast(provider_index), provider, diff --git a/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp b/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp index e8b82ee1..de1e39b8 100644 --- a/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp +++ b/projects/ZClaw/main/ui/zclaw_setup_workflow.cpp @@ -7,7 +7,6 @@ #include "zclaw_provider_catalog.h" #include "zclaw_provider_manager.h" #include "zclaw_settings_coordinator.h" -#include "zclaw_secret_input_model.h" #include "zclaw_ui_config_manager.h" #include @@ -86,12 +85,13 @@ void SetupWorkflow::edit_selected_field() const std::string *value = setup_field_value(&provider, settings_.state().setup_edit_field()); if (value) { - const bool secret = - settings_.state().setup_edit_field() == SetupEditField::ApiKey; + const bool uri = + settings_.state().setup_edit_field() == SetupEditField::Uri; input_.open_text(&fonts_, setup_field_name(settings_.state().setup_edit_field()), - secret ? secret_input_initial_text() : *value, - InputMode::SetupEdit, secret); + *value, + uri ? InputMode::SetupUriEdit : InputMode::SetupEdit, + false); } } @@ -100,11 +100,8 @@ void SetupWorkflow::apply_edit(const std::string &value) ProviderConfig provider = providers_.setup_provider(); std::string *field = setup_field_value(&provider, settings_.state().setup_edit_field()); - if (field) { - *field = settings_.state().setup_edit_field() == SetupEditField::ApiKey - ? apply_secret_input(*field, value) - : value; - } + if (field) + *field = value; settings_.state().set_setup_edit_field(SetupEditField::None); std::string error; if (!providers_.update_setup_provider(provider, &error)) diff --git a/projects/ZClaw/tests/zclaw_input_routing_test.cpp b/projects/ZClaw/tests/zclaw_input_routing_test.cpp index 67dcad7c..f017f9c6 100644 --- a/projects/ZClaw/tests/zclaw_input_routing_test.cpp +++ b/projects/ZClaw/tests/zclaw_input_routing_test.cpp @@ -15,7 +15,9 @@ int main() using zclaw::InputSubmissionAction; assert(!zclaw::input_is_single_line(InputMode::Chat)); assert(zclaw::input_is_single_line(InputMode::SetupEdit)); + assert(!zclaw::input_is_single_line(InputMode::SetupUriEdit)); assert(zclaw::input_is_single_line(InputMode::ProviderEdit)); + assert(!zclaw::input_is_single_line(InputMode::ProviderUriEdit)); assert(zclaw::input_is_single_line(InputMode::PairingCode)); zclaw::InputSubmission submission = zclaw::input_submission(InputMode::Chat, "hello"); @@ -29,8 +31,12 @@ int main() InputSubmissionAction::None); assert(zclaw::input_submission(InputMode::SetupEdit, "").action == InputSubmissionAction::ApplySetupEdit); + assert(zclaw::input_submission(InputMode::SetupUriEdit, "").action == + InputSubmissionAction::ApplySetupEdit); assert(zclaw::input_submission(InputMode::ProviderEdit, "").action == InputSubmissionAction::ApplyProviderEdit); + assert(zclaw::input_submission(InputMode::ProviderUriEdit, "").action == + InputSubmissionAction::ApplyProviderEdit); using zclaw::Key; using zclaw::KeyActionType; @@ -51,6 +57,8 @@ int main() assert(!adapted.shift && adapted.text.empty()); adapted = zclaw::adapt_key_event(KEY_ESC, KBD_KEY_RELEASED, 0, ""); assert(adapted.key == Key::Escape && adapted.phase == KeyPhase::Released); + adapted = zclaw::adapt_key_event(KEY_UP, KBD_KEY_PRESSED, KBD_MOD_FN, nullptr); + assert(adapted.key == Key::Up && adapted.fn); adapted = zclaw::adapt_key_event(KEY_RESERVED, 99, KBD_MOD_CTRL, "x"); assert(adapted.key == Key::Other && adapted.phase == KeyPhase::Unknown); assert(!adapted.shift && adapted.text == "x"); @@ -100,6 +108,11 @@ int main() KeyActionType::InputSubmit); assert(routed(context, KeyPhase::Released, Key::Tab).type == KeyActionType::InputToggleSecretVisibility); + context.input_mode = InputMode::ProviderUriEdit; + assert(routed(context, KeyPhase::Released, Key::Tab).type == + KeyActionType::InputInsertNewline); + assert(routed(context, KeyPhase::Released, Key::Escape).type == + KeyActionType::InputClose); assert(routed(context, KeyPhase::Released, Key::Enter, true).type == KeyActionType::None); assert(routed(context, KeyPhase::Released, Key::Down).type == From b2bb44dffaf2185d36d36f313c0c503621a44218 Mon Sep 17 00:00:00 2001 From: Merge Simulation Date: Fri, 21 Aug 2026 19:25:37 +0800 Subject: [PATCH 8/9] Fix ZCLAW display issue, remove check system from settings ->update --- .../main/ui/model/system_page_model.cpp | 16 ++++++++++++++-- .../main/ui/model/system_page_model.hpp | 10 +++++++++- .../main/ui/page_app/setting/menu_builder.cpp | 1 - projects/AppStore | 2 +- projects/CameraApp/main_CameraApp | 2 +- projects/FactoryTest/main_FactoryTest | 2 +- projects/ZClaw/main/ui/zclaw_input_dialog.cpp | 7 ++++--- 7 files changed, 30 insertions(+), 10 deletions(-) diff --git a/projects/APPLaunch/main/ui/model/system_page_model.cpp b/projects/APPLaunch/main/ui/model/system_page_model.cpp index 435d7b87..4402898d 100644 --- a/projects/APPLaunch/main/ui/model/system_page_model.cpp +++ b/projects/APPLaunch/main/ui/model/system_page_model.cpp @@ -42,7 +42,19 @@ std::string build_label(const std::string &date, const std::string &channel, (commit.empty() ? std::string("unknown") : commit) + ")"; } -std::string update_job_label(int result_code, const std::string &state) +const char *update_request(UpdateAction action) +{ + switch (action) { + case UpdateAction::CheckSystem: + return "AptUpdateStart"; + case UpdateAction::UpdateLauncher: + return "UpdateLauncherStart"; + } + return ""; +} + +std::string update_job_label(UpdateAction action, int result_code, + const std::string &state) { if (state == "running") return action == UpdateAction::CheckSystem @@ -75,7 +87,7 @@ std::string launcher_state_label(const std::string &state) if (state.empty()) return {}; if (state.rfind("succeeded:", 0) == 0) return "Launcher is up to date"; if (state.rfind("failed:", 0) == 0) - return update_job_label(-1, state); + return update_job_label(UpdateAction::UpdateLauncher, -1, state); return "Updating: " + state; } diff --git a/projects/APPLaunch/main/ui/model/system_page_model.hpp b/projects/APPLaunch/main/ui/model/system_page_model.hpp index 07a2e9f4..89ea6217 100644 --- a/projects/APPLaunch/main/ui/model/system_page_model.hpp +++ b/projects/APPLaunch/main/ui/model/system_page_model.hpp @@ -26,12 +26,20 @@ struct AccountInfo std::string hostname; }; +enum class UpdateAction +{ + CheckSystem, + UpdateLauncher, +}; + NetworkInfo parse_network_info(const std::string &payload); AccountInfo parse_account_info(const std::string &payload); std::string version_label(const std::string &version); std::string build_label(const std::string &date, const std::string &channel, const std::string &commit); -std::string update_job_label(int result_code, const std::string &state); +const char *update_request(UpdateAction action); +std::string update_job_label(UpdateAction action, int result_code, + const std::string &state); std::string launcher_state_label(const std::string &state); bool extport_toggle_value(bool previous, bool desired, bool gpio_succeeded); diff --git a/projects/APPLaunch/main/ui/page_app/setting/menu_builder.cpp b/projects/APPLaunch/main/ui/page_app/setting/menu_builder.cpp index b6864524..19b36bb9 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/menu_builder.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/menu_builder.cpp @@ -14,7 +14,6 @@ void build_menu(UISetupPage &page) access.screen().append(page, candidate); access.wifi().append(page, candidate); access.speaker().append(page, candidate); - access.camera().append(page, candidate); access.info().append(page, candidate); About::append(page, candidate); Help::append(page, candidate); diff --git a/projects/AppStore b/projects/AppStore index 155f0e31..65f1225d 160000 --- a/projects/AppStore +++ b/projects/AppStore @@ -1 +1 @@ -Subproject commit 155f0e313a9960ab4d31249c90193ece01baf230 +Subproject commit 65f1225dbfe4db560c73930060eac715a5c2da9d diff --git a/projects/CameraApp/main_CameraApp b/projects/CameraApp/main_CameraApp index 785ac2a3..0e080b92 160000 --- a/projects/CameraApp/main_CameraApp +++ b/projects/CameraApp/main_CameraApp @@ -1 +1 @@ -Subproject commit 785ac2a33adafde93a4af4d5ef89677dc28d1fb7 +Subproject commit 0e080b92a0af56f688a6b9ee29d04a180debc9f3 diff --git a/projects/FactoryTest/main_FactoryTest b/projects/FactoryTest/main_FactoryTest index 14ced93f..f3277963 160000 --- a/projects/FactoryTest/main_FactoryTest +++ b/projects/FactoryTest/main_FactoryTest @@ -1 +1 @@ -Subproject commit 14ced93fa76d146986934e5859d00a6ff72f8b30 +Subproject commit f327796399c017ead5cd812f037c014349d94d90 diff --git a/projects/ZClaw/main/ui/zclaw_input_dialog.cpp b/projects/ZClaw/main/ui/zclaw_input_dialog.cpp index d78480eb..a4ee37bf 100644 --- a/projects/ZClaw/main/ui/zclaw_input_dialog.cpp +++ b/projects/ZClaw/main/ui/zclaw_input_dialog.cpp @@ -7,8 +7,9 @@ namespace zclaw { namespace { -constexpr lv_coord_t kDialogHeight = 104; -constexpr lv_coord_t kDialogBottomInset = 4; +constexpr lv_coord_t kDialogHeight = 96; +// Keep the original 113 px dialog top at y=57 while shortening the bottom. +constexpr lv_coord_t kDialogTop = 57; } // namespace @@ -145,7 +146,7 @@ void InputDialog::open(const FontManager *fonts) dialog_ = lv_msgbox_create(lv_layer_top()); lv_obj_add_event_cb(dialog_, dialog_deleted, LV_EVENT_DELETE, this); lv_obj_set_size(dialog_, 300, kDialogHeight); - lv_obj_align(dialog_, LV_ALIGN_BOTTOM_MID, 0, -kDialogBottomInset); + lv_obj_align(dialog_, LV_ALIGN_TOP_MID, 0, kDialogTop); lv_obj_set_style_radius(dialog_, 8, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_color(dialog_, lv_color_hex(theme::kBar), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(dialog_, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT); From 077a8d0c91dc11798dcf84f81e2064d8e3480f3b Mon Sep 17 00:00:00 2001 From: Merge Simulation Date: Mon, 24 Aug 2026 09:37:21 +0800 Subject: [PATCH 9/9] Fix 'Device Discovered by Other Bluetooth Options' to automatically turn off after a few minutes of' Scan ' --- projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp b/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp index a7dc6454..e306a07c 100644 --- a/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp +++ b/projects/APPLaunch/main/ui/page_app/setting/bluetooth.cpp @@ -448,6 +448,7 @@ void BluetoothUiSession::handle_list_key(UISetupPage &page, uint32_t key) action_operation_.abort(action_token_); action_busy_ = false; SetupPageAccess access(page); + refresh_status(page); access.set_view(SetupViewState::SUB); access.build_sub_view(); break;