From 850c38f808d44014394103310948c8faa5dad1b8 Mon Sep 17 00:00:00 2001 From: jianning773 Date: Thu, 17 Sep 2026 15:09:00 +0800 Subject: [PATCH 1/4] feat(ai-ui): add guided WeChat UI --- .../config/TUYA_T5AI_BOARD_LCD_3.5.config | 2 + .../TUYA_T5AI_BOARD_LCD_3.5_CAMERA.config | 2 + ...TUYA_T5AI_BOARD_LCD_3.5_CAM_PRINTER.config | 2 + .../TUYA_T5AI_BOARD_LCD_3.5_WITH_L511.config | 2 + .../TUYA_T5AI_BOARD_LCD_3.5_v101.config | 2 + apps/tuya.ai/your_chat_bot/src/tuya_main.c | 18 + src/ai_components/ai_main/src/ai_chat_ui.c | 90 +- src/ai_components/ai_ui/CMakeLists.txt | 5 + src/ai_components/ai_ui/Kconfig | 15 +- .../guided_wechat/ai_ui_chat_guided_wechat.c | 939 ++++++++++++ .../guided_wechat/ai_ui_chat_guided_wechat.h | 21 + .../ai_ui/src/guided_wechat/ai_ui_locale.c | 52 + .../ai_ui/src/guided_wechat/ai_ui_locale.h | 16 + .../src/guided_wechat/guided_wechat_album.c | 963 ++++++++++++ .../src/guided_wechat/guided_wechat_camera.c | 391 +++++ .../src/guided_wechat/guided_wechat_chat.c | 1286 +++++++++++++++++ .../src/guided_wechat/guided_wechat_common.h | 62 + .../src/guided_wechat/guided_wechat_core.c | 311 ++++ .../src/guided_wechat/guided_wechat_core.h | 22 + .../src/guided_wechat/guided_wechat_locale.c | 28 + .../src/guided_wechat/guided_wechat_locale.h | 21 + .../src/guided_wechat/guided_wechat_state.c | 192 +++ .../src/guided_wechat/guided_wechat_state.h | 58 + src/liblvgl/Kconfig | 6 +- src/liblvgl/v9/CMakeLists.txt | 4 + src/liblvgl/v9/conf/lv_conf.h | 8 +- 26 files changed, 4507 insertions(+), 11 deletions(-) create mode 100644 src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.h create mode 100644 src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.h create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_chat.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.h create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.h create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.c create mode 100644 src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.h diff --git a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5.config b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5.config index f55b9310f..b4fe2736b 100644 --- a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5.config +++ b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5.config @@ -4,5 +4,7 @@ CONFIG_ENABLE_AI_UI_TEXT_STREAMING=y CONFIG_BUTTON_NAME="ai_chat_button" CONFIG_BOARD_CHOICE_T5AI=y CONFIG_TUYA_T5AI_BOARD_LCD_35565=y +CONFIG_ENABLE_AI_CHAT_GUI_GUIDED_WECHAT=y +CONFIG_LV_USE_QRCODE=y CONFIG_ENABLE_MBEDTLS_SSL_MAX_CONTENT_LEN=4096 CONFIG_ENABLE_LVGL_TP=y diff --git a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAMERA.config b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAMERA.config index 77e972406..423042122 100644 --- a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAMERA.config +++ b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAMERA.config @@ -8,6 +8,8 @@ CONFIG_BUTTON_NAME="ai_chat_button" CONFIG_BOARD_CHOICE_T5AI=y CONFIG_TUYA_T5AI_BOARD_LCD_35565=y CONFIG_TUYA_T5AI_BOARD_CAMERA=y +CONFIG_ENABLE_AI_CHAT_GUI_GUIDED_WECHAT=y +CONFIG_LV_USE_QRCODE=y CONFIG_AI_INPUT_STACK_SIZE=28672 CONFIG_ENABLE_MBEDTLS_SSL_MAX_CONTENT_LEN=4096 CONFIG_ENABLE_LVGL_TP=y diff --git a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAM_PRINTER.config b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAM_PRINTER.config index ca1f59efb..4da071944 100644 --- a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAM_PRINTER.config +++ b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_CAM_PRINTER.config @@ -10,6 +10,8 @@ CONFIG_BOARD_CHOICE_T5AI=y CONFIG_TUYA_T5AI_BOARD_LCD_35565=y CONFIG_TUYA_T5AI_BOARD_CAMERA=y CONFIG_TUYA_T5AI_BOARD_PRINTER_DP48=y +CONFIG_ENABLE_AI_CHAT_GUI_GUIDED_WECHAT=y +CONFIG_LV_USE_QRCODE=y CONFIG_AI_INPUT_STACK_SIZE=28672 CONFIG_ENABLE_MBEDTLS_SSL_MAX_CONTENT_LEN=4096 CONFIG_ENABLE_LVGL_TP=y diff --git a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_WITH_L511.config b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_WITH_L511.config index 54909950a..bfc46bc59 100644 --- a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_WITH_L511.config +++ b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_WITH_L511.config @@ -4,6 +4,8 @@ CONFIG_BUTTON_NAME="ai_chat_button" CONFIG_BOARD_CHOICE_T5AI=y CONFIG_TUYA_T5AI_BOARD_LCD_35565=y CONFIG_ENABLE_T5AI_BOARD_CELLULAR_USB=y +CONFIG_ENABLE_AI_CHAT_GUI_GUIDED_WECHAT=y +CONFIG_LV_USE_QRCODE=y CONFIG_ENABLE_AI_UI_TEXT_STREAMING=y CONFIG_ENABLE_MBEDTLS_SSL_MAX_CONTENT_LEN=4096 CONFIG_ENABLE_LVGL_TP=y diff --git a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_v101.config b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_v101.config index 6a4ebc62a..fb991b6dc 100755 --- a/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_v101.config +++ b/apps/tuya.ai/your_chat_bot/config/TUYA_T5AI_BOARD_LCD_3.5_v101.config @@ -5,6 +5,8 @@ CONFIG_BUTTON_NAME="ai_chat_button" CONFIG_BOARD_CHOICE_T5AI=y CONFIG_TUYA_T5AI_BOARD_VERSION_101=y CONFIG_TUYA_T5AI_BOARD_LCD_35565=y +CONFIG_ENABLE_AI_CHAT_GUI_GUIDED_WECHAT=y +CONFIG_LV_USE_QRCODE=y CONFIG_ENABLE_MBEDTLS_SSL_MAX_CONTENT_LEN=4096 CONFIG_ENABLE_LVGL_TP=y CONFIG_BUTTION_STACK_SIZE=4096 diff --git a/apps/tuya.ai/your_chat_bot/src/tuya_main.c b/apps/tuya.ai/your_chat_bot/src/tuya_main.c index 51a0bd812..a327a21dd 100644 --- a/apps/tuya.ai/your_chat_bot/src/tuya_main.c +++ b/apps/tuya.ai/your_chat_bot/src/tuya_main.c @@ -46,6 +46,9 @@ #include "app_chat_bot.h" #include "reset_netcfg.h" +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) +#include "guided_wechat_state.h" +#endif #if defined(ENABLE_BATTERY) && (ENABLE_BATTERY == 1) #include "app_battery.h" @@ -169,6 +172,10 @@ void user_event_handler_on(tuya_iot_client_t *client, tuya_event_msg_t *event) case TUYA_EVENT_BIND_START: PR_INFO("Device Bind Start!"); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + guided_wechat_pairing_begin(); +#endif + #if defined(ENABLE_COMP_AI_AUDIO) && (ENABLE_COMP_AI_AUDIO == 1) ai_audio_player_alert(AI_AUDIO_ALERT_NETWORK_CFG); #endif @@ -185,12 +192,19 @@ void user_event_handler_on(tuya_iot_client_t *client, tuya_event_msg_t *event) } break; case TUYA_EVENT_BIND_TOKEN_ON: +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + guided_wechat_pairing_data_received(); +#endif break; /* MQTT with tuya cloud is connected, device online */ case TUYA_EVENT_MQTT_CONNECTED: PR_INFO("Device MQTT Connected!"); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + guided_wechat_pairing_complete(); +#endif + static uint8_t first = 1; if (first) { first = 0; @@ -225,6 +239,10 @@ void user_event_handler_on(tuya_iot_client_t *client, tuya_event_msg_t *event) tuya_reset_type_t reset_type = (tuya_reset_type_t)event->value.asInteger; PR_INFO("Device Reset:%d", reset_type); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + guided_wechat_pairing_reset(); +#endif + // TUYA_RESET_TYPE_FACTORY, TUYA_RESET_TYPE_REMOTE_FACTORY, TUYA_RESET_TYPE_DATA_FACTORY // Need remove the device application data from the kv store } break; diff --git a/src/ai_components/ai_main/src/ai_chat_ui.c b/src/ai_components/ai_main/src/ai_chat_ui.c index f97617ef8..d30cb47a5 100644 --- a/src/ai_components/ai_main/src/ai_chat_ui.c +++ b/src/ai_components/ai_main/src/ai_chat_ui.c @@ -16,10 +16,18 @@ #include "ai_ui_chat_chatbot.h" #elif defined(ENABLE_AI_CHAT_GUI_OLED) && (ENABLE_AI_CHAT_GUI_OLED == 1) #include "ai_ui_chat_oled.h" +#elif defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) +#include "ai_ui_chat_guided_wechat.h" +#include "ai_ui_locale.h" #endif #include "ai_chat_main.h" +static AI_MODE_STATE_E sg_last_mode_state = AI_MODE_STATE_INIT; +static AI_CHAT_MODE_E sg_last_chat_mode; +static bool sg_last_mode_state_valid = false; +static bool sg_last_chat_mode_valid = false; + /*********************************************************** ************************macro define************************ ***********************************************************/ @@ -37,20 +45,45 @@ ***********************************************************/ static void __ai_chat_disp_mode_state(AI_MODE_STATE_E state) { + const char *status; + + sg_last_mode_state = state; + sg_last_mode_state_valid = true; + switch (state) { case AI_MODE_STATE_INIT: case AI_MODE_STATE_IDLE: ai_ui_disp_msg(AI_UI_DISP_EMOTION, (uint8_t *)EMOJI_NEUTRAL, strlen(EMOJI_NEUTRAL)); - ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)STANDBY, strlen(STANDBY)); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + status = ai_ui_locale_tr(STANDBY, "Standby"); +#else + status = STANDBY; +#endif + ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)status, strlen(status)); break; case AI_MODE_STATE_LISTEN: - ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)LISTENING, strlen(LISTENING)); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + status = ai_ui_locale_tr(LISTENING, "Listening"); +#else + status = LISTENING; +#endif + ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)status, strlen(status)); break; case AI_MODE_STATE_SPEAK: - ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)SPEAKING, strlen(SPEAKING)); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + status = ai_ui_locale_tr(SPEAKING, "Speaking"); +#else + status = SPEAKING; +#endif + ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)status, strlen(status)); break; case AI_MODE_STATE_UPLOAD: - ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)UPLOADING, strlen(UPLOADING)); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + status = ai_ui_locale_tr(UPLOADING, "Uploading"); +#else + status = UPLOADING; +#endif + ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)status, strlen(status)); break; default: break; @@ -114,12 +147,36 @@ void ai_chat_ui_handle_event(AI_NOTIFY_EVENT_T *event) case AI_USER_EVT_MODE_SWITCH: { AI_CHAT_MODE_E mode = (AI_CHAT_MODE_E)(event->data); char *name = ai_get_mode_name_str(mode); + const char *localized_name = name; + + sg_last_chat_mode = mode; + sg_last_chat_mode_valid = true; if (NULL == name) { PR_NOTICE("mode name str is null"); break; } - ai_ui_disp_msg(AI_UI_DISP_CHAT_MODE, (uint8_t *)name, strlen(name)); +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + if (ai_ui_locale_is_english()) { + switch (mode) { + case AI_CHAT_MODE_HOLD: + localized_name = "Hold to talk"; + break; + case AI_CHAT_MODE_ONE_SHOT: + localized_name = "Push to talk"; + break; + case AI_CHAT_MODE_WAKEUP: + localized_name = "Wake word"; + break; + case AI_CHAT_MODE_FREE: + localized_name = "Free talk"; + break; + default: + break; + } + } +#endif + ai_ui_disp_msg(AI_UI_DISP_CHAT_MODE, (uint8_t *)localized_name, strlen(localized_name)); } break; #if defined(ENABLE_COMP_AI_PICTURE) && (ENABLE_COMP_AI_PICTURE == 1) @@ -138,6 +195,21 @@ void ai_chat_ui_handle_event(AI_NOTIFY_EVENT_T *event) } } +void ai_chat_ui_refresh_locale(void) +{ + if (sg_last_mode_state_valid) { + __ai_chat_disp_mode_state(sg_last_mode_state); + } + + if (sg_last_chat_mode_valid) { + AI_NOTIFY_EVENT_T mode_event = { + .type = AI_USER_EVT_MODE_SWITCH, + .data = (void *)(uintptr_t)sg_last_chat_mode, + }; + ai_chat_ui_handle_event(&mode_event); + } +} + OPERATE_RET ai_chat_ui_init(void) { OPERATE_RET rt = OPRT_OK; @@ -146,12 +218,18 @@ OPERATE_RET ai_chat_ui_init(void) PR_NOTICE("use custom ai chat ui, need register ui by user"); #else +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + ai_ui_locale_load(); +#endif + #if defined(ENABLE_AI_CHAT_GUI_WECHAT) && (ENABLE_AI_CHAT_GUI_WECHAT == 1) TUYA_CALL_ERR_RETURN(ai_ui_chat_wechat_register()); #elif defined(ENABLE_AI_CHAT_GUI_CHATBOT) && (ENABLE_AI_CHAT_GUI_CHATBOT == 1) TUYA_CALL_ERR_RETURN(ai_ui_chat_chatbot_register()); #elif defined(ENABLE_AI_CHAT_GUI_OLED) && (ENABLE_AI_CHAT_GUI_OLED == 1) TUYA_CALL_ERR_RETURN(ai_ui_chat_oled_register()); +#elif defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + TUYA_CALL_ERR_RETURN(ai_ui_chat_guided_wechat_register()); #else #error "please select ai chat present ui" #endif @@ -161,4 +239,4 @@ OPERATE_RET ai_chat_ui_init(void) return rt; } -#endif \ No newline at end of file +#endif diff --git a/src/ai_components/ai_ui/CMakeLists.txt b/src/ai_components/ai_ui/CMakeLists.txt index c3b44fd90..fafcf2e90 100755 --- a/src/ai_components/ai_ui/CMakeLists.txt +++ b/src/ai_components/ai_ui/CMakeLists.txt @@ -22,6 +22,9 @@ elseif (CONFIG_ENABLE_AI_CHAT_GUI_CHATBOT STREQUAL "y") elseif (CONFIG_ENABLE_AI_CHAT_GUI_OLED STREQUAL "y") file(GLOB OLED_SRCS ${MODULE_PATH}/src/oled/*.c) list(APPEND LIB_SRCS ${OLED_SRCS}) +elseif (CONFIG_ENABLE_AI_CHAT_GUI_GUIDED_WECHAT STREQUAL "y") + file(GLOB GUIDED_WECHAT_SRCS CONFIGURE_DEPENDS ${MODULE_PATH}/src/guided_wechat/*.c) + list(APPEND LIB_SRCS ${GUIDED_WECHAT_SRCS}) endif() # Font sources @@ -49,6 +52,8 @@ set(LIB_PUBLIC_INC if (CONFIG_ENABLE_AI_CHAT_GUI_WECHAT STREQUAL "y") list(APPEND LIB_PUBLIC_INC ${MODULE_PATH}/src/wechat) +elseif (CONFIG_ENABLE_AI_CHAT_GUI_GUIDED_WECHAT STREQUAL "y") + list(APPEND LIB_PUBLIC_INC ${MODULE_PATH}/src/guided_wechat) endif() ######################################## diff --git a/src/ai_components/ai_ui/Kconfig b/src/ai_components/ai_ui/Kconfig index 2b311d43f..3739e70a6 100644 --- a/src/ai_components/ai_ui/Kconfig +++ b/src/ai_components/ai_ui/Kconfig @@ -24,6 +24,15 @@ if(!ENABLE_AI_CHAT_CUSTOM_UI) config ENABLE_AI_CHAT_GUI_OLED select ENABLE_LIBLVGL bool "Use OLED ui" + + config ENABLE_AI_CHAT_GUI_GUIDED_WECHAT + select ENABLE_LIBLVGL + select LV_USE_QRCODE + bool "Use guided full WeChat-like ui" + help + Keep the complete WeChat chat/camera/album UI and add a + first-run language, authorization and network guide together + with device settings and about pages. endchoice config ENABLE_CIRCLE_UI_STYLE @@ -32,7 +41,7 @@ if(!ENABLE_AI_CHAT_CUSTOM_UI) default n config AI_UI_WECHAT_SCREEN_CORNER_RADIUS - depends on ENABLE_AI_CHAT_GUI_WECHAT + depends on ENABLE_AI_CHAT_GUI_WECHAT || ENABLE_AI_CHAT_GUI_GUIDED_WECHAT int "WeChat UI rounded-screen corner radius in pixels (NOT degrees)" range 0 120 default 0 @@ -124,7 +133,7 @@ if(ENABLE_AI_UI_ICON_FONT) choice prompt "choose emo font type" - default FONT_EMOJI_SIZE_32 if(ENABLE_AI_CHAT_GUI_WECHAT) + default FONT_EMOJI_SIZE_32 if(ENABLE_AI_CHAT_GUI_WECHAT || ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) default FONT_EMOJI_SIZE_64 if(ENABLE_AI_CHAT_GUI_CHATBOT) default FONT_EMO_AWESOME if(ENABLE_AI_CHAT_GUI_OLED) @@ -140,4 +149,4 @@ if(ENABLE_AI_UI_ICON_FONT) endif -endif \ No newline at end of file +endif diff --git a/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c new file mode 100644 index 000000000..1f35cb648 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c @@ -0,0 +1,939 @@ +/** + * @file ai_ui_chat_guided_wechat.c + * @brief Guided device experience layered on the complete WeChat UI. + */ + +#include "tal_api.h" + +#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) + +#include +#include +#include + +#include "tuya_iot.h" +#include "netmgr.h" +#include "lvgl.h" +#include "lv_vendor.h" +#include "ai_ui_manage.h" +#include "ai_ui_icon_font.h" +#include "font_awesome_symbols.h" +#include "guided_wechat_core.h" +#include "ai_ui_chat_guided_wechat.h" +#include "guided_wechat_locale.h" +#include "guided_wechat_state.h" + +extern void ai_chat_ui_refresh_locale(void); + +#define GUIDED_GREEN 0x07C160 +#define GUIDED_BG 0xF4F5F7 +#define GUIDED_TEXT 0x202124 +#define GUIDED_MUTED 0x71757A +#define GUIDED_BORDER 0xE2E5E9 +#define GUIDED_DANGER 0xD94248 +#define GUIDED_HEADER_HEIGHT 44 +#define GUIDED_STEPPER_HEIGHT 64 +#define GUIDED_STEPPER_CIRCLE 28 +#define GUIDED_STEPPER_LINE 4 +#define GUIDED_STEP_COUNT 3 +#define GUIDED_BUTTON_HEIGHT 44 +#define GUIDED_RADIUS 14 +#define GUIDED_NETWORK_GUIDE_URL_ZH \ + "https://tuyaopen.ai/zh/docs/quick-start/device-network-configuration#add-device" +#define GUIDED_NETWORK_GUIDE_URL_EN \ + "https://tuyaopen.ai/docs/quick-start/device-network-configuration" +#define GUIDED_AUTH_GUIDE_URL_ZH "https://tuyaopen.ai/zh/pricing" +#define GUIDED_AUTH_GUIDE_URL_EN "https://tuyaopen.ai/pricing" + +typedef enum { + GUIDED_PAGE_LANGUAGE_FIRST = 0, + GUIDED_PAGE_AUTH_MISSING, + GUIDED_PAGE_AUTH_READY, + GUIDED_PAGE_NETWORK, + GUIDED_PAGE_MENU, + GUIDED_PAGE_LANGUAGE, + GUIDED_PAGE_ABOUT, + GUIDED_PAGE_RESET_CONFIRM, +} guided_page_t; + +typedef enum { + GUIDED_ACTION_NONE = 0, + GUIDED_ACTION_LANG_ZH, + GUIDED_ACTION_LANG_EN, + GUIDED_ACTION_NEXT, + GUIDED_ACTION_MENU, + GUIDED_ACTION_NETWORK, + GUIDED_ACTION_LANGUAGE, + GUIDED_ACTION_ABOUT, + GUIDED_ACTION_RESET, + GUIDED_ACTION_RESET_CONFIRM, + GUIDED_ACTION_STEP_LANGUAGE, + GUIDED_ACTION_STEP_AUTH, + GUIDED_ACTION_STEP_NETWORK, + GUIDED_ACTION_BACK, + GUIDED_ACTION_REFRESH, + GUIDED_ACTION_PAIRING, +} guided_action_t; + +typedef struct { + lv_obj_t *overlay; + lv_obj_t *menu_button; + lv_obj_t *status_label; + guided_page_t page; + guided_page_t back_page; + guided_wechat_device_state_t state; + lv_timer_t *refresh_timer; + lv_obj_t *back_button; + bool reset_pending; + guided_page_t reset_parent_page; + guided_page_t reset_parent_back_page; +} guided_ui_t; + +static guided_ui_t sg_ui = {0}; + +static void __show_page(guided_page_t page, guided_page_t back_page); +static void __action_cb(lv_event_t *event); + +static bool __setup_complete(void) +{ + guided_wechat_state_read(&sg_ui.state); + return sg_ui.state.link_up && sg_ui.state.activated && sg_ui.state.cloud_online; +} + +static uint8_t __guide_step(guided_page_t page) +{ + switch (page) { + case GUIDED_PAGE_LANGUAGE_FIRST: + return 1; + case GUIDED_PAGE_AUTH_MISSING: + case GUIDED_PAGE_AUTH_READY: + return 2; + case GUIDED_PAGE_NETWORK: + return (sg_ui.back_page == GUIDED_PAGE_AUTH_READY || + sg_ui.back_page == GUIDED_PAGE_AUTH_MISSING) + ? 3 + : 0; + default: + return 0; + } +} + +static bool __enter_chat(void) +{ + if (!__setup_complete()) { + __show_page(sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_AUTH_MISSING + : GUIDED_PAGE_NETWORK, + sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_LANGUAGE_FIRST + : GUIDED_PAGE_AUTH_READY); + return false; + } + + lv_obj_add_flag(sg_ui.overlay, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_ui.menu_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(sg_ui.menu_button); + return true; +} + +static const char *__tr(const char *zh, const char *en) +{ + return guided_wechat_tr(zh, en); +} + +static const char *__network_guide_url(void) +{ + return guided_wechat_locale_get() == GUIDED_WECHAT_LANG_EN + ? GUIDED_NETWORK_GUIDE_URL_EN + : GUIDED_NETWORK_GUIDE_URL_ZH; +} + +static const char *__auth_guide_url(void) +{ + return guided_wechat_locale_get() == GUIDED_WECHAT_LANG_EN + ? GUIDED_AUTH_GUIDE_URL_EN + : GUIDED_AUTH_GUIDE_URL_ZH; +} + +static void __set_text_font(lv_obj_t *obj) +{ + lv_obj_set_style_text_font(obj, ai_ui_get_text_font(), 0); +} + +static lv_obj_t *__make_label(lv_obj_t *parent, const char *text, lv_color_t color, + lv_text_align_t align) +{ + lv_obj_t *label = lv_label_create(parent); + lv_obj_set_width(label, LV_PCT(100)); + lv_obj_set_height(label, LV_SIZE_CONTENT); + __set_text_font(label); + lv_obj_set_style_text_color(label, color, 0); + lv_obj_set_style_text_align(label, align, 0); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + lv_label_set_text(label, text != NULL ? text : ""); + return label; +} + +static lv_obj_t *__make_card(lv_obj_t *parent) +{ + lv_obj_t *card = lv_obj_create(parent); + lv_obj_set_width(card, LV_PCT(100)); + lv_obj_set_height(card, LV_SIZE_CONTENT); + lv_obj_set_style_bg_color(card, lv_color_white(), 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(card, lv_color_hex(GUIDED_BORDER), 0); + lv_obj_set_style_border_width(card, 1, 0); + lv_obj_set_style_radius(card, GUIDED_RADIUS, 0); + lv_obj_set_style_pad_all(card, 14, 0); + lv_obj_set_style_pad_row(card, 10, 0); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + return card; +} + +static void __make_guide_qr(lv_obj_t *card, const char *url) +{ + lv_obj_t *qr_wrap; + lv_obj_t *qr; + + __make_label(card, __tr("扫码看教程", "Scan for guide"), + lv_color_hex(GUIDED_MUTED), LV_TEXT_ALIGN_CENTER); + qr_wrap = lv_obj_create(card); + lv_obj_set_width(qr_wrap, LV_PCT(100)); + lv_obj_set_height(qr_wrap, 184); + lv_obj_set_style_bg_color(qr_wrap, lv_color_white(), 0); + lv_obj_set_style_border_color(qr_wrap, lv_color_hex(GUIDED_BORDER), 0); + lv_obj_set_style_border_width(qr_wrap, 1, 0); + lv_obj_set_style_radius(qr_wrap, 8, 0); + lv_obj_set_style_pad_all(qr_wrap, 10, 0); + lv_obj_clear_flag(qr_wrap, LV_OBJ_FLAG_SCROLLABLE); + qr = lv_qrcode_create(qr_wrap); + lv_qrcode_set_size(qr, 156); + lv_qrcode_set_dark_color(qr, lv_color_hex(GUIDED_TEXT)); + lv_qrcode_set_light_color(qr, lv_color_white()); + lv_qrcode_update(qr, url, strlen(url)); + lv_obj_center(qr); +} + +static void __make_stepper(uint8_t current_step) +{ + static const char *step_numbers[] = {"1", "2", "3"}; + const char *step_names[] = { + __tr("语言", "Language"), + __tr("授权", "Auth"), + __tr("联网", "Network"), + }; + lv_obj_t *stepper; + int i; + + stepper = lv_obj_create(sg_ui.overlay); + lv_obj_set_size(stepper, LV_PCT(100), GUIDED_STEPPER_HEIGHT); + lv_obj_align(stepper, LV_ALIGN_TOP_MID, 0, GUIDED_HEADER_HEIGHT); + lv_obj_set_style_pad_all(stepper, 0, 0); + lv_obj_set_style_radius(stepper, 0, 0); + lv_obj_set_style_border_width(stepper, 0, 0); + lv_obj_set_style_bg_color(stepper, lv_color_white(), 0); + lv_obj_clear_flag(stepper, LV_OBJ_FLAG_SCROLLABLE); + + for (i = 0; i < GUIDED_STEP_COUNT - 1; i++) { + lv_obj_t *line = lv_obj_create(stepper); + lv_obj_set_size(line, LV_HOR_RES / 3 - GUIDED_STEPPER_CIRCLE, GUIDED_STEPPER_LINE); + lv_obj_align(line, LV_ALIGN_TOP_MID, + i == 0 ? -(LV_HOR_RES / 6) : (LV_HOR_RES / 6), 19); + lv_obj_set_style_pad_all(line, 0, 0); + lv_obj_set_style_radius(line, GUIDED_STEPPER_LINE / 2, 0); + lv_obj_set_style_border_width(line, 0, 0); + lv_obj_set_style_bg_color(line, + current_step > (uint8_t)(i + 1) + ? lv_color_hex(GUIDED_GREEN) + : lv_color_hex(0xDDE7E1), + 0); + lv_obj_clear_flag(line, LV_OBJ_FLAG_SCROLLABLE); + } + + for (i = 0; i < GUIDED_STEP_COUNT; i++) { + lv_obj_t *circle = lv_obj_create(stepper); + lv_obj_t *number = lv_label_create(circle); + bool completed = current_step > (uint8_t)i; + + lv_obj_set_size(circle, GUIDED_STEPPER_CIRCLE, GUIDED_STEPPER_CIRCLE); + lv_obj_align(circle, LV_ALIGN_TOP_MID, (i - 1) * (LV_HOR_RES / 3), 7); + lv_obj_set_style_pad_all(circle, 0, 0); + lv_obj_set_style_radius(circle, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(circle, completed ? 0 : 1, 0); + lv_obj_set_style_border_color(circle, lv_color_hex(0xB8C8BE), 0); + lv_obj_set_style_bg_color(circle, + completed ? lv_color_hex(GUIDED_GREEN) + : lv_color_hex(0xF3F6F4), + 0); + lv_obj_clear_flag(circle, LV_OBJ_FLAG_SCROLLABLE); + + __set_text_font(number); + lv_obj_set_style_text_color(number, + completed ? lv_color_white() : lv_color_hex(GUIDED_MUTED), + 0); + lv_label_set_text(number, step_numbers[i]); + lv_obj_center(number); + lv_obj_set_ext_click_area(circle, 8); + lv_obj_add_event_cb(circle, __action_cb, LV_EVENT_CLICKED, + (void *)(uintptr_t)(GUIDED_ACTION_STEP_LANGUAGE + i)); + + lv_obj_t *name = lv_label_create(stepper); + lv_obj_set_width(name, LV_HOR_RES / 3); + lv_obj_set_height(name, LV_SIZE_CONTENT); + __set_text_font(name); + lv_obj_set_style_text_align(name, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(name, + completed ? lv_color_hex(GUIDED_GREEN) + : lv_color_hex(GUIDED_MUTED), + 0); + lv_label_set_long_mode(name, LV_LABEL_LONG_DOT); + lv_label_set_text(name, step_names[i]); + lv_obj_align(name, LV_ALIGN_TOP_MID, (i - 1) * (LV_HOR_RES / 3), 39); + } +} + +static void __action_cb(lv_event_t *event) +{ + guided_action_t action = (guided_action_t)(uintptr_t)lv_event_get_user_data(event); + + switch (action) { + case GUIDED_ACTION_LANG_ZH: + guided_wechat_locale_set(GUIDED_WECHAT_LANG_ZH); + ai_chat_ui_refresh_locale(); + guided_wechat_state_read(&sg_ui.state); + if (sg_ui.page == GUIDED_PAGE_LANGUAGE_FIRST) { + if (sg_ui.state.activated) { + __enter_chat(); + } else { + __show_page(sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_AUTH_MISSING + : GUIDED_PAGE_AUTH_READY, + GUIDED_PAGE_LANGUAGE_FIRST); + } + } else { + __show_page(GUIDED_PAGE_MENU, GUIDED_PAGE_MENU); + } + break; + case GUIDED_ACTION_LANG_EN: + guided_wechat_locale_set(GUIDED_WECHAT_LANG_EN); + ai_chat_ui_refresh_locale(); + guided_wechat_state_read(&sg_ui.state); + if (sg_ui.page == GUIDED_PAGE_LANGUAGE_FIRST) { + if (sg_ui.state.activated) { + __enter_chat(); + } else { + __show_page(sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_AUTH_MISSING + : GUIDED_PAGE_AUTH_READY, + GUIDED_PAGE_LANGUAGE_FIRST); + } + } else { + __show_page(GUIDED_PAGE_MENU, GUIDED_PAGE_MENU); + } + break; + case GUIDED_ACTION_NEXT: + if (sg_ui.page == GUIDED_PAGE_AUTH_READY) { + __show_page(GUIDED_PAGE_NETWORK, GUIDED_PAGE_AUTH_READY); + } + break; + case GUIDED_ACTION_MENU: + __show_page(GUIDED_PAGE_MENU, GUIDED_PAGE_MENU); + break; + case GUIDED_ACTION_NETWORK: + /* Device settings opens the network details page. The guided stepper + * is reserved for the first-time setup flow entered from auth. */ + __show_page(GUIDED_PAGE_NETWORK, GUIDED_PAGE_MENU); + break; + case GUIDED_ACTION_LANGUAGE: + __show_page(GUIDED_PAGE_LANGUAGE, GUIDED_PAGE_MENU); + break; + case GUIDED_ACTION_ABOUT: + __show_page(GUIDED_PAGE_ABOUT, GUIDED_PAGE_MENU); + break; + case GUIDED_ACTION_RESET: + sg_ui.reset_parent_page = sg_ui.page; + sg_ui.reset_parent_back_page = sg_ui.back_page; + __show_page(GUIDED_PAGE_RESET_CONFIRM, sg_ui.page); + break; + case GUIDED_ACTION_RESET_CONFIRM: + if (sg_ui.reset_pending) { + break; + } + if (tuya_iot_client_get() == NULL) { + lv_label_set_text(sg_ui.status_label, + __tr("设备服务尚未就绪,暂不能解绑。", + "Device service is not ready; unbind is unavailable.")); + break; + } + { + OPERATE_RET net_rt = netmgr_conn_set(NETCONN_WIFI, NETCONN_CMD_RESET, NULL); + if (net_rt != OPRT_OK) { + PR_WARN("network config reset failed: %d", net_rt); + } + + /* tuya_iot_reset() schedules the SDK reset even when its remote + * unbind request returns an error. The local reset path is still + * required so the next boot can enter phone provisioning. */ + guided_wechat_reprovision_mark(); + OPERATE_RET reset_rt = tuya_iot_reset(tuya_iot_client_get()); + if (reset_rt != OPRT_OK) { + PR_WARN("native Tuya IoT reset returned: %d; local reset continues", reset_rt); + } + sg_ui.reset_pending = true; + lv_label_set_text(sg_ui.status_label, + __tr("正在清理并重启。", "Clearing settings and restarting.")); + if (sg_ui.back_button != NULL) { + lv_obj_add_state(sg_ui.back_button, LV_STATE_DISABLED); + } + } + break; + case GUIDED_ACTION_PAIRING: + guided_wechat_state_read(&sg_ui.state); + if (sg_ui.state.activated) { + sg_ui.reset_parent_page = sg_ui.page; + sg_ui.reset_parent_back_page = sg_ui.back_page; + __show_page(GUIDED_PAGE_RESET_CONFIRM, sg_ui.page); + } else if (sg_ui.state.pairing_state == GUIDED_WECHAT_PAIRING_IDLE) { + guided_wechat_pairing_request(); + __show_page(GUIDED_PAGE_NETWORK, sg_ui.back_page); + } + break; + case GUIDED_ACTION_STEP_LANGUAGE: + __show_page(GUIDED_PAGE_LANGUAGE_FIRST, GUIDED_PAGE_LANGUAGE_FIRST); + break; + case GUIDED_ACTION_STEP_AUTH: + guided_wechat_state_read(&sg_ui.state); + __show_page(sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_AUTH_MISSING + : GUIDED_PAGE_AUTH_READY, + GUIDED_PAGE_LANGUAGE_FIRST); + break; + case GUIDED_ACTION_STEP_NETWORK: + guided_wechat_state_read(&sg_ui.state); + __show_page(GUIDED_PAGE_NETWORK, + sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_AUTH_MISSING + : GUIDED_PAGE_AUTH_READY); + break; + case GUIDED_ACTION_BACK: + if (sg_ui.reset_pending) { + break; + } + if (sg_ui.page == GUIDED_PAGE_RESET_CONFIRM) { + __show_page(sg_ui.reset_parent_page, sg_ui.reset_parent_back_page); + break; + } + if (sg_ui.back_page == GUIDED_PAGE_MENU && sg_ui.page == GUIDED_PAGE_MENU) { + __enter_chat(); + } else if (sg_ui.page == GUIDED_PAGE_NETWORK && + (sg_ui.back_page == GUIDED_PAGE_AUTH_READY || + sg_ui.back_page == GUIDED_PAGE_AUTH_MISSING)) { + __show_page(sg_ui.back_page, GUIDED_PAGE_LANGUAGE_FIRST); + } else { + __show_page(sg_ui.back_page, GUIDED_PAGE_MENU); + } + break; + case GUIDED_ACTION_REFRESH: + guided_wechat_state_read(&sg_ui.state); + if (sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE) { + __show_page(GUIDED_PAGE_AUTH_MISSING, GUIDED_PAGE_LANGUAGE_FIRST); + } else if (__setup_complete()) { + if (sg_ui.page == GUIDED_PAGE_NETWORK && + sg_ui.back_page == GUIDED_PAGE_AUTH_READY) { + __enter_chat(); + } else { + __show_page(GUIDED_PAGE_NETWORK, sg_ui.back_page); + } + } else if (sg_ui.page == GUIDED_PAGE_NETWORK) { + __show_page(GUIDED_PAGE_NETWORK, sg_ui.back_page); + } else { + __show_page(GUIDED_PAGE_AUTH_READY, GUIDED_PAGE_LANGUAGE_FIRST); + } + break; + default: + break; + } +} + +static lv_obj_t *__make_button(lv_obj_t *parent, const char *text, guided_action_t action, + bool secondary, bool danger) +{ + lv_obj_t *button = lv_btn_create(parent); + lv_obj_t *label; + + lv_obj_set_width(button, LV_PCT(100)); + lv_obj_set_height(button, GUIDED_BUTTON_HEIGHT); + lv_obj_set_style_radius(button, 12, 0); + lv_obj_set_style_shadow_width(button, 0, 0); + lv_obj_set_style_border_width(button, secondary ? 1 : 0, 0); + lv_obj_set_style_border_color(button, lv_color_hex(danger ? GUIDED_DANGER : GUIDED_BORDER), 0); + lv_obj_set_style_bg_color(button, + secondary ? lv_color_white() + : lv_color_hex(danger ? GUIDED_DANGER : GUIDED_GREEN), + 0); + lv_obj_set_ext_click_area(button, 8); + label = lv_label_create(button); + __set_text_font(label); + lv_obj_set_style_text_color(label, + secondary ? lv_color_hex(danger ? GUIDED_DANGER : GUIDED_TEXT) + : lv_color_white(), + 0); + lv_label_set_text(label, text); + lv_obj_center(label); + lv_obj_add_event_cb(button, __action_cb, LV_EVENT_CLICKED, (void *)(uintptr_t)action); + return button; +} + +static lv_obj_t *__begin_page(const char *title, bool allow_back) +{ + lv_obj_t *header; + lv_obj_t *body; + lv_obj_t *title_label; + uint8_t guide_step = __guide_step(sg_ui.page); + lv_coord_t body_top = GUIDED_HEADER_HEIGHT; + + lv_obj_clean(sg_ui.overlay); + sg_ui.status_label = NULL; + sg_ui.back_button = NULL; + if (sg_ui.page != GUIDED_PAGE_RESET_CONFIRM) { + sg_ui.reset_pending = false; + } + + header = lv_obj_create(sg_ui.overlay); + lv_obj_set_size(header, LV_PCT(100), GUIDED_HEADER_HEIGHT); + lv_obj_align(header, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_set_style_radius(header, 0, 0); + lv_obj_set_style_border_width(header, 0, 0); + lv_obj_set_style_pad_all(header, 0, 0); + lv_obj_set_style_bg_color(header, lv_color_hex(GUIDED_GREEN), 0); + lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE); + + if (allow_back) { + lv_obj_t *back = lv_btn_create(header); + lv_obj_t *back_label; + lv_obj_set_size(back, 44, 40); + lv_obj_align(back, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_style_bg_opa(back, LV_OPA_TRANSP, 0); + lv_obj_set_style_shadow_width(back, 0, 0); + lv_obj_set_style_border_width(back, 0, 0); + lv_obj_set_ext_click_area(back, 8); + back_label = lv_label_create(back); + __set_text_font(back_label); + lv_obj_set_style_text_color(back_label, lv_color_white(), 0); + lv_label_set_text(back_label, "<"); + lv_obj_center(back_label); + sg_ui.back_button = back; + lv_obj_add_event_cb(back, __action_cb, LV_EVENT_CLICKED, + (void *)(uintptr_t)GUIDED_ACTION_BACK); + } + + title_label = lv_label_create(header); + lv_obj_set_width(title_label, LV_PCT(68)); + __set_text_font(title_label); + lv_obj_set_style_text_color(title_label, lv_color_white(), 0); + lv_obj_set_style_text_align(title_label, LV_TEXT_ALIGN_CENTER, 0); + lv_label_set_long_mode(title_label, LV_LABEL_LONG_DOT); + lv_label_set_text(title_label, title); + lv_obj_center(title_label); + + { + lv_obj_t *wifi_icon = lv_label_create(header); + const char *wifi_symbol = sg_ui.state.cloud_online || sg_ui.state.link_up + ? FONT_AWESOME_WIFI + : (sg_ui.state.ssid[0] != '\0' + ? FONT_AWESOME_WIFI_WEAK + : FONT_AWESOME_WIFI_OFF); + lv_obj_set_style_text_font(wifi_icon, ai_ui_get_icon_font(), 0); + lv_obj_set_style_text_color(wifi_icon, lv_color_white(), 0); + lv_label_set_text(wifi_icon, wifi_symbol); + lv_obj_align(wifi_icon, LV_ALIGN_RIGHT_MID, -14, 0); + } + + if (guide_step > 0) { + __make_stepper(guide_step); + body_top += GUIDED_STEPPER_HEIGHT; + } + + body = lv_obj_create(sg_ui.overlay); + lv_obj_set_width(body, LV_PCT(100)); + lv_obj_set_height(body, LV_VER_RES - body_top); + lv_obj_align(body, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_set_style_bg_color(body, lv_color_hex(GUIDED_BG), 0); + lv_obj_set_style_border_width(body, 0, 0); + lv_obj_set_style_radius(body, 0, 0); + lv_obj_set_style_pad_all(body, 14, 0); + lv_obj_set_style_pad_row(body, 12, 0); + lv_obj_set_flex_flow(body, LV_FLEX_FLOW_COLUMN); + lv_obj_set_scroll_dir(body, LV_DIR_VER); + lv_obj_set_scrollbar_mode(body, LV_SCROLLBAR_MODE_AUTO); + return body; +} + +static void __make_badge(lv_obj_t *parent, const char *text, bool ok) +{ + lv_obj_t *badge = lv_obj_create(parent); + lv_obj_t *label; + + lv_obj_set_width(badge, LV_PCT(100)); + lv_obj_set_height(badge, 42); + lv_obj_set_style_radius(badge, 10, 0); + lv_obj_set_style_border_width(badge, 0, 0); + lv_obj_set_style_bg_color(badge, lv_color_hex(ok ? 0xE8F7ED : 0xFFF2E5), 0); + lv_obj_clear_flag(badge, LV_OBJ_FLAG_SCROLLABLE); + label = lv_label_create(badge); + __set_text_font(label); + lv_obj_set_style_text_color(label, lv_color_hex(ok ? 0x16813A : 0xA85B00), 0); + lv_label_set_text(label, text); + lv_obj_center(label); +} + +static void __make_info_row(lv_obj_t *parent, const char *name, const char *value) +{ + char line[150]; + snprintf(line, sizeof(line), "%s\n%s", name, value != NULL && value[0] != '\0' ? value : "-"); + __make_label(parent, line, lv_color_hex(GUIDED_TEXT), LV_TEXT_ALIGN_LEFT); +} + +static void __make_list_row(lv_obj_t *parent, const char *name, const char *value) +{ + lv_obj_t *row = lv_obj_create(parent); + lv_obj_t *name_label = lv_label_create(row); + lv_obj_t *value_label = lv_label_create(row); + + lv_obj_set_width(row, LV_PCT(100)); + /* Keep normal rows compact, but let long values grow with wrapped text. */ + lv_obj_set_height(row, LV_SIZE_CONTENT); + lv_obj_set_style_min_height(row, 44, 0); + lv_obj_set_style_pad_left(row, 0, 0); + lv_obj_set_style_pad_right(row, 0, 0); + lv_obj_set_style_pad_top(row, 8, 0); + lv_obj_set_style_pad_bottom(row, 8, 0); + lv_obj_set_style_radius(row, 0, 0); + lv_obj_set_style_bg_color(row, lv_color_white(), 0); + lv_obj_set_style_border_color(row, lv_color_hex(GUIDED_BORDER), 0); + lv_obj_set_style_border_width(row, 1, 0); + lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE); + + __set_text_font(name_label); + lv_obj_set_width(name_label, LV_PCT(40)); + lv_obj_set_height(name_label, LV_SIZE_CONTENT); + lv_obj_set_style_text_color(name_label, lv_color_hex(GUIDED_MUTED), 0); + lv_label_set_long_mode(name_label, LV_LABEL_LONG_WRAP); + lv_label_set_text(name_label, name != NULL ? name : ""); + lv_obj_align(name_label, LV_ALIGN_LEFT_MID, 14, 0); + + __set_text_font(value_label); + lv_obj_set_width(value_label, LV_PCT(52)); + lv_obj_set_height(value_label, LV_SIZE_CONTENT); + lv_obj_set_style_text_color(value_label, lv_color_hex(GUIDED_TEXT), 0); + lv_obj_set_style_text_align(value_label, LV_TEXT_ALIGN_RIGHT, 0); + lv_label_set_long_mode(value_label, LV_LABEL_LONG_WRAP); + lv_label_set_text(value_label, value != NULL && value[0] != '\0' ? value : "-"); + lv_obj_align(value_label, LV_ALIGN_RIGHT_MID, -14, 0); +} + +static void __render_language_first(void) +{ + lv_obj_t *body = __begin_page("TuyaOpen", false); + lv_obj_t *card = __make_card(body); + __make_label(card, "欢迎使用 TuyaOpen AI\nWelcome to TuyaOpen AI", + lv_color_hex(GUIDED_TEXT), LV_TEXT_ALIGN_CENTER); + __make_label(card, "请选择设备语言\nChoose device language", + lv_color_hex(GUIDED_MUTED), LV_TEXT_ALIGN_CENTER); + __make_button(card, "中文", GUIDED_ACTION_LANG_ZH, + guided_wechat_locale_get() != GUIDED_WECHAT_LANG_ZH, false); + __make_button(card, "English", GUIDED_ACTION_LANG_EN, + guided_wechat_locale_get() != GUIDED_WECHAT_LANG_EN, false); +} + +static void __render_auth_missing(void) +{ + lv_obj_t *body = __begin_page(__tr("设备授权", "Device authorization"), true); + lv_obj_t *card = __make_card(body); + __make_badge(card, __tr("设备未授权", "Device not authorized"), false); + __make_label(card, + __tr("请先完成设备授权,才能使用 AI 云服务。", + "Authorize the device to use AI cloud services."), + lv_color_hex(GUIDED_TEXT), LV_TEXT_ALIGN_LEFT); + __make_guide_qr(card, __auth_guide_url()); + __make_button(card, __tr("重新检测", "Check again"), GUIDED_ACTION_REFRESH, false, false); +} + +static void __render_auth_ready(void) +{ + lv_obj_t *body = __begin_page(__tr("设备授权", "Device authorization"), true); + lv_obj_t *card = __make_card(body); + const char *source = sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_STORAGE + ? __tr("设备存储 / 模组授权", "Device storage / module") + : __tr("固件配置", "Firmware configuration"); + __make_badge(card, __tr("已授权", "Authorized"), true); + __make_info_row(card, __tr("授权来源", "Authorization source"), source); + __make_info_row(card, "UUID", sg_ui.state.uuid_masked); + __make_info_row(card, "AuthKey", sg_ui.state.authkey_masked); + __make_button(card, __tr("开始联网", "Connect now"), GUIDED_ACTION_NEXT, + false, false); +} + +static const char *__network_stage(void) +{ + if (sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE) { + return __tr("等待设备授权", "Waiting for device authorization"); + } + if (sg_ui.state.link_up && sg_ui.state.cloud_online) { + return __tr("已联网", "Connected"); + } + if (sg_ui.state.pairing_state == GUIDED_WECHAT_PAIRING_WAITING_PHONE && + !sg_ui.state.link_up) { + return __tr("等待手机配网", "Waiting for phone setup"); + } + if (sg_ui.state.pairing_state == GUIDED_WECHAT_PAIRING_STARTING) { + return __tr("正在准备", "Preparing"); + } + if (sg_ui.state.link_up || + sg_ui.state.pairing_state == GUIDED_WECHAT_PAIRING_CONNECTING) { + return __tr("正在联网", "Connecting"); + } + return __tr("未联网", "Not connected"); +} + +static void __render_network(void) +{ + char current_network[100]; + lv_obj_t *body = __begin_page(__tr("设备联网", "Device network"), true); + lv_obj_t *card = __make_card(body); + + __make_badge(card, __network_stage(), + sg_ui.state.link_up && sg_ui.state.cloud_online); + snprintf(current_network, sizeof(current_network), "%s: %s", + __tr("当前网络", "Current network"), + sg_ui.state.link_up && sg_ui.state.ssid[0] != '\0' + ? sg_ui.state.ssid + : __tr("未连接", "Not connected")); + __make_label(card, current_network, lv_color_hex(GUIDED_TEXT), LV_TEXT_ALIGN_CENTER); + + if (!sg_ui.state.cloud_online) { + __make_guide_qr(card, __network_guide_url()); + } + + if (sg_ui.state.activated) { + __make_button(card, __tr("重新配网", "Reconfigure"), + GUIDED_ACTION_PAIRING, false, false); + } else if (sg_ui.state.pairing_state == GUIDED_WECHAT_PAIRING_IDLE) { + __make_button(card, __tr("开始配网", "Start pairing"), + GUIDED_ACTION_PAIRING, false, false); + } +} + +static void __render_menu(void) +{ + lv_obj_t *body = __begin_page(__tr("设备设置", "Device settings"), true); + lv_obj_t *card = __make_card(body); + __make_button(card, __tr("设备联网", "Device network"), + GUIDED_ACTION_NETWORK, true, false); + __make_button(card, __tr("选择语言", "Language"), GUIDED_ACTION_LANGUAGE, true, false); + __make_button(card, __tr("关于设备", "About device"), GUIDED_ACTION_ABOUT, true, false); +} + +static void __render_language(void) +{ + lv_obj_t *body = __begin_page(__tr("语言", "Language"), true); + lv_obj_t *card = __make_card(body); + __make_label(card, __tr("选择界面语言", "Choose interface language"), + lv_color_hex(GUIDED_TEXT), LV_TEXT_ALIGN_CENTER); + __make_button(card, "中文", GUIDED_ACTION_LANG_ZH, + guided_wechat_locale_get() != GUIDED_WECHAT_LANG_ZH, false); + __make_button(card, "English", GUIDED_ACTION_LANG_EN, + guided_wechat_locale_get() != GUIDED_WECHAT_LANG_EN, false); +} + +static void __render_about(void) +{ + lv_obj_t *body = __begin_page(__tr("关于设备", "About device"), true); + lv_obj_t *card = __make_card(body); + lv_obj_set_style_pad_all(card, 0, 0); + lv_obj_set_style_pad_row(card, 0, 0); + __make_list_row(card, __tr("固件版本", "Firmware version"), sg_ui.state.version); + __make_list_row(card, "TuyaOpen SDK", sg_ui.state.sdk_version); + __make_list_row(card, "Product ID", sg_ui.state.product_id); + __make_list_row(card, "Device ID", sg_ui.state.device_id); + __make_list_row(card, "UUID", sg_ui.state.uuid_masked); + __make_list_row(card, "AuthKey", sg_ui.state.authkey_masked); + __make_list_row(card, __tr("Wi-Fi", "Wi-Fi"), + sg_ui.state.ssid[0] != '\0' ? sg_ui.state.ssid : "-"); + __make_list_row(card, __tr("云端状态", "Cloud state"), + sg_ui.state.cloud_online ? __tr("在线", "Online") : __tr("离线", "Offline")); +} + +static void __render_reset_confirm(void) +{ + lv_obj_t *body = __begin_page(__tr("重新配网", "Re-provision"), true); + lv_obj_t *card = __make_card(body); + __make_badge(card, __tr("确认重新配网", "Confirm reconfiguration"), false); + __make_label(card, + __tr("当前网络和设备绑定信息将被清除,设备重启后可以重新配网。", + "The current network and device binding will be cleared. The device will restart for setup."), + lv_color_hex(GUIDED_TEXT), LV_TEXT_ALIGN_LEFT); + sg_ui.status_label = __make_label(card, "", lv_color_hex(GUIDED_DANGER), LV_TEXT_ALIGN_CENTER); + __make_button(card, __tr("确认重配", "Confirm"), + GUIDED_ACTION_RESET_CONFIRM, false, true); +} + +static void __show_page(guided_page_t page, guided_page_t back_page) +{ + if (page == GUIDED_PAGE_MENU && !__setup_complete()) { + page = GUIDED_PAGE_NETWORK; + back_page = sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_AUTH_MISSING + : GUIDED_PAGE_AUTH_READY; + } + sg_ui.page = page; + sg_ui.back_page = back_page; + guided_wechat_state_read(&sg_ui.state); + lv_obj_clear_flag(sg_ui.overlay, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_ui.menu_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(sg_ui.overlay); + + switch (page) { + case GUIDED_PAGE_LANGUAGE_FIRST: + __render_language_first(); + break; + case GUIDED_PAGE_AUTH_MISSING: + __render_auth_missing(); + break; + case GUIDED_PAGE_AUTH_READY: + __render_auth_ready(); + break; + case GUIDED_PAGE_NETWORK: + __render_network(); + break; + case GUIDED_PAGE_MENU: + __render_menu(); + break; + case GUIDED_PAGE_LANGUAGE: + __render_language(); + break; + case GUIDED_PAGE_ABOUT: + __render_about(); + break; + case GUIDED_PAGE_RESET_CONFIRM: + __render_reset_confirm(); + break; + default: + break; + } +} + +static void __menu_button_cb(lv_event_t *event) +{ + (void)event; + if (__setup_complete()) { + __show_page(GUIDED_PAGE_MENU, GUIDED_PAGE_MENU); + } else { + __show_page(GUIDED_PAGE_NETWORK, GUIDED_PAGE_AUTH_READY); + } +} + +static void __refresh_cb(lv_timer_t *timer) +{ + guided_wechat_device_state_t next; + (void)timer; + + guided_wechat_state_read(&next); + if (guided_wechat_state_equal(&next, &sg_ui.state)) { + return; + } + sg_ui.state = next; + + if (sg_ui.page == GUIDED_PAGE_AUTH_MISSING && + sg_ui.state.auth_source != GUIDED_WECHAT_AUTH_NONE) { + __show_page(GUIDED_PAGE_AUTH_READY, GUIDED_PAGE_LANGUAGE_FIRST); + } else if (sg_ui.page == GUIDED_PAGE_NETWORK && __setup_complete()) { + if (sg_ui.back_page == GUIDED_PAGE_AUTH_READY) { + __enter_chat(); + } else { + __show_page(GUIDED_PAGE_NETWORK, sg_ui.back_page); + } + } else if (sg_ui.page == GUIDED_PAGE_AUTH_READY && + !sg_ui.state.activated && + sg_ui.state.pairing_state != GUIDED_WECHAT_PAIRING_IDLE) { + /* A bind event means the phone-provisioning flow has started. Keep + * the guided stepper visible even when the previous page was the + * startup authorization page. */ + __show_page(GUIDED_PAGE_NETWORK, GUIDED_PAGE_AUTH_READY); + } else if (!(lv_obj_has_flag(sg_ui.overlay, LV_OBJ_FLAG_HIDDEN)) && + (sg_ui.page == GUIDED_PAGE_NETWORK || sg_ui.page == GUIDED_PAGE_MENU || + sg_ui.page == GUIDED_PAGE_ABOUT)) { + __show_page(sg_ui.page, sg_ui.back_page); + } +} + +static OPERATE_RET __guided_ui_init(void) +{ + OPERATE_RET rt = OPRT_OK; + bool language_selected; + bool reprovision_pending; + lv_obj_t *menu_icon; + + TUYA_CALL_ERR_RETURN(guided_wechat_core_init()); + language_selected = guided_wechat_locale_load(); + reprovision_pending = guided_wechat_reprovision_pending(); + if (reprovision_pending) { + guided_wechat_reprovision_clear(); + } + guided_wechat_state_read(&sg_ui.state); + + lv_vendor_disp_lock(); + sg_ui.overlay = lv_obj_create(lv_scr_act()); + lv_obj_set_size(sg_ui.overlay, LV_HOR_RES, LV_VER_RES); + lv_obj_align(sg_ui.overlay, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_pad_all(sg_ui.overlay, 0, 0); + lv_obj_set_style_border_width(sg_ui.overlay, 0, 0); + lv_obj_set_style_radius(sg_ui.overlay, 0, 0); + lv_obj_set_style_bg_color(sg_ui.overlay, lv_color_hex(GUIDED_BG), 0); + lv_obj_clear_flag(sg_ui.overlay, LV_OBJ_FLAG_SCROLLABLE); + + sg_ui.menu_button = lv_btn_create(lv_scr_act()); + lv_obj_set_size(sg_ui.menu_button, 42, 42); + lv_obj_align(sg_ui.menu_button, LV_ALIGN_BOTTOM_LEFT, 18, -18); + lv_obj_set_style_radius(sg_ui.menu_button, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(sg_ui.menu_button, lv_color_white(), 0); + lv_obj_set_style_border_color(sg_ui.menu_button, lv_color_hex(GUIDED_BORDER), 0); + lv_obj_set_style_border_width(sg_ui.menu_button, 1, 0); + lv_obj_set_style_shadow_width(sg_ui.menu_button, 8, 0); + lv_obj_set_style_shadow_color(sg_ui.menu_button, lv_color_hex(0xB8BCC2), 0); + lv_obj_set_ext_click_area(sg_ui.menu_button, 8); + menu_icon = lv_label_create(sg_ui.menu_button); + lv_obj_set_style_text_font(menu_icon, ai_ui_get_icon_font(), 0); + lv_obj_set_style_text_color(menu_icon, lv_color_hex(GUIDED_TEXT), 0); + lv_label_set_text(menu_icon, FONT_AWESOME_GEAR); + lv_obj_center(menu_icon); + lv_obj_add_event_cb(sg_ui.menu_button, __menu_button_cb, LV_EVENT_CLICKED, NULL); + + if (reprovision_pending) { + __show_page(GUIDED_PAGE_NETWORK, + sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE + ? GUIDED_PAGE_AUTH_MISSING + : GUIDED_PAGE_AUTH_READY); + } else if (!language_selected) { + __show_page(GUIDED_PAGE_LANGUAGE_FIRST, GUIDED_PAGE_LANGUAGE_FIRST); + } else if (sg_ui.state.auth_source == GUIDED_WECHAT_AUTH_NONE) { + __show_page(GUIDED_PAGE_AUTH_MISSING, GUIDED_PAGE_LANGUAGE_FIRST); + } else if (!sg_ui.state.activated) { + __show_page(GUIDED_PAGE_AUTH_READY, GUIDED_PAGE_LANGUAGE_FIRST); + } else { + __enter_chat(); + } + + sg_ui.refresh_timer = lv_timer_create(__refresh_cb, 1000, NULL); + lv_vendor_disp_unlock(); + return OPRT_OK; +} + +OPERATE_RET ai_ui_chat_guided_wechat_register(void) +{ + return guided_wechat_register_with_init(__guided_ui_init); +} + +#endif /* ENABLE_AI_CHAT_GUI_GUIDED_WECHAT */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.h b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.h new file mode 100644 index 000000000..c28d9a026 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.h @@ -0,0 +1,21 @@ +/** + * @file ai_ui_chat_guided_wechat.h + * @brief Full WeChat UI decorated with first-run and device-management pages. + */ + +#ifndef __AI_UI_CHAT_GUIDED_WECHAT_H__ +#define __AI_UI_CHAT_GUIDED_WECHAT_H__ + +#include "tuya_cloud_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +OPERATE_RET ai_ui_chat_guided_wechat_register(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __AI_UI_CHAT_GUIDED_WECHAT_H__ */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.c b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.c new file mode 100644 index 000000000..bdd08a517 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.c @@ -0,0 +1,52 @@ +/** + * @file ai_ui_locale.c + * @brief Runtime language preference shared by all AI UI variants. + */ + +#include "tal_api.h" +#include "tal_kv.h" +#include "ai_ui_locale.h" + +#define AI_UI_LOCALE_KEY "guided_wechat_lang" + +static bool sg_english; + +bool ai_ui_locale_load(void) +{ + uint8_t *value = NULL; + size_t len = 0; + OPERATE_RET rt; + + sg_english = false; + rt = tal_kv_get(AI_UI_LOCALE_KEY, &value, &len); + if (rt != OPRT_OK || value == NULL || len == 0) { + if (value != NULL) { + tal_kv_free(value); + } + return false; + } + + sg_english = value[0] == '1'; + tal_kv_free(value); + return true; +} + +bool ai_ui_locale_is_english(void) +{ + return sg_english; +} + +void ai_ui_locale_set_english(bool english) +{ + uint8_t value = english ? '1' : '0'; + + sg_english = english; + if (OPRT_OK != tal_kv_set(AI_UI_LOCALE_KEY, &value, 1)) { + PR_WARN("AI UI language save failed"); + } +} + +const char *ai_ui_locale_tr(const char *zh, const char *en) +{ + return sg_english ? en : zh; +} diff --git a/src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.h b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.h new file mode 100644 index 000000000..72fb47f3a --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_locale.h @@ -0,0 +1,16 @@ +/** + * @file ai_ui_locale.h + * @brief Runtime language preference shared by the AI UI and onboarding UI. + */ + +#ifndef __AI_UI_LOCALE_H__ +#define __AI_UI_LOCALE_H__ + +#include + +bool ai_ui_locale_load(void); +bool ai_ui_locale_is_english(void); +void ai_ui_locale_set_english(bool english); +const char *ai_ui_locale_tr(const char *zh, const char *en); + +#endif /* __AI_UI_LOCALE_H__ */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c new file mode 100644 index 000000000..3bc277523 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c @@ -0,0 +1,963 @@ +/** + * @file guided_wechat_album.c + * @brief WeChat-style album page 鈥?single image view, all thumbnails grid + * with batch select/delete, and single-select for chat attachment. + * @copyright Copyright (c) 2021-2026 Tuya Inc. All Rights Reserved. + */ + +#include "tal_api.h" + +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) + +#include "lvgl.h" +#include "lv_vendor.h" + +#include "ai_ui_manage.h" +#include "ai_ui_icon_font.h" +#include "guided_wechat_common.h" +#include "tal_image.h" +#include "image_album.h" + +LV_IMG_DECLARE(icon_back_24_24); +LV_IMG_DECLARE(icon_photo_app); +LV_IMG_DECLARE(icon_choose); +LV_IMG_DECLARE(icon_delete); +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) +LV_IMG_DECLARE(icon_printer_app); +#endif +/*********************************************************** +************************macro define************************ +***********************************************************/ +#define CTRL_BAR_HEIGHT 48 +#define THUMB_SIZE 80 +#define THUMB_GAP 8 + +/*********************************************************** +***********************typedef define*********************** +***********************************************************/ +typedef struct { + /* Image view */ + lv_obj_t *view_page; + lv_obj_t *view_canvas; + lv_obj_t *view_empty_label; + lv_obj_t *view_delete_btn; + uint8_t *view_buf; + char view_current_name[ALBUM_FILENAME_MAX_LEN + 1]; + + /* All thumbnails */ + lv_obj_t *all_page; + lv_obj_t *all_grid; + lv_obj_t *all_empty_label; + lv_obj_t *all_select_btn; + lv_obj_t *all_bottom_bar; + bool all_select_mode; + uint8_t all_select_count; + + /* Select (chat attachment) */ + lv_obj_t *select_page; + lv_obj_t *select_grid; + lv_obj_t *select_empty_label; + +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + lv_obj_t *print_overlay; + lv_obj_t *print_status_label; + lv_obj_t *print_btn_row; + lv_timer_t *print_result_tm; +#endif +} GUIDED_WECHAT_ALBUM_T; + +/*********************************************************** +***********************variable define********************** +***********************************************************/ +static GUIDED_WECHAT_ALBUM_T sg_album = {0}; + +/*********************************************************** +***********************function define********************** +***********************************************************/ + +/* 鈹€鈹€ view page callbacks 鈹€鈹€ */ + +static void __view_gesture_cb(lv_event_t *e) +{ + (void)e; + lv_indev_t *indev = lv_indev_active(); + if (indev == NULL) return; + lv_dir_t dir = lv_indev_get_gesture_dir(indev); + if (dir == LV_DIR_LEFT) { + ai_ui_notify_action(AI_UI_ACT_VIEW_NEXT_IMG, NULL, 0); + } else if (dir == LV_DIR_RIGHT) { + ai_ui_notify_action(AI_UI_ACT_VIEW_PREV_IMG, NULL, 0); + } +} + +static void __view_all_btn_cb(lv_event_t *e) +{ + (void)e; + ai_ui_notify_action(AI_UI_ACT_VIEW_ALL_IMG, NULL, 0); +} + +static void __view_delete_btn_cb(lv_event_t *e) +{ + (void)e; + if (sg_album.view_current_name[0] == '\0') { + return; + } + ai_ui_notify_action(AI_UI_ACT_DELETE_IMG, + (uint8_t *)sg_album.view_current_name, + (uint32_t)strlen(sg_album.view_current_name)); +} + +static void __view_close_btn_cb(lv_event_t *e) +{ + (void)e; + ai_ui_notify_action(AI_UI_ACT_CLOSE_ALBUM, NULL, 0); +} + +/* 鈹€鈹€ all page callbacks 鈹€鈹€ */ + +static void __all_back_btn_cb(lv_event_t *e) +{ + (void)e; + ai_ui_notify_action(AI_UI_ACT_CLOSE_ALBUM, NULL, 0); +} + +static void __all_enter_select_mode(void) +{ + lv_vendor_disp_lock(); + + sg_album.all_select_mode = true; + sg_album.all_select_count = 0; + + lv_obj_add_flag(sg_album.all_select_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_album.all_bottom_bar, LV_OBJ_FLAG_HIDDEN); + + /* Show circle indicator on every thumbnail */ + uint32_t cnt = lv_obj_get_child_cnt(sg_album.all_grid); + uint32_t i; + for (i = 0; i < cnt; i++) { + lv_obj_t *item = lv_obj_get_child(sg_album.all_grid, i); + if (!item) continue; + uint32_t n = lv_obj_get_child_cnt(item); + if (n == 0) continue; + lv_obj_t *circle = lv_obj_get_child(item, n - 1); + if (circle) lv_obj_clear_flag(circle, LV_OBJ_FLAG_HIDDEN); + } + + lv_vendor_disp_unlock(); +} + +static void __all_exit_select_mode(void) +{ + lv_vendor_disp_lock(); + + sg_album.all_select_mode = false; + sg_album.all_select_count = 0; + + lv_obj_clear_flag(sg_album.all_select_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.all_bottom_bar, LV_OBJ_FLAG_HIDDEN); + + /* Hide circle indicators and clear their selection state */ + uint32_t cnt = lv_obj_get_child_cnt(sg_album.all_grid); + uint32_t i; + for (i = 0; i < cnt; i++) { + lv_obj_t *item = lv_obj_get_child(sg_album.all_grid, i); + if (!item) continue; + uint32_t n = lv_obj_get_child_cnt(item); + if (n == 0) continue; + lv_obj_t *circle = lv_obj_get_child(item, n - 1); + if (circle) { + lv_obj_add_flag(circle, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_state(circle, LV_STATE_CHECKED); + lv_obj_set_style_bg_opa(circle, LV_OPA_TRANSP, 0); + } + } + + lv_vendor_disp_unlock(); +} + +static void __all_select_btn_cb(lv_event_t *e) +{ + (void)e; + __all_enter_select_mode(); +} + +static void __all_item_toggle_cb(lv_event_t *e) +{ + lv_obj_t *container = lv_event_get_target(e); + uint32_t cnt = lv_obj_get_child_cnt(container); + if (cnt == 0) return; + + /* Circle indicator is always the last child of the container */ + lv_obj_t *circle = lv_obj_get_child(container, cnt - 1); + if (!circle) return; + + if (lv_obj_get_state(circle) & LV_STATE_CHECKED) { + lv_obj_clear_state(circle, LV_STATE_CHECKED); + lv_obj_set_style_bg_opa(circle, LV_OPA_TRANSP, 0); + if (sg_album.all_select_count > 0) sg_album.all_select_count--; + } else { + if (sg_album.all_select_count >= AI_UI_BATCH_DELETE_MAX) { + return; + } + lv_obj_add_state(circle, LV_STATE_CHECKED); + lv_obj_set_style_bg_color(circle, lv_color_white(), 0); + lv_obj_set_style_bg_opa(circle, LV_OPA_COVER, 0); + sg_album.all_select_count++; + } +} + +static void __all_select_delete_cb(lv_event_t *e) +{ + (void)e; + + AI_UI_BATCH_DELETE_T *batch = (AI_UI_BATCH_DELETE_T *)Malloc(sizeof(AI_UI_BATCH_DELETE_T)); + if (NULL == batch) { + return; + } + memset(batch, 0, sizeof(AI_UI_BATCH_DELETE_T)); + + /* Collect selected items and their LVGL object pointers */ + lv_obj_t *to_delete[AI_UI_BATCH_DELETE_MAX]; + uint32_t del_cnt = 0; + uint32_t child_cnt = lv_obj_get_child_cnt(sg_album.all_grid); + uint32_t i; + + for (i = 0; i < child_cnt && batch->count < AI_UI_BATCH_DELETE_MAX; i++) { + lv_obj_t *item = lv_obj_get_child(sg_album.all_grid, i); + if (!item) continue; + uint32_t cnt = lv_obj_get_child_cnt(item); + if (cnt == 0) continue; + lv_obj_t *circle = lv_obj_get_child(item, cnt - 1); + if (!circle) continue; + if (!(lv_obj_get_state(circle) & LV_STATE_CHECKED)) continue; + + char *name = (char *)lv_obj_get_user_data(item); + if (name && name[0]) { + strncpy(batch->names[batch->count], name, AI_UI_BATCH_DELETE_NAME_LEN); + batch->names[batch->count][AI_UI_BATCH_DELETE_NAME_LEN] = '\0'; + batch->count++; + to_delete[del_cnt++] = item; + } + } + + if (batch->count == 0) { + Free(batch); + __all_exit_select_mode(); + return; + } + + /* Remove selected thumbnails from the grid immediately */ + lv_vendor_disp_lock(); + for (i = 0; i < del_cnt; i++) { + lv_obj_del(to_delete[i]); + } + if (lv_obj_get_child_cnt(sg_album.all_grid) == 0) { + lv_obj_clear_flag(sg_album.all_empty_label, LV_OBJ_FLAG_HIDDEN); + } + lv_vendor_disp_unlock(); + + /* Restore normal mode UI on remaining items */ + __all_exit_select_mode(); + + /* Notify app to delete files from filesystem */ + ai_ui_notify_action(AI_UI_ACT_BATCH_DELETE_IMG, + (uint8_t *)batch, sizeof(AI_UI_BATCH_DELETE_T)); + Free(batch); +} + +/* 鈹€鈹€ select page callbacks (chat attachment) 鈹€鈹€ */ + +static void __select_cancel_btn_cb(lv_event_t *e) +{ + (void)e; + ai_ui_notify_action(AI_UI_ACT_CLOSE_ALBUM, NULL, 0); +} + +static void __select_item_click_cb(lv_event_t *e) +{ + lv_obj_t *target = lv_event_get_target(e); + char *name = (char *)lv_obj_get_user_data(target); + if (name != NULL) { + ai_ui_notify_action(AI_UI_ACT_ADD_IMG_ATTACH, + (uint8_t *)name, + (uint32_t)strlen(name)); + } + ai_ui_notify_action(AI_UI_ACT_CLOSE_ALBUM, NULL, 0); +} + +/* 鈹€鈹€ helper: create an image icon button 鈹€鈹€ */ + +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) +static void __view_print_btn_cb(lv_event_t *e) +{ + (void)e; + if (sg_album.view_current_name[0] == '\0') { + return; + } + lv_obj_clear_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(sg_album.print_overlay); +} + +static void __print_cancel_btn_cb(lv_event_t *e) +{ + (void)e; + lv_obj_add_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); +} + +static void __print_confirm_btn_cb(lv_event_t *e) +{ + (void)e; + /* Switch overlay to "printing" state 鈥?keep it visible so the user sees feedback */ + lv_label_set_text(sg_album.print_status_label, PRINTING); + lv_obj_add_flag(sg_album.print_btn_row, LV_OBJ_FLAG_HIDDEN); + ai_ui_notify_action(AI_UI_ACT_PRINT_IMG, + (uint8_t *)sg_album.view_current_name, + (uint32_t)strlen(sg_album.view_current_name)); +} + +static void __print_result_timeout_cb(lv_timer_t *timer) +{ + (void)timer; + lv_timer_del(sg_album.print_result_tm); + sg_album.print_result_tm = NULL; + lv_obj_add_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); + /* Restore overlay to confirmation state for next use */ + lv_label_set_text(sg_album.print_status_label, PRINT_IMAGE); + lv_obj_clear_flag(sg_album.print_btn_row, LV_OBJ_FLAG_HIDDEN); +} + +static void __disp_print_result(bool ok) +{ + lv_vendor_disp_lock(); + lv_label_set_text(sg_album.print_status_label, ok ? PRINT_SUCCESS : PRINT_FAILED); + if (sg_album.print_result_tm == NULL) { + sg_album.print_result_tm = lv_timer_create(__print_result_timeout_cb, 2000, NULL); + } else { + lv_timer_reset(sg_album.print_result_tm); + } + lv_vendor_disp_unlock(); +} +#endif /* ENABLE_PRINTER */ + +static lv_obj_t *__create_icon_btn(lv_obj_t *parent, const lv_img_dsc_t *src, + lv_event_cb_t cb) +{ + lv_obj_t *btn = lv_obj_create(parent); + lv_obj_set_size(btn, 40, 40); + lv_obj_set_style_bg_color(btn, lv_color_black(), 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_50, 0); + lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(btn, 0, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE); + + lv_obj_t *img = lv_img_create(btn); + lv_img_set_src(img, src); + lv_obj_set_style_img_recolor(img, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(img, LV_OPA_COVER, 0); + lv_obj_center(img); + + lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, NULL); + + return btn; +} + +/* 鈹€鈹€ sub-page creation 鈹€鈹€ */ + +static void __create_view_page(lv_obj_t *parent) +{ + lv_coord_t page_w = LV_HOR_RES; + lv_coord_t page_h = LV_VER_RES; + + /* View page container */ + sg_album.view_page = lv_obj_create(parent); + lv_obj_set_size(sg_album.view_page, page_w, page_h); + lv_obj_set_style_bg_color(sg_album.view_page, lv_color_black(), 0); + lv_obj_set_style_bg_opa(sg_album.view_page, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(sg_album.view_page, 0, 0); + lv_obj_set_style_pad_all(sg_album.view_page, 0, 0); + lv_obj_set_style_radius(sg_album.view_page, 0, 0); + lv_obj_set_scrollbar_mode(sg_album.view_page, LV_SCROLLBAR_MODE_OFF); + lv_obj_clear_flag(sg_album.view_page, LV_OBJ_FLAG_SCROLLABLE); + /* Stop gesture bubbling here so swipes fire on view_page, not the root screen */ + lv_obj_clear_flag(sg_album.view_page, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_set_pos(sg_album.view_page, 0, 0); + lv_obj_add_flag(sg_album.view_page, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.view_page, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(sg_album.view_page, __view_gesture_cb, LV_EVENT_GESTURE, NULL); + + /* Canvas for full-screen image display. + * lv_canvas_constructor clears CLICKABLE 鈥?touches pass through to view_page, + * and GESTURE_BUBBLE (default) means any canvas gesture bubbles up to view_page. */ + sg_album.view_canvas = lv_canvas_create(sg_album.view_page); + lv_obj_center(sg_album.view_canvas); + + /* Empty state label */ + sg_album.view_empty_label = lv_label_create(sg_album.view_page); + lv_obj_set_style_text_color(sg_album.view_empty_label, lv_color_white(), 0); + lv_label_set_text(sg_album.view_empty_label, NO_IMAGE); + lv_obj_center(sg_album.view_empty_label); + lv_obj_add_flag(sg_album.view_empty_label, LV_OBJ_FLAG_HIDDEN); + + /* Back button 鈥?top-left, inset from corner for rounded screens */ + lv_obj_t *view_back_btn = lv_obj_create(sg_album.view_page); + lv_obj_set_size(view_back_btn, 40, 40); + lv_obj_align(view_back_btn, LV_ALIGN_TOP_LEFT, 4 + WECHAT_SAFE_INSET, 4 + WECHAT_SAFE_INSET); + lv_obj_set_style_bg_opa(view_back_btn, LV_OPA_50, 0); + lv_obj_set_style_bg_color(view_back_btn, lv_color_black(), 0); + lv_obj_set_style_radius(view_back_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(view_back_btn, 0, 0); + lv_obj_set_style_pad_all(view_back_btn, 0, 0); + lv_obj_clear_flag(view_back_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(view_back_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(view_back_btn, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_set_ext_click_area(view_back_btn, WECHAT_BTN_EXT_CLICK); + lv_obj_add_event_cb(view_back_btn, __view_close_btn_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *back_icon = lv_img_create(view_back_btn); + lv_img_set_src(back_icon, &icon_back_24_24); + lv_obj_set_style_img_recolor(back_icon, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(back_icon, LV_OPA_COVER, 0); + lv_obj_center(back_icon); + + /* Delete button 鈥?bottom-left, inset from corner for rounded screens */ + sg_album.view_delete_btn = lv_obj_create(sg_album.view_page); + lv_obj_set_size(sg_album.view_delete_btn, 40, 40); + lv_obj_align(sg_album.view_delete_btn, LV_ALIGN_BOTTOM_LEFT, 8 + WECHAT_SAFE_INSET, -16 - WECHAT_SAFE_INSET); + lv_obj_set_style_bg_opa(sg_album.view_delete_btn, LV_OPA_50, 0); + lv_obj_set_style_bg_color(sg_album.view_delete_btn, lv_color_black(), 0); + lv_obj_set_style_radius(sg_album.view_delete_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(sg_album.view_delete_btn, 0, 0); + lv_obj_set_style_pad_all(sg_album.view_delete_btn, 0, 0); + lv_obj_clear_flag(sg_album.view_delete_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_album.view_delete_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(sg_album.view_delete_btn, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_set_ext_click_area(sg_album.view_delete_btn, WECHAT_BTN_EXT_CLICK); + lv_obj_add_event_cb(sg_album.view_delete_btn, __view_delete_btn_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *del_icon = lv_img_create(sg_album.view_delete_btn); + lv_img_set_src(del_icon, &icon_delete); + lv_obj_set_style_img_recolor(del_icon, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(del_icon, LV_OPA_COVER, 0); + lv_obj_center(del_icon); + + /* "鎵€鏈夌収鐗?>" button 鈥?bottom-right, text style with semi-transparent bg */ + lv_obj_t *all_photos_btn = lv_obj_create(sg_album.view_page); + lv_obj_remove_style_all(all_photos_btn); + lv_obj_set_size(all_photos_btn, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_bg_color(all_photos_btn, lv_color_black(), 0); + lv_obj_set_style_bg_opa(all_photos_btn, LV_OPA_50, 0); + lv_obj_set_style_radius(all_photos_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_pad_left(all_photos_btn, 12, 0); + lv_obj_set_style_pad_right(all_photos_btn, 12, 0); + lv_obj_set_style_pad_top(all_photos_btn, 8, 0); + lv_obj_set_style_pad_bottom(all_photos_btn, 8, 0); + lv_obj_add_flag(all_photos_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(all_photos_btn, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_set_ext_click_area(all_photos_btn, WECHAT_BTN_EXT_CLICK); + lv_obj_add_event_cb(all_photos_btn, __view_all_btn_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *all_photos_label = lv_label_create(all_photos_btn); + lv_obj_set_style_text_color(all_photos_label, lv_color_white(), 0); + lv_label_set_text_fmt(all_photos_label, "%s >", ALL_PHOTOS); + lv_obj_center(all_photos_label); + + /* Align after label text is set so LV_SIZE_CONTENT is computed correctly */ + lv_obj_align(all_photos_btn, LV_ALIGN_BOTTOM_RIGHT, -8 - WECHAT_SAFE_INSET, -12 - WECHAT_SAFE_INSET); + +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + /* Print button 鈥?top-right, inset from corner for rounded screens */ + lv_obj_t *print_btn = __create_icon_btn(sg_album.view_page, &icon_printer_app, + __view_print_btn_cb); + lv_obj_align(print_btn, LV_ALIGN_TOP_RIGHT, -4 - WECHAT_SAFE_INSET, 4 + WECHAT_SAFE_INSET); + lv_obj_add_flag(print_btn, LV_OBJ_FLAG_GESTURE_BUBBLE); + + /* Print confirmation overlay 鈥?full-screen semi-transparent panel */ + sg_album.print_overlay = lv_obj_create(sg_album.view_page); + lv_obj_set_size(sg_album.print_overlay, LV_HOR_RES, LV_VER_RES); + lv_obj_set_pos(sg_album.print_overlay, 0, 0); + lv_obj_set_style_bg_color(sg_album.print_overlay, lv_color_black(), 0); + lv_obj_set_style_bg_opa(sg_album.print_overlay, LV_OPA_70, 0); + lv_obj_set_style_border_width(sg_album.print_overlay, 0, 0); + lv_obj_set_style_radius(sg_album.print_overlay, 0, 0); + lv_obj_clear_flag(sg_album.print_overlay, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_flex_flow(sg_album.print_overlay, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(sg_album.print_overlay, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(sg_album.print_overlay, 24, 0); + + /* Title */ + lv_obj_t *print_title = lv_label_create(sg_album.print_overlay); + lv_obj_set_style_text_color(print_title, lv_color_white(), 0); + lv_label_set_text(print_title, PRINT_IMAGE); + sg_album.print_status_label = print_title; + + /* Button row */ + lv_obj_t *btn_row = lv_obj_create(sg_album.print_overlay); + lv_obj_remove_style_all(btn_row); + lv_obj_set_size(btn_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(btn_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(btn_row, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(btn_row, 24, 0); + sg_album.print_btn_row = btn_row; + + /* Cancel button */ + lv_obj_t *cancel_btn = lv_obj_create(btn_row); + lv_obj_set_size(cancel_btn, 80, 36); + lv_obj_set_style_bg_color(cancel_btn, lv_color_hex(0x555555), 0); + lv_obj_set_style_bg_opa(cancel_btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(cancel_btn, 8, 0); + lv_obj_set_style_border_width(cancel_btn, 0, 0); + lv_obj_set_style_pad_all(cancel_btn, 0, 0); + lv_obj_clear_flag(cancel_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(cancel_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_t *cancel_label = lv_label_create(cancel_btn); + lv_obj_set_style_text_color(cancel_label, lv_color_white(), 0); + lv_label_set_text(cancel_label, CANCEL); + lv_obj_center(cancel_label); + lv_obj_add_event_cb(cancel_btn, __print_cancel_btn_cb, LV_EVENT_CLICKED, NULL); + + /* Confirm button */ + lv_obj_t *confirm_btn = lv_obj_create(btn_row); + lv_obj_set_size(confirm_btn, 80, 36); + lv_obj_set_style_bg_color(confirm_btn, lv_color_hex(0x07C160), 0); + lv_obj_set_style_bg_opa(confirm_btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(confirm_btn, 8, 0); + lv_obj_set_style_border_width(confirm_btn, 0, 0); + lv_obj_set_style_pad_all(confirm_btn, 0, 0); + lv_obj_clear_flag(confirm_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(confirm_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_t *confirm_label = lv_label_create(confirm_btn); + lv_obj_set_style_text_color(confirm_label, lv_color_white(), 0); + lv_label_set_text(confirm_label, CONFIRM_TEXT); + lv_obj_center(confirm_label); + lv_obj_add_event_cb(confirm_btn, __print_confirm_btn_cb, LV_EVENT_CLICKED, NULL); +#endif /* ENABLE_PRINTER */ +} + +static void __create_all_page(lv_obj_t *parent) +{ + lv_coord_t page_w = LV_HOR_RES; + lv_coord_t page_h = LV_VER_RES; + + sg_album.all_page = lv_obj_create(parent); + lv_obj_set_size(sg_album.all_page, page_w, page_h); + lv_obj_set_style_bg_color(sg_album.all_page, lv_color_white(), 0); + lv_obj_set_style_bg_opa(sg_album.all_page, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(sg_album.all_page, 0, 0); + lv_obj_set_style_pad_all(sg_album.all_page, 0, 0); + lv_obj_set_style_radius(sg_album.all_page, 0, 0); + lv_obj_set_scrollbar_mode(sg_album.all_page, LV_SCROLLBAR_MODE_OFF); + lv_obj_add_flag(sg_album.all_page, LV_OBJ_FLAG_HIDDEN); + + /* Back button in top-left, inset from corner for rounded screens */ + lv_obj_t *back_btn = lv_obj_create(sg_album.all_page); + lv_obj_set_size(back_btn, 40, 40); + lv_obj_align(back_btn, LV_ALIGN_TOP_LEFT, 4 + WECHAT_SAFE_INSET, 4 + WECHAT_SAFE_INSET); + lv_obj_set_style_bg_color(back_btn, lv_color_black(), 0); + lv_obj_set_style_bg_opa(back_btn, LV_OPA_50, 0); + lv_obj_set_style_radius(back_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(back_btn, 0, 0); + lv_obj_set_style_pad_all(back_btn, 0, 0); + lv_obj_clear_flag(back_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(back_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(back_btn, WECHAT_BTN_EXT_CLICK); + + lv_obj_t *back_img = lv_img_create(back_btn); + lv_img_set_src(back_img, &icon_back_24_24); + lv_obj_set_style_img_recolor(back_img, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(back_img, LV_OPA_COVER, 0); + lv_obj_center(back_img); + lv_obj_add_event_cb(back_btn, __all_back_btn_cb, LV_EVENT_CLICKED, NULL); + + /* "閫夋嫨" icon button in top-right, inset from corner for rounded screens */ + sg_album.all_select_btn = __create_icon_btn(sg_album.all_page, + &icon_choose, __all_select_btn_cb); + lv_obj_align(sg_album.all_select_btn, LV_ALIGN_TOP_RIGHT, -4 - WECHAT_SAFE_INSET, 4 + WECHAT_SAFE_INSET); + lv_obj_set_ext_click_area(sg_album.all_select_btn, WECHAT_BTN_EXT_CLICK); + + /* Scrollable grid container 鈥?fixed position below top bar, never moves */ + sg_album.all_grid = lv_obj_create(sg_album.all_page); + lv_obj_set_pos(sg_album.all_grid, 0, CTRL_BAR_HEIGHT); + lv_obj_set_size(sg_album.all_grid, page_w, page_h - CTRL_BAR_HEIGHT); + lv_obj_set_style_bg_opa(sg_album.all_grid, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(sg_album.all_grid, 0, 0); + lv_obj_set_style_radius(sg_album.all_grid, 0, 0); + lv_obj_set_style_pad_row(sg_album.all_grid, THUMB_GAP, 0); + lv_obj_set_style_pad_column(sg_album.all_grid, THUMB_GAP, 0); + lv_obj_set_style_pad_top(sg_album.all_grid, THUMB_GAP, 0); + lv_obj_set_style_pad_bottom(sg_album.all_grid, THUMB_GAP, 0); + /* Center thumbnails: compute equal left/right padding so the grid is symmetric */ + { + lv_coord_t cols = (page_w + THUMB_GAP) / (THUMB_SIZE + THUMB_GAP); + if (cols < 1) cols = 1; + lv_coord_t content_w = cols * THUMB_SIZE + (cols - 1) * THUMB_GAP; + lv_coord_t side_pad = (page_w - content_w) / 2; + if (side_pad < 0) side_pad = 0; + lv_obj_set_style_pad_left(sg_album.all_grid, side_pad, 0); + lv_obj_set_style_pad_right(sg_album.all_grid, side_pad, 0); + } + lv_obj_set_flex_flow(sg_album.all_grid, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_scrollbar_mode(sg_album.all_grid, LV_SCROLLBAR_MODE_AUTO); + lv_obj_set_scroll_dir(sg_album.all_grid, LV_DIR_VER); + + /* Empty state label */ + sg_album.all_empty_label = lv_label_create(sg_album.all_page); + lv_obj_set_style_text_color(sg_album.all_empty_label, lv_color_hex(0x333333), 0); + lv_label_set_text(sg_album.all_empty_label, NO_IMAGE); + lv_obj_center(sg_album.all_empty_label); + lv_obj_add_flag(sg_album.all_empty_label, LV_OBJ_FLAG_HIDDEN); + + /* Bottom bar for batch select mode (hidden by default) */ + sg_album.all_bottom_bar = lv_obj_create(sg_album.all_page); + lv_obj_set_size(sg_album.all_bottom_bar, page_w, CTRL_BAR_HEIGHT); + lv_obj_align(sg_album.all_bottom_bar, LV_ALIGN_BOTTOM_MID, 0, -WECHAT_SAFE_INSET); + lv_obj_set_style_bg_color(sg_album.all_bottom_bar, lv_color_hex(0x333333), 0); + lv_obj_set_style_bg_opa(sg_album.all_bottom_bar, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(sg_album.all_bottom_bar, 0, 0); + lv_obj_set_style_pad_all(sg_album.all_bottom_bar, 4, 0); + lv_obj_set_style_radius(sg_album.all_bottom_bar, 0, 0); + lv_obj_set_flex_flow(sg_album.all_bottom_bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(sg_album.all_bottom_bar, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_add_flag(sg_album.all_bottom_bar, LV_OBJ_FLAG_HIDDEN); + + /* Delete icon button 鈥?centered */ + lv_obj_t *del_btn = lv_obj_create(sg_album.all_bottom_bar); + lv_obj_remove_style_all(del_btn); + lv_obj_set_size(del_btn, 40, 40); + lv_obj_add_flag(del_btn, LV_OBJ_FLAG_CLICKABLE); + + lv_obj_t *del_img = lv_img_create(del_btn); + lv_img_set_src(del_img, &icon_delete); + lv_obj_set_style_img_recolor(del_img, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(del_img, LV_OPA_COVER, 0); + lv_obj_center(del_img); + lv_obj_add_event_cb(del_btn, __all_select_delete_cb, LV_EVENT_CLICKED, NULL); +} + +static void __create_select_page(lv_obj_t *parent) +{ + lv_coord_t page_w = LV_HOR_RES; + lv_coord_t page_h = LV_VER_RES; + + sg_album.select_page = lv_obj_create(parent); + lv_obj_set_size(sg_album.select_page, page_w, page_h); + lv_obj_set_style_bg_color(sg_album.select_page, lv_color_white(), 0); + lv_obj_set_style_bg_opa(sg_album.select_page, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(sg_album.select_page, 0, 0); + lv_obj_set_style_pad_all(sg_album.select_page, 0, 0); + lv_obj_set_style_radius(sg_album.select_page, 0, 0); + lv_obj_set_scrollbar_mode(sg_album.select_page, LV_SCROLLBAR_MODE_OFF); + lv_obj_add_flag(sg_album.select_page, LV_OBJ_FLAG_HIDDEN); + + /* Back button in top-left, inset from corner for rounded screens */ + lv_obj_t *select_back_btn = __create_icon_btn(sg_album.select_page, + &icon_back_24_24, __select_cancel_btn_cb); + lv_obj_align(select_back_btn, LV_ALIGN_TOP_LEFT, 4 + WECHAT_SAFE_INSET, 4 + WECHAT_SAFE_INSET); + lv_obj_set_ext_click_area(select_back_btn, WECHAT_BTN_EXT_CLICK); + + /* Scrollable grid container 鈥?fixed position below top bar, never moves */ + sg_album.select_grid = lv_obj_create(sg_album.select_page); + lv_obj_set_pos(sg_album.select_grid, 0, CTRL_BAR_HEIGHT); + lv_obj_set_size(sg_album.select_grid, page_w, page_h - CTRL_BAR_HEIGHT); + lv_obj_set_style_bg_opa(sg_album.select_grid, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(sg_album.select_grid, 0, 0); + lv_obj_set_style_radius(sg_album.select_grid, 0, 0); + lv_obj_set_style_pad_row(sg_album.select_grid, THUMB_GAP, 0); + lv_obj_set_style_pad_column(sg_album.select_grid, THUMB_GAP, 0); + lv_obj_set_style_pad_top(sg_album.select_grid, THUMB_GAP, 0); + lv_obj_set_style_pad_bottom(sg_album.select_grid, THUMB_GAP, 0); + /* Center thumbnails: compute equal left/right padding so the grid is symmetric */ + { + lv_coord_t cols = (page_w + THUMB_GAP) / (THUMB_SIZE + THUMB_GAP); + if (cols < 1) cols = 1; + lv_coord_t content_w = cols * THUMB_SIZE + (cols - 1) * THUMB_GAP; + lv_coord_t side_pad = (page_w - content_w) / 2; + if (side_pad < 0) side_pad = 0; + lv_obj_set_style_pad_left(sg_album.select_grid, side_pad, 0); + lv_obj_set_style_pad_right(sg_album.select_grid, side_pad, 0); + } + lv_obj_set_flex_flow(sg_album.select_grid, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_scrollbar_mode(sg_album.select_grid, LV_SCROLLBAR_MODE_AUTO); + lv_obj_set_scroll_dir(sg_album.select_grid, LV_DIR_VER); + + /* Empty state label */ + sg_album.select_empty_label = lv_label_create(sg_album.select_page); + lv_obj_set_style_text_color(sg_album.select_empty_label, lv_color_hex(0x333333), 0); + lv_label_set_text(sg_album.select_empty_label, NO_IMAGE); + lv_obj_center(sg_album.select_empty_label); + lv_obj_add_flag(sg_album.select_empty_label, LV_OBJ_FLAG_HIDDEN); +} + +/* 鈹€鈹€ album display callbacks 鈹€鈹€ */ + +static void __disp_open(void) +{ + lv_vendor_disp_lock(); + lv_obj_clear_flag(sg_album.view_page, LV_OBJ_FLAG_HIDDEN); + lv_vendor_disp_unlock(); +} + +static void __disp_image(AI_UI_IMG_T *img) +{ + if (NULL == img || NULL == img->data || 0 == img->len) { + /* No image to display (album empty after delete) 鈥?show empty state in-place */ + sg_album.view_current_name[0] = '\0'; + lv_vendor_disp_lock(); + lv_obj_add_flag(sg_album.view_canvas, LV_OBJ_FLAG_HIDDEN); + if (sg_album.view_buf) { + Free(sg_album.view_buf); + sg_album.view_buf = NULL; + } + lv_obj_clear_flag(sg_album.view_empty_label, LV_OBJ_FLAG_HIDDEN); +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + lv_obj_add_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); +#endif + lv_vendor_disp_unlock(); + return; + } + + /* Record current image name for delete */ + if (img->name) { + strncpy(sg_album.view_current_name, img->name, ALBUM_FILENAME_MAX_LEN); + sg_album.view_current_name[ALBUM_FILENAME_MAX_LEN] = '\0'; + } else { + sg_album.view_current_name[0] = '\0'; + } + + TAL_IMAGE_JPEG_INFO_T info = {0}; + OPERATE_RET rt = tal_image_jpeg_get_info(img->data, img->len, &info); + if (rt != OPRT_OK) { + PR_ERR("jpeg get info failed, rt:%d", rt); + return; + } + + /* Decode JPEG at original size */ + uint32_t buf_size = (uint32_t)info.width * info.height * 2; + uint8_t *rgb565_buf = (uint8_t *)Malloc(buf_size); + if (NULL == rgb565_buf) { + PR_ERR("malloc decode buf failed, size:%u", buf_size); + return; + } + + TAL_IMAGE_JPEG_OUTPUT_T out = {0}; + out.out_buf = rgb565_buf; + out.out_buf_size = buf_size; + out.out_width = info.width; + out.out_height = info.height; + + rt = tal_image_jpeg_decode_rgb565(img->data, img->len, &out); + if (rt != OPRT_OK) { + PR_ERR("jpeg decode failed, rt:%d", rt); + Free(rgb565_buf); + return; + } + + lv_vendor_disp_lock(); + + /* Free previous buffer */ + if (sg_album.view_buf) { + Free(sg_album.view_buf); + sg_album.view_buf = NULL; + } + + sg_album.view_buf = rgb565_buf; + lv_canvas_set_buffer(sg_album.view_canvas, sg_album.view_buf, + info.width, info.height, LV_COLOR_FORMAT_RGB565); + lv_obj_set_size(sg_album.view_canvas, info.width, info.height); + lv_obj_center(sg_album.view_canvas); + lv_obj_clear_flag(sg_album.view_canvas, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.view_empty_label, LV_OBJ_FLAG_HIDDEN); +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + lv_obj_add_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); +#endif + lv_vendor_disp_unlock(); +} + +static void __disp_all_img_thumb_list(AI_UI_IMG_T *item_arr, uint32_t arr_cnt) +{ + lv_vendor_disp_lock(); + + /* Show all page, hide others */ + lv_obj_clear_flag(sg_album.all_page, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.view_page, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.select_page, LV_OBJ_FLAG_HIDDEN); + + /* Restore normal mode UI (select/cancel are local-only, delete triggers rebuild) */ + lv_obj_clear_flag(sg_album.all_select_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.all_bottom_bar, LV_OBJ_FLAG_HIDDEN); + sg_album.all_select_count = 0; + + /* Clear old grid children */ + lv_obj_clean(sg_album.all_grid); + + if (NULL == item_arr || 0 == arr_cnt) { + lv_obj_clear_flag(sg_album.all_empty_label, LV_OBJ_FLAG_HIDDEN); + lv_vendor_disp_unlock(); + return; + } + + lv_obj_add_flag(sg_album.all_empty_label, LV_OBJ_FLAG_HIDDEN); + + /* Create thumbnail items */ + uint32_t i; + for (i = 0; i < arr_cnt; i++) { + AI_UI_IMG_T *item = &item_arr[i]; + + lv_obj_t *container = lv_obj_create(sg_album.all_grid); + lv_obj_set_size(container, THUMB_SIZE, THUMB_SIZE); + lv_obj_set_style_pad_all(container, 0, 0); + lv_obj_set_style_border_width(container, 0, 0); + lv_obj_set_style_radius(container, 2, 0); + lv_obj_set_style_bg_color(container, lv_color_black(), 0); + lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_user_data(container, (void *)item->name); + + if (item->data != NULL && item->width > 0 && item->height > 0) { + lv_obj_t *canvas = lv_canvas_create(container); + lv_canvas_set_buffer(canvas, item->data, + item->width, item->height, + LV_COLOR_FORMAT_RGB565); + lv_obj_center(canvas); + } + + lv_obj_add_flag(container, LV_OBJ_FLAG_CLICKABLE); + + /* Circle indicator 鈥?always created, hidden until select mode is entered */ + lv_obj_t *circle = lv_obj_create(container); + lv_obj_remove_style_all(circle); + lv_obj_set_size(circle, 22, 22); + lv_obj_align(circle, LV_ALIGN_TOP_LEFT, 3, 3); + lv_obj_set_style_radius(circle, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_color(circle, lv_color_white(), 0); + lv_obj_set_style_border_width(circle, 2, 0); + lv_obj_set_style_border_opa(circle, LV_OPA_COVER, 0); + lv_obj_set_style_bg_opa(circle, LV_OPA_TRANSP, 0); + lv_obj_clear_flag(circle, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(circle, LV_OBJ_FLAG_HIDDEN); + + lv_obj_add_event_cb(container, __all_item_toggle_cb, LV_EVENT_CLICKED, NULL); + } + + lv_vendor_disp_unlock(); +} + +static void __disp_select_img_thumb_list(AI_UI_IMG_T *item_arr, uint32_t arr_cnt, + uint8_t select_num_max) +{ + (void)select_num_max; + + lv_vendor_disp_lock(); + + /* Show select page, hide others */ + lv_obj_clear_flag(sg_album.select_page, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.view_page, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.all_page, LV_OBJ_FLAG_HIDDEN); + + /* Clear old grid children */ + lv_obj_clean(sg_album.select_grid); + + if (NULL == item_arr || 0 == arr_cnt) { + lv_obj_clear_flag(sg_album.select_empty_label, LV_OBJ_FLAG_HIDDEN); + lv_vendor_disp_unlock(); + return; + } + + lv_obj_add_flag(sg_album.select_empty_label, LV_OBJ_FLAG_HIDDEN); + + /* Create thumbnail items 鈥?single-click to select and close */ + uint32_t i; + for (i = 0; i < arr_cnt; i++) { + AI_UI_IMG_T *item = &item_arr[i]; + + lv_obj_t *container = lv_obj_create(sg_album.select_grid); + lv_obj_set_size(container, THUMB_SIZE, THUMB_SIZE); + lv_obj_set_style_pad_all(container, 0, 0); + lv_obj_set_style_border_width(container, 0, 0); + lv_obj_set_style_radius(container, 2, 0); + lv_obj_set_style_bg_color(container, lv_color_black(), 0); + lv_obj_set_scrollbar_mode(container, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_user_data(container, (void *)item->name); + + if (item->data != NULL && item->width > 0 && item->height > 0) { + lv_obj_t *canvas = lv_canvas_create(container); + lv_canvas_set_buffer(canvas, item->data, + item->width, item->height, + LV_COLOR_FORMAT_RGB565); + lv_obj_center(canvas); + } + + /* Tap to select as attachment and auto-close */ + lv_obj_add_flag(container, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(container, __select_item_click_cb, LV_EVENT_CLICKED, NULL); + } + + lv_vendor_disp_unlock(); +} + +static void __disp_close(void) +{ + lv_vendor_disp_lock(); + + lv_obj_add_flag(sg_album.view_page, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.all_page, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_album.select_page, LV_OBJ_FLAG_HIDDEN); + + /* Reset select mode */ + sg_album.all_select_mode = false; + + /* Free view buffer */ + if (sg_album.view_buf) { + Free(sg_album.view_buf); + sg_album.view_buf = NULL; + } + +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + lv_obj_add_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); +#endif + lv_vendor_disp_unlock(); +} + +/* 鈹€鈹€ public API 鈹€鈹€ */ + +/** + * @brief Initialize album sub-pages. Called once during wechat UI init + * (inside lv_vendor_disp_lock). + * + * @param parent Parent LVGL object (main container). + */ +void guided_wechat_album_init(lv_obj_t *parent) +{ + memset(&sg_album, 0, sizeof(GUIDED_WECHAT_ALBUM_T)); + + __create_view_page(parent); + __create_all_page(parent); + __create_select_page(parent); +} + +/** + * @brief Register album display callbacks into ai_ui_image_album. + */ +void guided_wechat_album_register(void) +{ + AI_UI_ALBUM_INTFS_T intfs; + memset(&intfs, 0, sizeof(AI_UI_ALBUM_INTFS_T)); + + intfs.disp_open = __disp_open; + intfs.disp_image = __disp_image; + intfs.disp_all_img_thumb_list = __disp_all_img_thumb_list; + intfs.disp_select_img_thumb_list = __disp_select_img_thumb_list; + intfs.disp_close = __disp_close; +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + intfs.disp_print_result = __disp_print_result; +#endif + + ai_ui_image_album_register(&intfs); +} + +#endif diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c new file mode 100644 index 000000000..ed0056130 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c @@ -0,0 +1,391 @@ +/** + * @file guided_wechat_camera.c + * @brief WeChat-style camera page 鈥?LVGL preview, shutter, thumbnail and close controls. + * @copyright Copyright (c) 2021-2026 Tuya Inc. All Rights Reserved. + */ + +#include "tal_api.h" + +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) + +#include "lvgl.h" +#include "lv_vendor.h" + +#include "ai_ui_manage.h" +#include "ai_ui_icon_font.h" +#include "font_awesome_symbols.h" +#include "guided_wechat_common.h" +#include "tal_image.h" + +LV_IMG_DECLARE(icon_back_24_24); +LV_IMG_DECLARE(icon_photo_app); +LV_IMG_DECLARE(icon_ai_camera_off); +LV_IMG_DECLARE(icon_ai_camera_on); + +/*********************************************************** +************************macro define************************ +***********************************************************/ +#define CAMERA_THUMB_SIZE 60 +#define CAMERA_SHUTTER_SIZE 72 +#define CAMERA_CLOSE_SIZE 48 + +#if defined(ENABLE_EXT_RAM) && (ENABLE_EXT_RAM == 1) +#define CAMERA_UI_MALLOC tal_psram_malloc +#define CAMERA_UI_FREE tal_psram_free +#else +#define CAMERA_UI_MALLOC tal_malloc +#define CAMERA_UI_FREE tal_free +#endif + +/*********************************************************** +***********************typedef define*********************** +***********************************************************/ +typedef struct { + lv_obj_t *page; + lv_obj_t *preview_canvas; + lv_obj_t *thumb_img; + lv_obj_t *shutter_btn; + lv_obj_t *close_btn; + lv_obj_t *ai_vision_btn; + uint8_t *preview_buf; + uint16_t preview_w; + uint16_t preview_h; + uint8_t *thumb_buf; +} GUIDED_WECHAT_CAMERA_T; + +/*********************************************************** +***********************variable define********************** +***********************************************************/ +static GUIDED_WECHAT_CAMERA_T sg_camera = {0}; + +/*********************************************************** +***********************function define********************** +***********************************************************/ + +/* -- button event callbacks -- */ + +static void __preview_click_cb(lv_event_t *e) +{ + (void)e; + if (lv_obj_has_flag(sg_camera.shutter_btn, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_clear_flag(sg_camera.shutter_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_camera.thumb_img, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_camera.close_btn, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(sg_camera.shutter_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_camera.thumb_img, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_camera.close_btn, LV_OBJ_FLAG_HIDDEN); + } +} + +static void __thumb_btn_click_cb(lv_event_t *e) +{ + (void)e; + ai_ui_notify_action(AI_UI_ACT_OPEN_ALBUM, NULL, 0); +} + +static void __shutter_btn_click_cb(lv_event_t *e) +{ + (void)e; + ai_ui_notify_action(AI_UI_ACT_TAKE_PHOTO, NULL, 0); +} + +static void __ai_vision_btn_click_cb(lv_event_t *e) +{ + (void)e; + lv_obj_t *btn = lv_event_get_target(e); + lv_obj_t *img = lv_obj_get_child(btn, 0); + if (lv_obj_get_state(btn) & LV_STATE_CHECKED) { + lv_obj_clear_state(btn, LV_STATE_CHECKED); + if (img) lv_img_set_src(img, &icon_ai_camera_off); + ai_ui_notify_action(AI_UI_ACT_CAMERA_AI_OFF, NULL, 0); + } else { + lv_obj_add_state(btn, LV_STATE_CHECKED); + if (img) lv_img_set_src(img, &icon_ai_camera_on); + ai_ui_notify_action(AI_UI_ACT_CAMERA_AI_ON, NULL, 0); + } +} + +static void __close_btn_click_cb(lv_event_t *e) +{ + (void)e; + ai_ui_notify_action(AI_UI_ACT_CLOSE_CAMER, NULL, 0); +} + +/* -- display interface callbacks (registered via AI_UI_CAMERA_INTFS_T) -- */ + +static void __disp_open(void) +{ + if (NULL == sg_camera.page) { + return; + } + + lv_vendor_disp_lock(); + lv_obj_clear_flag(sg_camera.page, LV_OBJ_FLAG_HIDDEN); + /* Always restore controls 鈥?they may have been hidden by a preview tap before close */ + lv_obj_clear_flag(sg_camera.shutter_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_camera.thumb_img, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_camera.close_btn, LV_OBJ_FLAG_HIDDEN); + /* Reset AI vision toggle to off on each camera open */ + lv_obj_clear_state(sg_camera.ai_vision_btn, LV_STATE_CHECKED); + lv_obj_t *ai_vision_img = lv_obj_get_child(sg_camera.ai_vision_btn, 0); + if (ai_vision_img) lv_img_set_src(ai_vision_img, &icon_ai_camera_off); + lv_vendor_disp_unlock(); +} + +static void __disp_yuv_flush(AI_UI_VIDEO_T *video) +{ + if (NULL == video || NULL == video->yuv422 || 0 == video->width || 0 == video->height) { + return; + } + + /* Allocate a new RGB565 buffer for this frame */ + uint32_t buf_size = (uint32_t)video->width * video->height * 2; + uint8_t *rgb565_buf = (uint8_t *)CAMERA_UI_MALLOC(buf_size); + if (NULL == rgb565_buf) { + PR_ERR("camera preview: malloc rgb565 failed, size=%u", buf_size); + return; + } + + /* Convert YUV422 to RGB565 (outside display lock) */ + TAL_IMAGE_YUV422_TO_RGB_T conv = {0}; + conv.in_buf = video->yuv422; + conv.in_width = video->width; + conv.in_height = video->height; + conv.out_buf = rgb565_buf; + conv.out_width = video->width; + conv.out_height = video->height; + + if (tal_image_convert_yuv422_to_rgb565(&conv) != OPRT_OK) { + PR_ERR("camera preview: yuv422 to rgb565 failed"); + CAMERA_UI_FREE(rgb565_buf); + return; + } + + lv_vendor_disp_lock(); + lv_canvas_set_buffer(sg_camera.preview_canvas, rgb565_buf, + video->width, video->height, LV_COLOR_FORMAT_RGB565); + + /* Swap buffer and update canvas */ + if (sg_camera.preview_buf) { + CAMERA_UI_FREE(sg_camera.preview_buf); + } + sg_camera.preview_buf = rgb565_buf; + + sg_camera.preview_w = video->width; + sg_camera.preview_h = video->height; + lv_obj_center(sg_camera.preview_canvas); + lv_vendor_disp_unlock(); +} + +static void __disp_set_thumbnail_jpeg(uint8_t *jpeg, uint32_t len) +{ + if (NULL == sg_camera.thumb_img) { + return; + } + + lv_vendor_disp_lock(); + + if (NULL == jpeg || 0 == len) { + /* No thumbnail available 鈥?show a default image icon */ + if (sg_camera.thumb_buf) { + CAMERA_UI_FREE(sg_camera.thumb_buf); + sg_camera.thumb_buf = NULL; + } + + /* Remove any previous canvas source and display icon instead */ + lv_obj_clean(sg_camera.thumb_img); + lv_obj_t *icon = lv_img_create(sg_camera.thumb_img); + lv_img_set_src(icon, &icon_photo_app); + lv_obj_set_style_img_recolor(icon, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(icon, LV_OPA_COVER, 0); + lv_obj_center(icon); + + lv_vendor_disp_unlock(); + return; + } + + /* Decode JPEG and scale to thumbnail in one step */ + TAL_IMAGE_JPEG_SCALE_IN_T scale_in = {0}; + scale_in.method = TAL_IMAGE_SCALE_MTH_NEAREST; + scale_in.mode = TAL_IMAGE_SCALE_MODE_SIZE; + scale_in.data = jpeg; + scale_in.size = len; + scale_in.out_width = CAMERA_THUMB_SIZE; + scale_in.out_height = CAMERA_THUMB_SIZE; + + TAL_IMAGE_SCALE_OUT_T scale_out = {0}; + OPERATE_RET rt = tal_image_jpeg_scale_rgb565(&scale_in, &scale_out); + if (rt != OPRT_OK) { + PR_ERR("jpeg scale failed, rt:%d", rt); + lv_vendor_disp_unlock(); + return; + } + + /* Copy to our own buffer so we can free with CAMERA_UI_FREE later */ + uint32_t thumb_buf_size = CAMERA_THUMB_SIZE * CAMERA_THUMB_SIZE * 2; + + if (sg_camera.thumb_buf) { + CAMERA_UI_FREE(sg_camera.thumb_buf); + sg_camera.thumb_buf = NULL; + } + + sg_camera.thumb_buf = (uint8_t *)CAMERA_UI_MALLOC(thumb_buf_size); + if (NULL == sg_camera.thumb_buf) { + PR_ERR("malloc thumb buf failed"); + tal_image_scale_buf_free(&scale_out); + lv_vendor_disp_unlock(); + return; + } + memcpy(sg_camera.thumb_buf, scale_out.buf, thumb_buf_size); + tal_image_scale_buf_free(&scale_out); + + /* Update thumbnail canvas */ + lv_obj_clean(sg_camera.thumb_img); + lv_obj_t *canvas = lv_canvas_create(sg_camera.thumb_img); + lv_obj_set_style_border_width(canvas, 0, 0); + lv_obj_set_style_pad_all(canvas, 0, 0); + lv_canvas_set_buffer(canvas, sg_camera.thumb_buf, + CAMERA_THUMB_SIZE, CAMERA_THUMB_SIZE, LV_COLOR_FORMAT_RGB565); + lv_obj_center(canvas); + + lv_vendor_disp_unlock(); +} + +static void __disp_close(void) +{ + if (NULL == sg_camera.page) { + return; + } + + lv_vendor_disp_lock(); + lv_obj_add_flag(sg_camera.page, LV_OBJ_FLAG_HIDDEN); + lv_vendor_disp_unlock(); + + if (sg_camera.preview_buf) { + CAMERA_UI_FREE(sg_camera.preview_buf); + sg_camera.preview_buf = NULL; + sg_camera.preview_w = 0; + sg_camera.preview_h = 0; + } +} + +/* -- public API -- */ + +/** + * @brief Create the camera page widgets (called once during UI init, inside lv_vendor_disp_lock). + * + * @param parent Parent LVGL object (typically the main container). + */ +void guided_wechat_camera_init(lv_obj_t *parent) +{ + + /* ---- page root ---- */ + sg_camera.page = lv_obj_create(parent); + lv_obj_remove_style_all(sg_camera.page); + lv_obj_set_size(sg_camera.page, LV_HOR_RES, LV_VER_RES); + lv_obj_set_pos(sg_camera.page, 0, 0); + lv_obj_set_style_bg_color(sg_camera.page, lv_color_black(), 0); + lv_obj_set_style_bg_opa(sg_camera.page, LV_OPA_COVER, 0); + lv_obj_add_flag(sg_camera.page, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_camera.page, LV_OBJ_FLAG_SCROLLABLE); + + /* ---- preview area (canvas, centered, full page size) ---- */ + sg_camera.preview_canvas = lv_canvas_create(sg_camera.page); + lv_obj_remove_style_all(sg_camera.preview_canvas); + lv_obj_center(sg_camera.preview_canvas); + lv_obj_add_flag(sg_camera.preview_canvas, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(sg_camera.preview_canvas, __preview_click_cb, LV_EVENT_CLICKED, NULL); + + /* ---- close button 鈥?top-left corner, inset for rounded screens ---- */ + sg_camera.close_btn = lv_obj_create(sg_camera.page); + lv_obj_set_size(sg_camera.close_btn, CAMERA_CLOSE_SIZE, CAMERA_CLOSE_SIZE); + lv_obj_align(sg_camera.close_btn, LV_ALIGN_TOP_LEFT, 4 + WECHAT_SAFE_INSET, 4 + WECHAT_SAFE_INSET); + lv_obj_set_style_bg_opa(sg_camera.close_btn, LV_OPA_50, 0); + lv_obj_set_style_bg_color(sg_camera.close_btn, lv_color_black(), 0); + lv_obj_set_style_radius(sg_camera.close_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(sg_camera.close_btn, 0, 0); + lv_obj_set_style_pad_all(sg_camera.close_btn, 0, 0); + lv_obj_clear_flag(sg_camera.close_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_camera.close_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(sg_camera.close_btn, WECHAT_BTN_EXT_CLICK); + lv_obj_add_event_cb(sg_camera.close_btn, __close_btn_click_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *close_icon = lv_img_create(sg_camera.close_btn); + lv_img_set_src(close_icon, &icon_back_24_24); + lv_obj_set_style_img_recolor(close_icon, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(close_icon, LV_OPA_COVER, 0); + lv_obj_center(close_icon); + + /* ---- shutter button 鈥?bottom-center, moved up ---- */ + sg_camera.shutter_btn = lv_obj_create(sg_camera.page); + lv_obj_set_size(sg_camera.shutter_btn, CAMERA_SHUTTER_SIZE, CAMERA_SHUTTER_SIZE); + lv_obj_align(sg_camera.shutter_btn, LV_ALIGN_BOTTOM_MID, 0, -56); + lv_obj_set_style_bg_color(sg_camera.shutter_btn, lv_color_white(), 0); + lv_obj_set_style_bg_opa(sg_camera.shutter_btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(sg_camera.shutter_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(sg_camera.shutter_btn, 3, 0); + lv_obj_set_style_border_color(sg_camera.shutter_btn, lv_color_hex(0xCCCCCC), 0); + lv_obj_set_style_pad_all(sg_camera.shutter_btn, 0, 0); + lv_obj_clear_flag(sg_camera.shutter_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_camera.shutter_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(sg_camera.shutter_btn, __shutter_btn_click_cb, LV_EVENT_CLICKED, NULL); + + /* ---- thumbnail button 鈥?right of shutter, clipped to rounded corners ---- */ + sg_camera.thumb_img = lv_obj_create(sg_camera.page); + lv_obj_set_size(sg_camera.thumb_img, CAMERA_THUMB_SIZE, CAMERA_THUMB_SIZE); + lv_obj_align_to(sg_camera.thumb_img, sg_camera.shutter_btn, LV_ALIGN_OUT_RIGHT_MID, 38, 28); + lv_obj_set_style_bg_color(sg_camera.thumb_img, lv_color_hex(0x333333), 0); + lv_obj_set_style_bg_opa(sg_camera.thumb_img, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(sg_camera.thumb_img, 2, 0); + lv_obj_set_style_border_color(sg_camera.thumb_img, lv_color_white(), 0); + lv_obj_set_style_radius(sg_camera.thumb_img, 8, 0); + lv_obj_set_style_pad_all(sg_camera.thumb_img, 0, 0); + lv_obj_set_style_clip_corner(sg_camera.thumb_img, true, 0); + lv_obj_clear_flag(sg_camera.thumb_img, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_camera.thumb_img, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(sg_camera.thumb_img, __thumb_btn_click_cb, LV_EVENT_CLICKED, NULL); + + /* Default icon for thumbnail */ + lv_obj_t *thumb_icon = lv_img_create(sg_camera.thumb_img); + lv_img_set_src(thumb_icon, &icon_photo_app); + lv_obj_set_style_img_recolor(thumb_icon, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(thumb_icon, LV_OPA_COVER, 0); + lv_obj_center(thumb_icon); + + /* ---- AI vision toggle button 鈥?top-right corner, inset for rounded screens ---- */ + sg_camera.ai_vision_btn = lv_obj_create(sg_camera.page); + lv_obj_set_size(sg_camera.ai_vision_btn, CAMERA_CLOSE_SIZE, CAMERA_CLOSE_SIZE); + lv_obj_align(sg_camera.ai_vision_btn, LV_ALIGN_TOP_RIGHT, -4 - WECHAT_SAFE_INSET, 4 + WECHAT_SAFE_INSET); + lv_obj_set_style_bg_color(sg_camera.ai_vision_btn, lv_color_black(), 0); + lv_obj_set_style_bg_opa(sg_camera.ai_vision_btn, LV_OPA_50, 0); + lv_obj_set_style_radius(sg_camera.ai_vision_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(sg_camera.ai_vision_btn, 0, 0); + lv_obj_set_style_pad_all(sg_camera.ai_vision_btn, 0, 0); + lv_obj_clear_flag(sg_camera.ai_vision_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_camera.ai_vision_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(sg_camera.ai_vision_btn, WECHAT_BTN_EXT_CLICK); + lv_obj_add_event_cb(sg_camera.ai_vision_btn, __ai_vision_btn_click_cb, LV_EVENT_CLICKED, NULL); + + lv_obj_t *ai_vision_icon = lv_img_create(sg_camera.ai_vision_btn); + lv_img_set_src(ai_vision_icon, &icon_ai_camera_off); + lv_obj_center(ai_vision_icon); +} + +/** + * @brief Register camera display callbacks with the camera management layer. + */ +void guided_wechat_camera_register(void) +{ + AI_UI_CAMERA_INTFS_T intfs; + memset(&intfs, 0, sizeof(AI_UI_CAMERA_INTFS_T)); + + intfs.disp_open = __disp_open; + intfs.disp_yuv_flush = __disp_yuv_flush; + intfs.disp_set_thumbnail_jpeg = __disp_set_thumbnail_jpeg; + intfs.disp_close = __disp_close; + + ai_ui_camera_register(&intfs); +} + +#endif /* ENABLE_AI_CHAT_GUI_GUIDED_WECHAT */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_chat.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_chat.c new file mode 100644 index 000000000..55a8ac9b5 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_chat.c @@ -0,0 +1,1286 @@ +/** + * @file guided_wechat_chat.c + * @brief WeChat-style chat page 鈥?message list, streaming, link display, image display. + * @copyright Copyright (c) 2021-2026 Tuya Inc. All Rights Reserved. + */ + +#include "tal_api.h" + +#if (defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1)) || \ + (defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1)) + +#include "lvgl.h" +#include "lv_vendor.h" + +#include "ai_ui_manage.h" +#include "ai_ui_icon_font.h" +#include "font_awesome_symbols.h" +#include "guided_wechat_common.h" +#include "tal_image.h" +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) +#include "image_album.h" +#endif +#ifndef ALBUM_FILENAME_MAX_LEN +#define ALBUM_FILENAME_MAX_LEN 64 +#endif +LV_IMG_DECLARE(icon_ai_icon); +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) +LV_IMG_DECLARE(icon_camera_app); +#endif +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) +LV_IMG_DECLARE(icon_photo_app); +LV_IMG_DECLARE(icon_add_img); +#endif +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) +LV_IMG_DECLARE(icon_printer_app); +#endif +/*********************************************************** +************************macro define************************ +***********************************************************/ +#define MAX_MESSAGE_NUM 20 +#define ATTACH_THUMB_SIZE 48 +#define ATTACH_BAR_HEIGHT 56 +#define MAX_ATTACH_NUM 4 +#define PLUS_BTN_SIZE 40 +#define POPUP_WIDTH 120 +#define POPUP_ITEM_H 36 +#define POPUP_ICON_SIZE 24 + + +/*********************************************************** +***********************typedef define*********************** +***********************************************************/ +typedef struct { + lv_style_t style_avatar; + lv_style_t style_ai_bubble; + lv_style_t style_user_bubble; + lv_style_t style_link; + + lv_obj_t *content; + lv_obj_t *picture; + lv_obj_t *picture_canvas; + + lv_obj_t *stream_msg_cont; + lv_obj_t *stream_bubble; + lv_obj_t *stream_label; + bool is_streaming; + + lv_obj_t *attach_bar; + uint8_t *attach_bufs[MAX_ATTACH_NUM]; + uint8_t attach_count; + + lv_obj_t *plus_btn; + lv_obj_t *popup_menu; + + char cur_img_name[ALBUM_FILENAME_MAX_LEN + 1]; + + lv_obj_t *picture_action_bar; + lv_obj_t *picture_print_btn; + lv_obj_t *picture_attach_btn; +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + lv_obj_t *picture_print_overlay; + lv_obj_t *picture_print_status_label; + lv_obj_t *picture_print_btn_row; + lv_timer_t *picture_print_result_tm; +#endif +} GUIDED_WECHAT_CHAT_T; + + +/* 鈹€鈹€ link click event callback 鈹€鈹€ */ + +typedef struct { + AI_UI_CHAT_LINK_CB cb; + void *cb_arg; + uint32_t len; +} UI_LINK_CB_DATA_T; + +/*********************************************************** +***********************variable define********************** +***********************************************************/ +static GUIDED_WECHAT_CHAT_T sg_chat = {0}; +static uint8_t *sg_picture_buffer = NULL; +static lv_timer_t *sg_image_auto_return_tm = NULL; + +/*********************************************************** +***********************function define********************** +***********************************************************/ + + +/** + * @brief Delete the oldest message when message count exceeds limit. + */ +static void __chat_check_msg_limit(void) +{ + uint32_t child_count = lv_obj_get_child_cnt(sg_chat.content); + if (child_count >= MAX_MESSAGE_NUM) { + lv_obj_t *first_child = lv_obj_get_child(sg_chat.content, 0); + if (first_child) { + lv_obj_del(first_child); + } + } +} + +/** + * @brief Create a message container with avatar on the AI side (left). + * + * @param parent Parent object. + * @param text Text to display in the bubble. + * @return Pointer to the label inside the bubble. + */ +static lv_obj_t *__create_ai_msg_label(lv_obj_t *parent, char *text) +{ + lv_obj_t *msg_cont = lv_obj_create(parent); + lv_obj_remove_style_all(msg_cont); + lv_obj_set_size(msg_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(msg_cont, 6, 0); + lv_obj_set_style_pad_column(msg_cont, 10, 0); + + lv_obj_t *avatar = lv_obj_create(msg_cont); + lv_obj_add_style(avatar, &sg_chat.style_avatar, 0); + lv_obj_set_size(avatar, 40, 40); + lv_obj_align(avatar, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_bg_opa(avatar, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(avatar, 0, 0); + lv_obj_clear_flag(avatar, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *icon = lv_img_create(avatar); + lv_img_set_src(icon, &icon_ai_icon); + lv_obj_center(icon); + + lv_obj_t *bubble = lv_obj_create(msg_cont); + lv_obj_set_width(bubble, LV_PCT(75)); + lv_obj_set_height(bubble, LV_SIZE_CONTENT); + lv_obj_add_style(bubble, &sg_chat.style_ai_bubble, 0); + lv_obj_align_to(bubble, avatar, LV_ALIGN_OUT_RIGHT_TOP, 10, 0); + + lv_obj_set_scrollbar_mode(bubble, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_scroll_dir(bubble, LV_DIR_NONE); + + lv_obj_t *text_cont = lv_obj_create(bubble); + lv_obj_remove_style_all(text_cont); + lv_obj_set_size(text_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(text_cont, LV_FLEX_FLOW_COLUMN); + + lv_obj_t *label = lv_label_create(text_cont); + lv_label_set_text(label, text); + lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + + lv_obj_scroll_to_view_recursive(msg_cont, LV_ANIM_ON); + + return label; +} + +/** + * @brief Create a message container with avatar on the user side (right). + * + * @param parent Parent object. + * @param text Text to display in the bubble. + * @return Pointer to the label inside the bubble. + */ +static lv_obj_t *__create_user_msg_label(lv_obj_t *parent, char *text) +{ + const lv_font_t *icon_font = ai_ui_get_icon_font(); + + lv_obj_t *msg_cont = lv_obj_create(parent); + lv_obj_remove_style_all(msg_cont); + lv_obj_set_size(msg_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(msg_cont, 6, 0); + lv_obj_set_style_pad_column(msg_cont, 10, 0); + + lv_obj_t *avatar = lv_obj_create(msg_cont); + lv_obj_set_style_text_font(avatar, icon_font, 0); + lv_obj_add_style(avatar, &sg_chat.style_avatar, 0); + lv_obj_set_size(avatar, 40, 40); + lv_obj_align(avatar, LV_ALIGN_TOP_RIGHT, 0, 0); + + lv_obj_t *icon = lv_label_create(avatar); + lv_label_set_text(icon, FONT_AWESOME_USER); + lv_obj_center(icon); + + lv_obj_t *bubble = lv_obj_create(msg_cont); + lv_obj_set_width(bubble, LV_PCT(75)); + lv_obj_set_height(bubble, LV_SIZE_CONTENT); + lv_obj_add_style(bubble, &sg_chat.style_user_bubble, 0); + lv_obj_align_to(bubble, avatar, LV_ALIGN_OUT_LEFT_TOP, -10, 0); + + lv_obj_set_scrollbar_mode(bubble, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_scroll_dir(bubble, LV_DIR_NONE); + + lv_obj_t *text_cont = lv_obj_create(bubble); + lv_obj_remove_style_all(text_cont); + lv_obj_set_size(text_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(text_cont, LV_FLEX_FLOW_COLUMN); + + lv_obj_t *label = lv_label_create(text_cont); + lv_label_set_text(label, text); + lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + + lv_obj_scroll_to_view_recursive(msg_cont, LV_ANIM_ON); + + return label; +} + +/* 鈹€鈹€ picture view event callbacks 鈹€鈹€ */ + +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) +static void __picture_print_result_timeout_cb(lv_timer_t *timer) +{ + (void)timer; + lv_timer_del(sg_chat.picture_print_result_tm); + sg_chat.picture_print_result_tm = NULL; + if (sg_chat.picture_print_overlay != NULL) { + lv_obj_add_flag(sg_chat.picture_print_overlay, LV_OBJ_FLAG_HIDDEN); + } + if (sg_chat.picture_print_status_label != NULL) { + lv_label_set_text(sg_chat.picture_print_status_label, PRINT_IMAGE); + } + if (sg_chat.picture_print_btn_row != NULL) { + lv_obj_clear_flag(sg_chat.picture_print_btn_row, LV_OBJ_FLAG_HIDDEN); + } +} + +static void __picture_print_cancel_btn_cb(lv_event_t *e) +{ + lv_event_stop_bubbling(e); + lv_obj_add_flag(sg_chat.picture_print_overlay, LV_OBJ_FLAG_HIDDEN); +} + +static void __picture_print_confirm_btn_cb(lv_event_t *e) +{ + lv_event_stop_bubbling(e); + lv_label_set_text(sg_chat.picture_print_status_label, PRINTING); + lv_obj_add_flag(sg_chat.picture_print_btn_row, LV_OBJ_FLAG_HIDDEN); + ai_ui_notify_action(AI_UI_ACT_PRINT_IMG, + (uint8_t *)sg_chat.cur_img_name, + (uint32_t)strlen(sg_chat.cur_img_name)); +} + +static void __picture_disp_print_result(bool ok) +{ + lv_vendor_disp_lock(); + /* The print overlay/labels are created lazily when the user first opens + * the picture page. If a print-result message arrives before that + * (e.g. printing was triggered from another path), the labels may be + * NULL. Skip the UI update silently in that case to avoid a NULL deref + * crash inside lv_label_set_text(). */ + if (sg_chat.picture_print_status_label == NULL) { + PR_NOTICE("picture: print result(%d) ignored, overlay not ready", ok); + lv_vendor_disp_unlock(); + return; + } + lv_label_set_text(sg_chat.picture_print_status_label, + ok ? PRINT_SUCCESS : PRINT_FAILED); + if (sg_chat.picture_print_result_tm == NULL) { + sg_chat.picture_print_result_tm = + lv_timer_create(__picture_print_result_timeout_cb, 2000, NULL); + } else { + lv_timer_reset(sg_chat.picture_print_result_tm); + } + lv_vendor_disp_unlock(); +} + +static void __picture_print_btn_cb(lv_event_t *e) +{ + PR_NOTICE("picture: print btn clicked, cur_img='%s'", sg_chat.cur_img_name); + lv_event_stop_bubbling(e); + if (sg_chat.cur_img_name[0] == '\0') { + PR_NOTICE("picture: print btn 鈥?no image name, abort"); + return; + } + lv_obj_clear_flag(sg_chat.picture_print_overlay, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(sg_chat.picture_print_overlay); +} +#endif + +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) +static void __picture_attach_btn_cb(lv_event_t *e) +{ + PR_NOTICE("picture: attach btn clicked, cur_img='%s'", sg_chat.cur_img_name); + lv_event_stop_bubbling(e); + if (sg_chat.cur_img_name[0] == '\0') { + PR_NOTICE("picture: attach btn 鈥?no image name, abort"); + return; + } + ai_ui_notify_action(AI_UI_ACT_ADD_IMG_ATTACH, + (uint8_t *)sg_chat.cur_img_name, + (uint32_t)strlen(sg_chat.cur_img_name)); + /* Return to chat so the attach bar update is visible */ + lv_obj_add_flag(sg_chat.picture, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_chat.content, LV_OBJ_FLAG_HIDDEN); +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) + lv_obj_clear_flag(sg_chat.plus_btn, LV_OBJ_FLAG_HIDDEN); +#endif + sg_chat.cur_img_name[0] = '\0'; +} +#endif + +/** + * @brief Click picture canvas 鈥?hide picture view, show chat content. + */ +static void __do_return_chat_content(void) +{ + lv_obj_add_flag(sg_chat.picture, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_chat.content, LV_OBJ_FLAG_HIDDEN); +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) + lv_obj_clear_flag(sg_chat.plus_btn, LV_OBJ_FLAG_HIDDEN); +#endif + sg_chat.cur_img_name[0] = '\0'; +} + +static void __image_auto_return_cb(lv_timer_t *timer) +{ + lv_timer_del(timer); + sg_image_auto_return_tm = NULL; + __do_return_chat_content(); +} + +static void __return_chat_content_event_cb(lv_event_t *e) +{ + PR_NOTICE("picture: return_chat triggered, target=%p picture=%p", + lv_event_get_target(e), sg_chat.picture); + if (sg_image_auto_return_tm != NULL) { + lv_timer_del(sg_image_auto_return_tm); + sg_image_auto_return_tm = NULL; + } + __do_return_chat_content(); +} + +/** + * @brief Generic link click handler 鈥?invokes the stored callback with argument. + */ +static void __link_click_event_cb(lv_event_t *e) +{ + UI_LINK_CB_DATA_T *data = (UI_LINK_CB_DATA_T *)lv_event_get_user_data(e); + if (data && data->cb) { + data->cb(data->cb_arg); + } +} + +/** + * @brief Free link callback data when the label object is deleted. + */ +static void __link_delete_event_cb(lv_event_t *e) +{ + UI_LINK_CB_DATA_T *data = (UI_LINK_CB_DATA_T *)lv_event_get_user_data(e); + if (data) { + /* Free the copied cb_arg (allocated when len > 0 in __ui_disp_link) */ + if (data->len > 0 && data->cb_arg) { + Free(data->cb_arg); + } + Free(data); + } +} + +/* 鈹€鈹€ "+" button and popup menu callbacks 鈹€鈹€ */ + +static void __popup_dismiss(void) +{ +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) + if (!lv_obj_has_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_add_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN); + } +#endif +} + +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) +static void __popup_camera_cb(lv_event_t *e) +{ + (void)e; + __popup_dismiss(); + ai_ui_notify_action(AI_UI_ACT_OPEN_CAMERA, NULL, 0); +} +#endif + +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) +static void __popup_album_cb(lv_event_t *e) +{ + (void)e; + __popup_dismiss(); + ai_ui_notify_action(AI_UI_ACT_OPEN_ALBUM, NULL, 0); +} + +static void __popup_add_img_cb(lv_event_t *e) +{ + (void)e; + __popup_dismiss(); + ai_ui_notify_action(AI_UI_ACT_OPEN_IMG_ATTACH_LIST, NULL, 0); +} +#endif + + +/** + * @brief Dismiss popup when user clicks on the chat content area. + */ +static void __content_click_cb(lv_event_t *e) +{ + (void)e; + __popup_dismiss(); +} + +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) +static void __plus_btn_click_cb(lv_event_t *e) +{ + (void)e; + PR_DEBUG("plus_click: enter"); + if (lv_obj_has_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_clear_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN); + lv_obj_align_to(sg_chat.popup_menu, sg_chat.plus_btn, LV_ALIGN_OUT_TOP_RIGHT, 0, -4); + lv_obj_move_foreground(sg_chat.popup_menu); + PR_DEBUG("plus_click: popup shown"); + } else { + __popup_dismiss(); + PR_DEBUG("plus_click: popup hidden"); + } +} +#endif /* ENABLE_LVGL_TP */ + +/* 鈹€鈹€ chat page callbacks (registered into AI_UI_INTFS_T) 鈹€鈹€ */ + +/** + * @brief Show chat content area, hide picture view. + */ +static void __ui_open_chat(void) +{ + lv_vendor_disp_lock(); + lv_obj_clear_flag(sg_chat.content, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_chat.picture, LV_OBJ_FLAG_HIDDEN); +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) + lv_obj_clear_flag(sg_chat.plus_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN); +#endif + lv_vendor_disp_unlock(); +} + +/** + * @brief Hide chat content area. + */ +static void __ui_close_chat(void) +{ + lv_vendor_disp_lock(); + lv_obj_add_flag(sg_chat.content, LV_OBJ_FLAG_HIDDEN); +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) + lv_obj_add_flag(sg_chat.plus_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN); +#endif + lv_vendor_disp_unlock(); +} + +/** + * @brief Display a user message bubble (right-aligned, green, user avatar). + * + * @param text User message text. + */ +static void __ui_set_user_msg(char *text) +{ + if (sg_chat.content == NULL || text == NULL || strlen(text) == 0) { + return; + } + + lv_vendor_disp_lock(); + + __chat_check_msg_limit(); + + __create_user_msg_label(sg_chat.content, text); + + lv_obj_update_layout(sg_chat.content); + + lv_vendor_disp_unlock(); +} + +/** + * @brief Display an AI message bubble (left-aligned, white, robot avatar). + * + * @param text AI message text. + */ +static void __ui_set_ai_msg(char *text) +{ + if (sg_chat.content == NULL || text == NULL || strlen(text) == 0) { + return; + } + + lv_vendor_disp_lock(); + + __chat_check_msg_limit(); + + __create_ai_msg_label(sg_chat.content, text); + + lv_obj_update_layout(sg_chat.content); + + lv_vendor_disp_unlock(); +} + +/** + * @brief Start streaming AI message 鈥?create empty AI bubble. + */ +static void __ui_set_ai_msg_stream_start(void) +{ + if (sg_chat.content == NULL) { + return; + } + + lv_vendor_disp_lock(); + + __chat_check_msg_limit(); + + sg_chat.stream_msg_cont = lv_obj_create(sg_chat.content); + lv_obj_remove_style_all(sg_chat.stream_msg_cont); + lv_obj_set_size(sg_chat.stream_msg_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(sg_chat.stream_msg_cont, 6, 0); + lv_obj_set_style_pad_column(sg_chat.stream_msg_cont, 10, 0); + + lv_obj_t *avatar = lv_obj_create(sg_chat.stream_msg_cont); + lv_obj_add_style(avatar, &sg_chat.style_avatar, 0); + lv_obj_set_size(avatar, 40, 40); + lv_obj_align(avatar, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_bg_opa(avatar, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(avatar, 0, 0); + lv_obj_clear_flag(avatar, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *icon = lv_img_create(avatar); + lv_img_set_src(icon, &icon_ai_icon); + lv_obj_center(icon); + + sg_chat.stream_bubble = lv_obj_create(sg_chat.stream_msg_cont); + lv_obj_set_width(sg_chat.stream_bubble, LV_PCT(75)); + lv_obj_set_height(sg_chat.stream_bubble, LV_SIZE_CONTENT); + lv_obj_add_style(sg_chat.stream_bubble, &sg_chat.style_ai_bubble, 0); + lv_obj_align_to(sg_chat.stream_bubble, avatar, LV_ALIGN_OUT_RIGHT_TOP, 10, 0); + lv_obj_set_scrollbar_mode(sg_chat.stream_bubble, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_scroll_dir(sg_chat.stream_bubble, LV_DIR_VER); + + lv_obj_t *text_cont = lv_obj_create(sg_chat.stream_bubble); + lv_obj_remove_style_all(text_cont); + lv_obj_set_size(text_cont, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(text_cont, LV_FLEX_FLOW_COLUMN); + + sg_chat.stream_label = lv_label_create(text_cont); + lv_label_set_text(sg_chat.stream_label, ""); + lv_obj_set_width(sg_chat.stream_label, LV_PCT(100)); + lv_label_set_long_mode(sg_chat.stream_label, LV_LABEL_LONG_WRAP); + + lv_vendor_disp_unlock(); + + sg_chat.is_streaming = true; +} + +/** + * @brief Append text to the streaming AI bubble. + * + * @param text Text chunk to append. + */ +static void __ui_set_ai_msg_stream_data(char *text) +{ + if (false == sg_chat.is_streaming) { + return; + } + + lv_vendor_disp_lock(); + + lv_label_ins_text(sg_chat.stream_label, LV_LABEL_POS_LAST, text); + + lv_coord_t content_height = lv_obj_get_height(sg_chat.stream_msg_cont); + lv_coord_t height = lv_obj_get_height(sg_chat.content); + + if (content_height > height) { + lv_coord_t offset = 0; + offset = lv_obj_get_scroll_bottom(sg_chat.content); + if (offset > 0) { + lv_obj_scroll_by_bounded(sg_chat.content, 0, -offset, LV_ANIM_OFF); + } + } else { + lv_obj_scroll_to_view_recursive(sg_chat.stream_msg_cont, LV_ANIM_OFF); + } + + lv_obj_update_layout(sg_chat.content); + + lv_vendor_disp_unlock(); +} + +/** + * @brief Mark streaming done. + */ +static void __ui_set_ai_msg_stream_end(void) +{ + sg_chat.is_streaming = false; +} + +/** + * @brief Decode JPEG and display on picture canvas, add "View Image" link in chat. + * + * @param jpeg Pointer to JPEG data. + * @param len Length of JPEG data. + */ +static void __ui_disp_image(AI_UI_IMG_T *img) +{ + if (img == NULL || img->data == NULL || img->len == 0) { + return; + } + uint8_t *jpeg = img->data; + uint32_t len = img->len; + + /* Store album filename so the action buttons can reference it */ + if (img->name != NULL) { + strncpy(sg_chat.cur_img_name, img->name, ALBUM_FILENAME_MAX_LEN); + sg_chat.cur_img_name[ALBUM_FILENAME_MAX_LEN] = '\0'; + } else { + sg_chat.cur_img_name[0] = '\0'; + } + PR_NOTICE("picture: disp_image name='%s'", sg_chat.cur_img_name); + + TAL_IMAGE_JPEG_INFO_T info = {0}; + + if (tal_image_jpeg_get_info(jpeg, len, &info) != OPRT_OK) { + PR_ERR("wechat chat: jpeg get info failed"); + return; + } + + uint32_t rgb565_size = info.width * info.height * 2; + PR_NOTICE("wechat chat: rgb565 width=%u, height=%u, size=%u", info.width, info.height, rgb565_size); + uint8_t *rgb565_buf = Malloc(rgb565_size); + if (rgb565_buf == NULL) { + PR_ERR("wechat chat: malloc rgb565 buf failed, size=%u", rgb565_size); + return; + } + memset(rgb565_buf, 0, rgb565_size); + + TAL_IMAGE_JPEG_OUTPUT_T out = {0}; + out.out_buf = rgb565_buf; + out.out_buf_size = rgb565_size; + out.out_width = info.width; + out.out_height = info.height; + + if (tal_image_jpeg_decode_rgb565(jpeg, len, &out) != OPRT_OK) { + PR_ERR("wechat chat: jpeg decode rgb565 failed"); + Free(rgb565_buf); + return; + } + + lv_vendor_disp_lock(); + + if (NULL == sg_chat.picture_canvas) { + sg_chat.picture_canvas = lv_canvas_create(sg_chat.picture); + lv_obj_set_size(sg_chat.picture_canvas, LV_HOR_RES, LV_VER_RES); + } + + lv_canvas_set_buffer(sg_chat.picture_canvas, rgb565_buf, + info.width, info.height, LV_COLOR_FORMAT_RGB565); + lv_obj_set_size(sg_chat.picture_canvas, info.width, info.height); + lv_obj_center(sg_chat.picture_canvas); + + /* Create action bar lazily AFTER the canvas so it is always on top (higher z-order). */ + if (NULL == sg_chat.picture_action_bar) { + PR_NOTICE("picture: creating action bar (first show)"); + sg_chat.picture_action_bar = lv_obj_create(sg_chat.picture); + lv_obj_set_size(sg_chat.picture_action_bar, 44, 84); + lv_obj_align(sg_chat.picture_action_bar, LV_ALIGN_RIGHT_MID, -8 - WECHAT_SAFE_INSET, 0); + lv_obj_set_style_bg_color(sg_chat.picture_action_bar, lv_color_black(), 0); + lv_obj_set_style_bg_opa(sg_chat.picture_action_bar, 140, 0); + lv_obj_set_style_radius(sg_chat.picture_action_bar, 18, 0); + lv_obj_set_style_border_width(sg_chat.picture_action_bar, 0, 0); + lv_obj_set_style_pad_all(sg_chat.picture_action_bar, 0, 0); + lv_obj_clear_flag(sg_chat.picture_action_bar, LV_OBJ_FLAG_SCROLLABLE); + +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + sg_chat.picture_print_btn = lv_obj_create(sg_chat.picture_action_bar); + lv_obj_set_size(sg_chat.picture_print_btn, 40, 40); + lv_obj_align(sg_chat.picture_print_btn, LV_ALIGN_TOP_MID, 0, 2); + lv_obj_set_style_bg_opa(sg_chat.picture_print_btn, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(sg_chat.picture_print_btn, 0, 0); + lv_obj_set_style_pad_all(sg_chat.picture_print_btn, 0, 0); + lv_obj_set_style_radius(sg_chat.picture_print_btn, 0, 0); + lv_obj_clear_flag(sg_chat.picture_print_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_chat.picture_print_btn, LV_OBJ_FLAG_CLICKABLE); + + lv_obj_t *print_icon = lv_img_create(sg_chat.picture_print_btn); + lv_img_set_src(print_icon, &icon_printer_app); + lv_obj_set_style_img_recolor(print_icon, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(print_icon, LV_OPA_COVER, 0); + lv_obj_center(print_icon); + + lv_obj_add_event_cb(sg_chat.picture_print_btn, __picture_print_btn_cb, LV_EVENT_CLICKED, NULL); +#endif /* ENABLE_PRINTER */ + +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) + sg_chat.picture_attach_btn = lv_obj_create(sg_chat.picture_action_bar); + lv_obj_set_size(sg_chat.picture_attach_btn, 40, 40); + lv_obj_align(sg_chat.picture_attach_btn, LV_ALIGN_BOTTOM_MID, 0, -2); + lv_obj_set_style_bg_opa(sg_chat.picture_attach_btn, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(sg_chat.picture_attach_btn, 0, 0); + lv_obj_set_style_pad_all(sg_chat.picture_attach_btn, 0, 0); + lv_obj_set_style_radius(sg_chat.picture_attach_btn, 0, 0); + lv_obj_clear_flag(sg_chat.picture_attach_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_chat.picture_attach_btn, LV_OBJ_FLAG_CLICKABLE); + + lv_obj_t *attach_icon = lv_img_create(sg_chat.picture_attach_btn); + lv_img_set_src(attach_icon, &icon_add_img); + lv_obj_set_style_img_recolor(attach_icon, lv_color_white(), 0); + lv_obj_set_style_img_recolor_opa(attach_icon, LV_OPA_COVER, 0); + lv_obj_center(attach_icon); + + lv_obj_add_event_cb(sg_chat.picture_attach_btn, __picture_attach_btn_cb, LV_EVENT_CLICKED, NULL); +#endif /* ENABLE_IMAGE_ALBUM */ + } + +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + if (NULL == sg_chat.picture_print_overlay) { + PR_NOTICE("picture: creating print overlay (first show)"); + sg_chat.picture_print_overlay = lv_obj_create(sg_chat.picture); + lv_obj_set_size(sg_chat.picture_print_overlay, LV_HOR_RES, LV_VER_RES); + lv_obj_set_pos(sg_chat.picture_print_overlay, 0, 0); + lv_obj_set_style_bg_color(sg_chat.picture_print_overlay, lv_color_black(), 0); + lv_obj_set_style_bg_opa(sg_chat.picture_print_overlay, LV_OPA_70, 0); + lv_obj_set_style_border_width(sg_chat.picture_print_overlay, 0, 0); + lv_obj_set_style_radius(sg_chat.picture_print_overlay, 0, 0); + lv_obj_clear_flag(sg_chat.picture_print_overlay, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_chat.picture_print_overlay, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_HIDDEN); + lv_obj_set_flex_flow(sg_chat.picture_print_overlay, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(sg_chat.picture_print_overlay, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(sg_chat.picture_print_overlay, 24, 0); + + /* Title label */ + lv_obj_t *print_title = lv_label_create(sg_chat.picture_print_overlay); + lv_obj_set_style_text_color(print_title, lv_color_white(), 0); + lv_label_set_text(print_title, PRINT_IMAGE); + sg_chat.picture_print_status_label = print_title; + + /* Button row */ + lv_obj_t *btn_row = lv_obj_create(sg_chat.picture_print_overlay); + lv_obj_remove_style_all(btn_row); + lv_obj_set_size(btn_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(btn_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(btn_row, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(btn_row, 24, 0); + sg_chat.picture_print_btn_row = btn_row; + + /* Cancel button */ + lv_obj_t *cancel_btn = lv_obj_create(btn_row); + lv_obj_set_size(cancel_btn, 80, 36); + lv_obj_set_style_bg_color(cancel_btn, lv_color_hex(0x555555), 0); + lv_obj_set_style_bg_opa(cancel_btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(cancel_btn, 0, 0); + lv_obj_set_style_radius(cancel_btn, 8, 0); + lv_obj_set_style_pad_all(cancel_btn, 0, 0); + lv_obj_clear_flag(cancel_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(cancel_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_t *cancel_label = lv_label_create(cancel_btn); + lv_label_set_text(cancel_label, CANCEL); + lv_obj_set_style_text_color(cancel_label, lv_color_white(), 0); + lv_obj_center(cancel_label); + lv_obj_add_event_cb(cancel_btn, __picture_print_cancel_btn_cb, LV_EVENT_CLICKED, NULL); + + /* Confirm button */ + lv_obj_t *confirm_btn = lv_obj_create(btn_row); + lv_obj_set_size(confirm_btn, 80, 36); + lv_obj_set_style_bg_color(confirm_btn, lv_color_hex(0x07C160), 0); + lv_obj_set_style_bg_opa(confirm_btn, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(confirm_btn, 0, 0); + lv_obj_set_style_radius(confirm_btn, 8, 0); + lv_obj_set_style_pad_all(confirm_btn, 0, 0); + lv_obj_clear_flag(confirm_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(confirm_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_t *confirm_label = lv_label_create(confirm_btn); + lv_label_set_text(confirm_label, CONFIRM_TEXT); + lv_obj_set_style_text_color(confirm_label, lv_color_white(), 0); + lv_obj_center(confirm_label); + lv_obj_add_event_cb(confirm_btn, __picture_print_confirm_btn_cb, LV_EVENT_CLICKED, NULL); + } +#endif /* ENABLE_PRINTER */ + + if (sg_picture_buffer) { + Free(sg_picture_buffer); + sg_picture_buffer = NULL; + } + sg_picture_buffer = rgb565_buf; + + lv_obj_add_flag(sg_chat.content, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_chat.picture, LV_OBJ_FLAG_HIDDEN); +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) + lv_obj_add_flag(sg_chat.plus_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN); +#endif + + if (sg_image_auto_return_tm != NULL) { + lv_timer_del(sg_image_auto_return_tm); + sg_image_auto_return_tm = NULL; + } + sg_image_auto_return_tm = lv_timer_create(__image_auto_return_cb, 10000, NULL); + + lv_vendor_disp_unlock(); +} + +/** + * @brief Display a clickable link label in the chat area. + * + * @param is_ai true = AI side (left), false = user side (right). + * @param text Link text to display. + * @param cb Callback invoked when user clicks the link. + * @param cb_arg Argument passed to the callback. + * @param len Length of cb_arg data. + */ +static void __create_link_label(bool is_ai, char *text, AI_UI_CHAT_LINK_CB cb, void *cb_arg, uint32_t len) +{ + __chat_check_msg_limit(); + + lv_obj_t *label; + if (is_ai) { + label = __create_ai_msg_label(sg_chat.content, text); + } else { + label = __create_user_msg_label(sg_chat.content, text); + } + + lv_obj_add_style(label, &sg_chat.style_link, LV_STATE_DEFAULT); + lv_obj_add_flag(label, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(label, 12); + + if (cb) { + UI_LINK_CB_DATA_T *link_data = Malloc(sizeof(UI_LINK_CB_DATA_T)); + if (link_data) { + link_data->cb = cb; + link_data->len = len; + if (len > 0 && cb_arg != NULL) { + link_data->cb_arg = Malloc(len + 1); + if (link_data->cb_arg) { + memcpy(link_data->cb_arg, cb_arg, len); + ((char *)link_data->cb_arg)[len] = '\0'; + } + } else { + link_data->cb_arg = cb_arg; + } + lv_obj_add_event_cb(label, __link_click_event_cb, LV_EVENT_CLICKED, link_data); + lv_obj_add_event_cb(label, __link_delete_event_cb, LV_EVENT_DELETE, link_data); + } + } + + lv_obj_update_layout(sg_chat.content); + lv_obj_scroll_to_view_recursive(label, LV_ANIM_ON); +} + +static void __ui_disp_link(bool is_ai, char *text, AI_UI_CHAT_LINK_CB cb, void *cb_arg, uint32_t len) +{ + if (sg_chat.content == NULL || text == NULL) { + return; + } + + lv_vendor_disp_lock(); + __create_link_label(is_ai, text, cb, cb_arg, len); + lv_vendor_disp_unlock(); + + if (cb) { + cb(cb_arg); + } +} + +/** + * @brief Remove a single attached image when user taps the X button. + */ +static void __attach_remove_cb(lv_event_t *e) +{ + lv_obj_t *btn = lv_event_get_target(e); + lv_obj_t *container = lv_obj_get_parent(btn); + if (NULL == container) { + return; + } + + /* Free the thumbnail buffer stored in user_data */ + uint8_t *buf = (uint8_t *)lv_obj_get_user_data(container); + if (buf) { + for (uint8_t i = 0; i < sg_chat.attach_count; i++) { + if (sg_chat.attach_bufs[i] == buf) { + Free(sg_chat.attach_bufs[i]); + /* Shift remaining entries */ + for (uint8_t j = i; j < sg_chat.attach_count - 1; j++) { + sg_chat.attach_bufs[j] = sg_chat.attach_bufs[j + 1]; + } + sg_chat.attach_bufs[sg_chat.attach_count - 1] = NULL; + sg_chat.attach_count--; + break; + } + } + } + + lv_obj_del(container); + + /* Hide attach bar if empty */ + if (sg_chat.attach_count == 0) { + lv_obj_add_flag(sg_chat.attach_bar, LV_OBJ_FLAG_HIDDEN); + } +} + +/** + * @brief Display an attached image thumbnail in the chat attach bar. + * + * @param img Pointer to the image data (JPEG from album). + */ +static void __ui_add_chat_attch_img(AI_UI_IMG_T *img) +{ + if (sg_chat.attach_bar == NULL || img == NULL || img->data == NULL || img->len == 0) { + return; + } + + if (sg_chat.attach_count >= MAX_ATTACH_NUM) { + return; + } + + /* Decode JPEG and scale to thumbnail in one step */ + TAL_IMAGE_JPEG_SCALE_IN_T scale_in = {0}; + scale_in.method = TAL_IMAGE_SCALE_MTH_NEAREST; + scale_in.mode = TAL_IMAGE_SCALE_MODE_SIZE; + scale_in.data = img->data; + scale_in.size = img->len; + scale_in.out_width = ATTACH_THUMB_SIZE; + scale_in.out_height = ATTACH_THUMB_SIZE; + + TAL_IMAGE_SCALE_OUT_T scale_out = {0}; + if (tal_image_jpeg_scale_rgb565(&scale_in, &scale_out) != OPRT_OK) { + PR_ERR("attach: jpeg scale failed"); + return; + } + + /* Copy to our own buffer so we can free with Free() later */ + uint32_t thumb_buf_size = ATTACH_THUMB_SIZE * ATTACH_THUMB_SIZE * 2; + uint8_t *thumb_buf = (uint8_t *)Malloc(thumb_buf_size); + if (NULL == thumb_buf) { + PR_ERR("attach: malloc thumb failed"); + tal_image_scale_buf_free(&scale_out); + return; + } + memcpy(thumb_buf, scale_out.buf, thumb_buf_size); + tal_image_scale_buf_free(&scale_out); + + lv_vendor_disp_lock(); + + /* Container wrapping thumbnail + close button */ + lv_obj_t *container = lv_obj_create(sg_chat.attach_bar); + lv_obj_set_size(container, ATTACH_THUMB_SIZE, ATTACH_THUMB_SIZE); + lv_obj_set_style_pad_all(container, 0, 0); + lv_obj_set_style_border_width(container, 0, 0); + lv_obj_set_style_radius(container, 4, 0); + lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0); + lv_obj_clear_flag(container, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_user_data(container, thumb_buf); + + /* Thumbnail canvas */ + lv_obj_t *canvas = lv_canvas_create(container); + lv_canvas_set_buffer(canvas, thumb_buf, + ATTACH_THUMB_SIZE, ATTACH_THUMB_SIZE, LV_COLOR_FORMAT_RGB565); + + /* Close button 鈥?top-right corner, solid red badge */ + lv_obj_t *close_btn = lv_obj_create(container); + lv_obj_set_size(close_btn, 18, 18); + lv_obj_align(close_btn, LV_ALIGN_TOP_RIGHT, 0, 0); + lv_obj_set_style_bg_color(close_btn, lv_color_hex(0xFF3B30), 0); + lv_obj_set_style_bg_opa(close_btn, LV_OPA_COVER, 0); + lv_obj_set_style_opa(close_btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(close_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(close_btn, 0, 0); + lv_obj_set_style_pad_all(close_btn, 0, 0); + lv_obj_clear_flag(close_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(close_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_move_foreground(close_btn); + + lv_obj_t *close_label = lv_label_create(close_btn); + lv_obj_set_style_text_color(close_label, lv_color_white(), 0); + lv_obj_set_style_text_font(close_label, ai_ui_get_icon_font(), 0); + lv_label_set_text(close_label, FONT_AWESOME_XMARK); + lv_obj_center(close_label); + + lv_obj_add_event_cb(close_btn, __attach_remove_cb, LV_EVENT_CLICKED, NULL); + + sg_chat.attach_bufs[sg_chat.attach_count] = thumb_buf; + sg_chat.attach_count++; + + /* Show attach bar */ + lv_obj_clear_flag(sg_chat.attach_bar, LV_OBJ_FLAG_HIDDEN); + + lv_vendor_disp_unlock(); +} + +/** + * @brief Clear all attached image thumbnails from the chat attach bar. + */ +static void __ui_clear_chat_attach(void) +{ + if (sg_chat.attach_bar == NULL) { + return; + } + + lv_vendor_disp_lock(); + + lv_obj_clean(sg_chat.attach_bar); + + for (uint8_t i = 0; i < sg_chat.attach_count; i++) { + if (sg_chat.attach_bufs[i]) { + Free(sg_chat.attach_bufs[i]); + sg_chat.attach_bufs[i] = NULL; + } + } + sg_chat.attach_count = 0; + + lv_obj_add_flag(sg_chat.attach_bar, LV_OBJ_FLAG_HIDDEN); + + lv_vendor_disp_unlock(); +} + +/* 鈹€鈹€ public API 鈹€鈹€ */ + +/** + * @brief Initialize the wechat chat page widgets. Called once during UI init + * (already inside lv_vendor_disp_lock). + * + * @param parent Parent LVGL container (the main screen container). + */ +void guided_wechat_chat_init(lv_obj_t *parent) +{ + lv_coord_t content_w = LV_HOR_RES - (WECHAT_SAFE_INSET * 2); + lv_coord_t attach_w = LV_HOR_RES - (WECHAT_SAFE_INSET * 2); + lv_coord_t content_h = LV_VER_RES - 40 - WECHAT_SAFE_INSET; + + if (content_w < 1) { + content_w = LV_HOR_RES; + } + if (attach_w < 1) { + attach_w = LV_HOR_RES; + } + if (content_h < 1) { + content_h = LV_VER_RES - 40; + } + + /* Style init 鈥?already inside disp lock, safe to call LVGL style APIs */ + lv_style_init(&sg_chat.style_avatar); + lv_style_set_radius(&sg_chat.style_avatar, LV_RADIUS_CIRCLE); + lv_style_set_bg_color(&sg_chat.style_avatar, lv_palette_main(LV_PALETTE_GREY)); + lv_style_set_border_width(&sg_chat.style_avatar, 1); + lv_style_set_border_color(&sg_chat.style_avatar, lv_palette_darken(LV_PALETTE_GREY, 2)); + + lv_style_init(&sg_chat.style_ai_bubble); + lv_style_set_bg_color(&sg_chat.style_ai_bubble, lv_color_white()); + lv_style_set_radius(&sg_chat.style_ai_bubble, 15); + lv_style_set_pad_all(&sg_chat.style_ai_bubble, 12); + lv_style_set_shadow_width(&sg_chat.style_ai_bubble, 12); + lv_style_set_shadow_color(&sg_chat.style_ai_bubble, lv_color_hex(0xCCCCCC)); + + lv_style_init(&sg_chat.style_user_bubble); + lv_style_set_bg_color(&sg_chat.style_user_bubble, lv_palette_main(LV_PALETTE_GREEN)); + lv_style_set_text_color(&sg_chat.style_user_bubble, lv_color_white()); + lv_style_set_radius(&sg_chat.style_user_bubble, 15); + lv_style_set_pad_all(&sg_chat.style_user_bubble, 12); + lv_style_set_shadow_width(&sg_chat.style_user_bubble, 12); + lv_style_set_shadow_color(&sg_chat.style_user_bubble, lv_palette_darken(LV_PALETTE_GREEN, 2)); + + lv_style_init(&sg_chat.style_link); + lv_style_set_text_color(&sg_chat.style_link, lv_color_hex(0x0066CC)); + lv_style_set_text_decor(&sg_chat.style_link, LV_TEXT_DECOR_UNDERLINE); + + /* Chat content area */ + sg_chat.content = lv_obj_create(parent); + lv_obj_set_size(sg_chat.content, content_w, content_h); + lv_obj_set_flex_flow(sg_chat.content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_ver(sg_chat.content, 8, 0); + lv_obj_set_style_pad_hor(sg_chat.content, 10, 0); + lv_obj_align(sg_chat.content, LV_ALIGN_BOTTOM_MID, 0, -WECHAT_SAFE_INSET); + + lv_obj_set_scroll_dir(sg_chat.content, LV_DIR_VER); + lv_obj_set_scrollbar_mode(sg_chat.content, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_bg_opa(sg_chat.content, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(sg_chat.content, 0, 0); + lv_obj_add_flag(sg_chat.content, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(sg_chat.content, __content_click_cb, LV_EVENT_CLICKED, NULL); + + /* Picture view area 鈥?hidden by default */ + sg_chat.picture = lv_obj_create(lv_scr_act()); + lv_obj_set_size(sg_chat.picture, LV_HOR_RES, LV_VER_RES); + lv_obj_set_style_pad_all(sg_chat.picture, 0, 0); + lv_obj_set_style_border_width(sg_chat.picture, 0, 0); + lv_obj_set_style_radius(sg_chat.picture, 0, 0); + lv_obj_align(sg_chat.picture, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_flag(sg_chat.picture, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_HIDDEN); + lv_obj_add_event_cb(sg_chat.picture, __return_chat_content_event_cb, LV_EVENT_CLICKED, NULL); + + /* Attach bar 鈥?horizontal row of thumbnails at bottom, hidden by default */ + sg_chat.attach_bar = lv_obj_create(parent); + lv_obj_set_size(sg_chat.attach_bar, attach_w, ATTACH_BAR_HEIGHT); + lv_obj_align(sg_chat.attach_bar, LV_ALIGN_BOTTOM_MID, 0, -WECHAT_SAFE_INSET); + lv_obj_set_style_bg_color(sg_chat.attach_bar, lv_color_hex(0xF5F5F5), 0); + lv_obj_set_style_bg_opa(sg_chat.attach_bar, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(sg_chat.attach_bar, lv_color_hex(0xDDDDDD), 0); + lv_obj_set_style_border_width(sg_chat.attach_bar, 1, 0); + lv_obj_set_style_border_side(sg_chat.attach_bar, LV_BORDER_SIDE_TOP, 0); + lv_obj_set_style_pad_all(sg_chat.attach_bar, 6, 0); + lv_obj_set_style_pad_column(sg_chat.attach_bar, 6, 0); + lv_obj_set_style_radius(sg_chat.attach_bar, 0, 0); + lv_obj_set_flex_flow(sg_chat.attach_bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(sg_chat.attach_bar, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(sg_chat.attach_bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_chat.attach_bar, LV_OBJ_FLAG_HIDDEN); + +#if defined(ENABLE_LVGL_TP) && (ENABLE_LVGL_TP == 1) + /* "+" button 鈥?bottom-right, offset inward so it's not flush with the corner */ + /* Use the same LVGL button type as the bottom-left menu button so the + * default pressed-state feedback is consistent across both controls. */ + sg_chat.plus_btn = lv_btn_create(parent); + lv_obj_set_size(sg_chat.plus_btn, PLUS_BTN_SIZE, PLUS_BTN_SIZE); + lv_obj_align(sg_chat.plus_btn, LV_ALIGN_BOTTOM_RIGHT, -24 - WECHAT_SAFE_INSET, -20 - WECHAT_SAFE_INSET); + lv_obj_set_style_bg_color(sg_chat.plus_btn, lv_color_white(), 0); + lv_obj_set_style_bg_opa(sg_chat.plus_btn, LV_OPA_COVER, 0); + lv_obj_set_style_radius(sg_chat.plus_btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_shadow_width(sg_chat.plus_btn, 8, 0); + lv_obj_set_style_shadow_color(sg_chat.plus_btn, lv_color_hex(0xB8BCC2), 0); + lv_obj_set_style_border_width(sg_chat.plus_btn, 1, 0); + lv_obj_set_style_border_color(sg_chat.plus_btn, lv_color_hex(0xE2E5E9), 0); + lv_obj_set_style_pad_all(sg_chat.plus_btn, 0, 0); + lv_obj_clear_flag(sg_chat.plus_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(sg_chat.plus_btn, LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_SCROLL_CHAIN); + lv_obj_add_flag(sg_chat.plus_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(sg_chat.plus_btn, 10); + + lv_obj_t *plus_label = lv_label_create(sg_chat.plus_btn); + lv_obj_set_style_text_color(plus_label, lv_color_black(), 0); + lv_label_set_text(plus_label, "+"); + lv_obj_center(plus_label); + lv_obj_add_event_cb(sg_chat.plus_btn, __plus_btn_click_cb, LV_EVENT_CLICKED, NULL); + + /* Popup menu 鈥?above "+" button, hidden by default */ + sg_chat.popup_menu = lv_obj_create(parent); + lv_obj_set_size(sg_chat.popup_menu, POPUP_WIDTH, POPUP_ITEM_H * 3 + 4); + lv_obj_align_to(sg_chat.popup_menu, sg_chat.plus_btn, LV_ALIGN_OUT_TOP_RIGHT, 0, -4); + lv_obj_set_style_bg_color(sg_chat.popup_menu, lv_color_white(), 0); + lv_obj_set_style_bg_opa(sg_chat.popup_menu, LV_OPA_COVER, 0); + lv_obj_set_style_radius(sg_chat.popup_menu, 8, 0); + lv_obj_set_style_border_width(sg_chat.popup_menu, 1, 0); + lv_obj_set_style_border_color(sg_chat.popup_menu, lv_color_hex(0xDDDDDD), 0); + lv_obj_set_style_shadow_width(sg_chat.popup_menu, 8, 0); + lv_obj_set_style_shadow_color(sg_chat.popup_menu, lv_color_hex(0xBBBBBB), 0); + lv_obj_set_style_pad_all(sg_chat.popup_menu, 2, 0); + lv_obj_set_style_pad_row(sg_chat.popup_menu, 0, 0); + lv_obj_set_flex_flow(sg_chat.popup_menu, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(sg_chat.popup_menu, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_chat.popup_menu, LV_OBJ_FLAG_HIDDEN); + +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) + /* Camera option */ + lv_obj_t *cam_btn = lv_obj_create(sg_chat.popup_menu); + lv_obj_set_size(cam_btn, POPUP_WIDTH - 4, POPUP_ITEM_H); + lv_obj_set_style_bg_opa(cam_btn, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(cam_btn, 0, 0); + lv_obj_set_style_pad_all(cam_btn, 0, 0); + lv_obj_set_style_pad_left(cam_btn, 8, 0); + lv_obj_set_style_pad_column(cam_btn, 8, 0); + lv_obj_set_style_radius(cam_btn, 6, 0); + lv_obj_clear_flag(cam_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(cam_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_flex_flow(cam_btn, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(cam_btn, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *cam_icon_wrap = lv_obj_create(cam_btn); + lv_obj_remove_style_all(cam_icon_wrap); + lv_obj_set_size(cam_icon_wrap, POPUP_ICON_SIZE, POPUP_ICON_SIZE); + lv_obj_clear_flag(cam_icon_wrap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t *cam_icon = lv_img_create(cam_icon_wrap); + lv_img_set_src(cam_icon, &icon_camera_app); + lv_obj_set_style_img_recolor(cam_icon, lv_color_hex(0x333333), 0); + lv_obj_set_style_img_recolor_opa(cam_icon, LV_OPA_COVER, 0); + lv_obj_center(cam_icon); + + lv_obj_t *cam_label = lv_label_create(cam_btn); + lv_obj_set_style_text_color(cam_label, lv_color_hex(0x333333), 0); + lv_label_set_text(cam_label, CAMERA); + lv_obj_add_event_cb(cam_btn, __popup_camera_cb, LV_EVENT_CLICKED, NULL); +#endif /* ENABLE_COMP_AI_VIDEO */ + +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) + /* Album option */ + lv_obj_t *album_btn = lv_obj_create(sg_chat.popup_menu); + lv_obj_set_size(album_btn, POPUP_WIDTH - 4, POPUP_ITEM_H); + lv_obj_set_style_bg_opa(album_btn, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(album_btn, 0, 0); + lv_obj_set_style_pad_all(album_btn, 0, 0); + lv_obj_set_style_pad_left(album_btn, 8, 0); + lv_obj_set_style_pad_column(album_btn, 8, 0); + lv_obj_set_style_radius(album_btn, 6, 0); + lv_obj_clear_flag(album_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(album_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_flex_flow(album_btn, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(album_btn, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *album_icon_wrap = lv_obj_create(album_btn); + lv_obj_remove_style_all(album_icon_wrap); + lv_obj_set_size(album_icon_wrap, POPUP_ICON_SIZE, POPUP_ICON_SIZE); + lv_obj_clear_flag(album_icon_wrap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t *album_icon = lv_img_create(album_icon_wrap); + lv_img_set_src(album_icon, &icon_photo_app); + lv_obj_set_style_img_recolor(album_icon, lv_color_hex(0x333333), 0); + lv_obj_set_style_img_recolor_opa(album_icon, LV_OPA_COVER, 0); + lv_obj_center(album_icon); + + lv_obj_t *album_label = lv_label_create(album_btn); + lv_obj_set_style_text_color(album_label, lv_color_hex(0x333333), 0); + lv_label_set_text(album_label, ALBUM); + lv_obj_add_event_cb(album_btn, __popup_album_cb, LV_EVENT_CLICKED, NULL); + + /* Add Image option */ + lv_obj_t *add_img_btn = lv_obj_create(sg_chat.popup_menu); + lv_obj_set_size(add_img_btn, POPUP_WIDTH - 4, POPUP_ITEM_H); + lv_obj_set_style_bg_opa(add_img_btn, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(add_img_btn, 0, 0); + lv_obj_set_style_pad_all(add_img_btn, 0, 0); + lv_obj_set_style_pad_left(add_img_btn, 8, 0); + lv_obj_set_style_pad_column(add_img_btn, 8, 0); + lv_obj_set_style_radius(add_img_btn, 6, 0); + lv_obj_clear_flag(add_img_btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(add_img_btn, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_flex_flow(add_img_btn, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(add_img_btn, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *add_img_icon_wrap = lv_obj_create(add_img_btn); + lv_obj_remove_style_all(add_img_icon_wrap); + lv_obj_set_size(add_img_icon_wrap, POPUP_ICON_SIZE, POPUP_ICON_SIZE); + lv_obj_clear_flag(add_img_icon_wrap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t *add_img_icon = lv_img_create(add_img_icon_wrap); + lv_img_set_src(add_img_icon, &icon_add_img); + lv_obj_set_style_img_recolor(add_img_icon, lv_color_hex(0x333333), 0); + lv_obj_set_style_img_recolor_opa(add_img_icon, LV_OPA_COVER, 0); + lv_obj_center(add_img_icon); + + lv_obj_t *add_img_label = lv_label_create(add_img_btn); + lv_obj_set_style_text_color(add_img_label, lv_color_hex(0x333333), 0); + lv_label_set_text(add_img_label, ADD_IMAGE); + lv_obj_add_event_cb(add_img_btn, __popup_add_img_cb, LV_EVENT_CLICKED, NULL); +#endif /* ENABLE_IMAGE_ALBUM */ +#endif /* ENABLE_LVGL_TP */ +} + +/** + * @brief Register chat-specific callbacks with the chat management layer. + */ +void guided_wechat_chat_register(void) +{ + AI_UI_CHAT_INTFS_T intfs; + memset(&intfs, 0, sizeof(AI_UI_CHAT_INTFS_T)); + + intfs.disp_open = __ui_open_chat; + intfs.disp_close = __ui_close_chat; + intfs.disp_user_msg = __ui_set_user_msg; + intfs.disp_ai_msg = __ui_set_ai_msg; + intfs.disp_ai_msg_stream_start = __ui_set_ai_msg_stream_start; + intfs.disp_ai_msg_stream_data = __ui_set_ai_msg_stream_data; + intfs.disp_ai_msg_stream_end = __ui_set_ai_msg_stream_end; + intfs.disp_image = __ui_disp_image; + intfs.disp_link = __ui_disp_link; + intfs.disp_add_chat_attch_img = __ui_add_chat_attch_img; + intfs.disp_clear_chat_attach = __ui_clear_chat_attach; +#if defined(ENABLE_PRINTER) && (ENABLE_PRINTER == 1) + intfs.disp_print_result = __picture_disp_print_result; +#endif + + ai_ui_chat_register(&intfs); +} + +#endif diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h new file mode 100644 index 000000000..960897be5 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h @@ -0,0 +1,62 @@ +/** + * @file guided_wechat_common.h + * @brief Internal header shared among wechat UI page files. + * @copyright Copyright (c) 2021-2026 Tuya Inc. All Rights Reserved. + */ + +#ifndef __GUIDED_WECHAT_COMMON_H__ +#define __GUIDED_WECHAT_COMMON_H__ + +#include "tuya_cloud_types.h" +#include "ai_ui_manage.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* --------------------------------------------------------------------------- + * Rounded-screen safe inset + * --------------------------------------------------------------------------- */ +#if defined(GUIDED_WECHAT_SCREEN_CORNER_RADIUS) && (GUIDED_WECHAT_SCREEN_CORNER_RADIUS > 0) +#define WECHAT_ROUNDED_CORNER_RADIUS GUIDED_WECHAT_SCREEN_CORNER_RADIUS +#else +#define WECHAT_ROUNDED_CORNER_RADIUS 0 +#endif + +/** + * Safe inset derived from physical corner radius R. + * Max clip depth at 45掳 = R*(1-cos45掳) 鈮?R*0.293, so R/2 is conservative. + */ +#define WECHAT_SAFE_INSET ((WECHAT_ROUNDED_CORNER_RADIUS + 1) / 2) + +/** + * Extended touch area added to every corner button so small targets near + * the rounded-screen edge are easier to tap. + */ +#define WECHAT_BTN_EXT_CLICK 12 + +/* --------------------------------------------------------------------------- + * Function declarations + * --------------------------------------------------------------------------- */ + +/* Chat page */ +void guided_wechat_chat_init(lv_obj_t *parent); +void guided_wechat_chat_register(void); + +/* Camera page */ +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) +void guided_wechat_camera_init(lv_obj_t *parent); +void guided_wechat_camera_register(void); +#endif + +/* Album page */ +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) +void guided_wechat_album_init(lv_obj_t *parent); +void guided_wechat_album_register(void); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __GUIDED_WECHAT_COMMON_H__ */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c new file mode 100644 index 000000000..26b4b9996 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c @@ -0,0 +1,311 @@ +/** + * @file guided_wechat_core.c + * @brief WeChat-style UI 鈥?main entry: LVGL init, screen, status bar, sub-page register. + * @copyright Copyright (c) 2021-2026 Tuya Inc. All Rights Reserved. + */ + +#include "tal_api.h" + +#if (defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1)) || \ + (defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1)) +#include "lv_vendor.h" + +#include "ai_ui_manage.h" +#include "ai_ui_icon_font.h" +#include "font_awesome_symbols.h" +#include "guided_wechat_core.h" +#include "guided_wechat_common.h" +#include "ai_ui_locale.h" + +LV_FONT_DECLARE(font_puhui_14_1); + +/*********************************************************** +************************macro define************************ +***********************************************************/ +#define STATUS_BAR_HEIGHT 40 +#define STATUS_BAR_MODE_WIDTH_PERCENT 30 +#define STATUS_BAR_CENTER_WIDTH_PERCENT 50 +#define STATUS_BAR_CENTER_LEFT_PERCENT 25 +#define STATUS_BAR_EMOTION_GAP 5 + +/*********************************************************** +***********************typedef define*********************** +***********************************************************/ +typedef struct { + lv_obj_t *container; + lv_obj_t *status_bar; + lv_obj_t *status_group; + lv_obj_t *emotion_label; + lv_obj_t *status_label; + lv_obj_t *network_label; + lv_obj_t *notification_label; + lv_obj_t *mode_label; +} GUIDED_WECHAT_MAIN_T; + +/*********************************************************** +***********************variable define********************** +***********************************************************/ +static GUIDED_WECHAT_MAIN_T sg_main = {0}; +static lv_timer_t *sg_notification_tm = NULL; + +static const lv_font_t *__status_bar_text_font(void) +{ + /* English mode labels are materially longer than their Chinese + * equivalents. Use the already bundled 14 px font only in the status bar + * so the mode, emotion, and state keep separate visual space. */ + return ai_ui_locale_is_english() ? &font_puhui_14_1 : ai_ui_get_text_font(); +} + +/*********************************************************** +***********************function define********************** +***********************************************************/ +static void __lvgl_init(void) +{ + lv_vendor_init(DISPLAY_NAME); + lv_vendor_start(5, 1024 * 8); +} + +static void __ui_notification_timeout_cb(lv_timer_t *timer) +{ + lv_timer_del(sg_notification_tm); + sg_notification_tm = NULL; + + lv_obj_add_flag(sg_main.notification_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_main.status_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_main.emotion_label, LV_OBJ_FLAG_HIDDEN); +} + +/* 鈹€鈹€ status bar callbacks (registered into AI_UI_INTFS_T) 鈹€鈹€ */ + +static void __ui_set_emotion(char *emotion) +{ + if (NULL == sg_main.emotion_label) { + return; + } + + AI_UI_FONT_LIST_T sg_font; + sg_font.emoji = ai_ui_get_emo_font(); + sg_font.emoji_list = ai_ui_get_emo_list(); + + char *emo_icon = sg_font.emoji_list[0].emo_icon; + for (int i = 0; i < FONT_EMO_ICON_MAX_NUM; i++) { + if (strcmp(emotion, sg_font.emoji_list[i].emo_name) == 0) { + emo_icon = sg_font.emoji_list[i].emo_icon; + break; + } + } + + lv_vendor_disp_lock(); + lv_obj_set_style_text_font(sg_main.emotion_label, sg_font.emoji, 0); + lv_label_set_text(sg_main.emotion_label, emo_icon); + if (sg_main.status_label != NULL) { + lv_obj_align_to(sg_main.emotion_label, sg_main.status_label, + LV_ALIGN_OUT_LEFT_MID, -STATUS_BAR_EMOTION_GAP, 0); + } + lv_vendor_disp_unlock(); +} + +static void __ui_set_status(char *status) +{ + if (sg_main.status_label == NULL) { + return; + } + + lv_vendor_disp_lock(); + lv_obj_set_style_text_font(sg_main.status_label, __status_bar_text_font(), 0); + lv_label_set_text(sg_main.status_label, status); + lv_obj_set_style_text_align(sg_main.status_label, LV_TEXT_ALIGN_CENTER, 0); + if (sg_main.emotion_label != NULL) { + lv_obj_align_to(sg_main.emotion_label, sg_main.status_label, + LV_ALIGN_OUT_LEFT_MID, -STATUS_BAR_EMOTION_GAP, 0); + } + lv_vendor_disp_unlock(); +} + +static void __ui_set_notification(char *notification) +{ + if (sg_main.notification_label == NULL) { + return; + } + + lv_vendor_disp_lock(); + lv_obj_set_style_text_font(sg_main.notification_label, __status_bar_text_font(), 0); + lv_label_set_text(sg_main.notification_label, notification); + lv_obj_add_flag(sg_main.status_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_main.notification_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(sg_main.emotion_label, LV_OBJ_FLAG_HIDDEN); + + if (NULL == sg_notification_tm) { + sg_notification_tm = lv_timer_create(__ui_notification_timeout_cb, 3000, NULL); + } else { + lv_timer_reset(sg_notification_tm); + } + lv_vendor_disp_unlock(); +} + +static void __ui_set_network(AI_UI_WIFI_STATUS_E wifi_status) +{ + char *wifi_icon = ai_ui_get_wifi_icon(wifi_status); + + if (sg_main.network_label == NULL || wifi_icon == NULL) { + return; + } + + lv_vendor_disp_lock(); + lv_label_set_text(sg_main.network_label, wifi_icon); + lv_obj_align(sg_main.network_label, LV_ALIGN_RIGHT_MID, -WECHAT_SAFE_INSET, 0); + lv_vendor_disp_unlock(); +} + +static void __ui_set_chat_mode(char *chat_mode) +{ + if (sg_main.mode_label == NULL || chat_mode == NULL) { + return; + } + + lv_vendor_disp_lock(); + lv_obj_set_style_text_font(sg_main.mode_label, __status_bar_text_font(), 0); + lv_label_set_text(sg_main.mode_label, chat_mode); + lv_obj_align(sg_main.mode_label, LV_ALIGN_LEFT_MID, WECHAT_SAFE_INSET, 0); + lv_vendor_disp_unlock(); +} + +/* 鈹€鈹€ UI init 鈹€鈹€ */ + +OPERATE_RET guided_wechat_core_init(void) +{ + __lvgl_init(); + + lv_vendor_disp_lock(); + + /* Disable scrolling on the root screen so gesture events are not blocked */ + lv_obj_clear_flag(lv_scr_act(), LV_OBJ_FLAG_SCROLLABLE); + + const lv_font_t *text_font = ai_ui_get_text_font(); + const lv_font_t *icon_font = ai_ui_get_icon_font(); + + /* Screen */ + lv_obj_t *screen = lv_obj_create(lv_scr_act()); + lv_obj_set_size(screen, LV_HOR_RES, LV_VER_RES); + lv_obj_set_style_bg_color(screen, lv_color_hex(0xF0F0F0), 0); + lv_obj_set_style_pad_all(screen, 0, 0); + lv_obj_set_style_border_width(screen, 0, 0); + lv_obj_set_style_radius(screen, 0, 0); + lv_obj_set_style_text_font(screen, text_font, 0); + lv_obj_set_style_text_color(screen, lv_color_black(), 0); + lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF); + lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE); + + /* Container */ + sg_main.container = lv_obj_create(screen); + lv_obj_set_size(sg_main.container, LV_HOR_RES, LV_VER_RES); + lv_obj_set_style_pad_all(sg_main.container, 0, 0); + lv_obj_set_style_border_width(sg_main.container, 0, 0); + lv_obj_set_style_pad_row(sg_main.container, 0, 0); + lv_obj_set_style_radius(sg_main.container, 0, 0); + lv_obj_clear_flag(sg_main.container, LV_OBJ_FLAG_SCROLLABLE); + + /* Status bar */ + sg_main.status_bar = lv_obj_create(sg_main.container); + lv_obj_set_size(sg_main.status_bar, LV_HOR_RES, STATUS_BAR_HEIGHT); + lv_obj_set_style_radius(sg_main.status_bar, 0, 0); + lv_obj_set_style_border_width(sg_main.status_bar, 0, 0); + lv_obj_set_style_pad_all(sg_main.status_bar, 0, 0); + lv_obj_set_style_bg_color(sg_main.status_bar, lv_palette_main(LV_PALETTE_GREEN), 0); + + /* Reserve independent regions so long English labels cannot overlap the + * emotion/status pair or the Wi-Fi indicator. */ + sg_main.status_group = lv_obj_create(sg_main.status_bar); + lv_obj_set_size(sg_main.status_group, + LV_HOR_RES * STATUS_BAR_CENTER_WIDTH_PERCENT / 100, + STATUS_BAR_HEIGHT); + lv_obj_align(sg_main.status_group, LV_ALIGN_TOP_LEFT, + LV_HOR_RES * STATUS_BAR_CENTER_LEFT_PERCENT / 100, 0); + lv_obj_set_style_pad_all(sg_main.status_group, 0, 0); + lv_obj_set_style_radius(sg_main.status_group, 0, 0); + lv_obj_set_style_border_width(sg_main.status_group, 0, 0); + lv_obj_set_style_bg_opa(sg_main.status_group, LV_OPA_TRANSP, 0); + lv_obj_clear_flag(sg_main.status_group, LV_OBJ_FLAG_SCROLLABLE); + + sg_main.mode_label = lv_label_create(sg_main.status_bar); + lv_obj_set_style_text_font(sg_main.mode_label, __status_bar_text_font(), 0); + lv_obj_set_width(sg_main.mode_label, + LV_HOR_RES * STATUS_BAR_MODE_WIDTH_PERCENT / 100); + lv_label_set_long_mode(sg_main.mode_label, LV_LABEL_LONG_DOT); + lv_label_set_text(sg_main.mode_label, ""); + lv_obj_align(sg_main.mode_label, LV_ALIGN_LEFT_MID, WECHAT_SAFE_INSET, 0); + + sg_main.status_label = lv_label_create(sg_main.status_group); + lv_obj_set_style_text_font(sg_main.status_label, __status_bar_text_font(), 0); + lv_label_set_long_mode(sg_main.status_label, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_align(sg_main.status_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(sg_main.status_label, INITIALIZING); + + sg_main.emotion_label = lv_label_create(sg_main.status_group); + lv_obj_set_style_text_font(sg_main.emotion_label, icon_font, 0); + lv_label_set_text(sg_main.emotion_label, FONT_AWESOME_AI_CHIP); + lv_obj_align_to(sg_main.emotion_label, sg_main.status_label, + LV_ALIGN_OUT_LEFT_MID, -STATUS_BAR_EMOTION_GAP, 0); + + sg_main.notification_label = lv_label_create(sg_main.status_group); + lv_obj_set_style_text_font(sg_main.notification_label, __status_bar_text_font(), 0); + lv_obj_set_width(sg_main.notification_label, LV_PCT(100)); + lv_label_set_long_mode(sg_main.notification_label, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_center(sg_main.notification_label); + lv_label_set_text(sg_main.notification_label, ""); + lv_obj_add_flag(sg_main.notification_label, LV_OBJ_FLAG_HIDDEN); + + sg_main.network_label = lv_label_create(sg_main.status_bar); + lv_obj_set_style_text_font(sg_main.network_label, icon_font, 0); + lv_label_set_text(sg_main.network_label, ""); + lv_obj_align(sg_main.network_label, LV_ALIGN_RIGHT_MID, -WECHAT_SAFE_INSET, 0); + + /* Init sub-pages */ + guided_wechat_chat_init(sg_main.container); +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) + guided_wechat_camera_init(screen); +#endif +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) + guided_wechat_album_init(screen); +#endif + + lv_vendor_disp_unlock(); + + return OPRT_OK; +} + +/** + * @brief Register WeChat-style chat UI implementation. + */ +OPERATE_RET guided_wechat_register_with_init(OPERATE_RET (*disp_init)(void)) +{ + AI_UI_INTFS_T intfs; + memset(&intfs, 0, sizeof(AI_UI_INTFS_T)); + + intfs.disp_init = disp_init != NULL ? disp_init : guided_wechat_core_init; + intfs.disp_emotion = __ui_set_emotion; + intfs.disp_ai_mode_state = __ui_set_status; + intfs.disp_notification = __ui_set_notification; + intfs.disp_wifi_state = __ui_set_network; + intfs.disp_ai_chat_mode = __ui_set_chat_mode; + + ai_ui_register(&intfs); + + /* Sub-pages register independently */ + guided_wechat_chat_register(); +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) + guided_wechat_camera_register(); +#endif +#if defined(ENABLE_IMAGE_ALBUM) && (ENABLE_IMAGE_ALBUM == 1) + guided_wechat_album_register(); +#endif + + return OPRT_OK; +} + +OPERATE_RET guided_wechat_core_register(void) +{ + return guided_wechat_register_with_init(guided_wechat_core_init); +} + +#endif /* WeChat UI variants */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.h b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.h new file mode 100644 index 000000000..3e9f72e54 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.h @@ -0,0 +1,22 @@ +/** + * @file guided_wechat_core.h + * @brief Private entry points for the standalone Guided WeChat UI. + */ +#ifndef __GUIDED_WECHAT_CORE_H__ +#define __GUIDED_WECHAT_CORE_H__ + +#include "tuya_cloud_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +OPERATE_RET guided_wechat_core_init(void); +OPERATE_RET guided_wechat_register_with_init(OPERATE_RET (*disp_init)(void)); +OPERATE_RET guided_wechat_core_register(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __GUIDED_WECHAT_CORE_H__ */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.c new file mode 100644 index 000000000..95e76d7b7 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.c @@ -0,0 +1,28 @@ +/** + * @file guided_wechat_locale.c + * @brief Runtime language persistence for the guided WeChat overlay. + */ + +#include "tal_api.h" +#include "ai_ui_locale.h" +#include "guided_wechat_locale.h" + +bool guided_wechat_locale_load(void) +{ + return ai_ui_locale_load(); +} + +guided_wechat_lang_t guided_wechat_locale_get(void) +{ + return ai_ui_locale_is_english() ? GUIDED_WECHAT_LANG_EN : GUIDED_WECHAT_LANG_ZH; +} + +void guided_wechat_locale_set(guided_wechat_lang_t lang) +{ + ai_ui_locale_set_english(lang == GUIDED_WECHAT_LANG_EN); +} + +const char *guided_wechat_tr(const char *zh, const char *en) +{ + return ai_ui_locale_tr(zh, en); +} diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.h b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.h new file mode 100644 index 000000000..82840a000 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_locale.h @@ -0,0 +1,21 @@ +/** + * @file guided_wechat_locale.h + * @brief Small runtime locale store for the guided WeChat overlay. + */ + +#ifndef __GUIDED_WECHAT_LOCALE_H__ +#define __GUIDED_WECHAT_LOCALE_H__ + +#include + +typedef enum { + GUIDED_WECHAT_LANG_ZH = 0, + GUIDED_WECHAT_LANG_EN, +} guided_wechat_lang_t; + +bool guided_wechat_locale_load(void); +guided_wechat_lang_t guided_wechat_locale_get(void); +void guided_wechat_locale_set(guided_wechat_lang_t lang); +const char *guided_wechat_tr(const char *zh, const char *en); + +#endif /* __GUIDED_WECHAT_LOCALE_H__ */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.c new file mode 100644 index 000000000..ac9966979 --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.c @@ -0,0 +1,192 @@ +/** + * @file guided_wechat_state.c + * @brief Device-state adapter used by the guided WeChat UI. + */ + +#include +#include + +#include "tal_api.h" +#include "tal_kv.h" +#include "tuya_authorize.h" +#include "tuya_iot.h" +#include "netmgr.h" +#if defined(ENABLE_WIFI) && (ENABLE_WIFI == 1) +#include "netconn_wifi.h" +#endif + +#include "guided_wechat_state.h" + +static volatile guided_wechat_pairing_state_t sg_pairing_state = GUIDED_WECHAT_PAIRING_IDLE; + +#define GUIDED_WECHAT_REPROVISION_KEY "guided_wechat_reprovision" + +static bool __contains_placeholder(const char *value) +{ + return value == NULL || value[0] == '\0' || strstr(value, "xxx") != NULL || + strstr(value, "your_") != NULL || strstr(value, "here") != NULL; +} + +static bool __license_valid(const char *uuid, const char *authkey) +{ + size_t uuid_len; + + if (__contains_placeholder(uuid) || __contains_placeholder(authkey)) { + return false; + } + + uuid_len = strlen(uuid); + return (uuid_len == 16 || uuid_len == 20) && strlen(authkey) == 32; +} + +static void __copy_value(char *out, size_t out_len, const char *value) +{ + if (out == NULL || out_len == 0) { + return; + } + snprintf(out, out_len, "%s", value != NULL && value[0] != '\0' ? value : "-"); +} + +static void __mask_value(char *out, size_t out_len, const char *value) +{ + size_t len; + + if (out == NULL || out_len == 0) { + return; + } + if (value == NULL || value[0] == '\0') { + __copy_value(out, out_len, "-"); + return; + } + + len = strlen(value); + if (len <= 6) { + __copy_value(out, out_len, "***"); + return; + } + snprintf(out, out_len, "%.3s***%s", value, value + len - 3); +} + +void guided_wechat_pairing_request(void) +{ + if (sg_pairing_state == GUIDED_WECHAT_PAIRING_IDLE) { + sg_pairing_state = GUIDED_WECHAT_PAIRING_STARTING; + } +} + +void guided_wechat_pairing_begin(void) +{ + sg_pairing_state = GUIDED_WECHAT_PAIRING_WAITING_PHONE; +} + +void guided_wechat_pairing_data_received(void) +{ + sg_pairing_state = GUIDED_WECHAT_PAIRING_CONNECTING; +} + +void guided_wechat_pairing_complete(void) +{ + sg_pairing_state = GUIDED_WECHAT_PAIRING_IDLE; +} + +void guided_wechat_pairing_reset(void) +{ + sg_pairing_state = GUIDED_WECHAT_PAIRING_IDLE; +} + +void guided_wechat_reprovision_mark(void) +{ + const uint8_t pending = 1; + + if (OPRT_OK != tal_kv_set(GUIDED_WECHAT_REPROVISION_KEY, &pending, sizeof(pending))) { + PR_WARN("guided re-provision intent save failed"); + } +} + +bool guided_wechat_reprovision_pending(void) +{ + uint8_t *value = NULL; + size_t length = 0; + bool pending = false; + + if (OPRT_OK == tal_kv_get(GUIDED_WECHAT_REPROVISION_KEY, &value, &length) && + value != NULL && length > 0 && value[0] == 1) { + pending = true; + } + + if (value != NULL) { + tal_kv_free(value); + } + return pending; +} + +void guided_wechat_reprovision_clear(void) +{ + if (OPRT_OK != tal_kv_del(GUIDED_WECHAT_REPROVISION_KEY)) { + PR_WARN("guided re-provision intent clear failed"); + } +} + +void guided_wechat_state_read(guided_wechat_device_state_t *state) +{ + tuya_iot_license_t license = {0}; + tuya_iot_client_t *client; + netmgr_status_e link_status = NETMGR_LINK_DOWN; + const char *uuid = NULL; + const char *authkey = NULL; + + if (state == NULL) { + return; + } + memset(state, 0, sizeof(*state)); + state->pairing_state = sg_pairing_state; + __copy_value(state->version, sizeof(state->version), PROJECT_VERSION); + __copy_value(state->sdk_version, sizeof(state->sdk_version), OPEN_VERSION); + + if (OPRT_OK == tuya_authorize_read(&license) && __license_valid(license.uuid, license.authkey)) { + state->auth_source = GUIDED_WECHAT_AUTH_STORAGE; + uuid = license.uuid; + authkey = license.authkey; + } + + client = tuya_iot_client_get(); + if (client != NULL) { + state->activated = tuya_iot_activated(client); + state->cloud_online = tuya_iot_is_connected(); + __copy_value(state->version, sizeof(state->version), client->config.software_ver); + __copy_value(state->product_id, sizeof(state->product_id), client->config.productkey); + __copy_value(state->device_id, sizeof(state->device_id), tuya_iot_devid_get(client)); + + if (state->auth_source == GUIDED_WECHAT_AUTH_NONE && + __license_valid(client->config.uuid, client->config.authkey)) { + state->auth_source = GUIDED_WECHAT_AUTH_COMPILED; + uuid = client->config.uuid; + authkey = client->config.authkey; + } + } + + __mask_value(state->uuid_masked, sizeof(state->uuid_masked), uuid); + __mask_value(state->authkey_masked, sizeof(state->authkey_masked), authkey); + + if (OPRT_OK == netmgr_conn_get(NETCONN_AUTO, NETCONN_CMD_STATUS, &link_status)) { + state->link_up = link_status == NETMGR_LINK_UP || link_status == NETMGR_LINK_UP_SWITH; + } + +#if defined(ENABLE_WIFI) && (ENABLE_WIFI == 1) + netconn_wifi_info_t wifi_info = {0}; + /* NETCONN_CMD_SSID_PSWD is the configured/last requested network. Only + * expose it as the current network while the link is actually up; when + * the station is down this value would otherwise make the UI report a + * stale SSID as if the device were still connecting to it. */ + if (state->link_up && + OPRT_OK == netmgr_conn_get(NETCONN_WIFI, NETCONN_CMD_SSID_PSWD, &wifi_info)) { + snprintf(state->ssid, sizeof(state->ssid), "%s", wifi_info.ssid); + } +#endif +} + +bool guided_wechat_state_equal(const guided_wechat_device_state_t *left, + const guided_wechat_device_state_t *right) +{ + return left != NULL && right != NULL && memcmp(left, right, sizeof(*left)) == 0; +} diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.h b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.h new file mode 100644 index 000000000..88339a6ce --- /dev/null +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_state.h @@ -0,0 +1,58 @@ +/** + * @file guided_wechat_state.h + * @brief Read-only snapshot of authorization, networking and cloud state. + */ + +#ifndef __GUIDED_WECHAT_STATE_H__ +#define __GUIDED_WECHAT_STATE_H__ + +#include + +#define GUIDED_WECHAT_VALUE_LEN 64 +#define GUIDED_WECHAT_SSID_LEN 34 + +typedef enum { + GUIDED_WECHAT_AUTH_NONE = 0, + GUIDED_WECHAT_AUTH_STORAGE, + GUIDED_WECHAT_AUTH_COMPILED, +} guided_wechat_auth_source_t; + +typedef enum { + GUIDED_WECHAT_PAIRING_IDLE = 0, + GUIDED_WECHAT_PAIRING_STARTING, + GUIDED_WECHAT_PAIRING_WAITING_PHONE, + GUIDED_WECHAT_PAIRING_CONNECTING, +} guided_wechat_pairing_state_t; + +typedef struct { + guided_wechat_auth_source_t auth_source; + guided_wechat_pairing_state_t pairing_state; + bool link_up; + bool activated; + bool cloud_online; + char ssid[GUIDED_WECHAT_SSID_LEN]; + char version[GUIDED_WECHAT_VALUE_LEN]; + char sdk_version[GUIDED_WECHAT_VALUE_LEN]; + char product_id[GUIDED_WECHAT_VALUE_LEN]; + char device_id[GUIDED_WECHAT_VALUE_LEN]; + char uuid_masked[GUIDED_WECHAT_VALUE_LEN]; + char authkey_masked[GUIDED_WECHAT_VALUE_LEN]; +} guided_wechat_device_state_t; + +void guided_wechat_state_read(guided_wechat_device_state_t *state); +bool guided_wechat_state_equal(const guided_wechat_device_state_t *left, + const guided_wechat_device_state_t *right); + +/* Pairing state notifications from the application/SDK event boundary. */ +void guided_wechat_pairing_request(void); +void guided_wechat_pairing_begin(void); +void guided_wechat_pairing_data_received(void); +void guided_wechat_pairing_complete(void); +void guided_wechat_pairing_reset(void); + +/* Persist the user intent to resume the guided network step after reboot. */ +void guided_wechat_reprovision_mark(void); +bool guided_wechat_reprovision_pending(void); +void guided_wechat_reprovision_clear(void); + +#endif /* __GUIDED_WECHAT_STATE_H__ */ diff --git a/src/liblvgl/Kconfig b/src/liblvgl/Kconfig index 2a7fef602..adde14a03 100755 --- a/src/liblvgl/Kconfig +++ b/src/liblvgl/Kconfig @@ -47,6 +47,10 @@ menuconfig ENABLE_LIBLVGL select ENABLE_TP default n + config LV_USE_QRCODE + bool "enable lvgl QR code library" + default n + config ENABLE_LVGL_DEMO bool "enable lvgl demo" default n @@ -108,4 +112,4 @@ menuconfig ENABLE_LIBLVGL # PC Simulator rsource "simulator/Kconfig" - endif \ No newline at end of file + endif diff --git a/src/liblvgl/v9/CMakeLists.txt b/src/liblvgl/v9/CMakeLists.txt index 06d675be2..ed1cc7446 100755 --- a/src/liblvgl/v9/CMakeLists.txt +++ b/src/liblvgl/v9/CMakeLists.txt @@ -55,6 +55,10 @@ target_compile_options(${MODULE_NAME} PRIVATE ${LIB_OPTIONS} ) + +if (CONFIG_LV_USE_QRCODE STREQUAL "y") + target_compile_definitions(${MODULE_NAME} PRIVATE LV_USE_QRCODE=1) +endif() ######################################## # Layer Configure ######################################## diff --git a/src/liblvgl/v9/conf/lv_conf.h b/src/liblvgl/v9/conf/lv_conf.h index c9bb4af74..bf929063b 100755 --- a/src/liblvgl/v9/conf/lv_conf.h +++ b/src/liblvgl/v9/conf/lv_conf.h @@ -812,7 +812,13 @@ #define LV_USE_RLE 0 /*QR code library*/ -#define LV_USE_QRCODE 0 +#ifndef LV_USE_QRCODE + #ifdef CONFIG_LV_USE_QRCODE + #define LV_USE_QRCODE CONFIG_LV_USE_QRCODE + #else + #define LV_USE_QRCODE 0 + #endif +#endif /*Barcode code library*/ #define LV_USE_BARCODE 0 From a51c7d61b5df99bc314773f4312640b777e1f3f0 Mon Sep 17 00:00:00 2001 From: jianning773 Date: Fri, 18 Sep 2026 09:45:33 +0800 Subject: [PATCH 2/4] refactor(ai-ui): route pairing events through ai_ui disp interface The app layer called guided_wechat_pairing_*() directly behind ENABLE_AI_CHAT_GUI_GUIDED_WECHAT guards, coupling your_chat_bot to one specific GUI variant. Add AI_UI_DISP_NETCFG_BEGIN / _TOKEN_RECEIVED / _COMPLETE / _RESET display commands and translate them inside the guided WeChat UI via its disp_other_msg handler, so the application stays GUI-agnostic: any UI variant can implement (or ignore) the pairing notifications without app-level #if churn. Co-Authored-By: Claude Code --- apps/tuya.ai/your_chat_bot/src/tuya_main.c | 20 ++++++------- .../ai_ui/include/ai_ui_manage.h | 5 ++++ .../src/guided_wechat/guided_wechat_core.c | 28 +++++++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/tuya.ai/your_chat_bot/src/tuya_main.c b/apps/tuya.ai/your_chat_bot/src/tuya_main.c index a327a21dd..e1d03f638 100644 --- a/apps/tuya.ai/your_chat_bot/src/tuya_main.c +++ b/apps/tuya.ai/your_chat_bot/src/tuya_main.c @@ -46,8 +46,8 @@ #include "app_chat_bot.h" #include "reset_netcfg.h" -#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) -#include "guided_wechat_state.h" +#if defined(ENABLE_COMP_AI_DISPLAY) && (ENABLE_COMP_AI_DISPLAY == 1) +#include "ai_ui_manage.h" #endif #if defined(ENABLE_BATTERY) && (ENABLE_BATTERY == 1) @@ -172,8 +172,8 @@ void user_event_handler_on(tuya_iot_client_t *client, tuya_event_msg_t *event) case TUYA_EVENT_BIND_START: PR_INFO("Device Bind Start!"); -#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) - guided_wechat_pairing_begin(); +#if defined(ENABLE_COMP_AI_DISPLAY) && (ENABLE_COMP_AI_DISPLAY == 1) + ai_ui_disp_msg(AI_UI_DISP_NETCFG_BEGIN, NULL, 0); #endif #if defined(ENABLE_COMP_AI_AUDIO) && (ENABLE_COMP_AI_AUDIO == 1) @@ -192,8 +192,8 @@ void user_event_handler_on(tuya_iot_client_t *client, tuya_event_msg_t *event) } break; case TUYA_EVENT_BIND_TOKEN_ON: -#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) - guided_wechat_pairing_data_received(); +#if defined(ENABLE_COMP_AI_DISPLAY) && (ENABLE_COMP_AI_DISPLAY == 1) + ai_ui_disp_msg(AI_UI_DISP_NETCFG_TOKEN_RECEIVED, NULL, 0); #endif break; @@ -201,8 +201,8 @@ void user_event_handler_on(tuya_iot_client_t *client, tuya_event_msg_t *event) case TUYA_EVENT_MQTT_CONNECTED: PR_INFO("Device MQTT Connected!"); -#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) - guided_wechat_pairing_complete(); +#if defined(ENABLE_COMP_AI_DISPLAY) && (ENABLE_COMP_AI_DISPLAY == 1) + ai_ui_disp_msg(AI_UI_DISP_NETCFG_COMPLETE, NULL, 0); #endif static uint8_t first = 1; @@ -239,8 +239,8 @@ void user_event_handler_on(tuya_iot_client_t *client, tuya_event_msg_t *event) tuya_reset_type_t reset_type = (tuya_reset_type_t)event->value.asInteger; PR_INFO("Device Reset:%d", reset_type); -#if defined(ENABLE_AI_CHAT_GUI_GUIDED_WECHAT) && (ENABLE_AI_CHAT_GUI_GUIDED_WECHAT == 1) - guided_wechat_pairing_reset(); +#if defined(ENABLE_COMP_AI_DISPLAY) && (ENABLE_COMP_AI_DISPLAY == 1) + ai_ui_disp_msg(AI_UI_DISP_NETCFG_RESET, NULL, 0); #endif // TUYA_RESET_TYPE_FACTORY, TUYA_RESET_TYPE_REMOTE_FACTORY, TUYA_RESET_TYPE_DATA_FACTORY diff --git a/src/ai_components/ai_ui/include/ai_ui_manage.h b/src/ai_components/ai_ui/include/ai_ui_manage.h index 4dbe8ee7d..e6925fac0 100644 --- a/src/ai_components/ai_ui/include/ai_ui_manage.h +++ b/src/ai_components/ai_ui/include/ai_ui_manage.h @@ -50,6 +50,11 @@ typedef enum { AI_UI_DISP_NETWORK, AI_UI_DISP_CHAT_MODE, + AI_UI_DISP_NETCFG_BEGIN, + AI_UI_DISP_NETCFG_TOKEN_RECEIVED, + AI_UI_DISP_NETCFG_COMPLETE, + AI_UI_DISP_NETCFG_RESET, + AI_UI_DISP_CAMERA_OPEN, AI_UI_DISP_CAMERA_FLUSH, AI_UI_DISP_CAMERA_THUMB, diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c index 26b4b9996..de4e87ff8 100644 --- a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_core.c @@ -15,6 +15,7 @@ #include "font_awesome_symbols.h" #include "guided_wechat_core.h" #include "guided_wechat_common.h" +#include "guided_wechat_state.h" #include "ai_ui_locale.h" LV_FONT_DECLARE(font_puhui_14_1); @@ -170,6 +171,32 @@ static void __ui_set_chat_mode(char *chat_mode) lv_vendor_disp_unlock(); } +/* Pairing-guide state notifications, routed from AI_UI_DISP_NETCFG_* messages. + * The application only knows the generic AI_UI_DISP commands; this UI variant + * translates them into its own pairing state machine. */ +static void __ui_other_msg(uint32_t type, uint8_t *data, int len) +{ + (void)data; + (void)len; + + switch ((AI_UI_DISP_TYPE_E)type) { + case AI_UI_DISP_NETCFG_BEGIN: + guided_wechat_pairing_begin(); + break; + case AI_UI_DISP_NETCFG_TOKEN_RECEIVED: + guided_wechat_pairing_data_received(); + break; + case AI_UI_DISP_NETCFG_COMPLETE: + guided_wechat_pairing_complete(); + break; + case AI_UI_DISP_NETCFG_RESET: + guided_wechat_pairing_reset(); + break; + default: + break; + } +} + /* 鈹€鈹€ UI init 鈹€鈹€ */ OPERATE_RET guided_wechat_core_init(void) @@ -288,6 +315,7 @@ OPERATE_RET guided_wechat_register_with_init(OPERATE_RET (*disp_init)(void)) intfs.disp_notification = __ui_set_notification; intfs.disp_wifi_state = __ui_set_network; intfs.disp_ai_chat_mode = __ui_set_chat_mode; + intfs.disp_other_msg = __ui_other_msg; ai_ui_register(&intfs); From d86849c283385f4ad9b30aaaee9d51bb3e62b868 Mon Sep 17 00:00:00 2001 From: jianning773 Date: Fri, 18 Sep 2026 12:58:51 +0800 Subject: [PATCH 3/4] feat(lvgl): make v8 LV_USE_QRCODE Kconfig-driven like v9 Mirror the v9 Kconfig plumbing into liblvgl v8: the shared LV_USE_QRCODE option now defaults to y when LVGL_VERSION_8 is selected (preserving the previous hardcoded "#define LV_USE_QRCODE 1" behavior for existing v8 users) and n otherwise. The conf-level fallback stays 0 on both versions so an explicit CONFIG_LV_USE_QRCODE=n is always honored. - liblvgl/Kconfig: default y if LVGL_VERSION_8 - v8/CMakeLists.txt: bridge CONFIG_LV_USE_QRCODE to -DLV_USE_QRCODE=1 - v8/conf/lv_conf.h: replace the hardcode with the v9-style wrapper Verified on DNESP32S3_BOX (esp32s3, LVGL v8): default build keeps qrcode compiled in (lv_qrcode.c.o carries lv_qrcode_create), and an explicit "# CONFIG_LV_USE_QRCODE is not set" produces an empty translation unit. Co-Authored-By: Claude Code --- src/liblvgl/Kconfig | 1 + src/liblvgl/v8/CMakeLists.txt | 4 ++++ src/liblvgl/v8/conf/lv_conf.h | 8 +++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/liblvgl/Kconfig b/src/liblvgl/Kconfig index adde14a03..daeb35f72 100755 --- a/src/liblvgl/Kconfig +++ b/src/liblvgl/Kconfig @@ -49,6 +49,7 @@ menuconfig ENABLE_LIBLVGL config LV_USE_QRCODE bool "enable lvgl QR code library" + default y if LVGL_VERSION_8 default n config ENABLE_LVGL_DEMO diff --git a/src/liblvgl/v8/CMakeLists.txt b/src/liblvgl/v8/CMakeLists.txt index e538b6e55..13e87399e 100755 --- a/src/liblvgl/v8/CMakeLists.txt +++ b/src/liblvgl/v8/CMakeLists.txt @@ -56,6 +56,10 @@ target_compile_options(${MODULE_NAME} ${LIB_OPTIONS} -Wno-error=format= ) + +if (CONFIG_LV_USE_QRCODE STREQUAL "y") + target_compile_definitions(${MODULE_NAME} PRIVATE LV_USE_QRCODE=1) +endif() ######################################## # Layer Configure ######################################## diff --git a/src/liblvgl/v8/conf/lv_conf.h b/src/liblvgl/v8/conf/lv_conf.h index 68bd8ea89..64f088634 100755 --- a/src/liblvgl/v8/conf/lv_conf.h +++ b/src/liblvgl/v8/conf/lv_conf.h @@ -758,7 +758,13 @@ #define LV_USE_GIF 1 /*QR code library*/ -#define LV_USE_QRCODE 1 +#ifndef LV_USE_QRCODE + #ifdef CONFIG_LV_USE_QRCODE + #define LV_USE_QRCODE CONFIG_LV_USE_QRCODE + #else + #define LV_USE_QRCODE 0 + #endif +#endif /*FreeType library*/ #define LV_USE_FREETYPE 0 From 69a9b2e6fb48ef34f7c5406ac6b85f9e282eb322 Mon Sep 17 00:00:00 2001 From: jianning773 Date: Fri, 18 Sep 2026 16:24:37 +0800 Subject: [PATCH 4/4] feat(ai-ui): hide menu button while camera or album overlay is open The guided WeChat shell's bottom-left menu button stayed visible on top of the full-screen camera and album pages because those overlays bypass the guided page state machine that owns its visibility. Add a shell-owned helper guided_wechat_menu_btn_set_hidden() and call it from the camera/album display callbacks: hide on open (including the select-mode entry that skips disp_open) and restore on close, only when the chat surface is active so guided pages keep it hidden. Co-Authored-By: Claude Code --- .../guided_wechat/ai_ui_chat_guided_wechat.c | 18 ++++++++++++++++++ .../src/guided_wechat/guided_wechat_album.c | 10 ++++++++++ .../src/guided_wechat/guided_wechat_camera.c | 6 ++++++ .../src/guided_wechat/guided_wechat_common.h | 3 +++ 4 files changed, 37 insertions(+) diff --git a/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c index 1f35cb648..7c0f13b28 100644 --- a/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c +++ b/src/ai_components/ai_ui/src/guided_wechat/ai_ui_chat_guided_wechat.c @@ -835,6 +835,24 @@ static void __menu_button_cb(lv_event_t *event) } } +void guided_wechat_menu_btn_set_hidden(bool hidden) +{ + if (NULL == sg_ui.menu_button) { + return; + } + + lv_vendor_disp_lock(); + if (hidden) { + lv_obj_add_flag(sg_ui.menu_button, LV_OBJ_FLAG_HIDDEN); + } else if (lv_obj_has_flag(sg_ui.overlay, LV_OBJ_FLAG_HIDDEN)) { + /* Overlay hidden means the chat surface is active; guided pages keep + * the menu button hidden via __show_page(). */ + lv_obj_clear_flag(sg_ui.menu_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(sg_ui.menu_button); + } + lv_vendor_disp_unlock(); +} + static void __refresh_cb(lv_timer_t *timer) { guided_wechat_device_state_t next; diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c index 3bc277523..0d53d1f1a 100644 --- a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_album.c @@ -697,6 +697,9 @@ static void __create_select_page(lv_obj_t *parent) static void __disp_open(void) { + /* Full-screen album page: hide the shell menu button while active. */ + guided_wechat_menu_btn_set_hidden(true); + lv_vendor_disp_lock(); lv_obj_clear_flag(sg_album.view_page, LV_OBJ_FLAG_HIDDEN); lv_vendor_disp_unlock(); @@ -852,6 +855,10 @@ static void __disp_select_img_thumb_list(AI_UI_IMG_T *item_arr, uint32_t arr_cnt { (void)select_num_max; + /* Select mode can be entered directly from chat (add-image) without + * disp_open(): hide the shell menu button on this path too. */ + guided_wechat_menu_btn_set_hidden(true); + lv_vendor_disp_lock(); /* Show select page, hide others */ @@ -921,6 +928,9 @@ static void __disp_close(void) lv_obj_add_flag(sg_album.print_overlay, LV_OBJ_FLAG_HIDDEN); #endif lv_vendor_disp_unlock(); + + /* Restore the shell menu button (only when back on the chat surface). */ + guided_wechat_menu_btn_set_hidden(false); } /* 鈹€鈹€ public API 鈹€鈹€ */ diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c index ed0056130..5426534f3 100644 --- a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_camera.c @@ -120,6 +120,9 @@ static void __disp_open(void) return; } + /* Full-screen camera page: hide the shell menu button while active. */ + guided_wechat_menu_btn_set_hidden(true); + lv_vendor_disp_lock(); lv_obj_clear_flag(sg_camera.page, LV_OBJ_FLAG_HIDDEN); /* Always restore controls 鈥?they may have been hidden by a preview tap before close */ @@ -262,6 +265,9 @@ static void __disp_close(void) lv_obj_add_flag(sg_camera.page, LV_OBJ_FLAG_HIDDEN); lv_vendor_disp_unlock(); + /* Restore the shell menu button (only when back on the chat surface). */ + guided_wechat_menu_btn_set_hidden(false); + if (sg_camera.preview_buf) { CAMERA_UI_FREE(sg_camera.preview_buf); sg_camera.preview_buf = NULL; diff --git a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h index 960897be5..b514796cf 100644 --- a/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h +++ b/src/ai_components/ai_ui/src/guided_wechat/guided_wechat_common.h @@ -43,6 +43,9 @@ extern "C" { void guided_wechat_chat_init(lv_obj_t *parent); void guided_wechat_chat_register(void); +/* Shell chrome (chat-surface menu button owned by the guided shell) */ +void guided_wechat_menu_btn_set_hidden(bool hidden); + /* Camera page */ #if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) void guided_wechat_camera_init(lv_obj_t *parent);