All checks were successful
ESP32 Build & Release / build (push) Successful in 1m16s
2426 lines
91 KiB
C
2426 lines
91 KiB
C
/*
|
|
* MTG RFID Companion - UI task with LVGL
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "esp_log.h"
|
|
#include "esp_random.h"
|
|
#include "driver/gpio.h"
|
|
#include "hw_pins.h"
|
|
#include "cJSON.h"
|
|
#include "display_manager.h"
|
|
#include "rfid_manager.h"
|
|
#include "storage_manager.h"
|
|
#include "scryfall_client.h"
|
|
#include "wifi_manager.h"
|
|
#include "app_state.h"
|
|
#include "encoder.h"
|
|
#include "buzzer.h"
|
|
#include "jpeg_decoder.h"
|
|
#include "ota_manager.h"
|
|
#include "ui_task.h"
|
|
|
|
static const char *TAG = "ui";
|
|
|
|
#define UI_TASK_STACK (8 * 1024)
|
|
#define UI_TASK_PRIO (5)
|
|
#define UI_CORE (1)
|
|
#define COLOR_AMBER (0xFFB300)
|
|
#define COLOR_GOLD (0xFFCC00)
|
|
|
|
static TaskHandle_t s_ui_task = NULL;
|
|
static char s_uid[RFID_UID_STR_MAX_LEN] = {0};
|
|
static char s_card_name[128] = {0};
|
|
static bool s_waiting_fetch = false;
|
|
|
|
static lv_img_dsc_t s_art_img_dsc;
|
|
static uint16_t *s_art_pixels = NULL;
|
|
|
|
/* Inactivity sleep timer (3 minutes) */
|
|
#define UI_SLEEP_TIMEOUT_MS (180000)
|
|
static uint32_t s_last_activity_ms = 0;
|
|
|
|
/* Menu items */
|
|
#define MAIN_MENU_ITEM_COUNT 6
|
|
static int s_menu_idx = 0;
|
|
static const char *s_menu_labels[MAIN_MENU_ITEM_COUNT] = {
|
|
"1. CARD SCANNER",
|
|
"2. LIFE & COUNTERS",
|
|
"3. DICE & COIN FLIP",
|
|
"4. COMMANDER TAX",
|
|
"5. TOKEN & COUNTERS",
|
|
"6. SETTINGS & TOOLS"
|
|
};
|
|
|
|
#define SETTINGS_MENU_ITEM_COUNT 5
|
|
static int s_settings_idx = 0;
|
|
static const char *s_settings_labels[SETTINGS_MENU_ITEM_COUNT] = {
|
|
"< BACK TO MAIN MENU",
|
|
"1. AUDIO: CHIPTUNE",
|
|
"2. DECK STORAGE",
|
|
"3. DEVICE & WIFI INFO",
|
|
"4. UPDATE FIRMWARE"
|
|
};
|
|
|
|
/* Life & Counters */
|
|
typedef enum {
|
|
LIFE_MODE_TOTAL = 0,
|
|
LIFE_MODE_POISON,
|
|
LIFE_MODE_CMD_DMG,
|
|
} life_counter_mode_t;
|
|
|
|
static life_counter_mode_t s_life_mode = LIFE_MODE_TOTAL;
|
|
static int32_t s_poison = 0;
|
|
static int32_t s_cmd_dmg = 0;
|
|
static bool s_was_lethal = false;
|
|
|
|
/* Dice & Coin */
|
|
typedef enum {
|
|
DICE_MODE_D20 = 0,
|
|
DICE_MODE_D6,
|
|
DICE_MODE_COIN,
|
|
} dice_mode_t;
|
|
|
|
static dice_mode_t s_dice_mode = DICE_MODE_D20;
|
|
static int s_dice_result = 20;
|
|
static bool s_dice_rolled = false;
|
|
|
|
/* Commander Tax & Turn Tracker */
|
|
static int s_cmd_casts = 0;
|
|
static int s_turn_count = 1;
|
|
static bool s_tax_edit_turns = false;
|
|
|
|
/* Token & Counter Spawner */
|
|
typedef struct {
|
|
const char *name;
|
|
const char *type_line;
|
|
const char *pt;
|
|
const char *oracle;
|
|
uint32_t color_hex;
|
|
int count;
|
|
} mtg_token_t;
|
|
|
|
#define TOKEN_COUNT 8
|
|
static mtg_token_t s_tokens[TOKEN_COUNT] = {
|
|
{"TREASURE", "Artifact Token", "", "{T}, Sacrifice: Add one mana of any color.", 0xFFB300, 0},
|
|
{"FOOD", "Artifact Token", "", "{2}, {T}, Sacrifice: You gain 3 life.", 0x00E676, 0},
|
|
{"CLUE", "Artifact Token", "", "{2}, Sacrifice: Draw a card.", 0x00E5FF, 0},
|
|
{"SOLDIER", "Creature Token — Soldier", "1 / 1", "White Soldier creature token.", 0xFFFFFF, 0},
|
|
{"ZOMBIE", "Creature Token — Zombie", "2 / 2", "Black Zombie creature token.", 0xBA68C8, 0},
|
|
{"GOBLIN", "Creature Token — Goblin", "1 / 1", "Red Goblin creature token.", 0xFF5252, 0},
|
|
{"BEAST", "Creature Token — Beast", "3 / 3", "Green Beast creature token.", 0x66BB6A, 0},
|
|
{"+1/+1 COUNTER", "Stat Counter", "", "Put a +1/+1 counter on target permanent.", 0xFFD700, 0},
|
|
};
|
|
|
|
static int s_token_idx = 0;
|
|
static bool s_token_edit_count = false;
|
|
|
|
/* Storage Manager */
|
|
static bool s_storage_confirm_dialog = false;
|
|
static int s_storage_option_idx = 0;
|
|
|
|
/* Button holding timer */
|
|
static uint32_t s_btn_held_ms = 0;
|
|
static bool s_btn_long_fired = false;
|
|
|
|
/* OTA progress widgets */
|
|
static lv_obj_t *s_ota_bar = NULL;
|
|
static lv_obj_t *s_ota_pct_lbl = NULL;
|
|
static lv_obj_t *s_ota_bytes_lbl = NULL;
|
|
|
|
/* Card view scrollable container */
|
|
static lv_obj_t *s_card_scroll_cont = NULL;
|
|
|
|
/* Forward declarations */
|
|
static void view_menu(void);
|
|
static void view_settings_menu(void);
|
|
static void view_scanner(void);
|
|
static void view_card(void);
|
|
static void view_life(void);
|
|
static void view_dice(void);
|
|
static void view_commander_tax(void);
|
|
static void view_token_spawner(void);
|
|
static void view_storage(void);
|
|
static void view_device_info(void);
|
|
static void view_unmapped(const char *uid);
|
|
static void view_fetching(const char *name);
|
|
static void view_ota(void);
|
|
|
|
static void free_art_buffer(void)
|
|
{
|
|
if (s_art_pixels != NULL) {
|
|
free(s_art_pixels);
|
|
s_art_pixels = NULL;
|
|
}
|
|
}
|
|
|
|
static bool load_art_crop(const char *uid)
|
|
{
|
|
free_art_buffer();
|
|
|
|
char jpg_path[72];
|
|
snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", uid);
|
|
if (!storage_file_exists(jpg_path)) {
|
|
return false;
|
|
}
|
|
|
|
FILE *f = fopen(jpg_path, "rb");
|
|
if (f == NULL) {
|
|
return false;
|
|
}
|
|
fseek(f, 0, SEEK_END);
|
|
long fsize = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (fsize <= 0 || fsize > 250 * 1024) {
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
|
|
uint8_t *jpg_buf = malloc((size_t)fsize);
|
|
if (jpg_buf == NULL) {
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
|
|
if (fread(jpg_buf, 1, (size_t)fsize, f) != (size_t)fsize) {
|
|
free(jpg_buf);
|
|
fclose(f);
|
|
return false;
|
|
}
|
|
fclose(f);
|
|
|
|
esp_jpeg_image_cfg_t cfg = {
|
|
.indata = jpg_buf,
|
|
.indata_size = (uint32_t)fsize,
|
|
.outbuf = NULL,
|
|
.outbuf_size = 0,
|
|
.out_format = JPEG_IMAGE_FORMAT_RGB565,
|
|
.out_scale = JPEG_IMAGE_SCALE_0,
|
|
.flags = {
|
|
.swap_color_bytes = 1,
|
|
},
|
|
};
|
|
esp_jpeg_image_output_t out_info = {0};
|
|
if (esp_jpeg_get_image_info(&cfg, &out_info) != ESP_OK) {
|
|
ESP_LOGW(TAG, "Unsupported or corrupt JPEG: %s", jpg_path);
|
|
free(jpg_buf);
|
|
remove(jpg_path);
|
|
return false;
|
|
}
|
|
|
|
/* Automatically purge legacy 200x110 cropped art and re-fetch with full uncropped aspect ratio */
|
|
if (out_info.width == 200 && out_info.height == 110 && wifi_manager_connected()) {
|
|
ESP_LOGI(TAG, "Purging legacy 200x110 cropped art: %s", jpg_path);
|
|
free(jpg_buf);
|
|
remove(jpg_path);
|
|
if (s_card_name[0] != '\0') {
|
|
scryfall_fetch_request(s_card_name, uid);
|
|
} else {
|
|
char *looked_up_name = NULL;
|
|
if (storage_manager_lookup_name(uid, &looked_up_name) == ESP_OK && looked_up_name != NULL) {
|
|
scryfall_fetch_request(looked_up_name, uid);
|
|
free(looked_up_name);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (out_info.width > 240) {
|
|
cfg.out_scale = (out_info.width > 480) ? JPEG_IMAGE_SCALE_1_4 : JPEG_IMAGE_SCALE_1_2;
|
|
esp_jpeg_get_image_info(&cfg, &out_info);
|
|
}
|
|
|
|
s_art_pixels = malloc(out_info.output_len);
|
|
if (s_art_pixels == NULL) {
|
|
ESP_LOGE(TAG, "Failed to allocate %d bytes for art crop", (int)out_info.output_len);
|
|
free(jpg_buf);
|
|
return false;
|
|
}
|
|
|
|
cfg.outbuf = (uint8_t *)s_art_pixels;
|
|
cfg.outbuf_size = (uint32_t)out_info.output_len;
|
|
|
|
esp_err_t err = esp_jpeg_decode(&cfg, &out_info);
|
|
free(jpg_buf);
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG, "Decode failed for %s, removing", jpg_path);
|
|
remove(jpg_path);
|
|
free_art_buffer();
|
|
return false;
|
|
}
|
|
|
|
s_art_img_dsc.header.always_zero = 0;
|
|
s_art_img_dsc.header.w = out_info.width;
|
|
s_art_img_dsc.header.h = out_info.height;
|
|
s_art_img_dsc.data_size = out_info.output_len;
|
|
s_art_img_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
|
|
s_art_img_dsc.data = (const uint8_t *)s_art_pixels;
|
|
|
|
ESP_LOGI(TAG, "Decoded art crop: %dx%d (%d bytes)", out_info.width, out_info.height, (int)out_info.output_len);
|
|
return true;
|
|
}
|
|
|
|
static const char *get_json_str(cJSON *root, const char *key, const char *fallback)
|
|
{
|
|
cJSON *item = cJSON_GetObjectItem(root, key);
|
|
if (item != NULL && cJSON_IsString(item) && item->valuestring != NULL) {
|
|
return item->valuestring;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* LVGL Screens */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void ui_clean_screen(void)
|
|
{
|
|
free_art_buffer();
|
|
s_card_scroll_cont = NULL;
|
|
s_ota_bar = NULL;
|
|
s_ota_pct_lbl = NULL;
|
|
s_ota_bytes_lbl = NULL;
|
|
lv_obj_clean(lv_scr_act());
|
|
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_black(), 0);
|
|
lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0);
|
|
}
|
|
|
|
/* single shared header builder across all screens */
|
|
static void ui_draw_header(const char *title, const char *subtitle)
|
|
{
|
|
lv_obj_t *bar = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(bar, DISP_W, 4);
|
|
lv_obj_set_style_bg_color(bar, lv_color_hex(COLOR_GOLD), 0);
|
|
lv_obj_set_style_border_width(bar, 0, 0);
|
|
lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0);
|
|
|
|
if (title != NULL) {
|
|
lv_obj_t *t = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(t, title);
|
|
lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(t, lv_color_hex(COLOR_GOLD), 0);
|
|
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, (subtitle != NULL) ? 10 : 14);
|
|
}
|
|
|
|
if (subtitle != NULL) {
|
|
lv_obj_t *sub = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(sub, subtitle);
|
|
lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(sub, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 34);
|
|
}
|
|
}
|
|
|
|
/* 1. Main Menu */
|
|
static void view_menu(void)
|
|
{
|
|
encoder_set_steps_per_detent(4);
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
ui_draw_header("MTG COMPANION","");
|
|
|
|
/* 6 Menu Items */
|
|
int start_y = 44;
|
|
int row_height = 34;
|
|
int row_gap = 7;
|
|
for (int i = 0; i < MAIN_MENU_ITEM_COUNT; i++) {
|
|
bool selected = (i == s_menu_idx);
|
|
lv_obj_t *item_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(item_box, DISP_W - 24, row_height);
|
|
lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * (row_height + row_gap));
|
|
lv_obj_set_style_radius(item_box, 6, 0);
|
|
lv_obj_set_style_pad_all(item_box, 2, 0);
|
|
lv_obj_set_scrollbar_mode(item_box, LV_SCROLLBAR_MODE_OFF);
|
|
|
|
if (selected) {
|
|
lv_obj_set_style_bg_color(item_box, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_color(item_box, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(item_box, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(item_box, lv_color_hex(0x1C1C1C), 0);
|
|
lv_obj_set_style_border_color(item_box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(item_box, 1, 0);
|
|
}
|
|
|
|
const char *item_txt = s_menu_labels[i];
|
|
lv_obj_t *lbl = lv_label_create(item_box);
|
|
if (selected) {
|
|
lv_label_set_text_fmt(lbl, "> %s <", item_txt);
|
|
lv_obj_set_style_text_color(lbl, lv_color_black(), 0);
|
|
} else {
|
|
lv_label_set_text_fmt(lbl, " %s", item_txt);
|
|
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
|
|
}
|
|
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
/* Bottom navigation hint */
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "ROTATE: NAV • CLICK: SELECT");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* Settings & Tools Submenu */
|
|
static void view_settings_menu(void)
|
|
{
|
|
encoder_set_steps_per_detent(4);
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
ui_draw_header("SETTINGS & TOOLS", "");
|
|
|
|
char audio_lbl_buf[36];
|
|
snprintf(audio_lbl_buf, sizeof(audio_lbl_buf), "1. AUDIO: %s", buzzer_get_profile_name(buzzer_get_profile()));
|
|
|
|
int start_y = 50;
|
|
int row_height = 34;
|
|
int row_gap = 8;
|
|
|
|
for (int i = 0; i < SETTINGS_MENU_ITEM_COUNT; i++) {
|
|
bool selected = (i == s_settings_idx);
|
|
lv_obj_t *item_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(item_box, DISP_W - 24, row_height);
|
|
lv_obj_align(item_box, LV_ALIGN_TOP_MID, 0, start_y + i * (row_height + row_gap));
|
|
lv_obj_set_style_radius(item_box, 6, 0);
|
|
lv_obj_set_style_pad_all(item_box, 2, 0);
|
|
lv_obj_set_scrollbar_mode(item_box, LV_SCROLLBAR_MODE_OFF);
|
|
|
|
if (selected) {
|
|
lv_obj_set_style_bg_color(item_box, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_color(item_box, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(item_box, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(item_box, lv_color_hex(0x1C1C1C), 0);
|
|
lv_obj_set_style_border_color(item_box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(item_box, 1, 0);
|
|
}
|
|
|
|
const char *item_txt = (i == 1) ? audio_lbl_buf : s_settings_labels[i];
|
|
lv_obj_t *lbl = lv_label_create(item_box);
|
|
if (i == 0) {
|
|
/* Back button item */
|
|
if (selected) {
|
|
lv_label_set_text(lbl, "> < BACK TO MAIN MENU <");
|
|
lv_obj_set_style_text_color(lbl, lv_color_black(), 0);
|
|
} else {
|
|
lv_label_set_text(lbl, "< BACK TO MAIN MENU");
|
|
lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_AMBER), 0);
|
|
}
|
|
} else {
|
|
if (selected) {
|
|
lv_label_set_text_fmt(lbl, "> %s <", item_txt);
|
|
lv_obj_set_style_text_color(lbl, lv_color_black(), 0);
|
|
} else {
|
|
lv_label_set_text_fmt(lbl, " %s", item_txt);
|
|
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
|
|
}
|
|
}
|
|
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
/* Bottom navigation hint */
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "ROTATE: NAV • CLICK: SELECT");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* 2. Card Scanner */
|
|
static void view_scanner(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
lv_obj_t *title = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(title, "CARD SCANNER");
|
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 25);
|
|
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 32, 140);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -15);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_width(box, 2, 0);
|
|
lv_obj_set_style_radius(box, 10, 0);
|
|
|
|
lv_obj_t *sub = lv_label_create(box);
|
|
lv_label_set_text(sub, "TAP RFID CARD\nTO SCAN");
|
|
lv_obj_set_style_text_font(sub, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(sub, lv_color_white(), 0);
|
|
lv_obj_set_style_text_align(sub, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(sub, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
if (s_uid[0] != '\0') {
|
|
lv_obj_t *uid_lbl = lv_label_create(lv_scr_act());
|
|
lv_label_set_text_fmt(uid_lbl, "LAST SCANNED: %s", s_uid);
|
|
lv_obj_set_style_text_font(uid_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(uid_lbl, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(uid_lbl, LV_ALIGN_BOTTOM_MID, 0, -50);
|
|
}
|
|
|
|
lv_obj_t *hint1 = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint1, "HOLD BUTTON: MAIN MENU");
|
|
lv_obj_set_style_text_font(hint1, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint1, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint1, LV_ALIGN_BOTTOM_MID, 0, -28);
|
|
|
|
lv_obj_t *hint2 = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint2, "CLICK BUTTON: LIFE TOTAL");
|
|
lv_obj_set_style_text_font(hint2, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint2, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint2, LV_ALIGN_BOTTOM_MID, 0, -10);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* 3. Life & Counters */
|
|
static void view_life(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
/* Top yellow accent bar */
|
|
lv_obj_t *bar = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(bar, DISP_W, 4);
|
|
lv_obj_set_style_bg_color(bar, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_width(bar, 0, 0);
|
|
lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0);
|
|
|
|
/* Counter Selector Bar */
|
|
const char *modes[3] = {"LIFE", "POISON", "CMD"};
|
|
int tab_w = (DISP_W - 24) / 3;
|
|
for (int i = 0; i < 3; i++) {
|
|
bool active = (i == (int)s_life_mode);
|
|
lv_obj_t *tbox = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(tbox, tab_w, 30);
|
|
lv_obj_align(tbox, LV_ALIGN_TOP_LEFT, 12 + i * tab_w, 10);
|
|
lv_obj_set_style_radius(tbox, 4, 0);
|
|
if (active) {
|
|
lv_obj_set_style_bg_color(tbox, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_color(tbox, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(tbox, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(tbox, lv_color_hex(0x202020), 0);
|
|
lv_obj_set_style_border_color(tbox, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(tbox, 1, 0);
|
|
}
|
|
lv_obj_t *tlbl = lv_label_create(tbox);
|
|
lv_label_set_text(tlbl, modes[i]);
|
|
lv_obj_set_style_text_font(tlbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(tlbl, active ? lv_color_black() : lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(tlbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
/* Main Big Number Display */
|
|
lv_obj_t *life_label = lv_label_create(lv_scr_act());
|
|
lv_obj_set_style_text_font(life_label, &lv_font_montserrat_48, 0);
|
|
lv_obj_align(life_label, LV_ALIGN_CENTER, 0, -20);
|
|
|
|
bool lethal = false;
|
|
if (s_life_mode == LIFE_MODE_TOTAL) {
|
|
int32_t life = app_get_life();
|
|
lv_label_set_text_fmt(life_label, "%ld", (long)life);
|
|
lv_color_t color = (life <= 0) ? lv_color_hex(0xFF1744) :
|
|
(life <= 10) ? lv_color_hex(0xFF9100) :
|
|
(life <= 15) ? lv_color_hex(0xFFCC00) :
|
|
lv_color_hex(0x00E676);
|
|
lv_obj_set_style_text_color(life_label, color, 0);
|
|
if (life <= 0) lethal = true;
|
|
} else if (s_life_mode == LIFE_MODE_POISON) {
|
|
lv_label_set_text_fmt(life_label, "%ld / 10", (long)s_poison);
|
|
lv_color_t color = (s_poison >= 10) ? lv_color_hex(0xFF1744) :
|
|
(s_poison >= 6) ? lv_color_hex(0xFF9100) :
|
|
lv_color_hex(0x00E5FF);
|
|
lv_obj_set_style_text_color(life_label, color, 0);
|
|
if (s_poison >= 10) lethal = true;
|
|
} else {
|
|
lv_label_set_text_fmt(life_label, "%ld / 21", (long)s_cmd_dmg);
|
|
lv_color_t color = (s_cmd_dmg >= 21) ? lv_color_hex(0xFF1744) :
|
|
(s_cmd_dmg >= 15) ? lv_color_hex(0xFF9100) :
|
|
lv_color_hex(0xE040FB);
|
|
lv_obj_set_style_text_color(life_label, color, 0);
|
|
if (s_cmd_dmg >= 21) lethal = true;
|
|
}
|
|
|
|
if (lethal) {
|
|
if (!s_was_lethal) {
|
|
s_was_lethal = true;
|
|
buzzer_play_sound(BUZZER_SOUND_LOSS);
|
|
}
|
|
lv_obj_t *dead_lbl = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(dead_lbl, "*** LETHAL / DEFEAT ***");
|
|
lv_obj_set_style_text_font(dead_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(dead_lbl, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_align(dead_lbl, LV_ALIGN_CENTER, 0, 30);
|
|
} else {
|
|
s_was_lethal = false;
|
|
}
|
|
|
|
/* Subtitle details */
|
|
lv_obj_t *preset_lbl = lv_label_create(lv_scr_act());
|
|
if (s_life_mode == LIFE_MODE_TOTAL) {
|
|
lv_label_set_text_fmt(preset_lbl, "PRESET: %ld (CLICK: CYCLE)", (long)app_get_life_preset());
|
|
} else if (s_life_mode == LIFE_MODE_POISON) {
|
|
lv_label_set_text(preset_lbl, "10 POISON COUNTERS = LOSS");
|
|
} else {
|
|
lv_label_set_text(preset_lbl, "21 COMBAT DAMAGE = LOSS");
|
|
}
|
|
lv_obj_set_style_text_font(preset_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(preset_lbl, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(preset_lbl, LV_ALIGN_BOTTOM_MID, 0, -35);
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "ROTATE: +/- • HOLD: MENU");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -10);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* 4. Dice & Coin Flip */
|
|
static void view_dice(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
ui_draw_header(NULL, NULL);
|
|
|
|
/* Selector tabs */
|
|
const char *tabs[3] = {"[ D20 ]", "[ D6 ]", "[ COIN ]"};
|
|
int tab_w = (DISP_W - 24) / 3;
|
|
for (int i = 0; i < 3; i++) {
|
|
bool active = (i == (int)s_dice_mode);
|
|
lv_obj_t *tbox = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(tbox, tab_w, 32);
|
|
lv_obj_align(tbox, LV_ALIGN_TOP_LEFT, 12 + i * tab_w, 12);
|
|
lv_obj_set_style_radius(tbox, 4, 0);
|
|
if (active) {
|
|
lv_obj_set_style_bg_color(tbox, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_color(tbox, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(tbox, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(tbox, lv_color_hex(0x222222), 0);
|
|
lv_obj_set_style_border_color(tbox, lv_color_hex(0x444444), 0);
|
|
lv_obj_set_style_border_width(tbox, 1, 0);
|
|
}
|
|
lv_obj_t *tlbl = lv_label_create(tbox);
|
|
lv_label_set_text(tlbl, tabs[i]);
|
|
lv_obj_set_style_text_font(tlbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(tlbl, active ? lv_color_black() : lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(tlbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
/* Result Container Box */
|
|
lv_obj_t *rbox = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(rbox, DISP_W - 24, 180);
|
|
lv_obj_align(rbox, LV_ALIGN_CENTER, 0, -10);
|
|
lv_obj_set_style_bg_color(rbox, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(rbox, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_width(rbox, 2, 0);
|
|
lv_obj_set_style_radius(rbox, 12, 0);
|
|
|
|
lv_obj_t *res_lbl = lv_label_create(rbox);
|
|
lv_obj_set_style_text_font(res_lbl, &lv_font_montserrat_48, 0);
|
|
|
|
if (s_dice_mode == DICE_MODE_COIN) {
|
|
if (!s_dice_rolled) {
|
|
lv_label_set_text(res_lbl, "?");
|
|
lv_obj_set_style_text_color(res_lbl, lv_color_hex(COLOR_AMBER), 0);
|
|
} else if (s_dice_result == 1) {
|
|
lv_label_set_text(res_lbl, "HEADS");
|
|
lv_obj_set_style_text_font(res_lbl, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(res_lbl, lv_color_hex(0x00E676), 0);
|
|
} else {
|
|
lv_label_set_text(res_lbl, "TAILS");
|
|
lv_obj_set_style_text_font(res_lbl, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(res_lbl, lv_color_hex(0xFF9100), 0);
|
|
}
|
|
} else {
|
|
if (!s_dice_rolled) {
|
|
lv_label_set_text(res_lbl, s_dice_mode == DICE_MODE_D20 ? "D20" : "D6");
|
|
lv_obj_set_style_text_font(res_lbl, &lv_font_montserrat_48, 0);
|
|
lv_obj_set_style_text_color(res_lbl, lv_color_hex(COLOR_AMBER), 0);
|
|
} else {
|
|
lv_label_set_text_fmt(res_lbl, "%d", s_dice_result);
|
|
if (s_dice_mode == DICE_MODE_D20 && s_dice_result == 20) {
|
|
lv_obj_set_style_text_color(res_lbl, lv_color_hex(0x00E676), 0);
|
|
} else if (s_dice_mode == DICE_MODE_D20 && s_dice_result == 1) {
|
|
lv_obj_set_style_text_color(res_lbl, lv_color_hex(0xFF1744), 0);
|
|
} else {
|
|
lv_obj_set_style_text_color(res_lbl, lv_color_hex(0x00E5FF), 0);
|
|
}
|
|
}
|
|
}
|
|
lv_obj_align(res_lbl, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Status descriptor under result */
|
|
if (s_dice_rolled && s_dice_mode == DICE_MODE_D20) {
|
|
lv_obj_t *sub = lv_label_create(rbox);
|
|
if (s_dice_result == 20) {
|
|
lv_label_set_text(sub, "NATURAL 20!");
|
|
lv_obj_set_style_text_color(sub, lv_color_hex(0x00E676), 0);
|
|
} else if (s_dice_result == 1) {
|
|
lv_label_set_text(sub, "CRITICAL MISS!");
|
|
lv_obj_set_style_text_color(sub, lv_color_hex(0xFF1744), 0);
|
|
} else {
|
|
lv_label_set_text(sub, "ROLL RESULT");
|
|
lv_obj_set_style_text_color(sub, lv_color_hex(COLOR_AMBER), 0);
|
|
}
|
|
lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0);
|
|
lv_obj_align(sub, LV_ALIGN_BOTTOM_MID, 0, -10);
|
|
}
|
|
|
|
/* Bottom prompt */
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "ROTATE: DIE • CLICK: ROLL");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
static void roll_current_dice(void)
|
|
{
|
|
/* Quick 3-tick animation */
|
|
for (int i = 0; i < 3; i++) {
|
|
buzzer_chirp(700 + i * 150, 20);
|
|
vTaskDelay(pdMS_TO_TICKS(40));
|
|
}
|
|
|
|
if (s_dice_mode == DICE_MODE_D20) {
|
|
s_dice_result = (int)(esp_random() % 20) + 1;
|
|
if (s_dice_result == 20) {
|
|
/* Natural 20 - Victory Fanfare! */
|
|
buzzer_play_sound(BUZZER_SOUND_VICTORY);
|
|
buzzer_chirp(1046, 20);
|
|
} else if (s_dice_result == 1) {
|
|
/* Critical Fail - Sad slide fanfare */
|
|
buzzer_play_sound(BUZZER_SOUND_LOSS);
|
|
buzzer_chirp(220, 30);
|
|
} else {
|
|
buzzer_chirp(880, 25);
|
|
}
|
|
} else if (s_dice_mode == DICE_MODE_D6) {
|
|
s_dice_result = (int)(esp_random() % 6) + 1;
|
|
buzzer_chirp(880, 50);
|
|
} else {
|
|
/* Coin */
|
|
s_dice_result = (int)(esp_random() % 2); // 0 = Tails, 1 = Heads
|
|
buzzer_chirp(988, 60);
|
|
}
|
|
s_dice_rolled = true;
|
|
view_dice();
|
|
}
|
|
|
|
/* 5. Commander Tax & Turn Tracker */
|
|
static void view_commander_tax(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
ui_draw_header("COMMANDER TAX", NULL);
|
|
|
|
/* Casts Box */
|
|
lv_obj_t *cast_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(cast_box, DISP_W - 24, 100);
|
|
lv_obj_align(cast_box, LV_ALIGN_TOP_MID, 0, 48);
|
|
lv_obj_set_style_radius(cast_box, 8, 0);
|
|
if (!s_tax_edit_turns) {
|
|
lv_obj_set_style_bg_color(cast_box, lv_color_hex(0x282410), 0);
|
|
lv_obj_set_style_border_color(cast_box, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_width(cast_box, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(cast_box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(cast_box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(cast_box, 1, 0);
|
|
}
|
|
|
|
lv_obj_t *c_lbl = lv_label_create(cast_box);
|
|
lv_label_set_text(c_lbl, !s_tax_edit_turns ? "> CASTS FROM ZONE <" : "CASTS FROM ZONE");
|
|
lv_obj_set_style_text_font(c_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(c_lbl, !s_tax_edit_turns ? lv_color_hex(0xFFCC00) : lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(c_lbl, LV_ALIGN_TOP_MID, 0, 0);
|
|
|
|
lv_obj_t *c_val = lv_label_create(cast_box);
|
|
lv_label_set_text_fmt(c_val, "%d", s_cmd_casts);
|
|
lv_obj_set_style_text_font(c_val, &lv_font_montserrat_48, 0);
|
|
lv_obj_set_style_text_color(c_val, lv_color_white(), 0);
|
|
lv_obj_align(c_val, LV_ALIGN_CENTER, 0, 10);
|
|
|
|
/* Huge Tax Banner */
|
|
lv_obj_t *tax_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(tax_box, DISP_W - 24, 52);
|
|
lv_obj_align(tax_box, LV_ALIGN_TOP_MID, 0, 156);
|
|
lv_obj_set_style_bg_color(tax_box, lv_color_hex(0x332800), 0);
|
|
lv_obj_set_style_border_color(tax_box, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_width(tax_box, 2, 0);
|
|
lv_obj_set_style_radius(tax_box, 6, 0);
|
|
|
|
lv_obj_t *tax_val = lv_label_create(tax_box);
|
|
lv_label_set_text_fmt(tax_val, "TAX: +{%d} MANA", s_cmd_casts * 2);
|
|
lv_obj_set_style_text_font(tax_val, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(tax_val, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(tax_val, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Turn Box */
|
|
lv_obj_t *turn_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(turn_box, DISP_W - 24, 55);
|
|
lv_obj_align(turn_box, LV_ALIGN_TOP_MID, 0, 216);
|
|
lv_obj_set_style_radius(turn_box, 6, 0);
|
|
if (s_tax_edit_turns) {
|
|
lv_obj_set_style_bg_color(turn_box, lv_color_hex(0x102528), 0);
|
|
lv_obj_set_style_border_color(turn_box, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_set_style_border_width(turn_box, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(turn_box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(turn_box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(turn_box, 1, 0);
|
|
}
|
|
|
|
lv_obj_t *t_val = lv_label_create(turn_box);
|
|
lv_label_set_text_fmt(t_val, s_tax_edit_turns ? "> TURN: %d <" : "TURN: %d", s_turn_count);
|
|
lv_obj_set_style_text_font(t_val, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(t_val, s_tax_edit_turns ? lv_color_hex(0x00E5FF) : lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(t_val, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Bottom prompt */
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "CLICK: SWITCH • ROTATE: +/-");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* 5. Token & Counter Spawner */
|
|
static void view_token_spawner(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
ui_draw_header("TOKEN SPAWNER", NULL);
|
|
|
|
/* Carousel indicator: < 1 OF 8 > */
|
|
lv_obj_t *idx_lbl = lv_label_create(lv_scr_act());
|
|
lv_label_set_text_fmt(idx_lbl, "< %d OF %d >", s_token_idx + 1, TOKEN_COUNT);
|
|
lv_obj_set_style_text_font(idx_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(idx_lbl, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(idx_lbl, LV_ALIGN_TOP_MID, 0, 34);
|
|
|
|
mtg_token_t *tok = &s_tokens[s_token_idx];
|
|
|
|
/* Token Card Display Box */
|
|
lv_obj_t *card_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(card_box, DISP_W - 24, 155);
|
|
lv_obj_align(card_box, LV_ALIGN_TOP_MID, 0, 54);
|
|
lv_obj_set_style_bg_color(card_box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(card_box, lv_color_hex(tok->color_hex), 0);
|
|
lv_obj_set_style_border_width(card_box, 2, 0);
|
|
lv_obj_set_style_radius(card_box, 8, 0);
|
|
lv_obj_set_style_pad_all(card_box, 8, 0);
|
|
lv_obj_set_scrollbar_mode(card_box, LV_SCROLLBAR_MODE_OFF);
|
|
|
|
/* Token Name */
|
|
lv_obj_t *name_lbl = lv_label_create(card_box);
|
|
lv_label_set_text(name_lbl, tok->name);
|
|
lv_obj_set_style_text_font(name_lbl, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(name_lbl, lv_color_hex(tok->color_hex), 0);
|
|
lv_obj_align(name_lbl, LV_ALIGN_TOP_LEFT, 0, 0);
|
|
|
|
/* Token Type Line */
|
|
lv_obj_t *type_lbl = lv_label_create(card_box);
|
|
lv_label_set_text(type_lbl, tok->type_line);
|
|
lv_obj_set_style_text_font(type_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(type_lbl, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(type_lbl, LV_ALIGN_TOP_LEFT, 0, 26);
|
|
|
|
/* Rules / Oracle text */
|
|
lv_obj_t *desc = lv_label_create(card_box);
|
|
lv_label_set_text(desc, tok->oracle);
|
|
lv_label_set_long_mode(desc, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(desc, DISP_W - 44);
|
|
lv_obj_set_style_text_font(desc, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(desc, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(desc, LV_ALIGN_TOP_LEFT, 0, 48);
|
|
|
|
/* P/T Badge if creature token */
|
|
if (tok->pt[0] != '\0') {
|
|
lv_obj_t *pt_box = lv_obj_create(card_box);
|
|
lv_obj_set_size(pt_box, 65, 30);
|
|
lv_obj_align(pt_box, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
|
|
lv_obj_set_style_bg_color(pt_box, lv_color_hex(0x282828), 0);
|
|
lv_obj_set_style_border_color(pt_box, lv_color_hex(tok->color_hex), 0);
|
|
lv_obj_set_style_border_width(pt_box, 1, 0);
|
|
lv_obj_set_style_radius(pt_box, 4, 0);
|
|
lv_obj_set_style_pad_all(pt_box, 0, 0);
|
|
|
|
lv_obj_t *pt_lbl = lv_label_create(pt_box);
|
|
lv_label_set_text(pt_lbl, tok->pt);
|
|
lv_obj_set_style_text_font(pt_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(pt_lbl, lv_color_white(), 0);
|
|
lv_obj_align(pt_lbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
/* Quantity Box */
|
|
lv_obj_t *q_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(q_box, DISP_W - 24, 52);
|
|
lv_obj_align(q_box, LV_ALIGN_TOP_MID, 0, 218);
|
|
lv_obj_set_style_radius(q_box, 6, 0);
|
|
if (s_token_edit_count) {
|
|
lv_obj_set_style_bg_color(q_box, lv_color_hex(0x282410), 0);
|
|
lv_obj_set_style_border_color(q_box, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_width(q_box, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(q_box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(q_box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(q_box, 1, 0);
|
|
}
|
|
|
|
lv_obj_t *q_lbl = lv_label_create(q_box);
|
|
if (s_token_edit_count) {
|
|
lv_label_set_text_fmt(q_lbl, "> COUNT: %d <", tok->count);
|
|
lv_obj_set_style_text_color(q_lbl, lv_color_hex(0xFFCC00), 0);
|
|
} else {
|
|
lv_label_set_text_fmt(q_lbl, "COUNT: %d", tok->count);
|
|
lv_obj_set_style_text_color(q_lbl, tok->count > 0 ? lv_color_white() : lv_color_hex(COLOR_AMBER), 0);
|
|
}
|
|
lv_obj_set_style_text_font(q_lbl, &lv_font_montserrat_20, 0);
|
|
lv_obj_align(q_lbl, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Bottom Prompt */
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
if (s_token_edit_count) {
|
|
lv_label_set_text(hint, "TURN: +/- • CLICK: DONE");
|
|
} else {
|
|
lv_label_set_text(hint, "TURN: PICK • CLICK: COUNT");
|
|
}
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* 6. Deck & Storage Manager */
|
|
static void view_storage(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
ui_draw_header("DECK & STORAGE", NULL);
|
|
|
|
size_t total_b = 0, used_b = 0;
|
|
storage_manager_get_info(&total_b, &used_b);
|
|
int json_c = 0, jpg_c = 0;
|
|
storage_manager_count_cached(&json_c, &jpg_c);
|
|
|
|
if (s_storage_confirm_dialog) {
|
|
/* Confirmation dialog */
|
|
lv_obj_t *warn_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(warn_box, DISP_W - 24, 210);
|
|
lv_obj_align(warn_box, LV_ALIGN_TOP_MID, 0, 48);
|
|
lv_obj_set_style_bg_color(warn_box, lv_color_hex(0x281010), 0);
|
|
lv_obj_set_style_border_color(warn_box, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_set_style_border_width(warn_box, 2, 0);
|
|
lv_obj_set_style_radius(warn_box, 8, 0);
|
|
|
|
lv_obj_t *wtitle = lv_label_create(warn_box);
|
|
lv_label_set_text(wtitle, "CLEAR ALL CARDS?");
|
|
lv_obj_set_style_text_font(wtitle, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(wtitle, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_align(wtitle, LV_ALIGN_TOP_MID, 0, 8);
|
|
|
|
lv_obj_t *wdesc = lv_label_create(warn_box);
|
|
lv_label_set_text(wdesc, "This wipes all cached\ncard art & data for\na fresh new deck.");
|
|
lv_obj_set_style_text_font(wdesc, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(wdesc, lv_color_white(), 0);
|
|
lv_obj_align(wdesc, LV_ALIGN_TOP_MID, 0, 38);
|
|
|
|
/* Confirm button */
|
|
lv_obj_t *c_btn = lv_obj_create(warn_box);
|
|
lv_obj_set_size(c_btn, DISP_W - 56, 38);
|
|
lv_obj_align(c_btn, LV_ALIGN_TOP_MID, 0, 95);
|
|
lv_obj_set_style_radius(c_btn, 4, 0);
|
|
if (s_storage_option_idx == 0) {
|
|
lv_obj_set_style_bg_color(c_btn, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_set_style_border_color(c_btn, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(c_btn, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(c_btn, lv_color_hex(0x401010), 0);
|
|
lv_obj_set_style_border_width(c_btn, 0, 0);
|
|
}
|
|
lv_obj_t *c_lbl = lv_label_create(c_btn);
|
|
lv_label_set_text(c_lbl, s_storage_option_idx == 0 ? "> CONFIRM WIPE <" : "CONFIRM WIPE");
|
|
lv_obj_set_style_text_font(c_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(c_lbl, lv_color_white(), 0);
|
|
lv_obj_align(c_lbl, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Cancel button */
|
|
lv_obj_t *cancel_btn = lv_obj_create(warn_box);
|
|
lv_obj_set_size(cancel_btn, DISP_W - 56, 38);
|
|
lv_obj_align(cancel_btn, LV_ALIGN_TOP_MID, 0, 142);
|
|
lv_obj_set_style_radius(cancel_btn, 4, 0);
|
|
if (s_storage_option_idx == 1) {
|
|
lv_obj_set_style_bg_color(cancel_btn, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_color(cancel_btn, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(cancel_btn, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(cancel_btn, lv_color_hex(0x303030), 0);
|
|
lv_obj_set_style_border_width(cancel_btn, 0, 0);
|
|
}
|
|
lv_obj_t *cancel_lbl = lv_label_create(cancel_btn);
|
|
lv_label_set_text(cancel_lbl, s_storage_option_idx == 1 ? "> CANCEL <" : "CANCEL");
|
|
lv_obj_set_style_text_font(cancel_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(cancel_lbl, s_storage_option_idx == 1 ? lv_color_black() : lv_color_white(), 0);
|
|
lv_obj_align(cancel_lbl, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
} else {
|
|
/* Status Card */
|
|
lv_obj_t *stat_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(stat_box, DISP_W - 24, 136);
|
|
lv_obj_align(stat_box, LV_ALIGN_TOP_MID, 0, 44);
|
|
lv_obj_set_style_bg_color(stat_box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(stat_box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(stat_box, 1, 0);
|
|
lv_obj_set_style_radius(stat_box, 6, 0);
|
|
lv_obj_set_style_pad_all(stat_box, 8, 0);
|
|
lv_obj_set_scrollbar_mode(stat_box, LV_SCROLLBAR_MODE_OFF);
|
|
|
|
struct {
|
|
const char *key;
|
|
char val[32];
|
|
} stat_rows[5];
|
|
|
|
snprintf(stat_rows[0].val, sizeof(stat_rows[0].val), "%d", json_c);
|
|
stat_rows[0].key = "CACHED CARDS:";
|
|
|
|
snprintf(stat_rows[1].val, sizeof(stat_rows[1].val), "%d", jpg_c);
|
|
stat_rows[1].key = "ART CROPS:";
|
|
|
|
snprintf(stat_rows[2].val, sizeof(stat_rows[2].val), "%d / %d KB", (int)(used_b / 1024), (int)(total_b / 1024));
|
|
stat_rows[2].key = "STORAGE USED:";
|
|
|
|
int percent = total_b > 0 ? (int)((used_b * 100) / total_b) : 0;
|
|
snprintf(stat_rows[3].val, sizeof(stat_rows[3].val), "%d%%", percent);
|
|
stat_rows[3].key = "PERCENT USED:";
|
|
|
|
snprintf(stat_rows[4].val, sizeof(stat_rows[4].val), "~100 (EDH)");
|
|
stat_rows[4].key = "DECK CAPACITY:";
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
int y_pos = (i < 2) ? (i * 20) : (i * 20 + 8);
|
|
|
|
lv_obj_t *k = lv_label_create(stat_box);
|
|
lv_label_set_text(k, stat_rows[i].key);
|
|
lv_obj_set_style_text_font(k, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(k, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(k, LV_ALIGN_TOP_LEFT, 0, y_pos);
|
|
|
|
lv_obj_t *v = lv_label_create(stat_box);
|
|
lv_label_set_text(v, stat_rows[i].val);
|
|
lv_obj_set_width(v, 90);
|
|
lv_label_set_long_mode(v, LV_LABEL_LONG_DOT);
|
|
lv_obj_set_style_text_font(v, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(v, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0);
|
|
lv_obj_align(v, LV_ALIGN_TOP_RIGHT, 0, y_pos);
|
|
}
|
|
|
|
/* Action 0: Clear Cards Button */
|
|
lv_obj_t *btn0 = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(btn0, DISP_W - 24, 42);
|
|
lv_obj_align(btn0, LV_ALIGN_TOP_MID, 0, 192);
|
|
lv_obj_set_style_radius(btn0, 6, 0);
|
|
if (s_storage_option_idx == 0) {
|
|
lv_obj_set_style_bg_color(btn0, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_set_style_border_color(btn0, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(btn0, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(btn0, lv_color_hex(0x301515), 0);
|
|
lv_obj_set_style_border_color(btn0, lv_color_hex(0x502020), 0);
|
|
lv_obj_set_style_border_width(btn0, 1, 0);
|
|
}
|
|
lv_obj_t *b0_lbl = lv_label_create(btn0);
|
|
lv_label_set_text(b0_lbl, s_storage_option_idx == 0 ? "> CLEAR FOR NEW DECK <" : "CLEAR FOR NEW DECK");
|
|
lv_obj_set_style_text_font(b0_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(b0_lbl, lv_color_white(), 0);
|
|
lv_obj_align(b0_lbl, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Action 1: Back to Menu Button */
|
|
lv_obj_t *btn1 = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(btn1, DISP_W - 24, 38);
|
|
lv_obj_align(btn1, LV_ALIGN_TOP_MID, 0, 242);
|
|
lv_obj_set_style_radius(btn1, 6, 0);
|
|
if (s_storage_option_idx == 1) {
|
|
lv_obj_set_style_bg_color(btn1, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_color(btn1, lv_color_white(), 0);
|
|
lv_obj_set_style_border_width(btn1, 2, 0);
|
|
} else {
|
|
lv_obj_set_style_bg_color(btn1, lv_color_hex(0x202020), 0);
|
|
lv_obj_set_style_border_color(btn1, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(btn1, 1, 0);
|
|
}
|
|
lv_obj_t *b1_lbl = lv_label_create(btn1);
|
|
lv_label_set_text(b1_lbl, s_storage_option_idx == 1 ? "> BACK <" : "BACK");
|
|
lv_obj_set_style_text_font(b1_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(b1_lbl, s_storage_option_idx == 1 ? lv_color_black() : lv_color_white(), 0);
|
|
lv_obj_align(b1_lbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "ROTATE: OPTION • CLICK: SELECT");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* 7. Device & Wi-Fi Info */
|
|
static void view_device_info(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
ui_draw_header("DEVICE INFO", NULL);
|
|
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 220);
|
|
lv_obj_align(box, LV_ALIGN_TOP_MID, 0, 48);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_radius(box, 6, 0);
|
|
lv_obj_set_style_pad_all(box, 8, 0);
|
|
|
|
size_t total_b = 0, used_b = 0;
|
|
storage_manager_get_info(&total_b, &used_b);
|
|
int json_c = 0, jpg_c = 0;
|
|
storage_manager_count_cached(&json_c, &jpg_c);
|
|
uint32_t free_heap = esp_get_free_heap_size();
|
|
|
|
const char *ssid = wifi_manager_get_connected_ssid();
|
|
int rssi = wifi_manager_get_rssi();
|
|
char wifi_line[64];
|
|
if (wifi_manager_connected() && ssid && strlen(ssid) > 0) {
|
|
snprintf(wifi_line, sizeof(wifi_line), "%s (%ddB)", ssid, rssi);
|
|
} else {
|
|
snprintf(wifi_line, sizeof(wifi_line), "Disconnected");
|
|
}
|
|
|
|
struct {
|
|
const char *key;
|
|
char val[64];
|
|
} rows[10];
|
|
|
|
snprintf(rows[0].val, sizeof(rows[0].val), "%s", ota_manager_get_current_version());
|
|
rows[0].key = "VERSION:";
|
|
|
|
snprintf(rows[1].val, sizeof(rows[1].val), "%s", wifi_line);
|
|
rows[1].key = "WI-FI:";
|
|
|
|
snprintf(rows[2].val, sizeof(rows[2].val), "%s", wifi_manager_get_ip());
|
|
rows[2].key = "IP ADDR:";
|
|
|
|
snprintf(rows[3].val, sizeof(rows[3].val), "192.168.4.1");
|
|
rows[3].key = "HOTSPOT:";
|
|
|
|
snprintf(rows[4].val, sizeof(rows[4].val), "%d / %d KB", (int)(used_b / 1024), (int)(total_b / 1024));
|
|
rows[4].key = "STORAGE:";
|
|
|
|
snprintf(rows[5].val, sizeof(rows[5].val), "%d cached", json_c);
|
|
rows[5].key = "CARDS:";
|
|
|
|
snprintf(rows[6].val, sizeof(rows[6].val), "%d KB", (int)(free_heap / 1024));
|
|
rows[6].key = "FREE RAM:";
|
|
|
|
snprintf(rows[7].val, sizeof(rows[7].val), "%d steps", (int)encoder_get_steps_per_detent());
|
|
rows[7].key = "ENCODER:";
|
|
|
|
snprintf(rows[8].val, sizeof(rows[8].val), "ESP32+ST7789");
|
|
rows[8].key = "HARDWARE:";
|
|
|
|
snprintf(rows[9].val, sizeof(rows[9].val), "MFRC522");
|
|
rows[9].key = "RFID:";
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
int y_pos = i * 20;
|
|
|
|
lv_obj_t *k = lv_label_create(box);
|
|
lv_label_set_text(k, rows[i].key);
|
|
lv_obj_set_style_text_font(k, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(k, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(k, LV_ALIGN_TOP_LEFT, 0, y_pos);
|
|
|
|
lv_obj_t *v = lv_label_create(box);
|
|
lv_label_set_text(v, rows[i].val);
|
|
lv_obj_set_width(v, 120);
|
|
lv_label_set_long_mode(v, LV_LABEL_LONG_DOT);
|
|
lv_obj_set_style_text_font(v, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(v, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0);
|
|
lv_obj_align(v, LV_ALIGN_TOP_RIGHT, 0, y_pos);
|
|
}
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "CLICK: BACK • HOLD: MENU");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
/* 8. Firmware OTA Update Screen */
|
|
static ota_status_t s_last_ota_rendered_st = OTA_STATUS_IDLE;
|
|
static int s_last_ota_percent = -1;
|
|
|
|
static void update_ota_progress(int percent, size_t rb, size_t tb)
|
|
{
|
|
lvgl_port_lock(0);
|
|
if (s_ota_bar && s_ota_pct_lbl && s_ota_bytes_lbl) {
|
|
lv_bar_set_value(s_ota_bar, percent, LV_ANIM_OFF);
|
|
lv_label_set_text_fmt(s_ota_pct_lbl, "%d%%", percent);
|
|
lv_label_set_text_fmt(s_ota_bytes_lbl, "%d / %d KB", (int)(rb / 1024), (int)(tb / 1024));
|
|
}
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
static void view_ota(void)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
ota_status_t st = ota_manager_get_status();
|
|
s_last_ota_rendered_st = st;
|
|
|
|
ui_draw_header("FIRMWARE UPDATE", "GITEA OTA");
|
|
|
|
switch (st) {
|
|
case OTA_STATUS_IDLE:
|
|
case OTA_STATUS_CHECKING: {
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 180);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x1C1C1C), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_set_style_border_width(box, 2, 0);
|
|
lv_obj_set_style_radius(box, 8, 0);
|
|
|
|
lv_obj_t *lbl = lv_label_create(box);
|
|
lv_label_set_text(lbl, "CHECKING GITEA...\n\nConnecting to\nreleases API");
|
|
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(lbl, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "HOLD BUTTON: MAIN MENU");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
break;
|
|
}
|
|
|
|
case OTA_STATUS_UPDATE_AVAILABLE: {
|
|
const ota_release_info_t *rel = ota_manager_get_release_info();
|
|
const char *cur_ver = ota_manager_get_current_version();
|
|
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 200);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -15);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x102818), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0x00E676), 0);
|
|
lv_obj_set_style_border_width(box, 2, 0);
|
|
lv_obj_set_style_radius(box, 8, 0);
|
|
|
|
lv_obj_t *t = lv_label_create(box);
|
|
lv_label_set_text(t, "NEW VERSION AVAILABLE!");
|
|
lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(t, lv_color_hex(0x00E676), 0);
|
|
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 8);
|
|
|
|
char target_str[32];
|
|
if (strlen(rel->remote_hash) >= 7) {
|
|
snprintf(target_str, sizeof(target_str), "%.8s", rel->remote_hash);
|
|
} else {
|
|
snprintf(target_str, sizeof(target_str), "%s", rel->tag_name);
|
|
}
|
|
lv_obj_t *v = lv_label_create(box);
|
|
lv_label_set_text_fmt(v, "NEW: %s", target_str);
|
|
lv_obj_set_style_text_font(v, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(v, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(v, LV_ALIGN_TOP_MID, 0, 38);
|
|
|
|
const char *cur_sha = ota_manager_get_running_sha256();
|
|
char cur_str[32];
|
|
if (strlen(cur_sha) >= 7) {
|
|
snprintf(cur_str, sizeof(cur_str), "%.8s", cur_sha);
|
|
} else {
|
|
snprintf(cur_str, sizeof(cur_str), "%.8s", cur_ver);
|
|
}
|
|
lv_obj_t *c = lv_label_create(box);
|
|
lv_label_set_text_fmt(c, "Current: %s", cur_str);
|
|
lv_obj_set_style_text_font(c, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(c, lv_color_white(), 0);
|
|
lv_obj_align(c, LV_ALIGN_TOP_MID, 0, 70);
|
|
|
|
lv_obj_t *s = lv_label_create(box);
|
|
if (rel->binary_size > 0) {
|
|
lv_label_set_text_fmt(s, "Size: %d KB", (int)(rel->binary_size / 1024));
|
|
} else {
|
|
lv_label_set_text(s, "Ready to stream");
|
|
}
|
|
lv_obj_set_style_text_font(s, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(s, lv_color_hex(0x888888), 0);
|
|
lv_obj_align(s, LV_ALIGN_TOP_MID, 0, 95);
|
|
|
|
lv_obj_t *act = lv_label_create(box);
|
|
lv_label_set_text(act, "> CLICK: INSTALL NOW <");
|
|
lv_obj_set_style_text_font(act, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(act, lv_color_hex(0x00E676), 0);
|
|
lv_obj_align(act, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "ROTATE / HOLD: SKIP FOR NOW");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
break;
|
|
}
|
|
|
|
case OTA_STATUS_UP_TO_DATE: {
|
|
const char *cur_ver = ota_manager_get_current_version();
|
|
const char *cur_sha = ota_manager_get_running_sha256();
|
|
char disp_ver[32];
|
|
if (strlen(cur_sha) >= 7) {
|
|
snprintf(disp_ver, sizeof(disp_ver), "%.8s", cur_sha);
|
|
} else {
|
|
snprintf(disp_ver, sizeof(disp_ver), "%s", cur_ver);
|
|
}
|
|
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 180);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x1C1C1C), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_border_width(box, 1, 0);
|
|
lv_obj_set_style_radius(box, 8, 0);
|
|
|
|
lv_obj_t *t = lv_label_create(box);
|
|
lv_label_set_text(t, "UP TO DATE");
|
|
lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(t, lv_color_hex(0x00E676), 0);
|
|
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 15);
|
|
|
|
lv_obj_t *v = lv_label_create(box);
|
|
lv_label_set_text_fmt(v, "Hash: %s\n\nNo newer releases\nfound on Gitea.", disp_ver);
|
|
lv_obj_set_style_text_font(v, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(v, lv_color_white(), 0);
|
|
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(v, LV_ALIGN_CENTER, 0, 15);
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "CLICK: BACK • HOLD: MENU");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
break;
|
|
}
|
|
|
|
case OTA_STATUS_DOWNLOADING: {
|
|
int percent = ota_manager_get_progress();
|
|
s_last_ota_percent = percent;
|
|
size_t rb = 0, tb = 0;
|
|
ota_manager_get_bytes(&rb, &tb);
|
|
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 200);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x181828), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_set_style_border_width(box, 2, 0);
|
|
lv_obj_set_style_radius(box, 8, 0);
|
|
|
|
lv_obj_t *t = lv_label_create(box);
|
|
lv_label_set_text(t, "FLASHING FIRMWARE...");
|
|
lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(t, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 10);
|
|
|
|
s_ota_pct_lbl = lv_label_create(box);
|
|
lv_label_set_text_fmt(s_ota_pct_lbl, "%d%%", percent);
|
|
lv_obj_set_style_text_font(s_ota_pct_lbl, &lv_font_montserrat_48, 0);
|
|
lv_obj_set_style_text_color(s_ota_pct_lbl, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(s_ota_pct_lbl, LV_ALIGN_CENTER, 0, -15);
|
|
|
|
s_ota_bar = lv_bar_create(box);
|
|
lv_obj_set_size(s_ota_bar, DISP_W - 60, 14);
|
|
lv_obj_align(s_ota_bar, LV_ALIGN_CENTER, 0, 35);
|
|
lv_bar_set_range(s_ota_bar, 0, 100);
|
|
lv_bar_set_value(s_ota_bar, percent, LV_ANIM_OFF);
|
|
lv_obj_set_style_bg_color(s_ota_bar, lv_color_hex(0x333333), 0);
|
|
lv_obj_set_style_bg_color(s_ota_bar, lv_color_hex(0x00E676), LV_PART_INDICATOR);
|
|
|
|
s_ota_bytes_lbl = lv_label_create(box);
|
|
lv_label_set_text_fmt(s_ota_bytes_lbl, "%d / %d KB", (int)(rb / 1024), (int)(tb / 1024));
|
|
lv_obj_set_style_text_font(s_ota_bytes_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(s_ota_bytes_lbl, lv_color_hex(0xAAAAAA), 0);
|
|
lv_obj_align(s_ota_bytes_lbl, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lv_obj_t *warn = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(warn, "DO NOT TURN OFF POWER");
|
|
lv_obj_set_style_text_font(warn, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(warn, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_align(warn, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
break;
|
|
}
|
|
|
|
case OTA_STATUS_SUCCESS_REBOOTING: {
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 180);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x102818), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0x00E676), 0);
|
|
lv_obj_set_style_border_width(box, 2, 0);
|
|
lv_obj_set_style_radius(box, 8, 0);
|
|
|
|
lv_obj_t *t = lv_label_create(box);
|
|
lv_label_set_text(t, "UPDATE SUCCESSFUL!");
|
|
lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(t, lv_color_hex(0x00E676), 0);
|
|
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 20);
|
|
|
|
lv_obj_t *m = lv_label_create(box);
|
|
lv_label_set_text(m, "New firmware verified.\n\nRebooting device...");
|
|
lv_obj_set_style_text_font(m, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(m, lv_color_white(), 0);
|
|
lv_obj_set_style_text_align(m, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(m, LV_ALIGN_CENTER, 0, 25);
|
|
break;
|
|
}
|
|
|
|
case OTA_STATUS_FAILED:
|
|
default: {
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 180);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x281010), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_set_style_border_width(box, 2, 0);
|
|
lv_obj_set_style_radius(box, 8, 0);
|
|
|
|
lv_obj_t *t = lv_label_create(box);
|
|
lv_label_set_text(t, "UPDATE FAILED");
|
|
lv_obj_set_style_text_font(t, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(t, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 15);
|
|
|
|
lv_obj_t *msg = lv_label_create(box);
|
|
lv_label_set_text_fmt(msg, "%s\n\nExisting firmware intact.", ota_manager_get_message());
|
|
lv_obj_set_style_text_font(msg, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(msg, lv_color_white(), 0);
|
|
lv_obj_set_style_text_align(msg, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(msg, LV_ALIGN_CENTER, 0, 20);
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "CLICK: BACK • HOLD: MENU");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
break;
|
|
}
|
|
}
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
static void get_mana_badge_colors(const char *sym, uint32_t *bg, uint32_t *text, uint32_t *border)
|
|
{
|
|
if (strcasecmp(sym, "W") == 0) {
|
|
*bg = 0xF9F6DE; *text = 0x222222; *border = 0xD8D19F; /* MTG White / Ivory */
|
|
} else if (strcasecmp(sym, "U") == 0) {
|
|
*bg = 0x0E68AB; *text = 0xFFFFFF; *border = 0x074270; /* MTG Blue */
|
|
} else if (strcasecmp(sym, "B") == 0) {
|
|
*bg = 0x242022; *text = 0xEEEEEE; *border = 0x5C5458; /* MTG Black / Charcoal */
|
|
} else if (strcasecmp(sym, "R") == 0) {
|
|
*bg = 0xD3202A; *text = 0xFFFFFF; *border = 0x8C151B; /* MTG Red */
|
|
} else if (strcasecmp(sym, "G") == 0) {
|
|
*bg = 0x00733E; *text = 0xFFFFFF; *border = 0x004A28; /* MTG Green */
|
|
} else if (strcasecmp(sym, "C") == 0) {
|
|
*bg = 0x9E9E9E; *text = 0x111111; *border = 0x616161; /* Colorless Diamond */
|
|
} else if (strchr(sym, 'P') != NULL || strchr(sym, 'p') != NULL) {
|
|
*bg = 0x8B0000; *text = 0xFFFFFF; *border = 0x550000; /* Phyrexian Crimson */
|
|
} else if (strchr(sym, '/') != NULL) {
|
|
*bg = 0xD4AF37; *text = 0x111111; *border = 0xAA8820; /* Hybrid Gold */
|
|
} else {
|
|
*bg = 0xB0BEC5; *text = 0x111111; *border = 0x78909C; /* Mana Silver (0-9, X) */
|
|
}
|
|
}
|
|
|
|
static int render_mana_symbols(lv_obj_t *parent, const char *mana)
|
|
{
|
|
if (mana == NULL || mana[0] == '\0') {
|
|
return 0;
|
|
}
|
|
|
|
int sym_count = 0;
|
|
const char *p = mana;
|
|
while (*p) {
|
|
if (*p == '{') sym_count++;
|
|
p++;
|
|
}
|
|
if (sym_count == 0) {
|
|
return 0;
|
|
}
|
|
|
|
int badge_sz = (sym_count >= 6) ? 15 : 18;
|
|
int gap = (sym_count >= 6) ? 2 : 3;
|
|
|
|
lv_obj_t *cont = lv_obj_create(parent);
|
|
lv_obj_remove_style_all(cont);
|
|
lv_obj_set_size(cont, LV_SIZE_CONTENT, badge_sz);
|
|
lv_obj_set_layout(cont, LV_LAYOUT_FLEX);
|
|
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_column(cont, gap, 0);
|
|
lv_obj_align(cont, LV_ALIGN_TOP_RIGHT, 0, 0);
|
|
lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
p = mana;
|
|
while (*p) {
|
|
if (*p == '{') {
|
|
p++;
|
|
const char *end = strchr(p, '}');
|
|
if (!end) break;
|
|
size_t token_len = end - p;
|
|
if (token_len > 0 && token_len < 8) {
|
|
char sym[8];
|
|
memcpy(sym, p, token_len);
|
|
sym[token_len] = '\0';
|
|
|
|
uint32_t bg, txt, border;
|
|
get_mana_badge_colors(sym, &bg, &txt, &border);
|
|
|
|
lv_obj_t *badge = lv_obj_create(cont);
|
|
lv_obj_set_size(badge, badge_sz, badge_sz);
|
|
lv_obj_set_style_radius(badge, LV_RADIUS_CIRCLE, 0);
|
|
lv_obj_set_style_bg_color(badge, lv_color_hex(bg), 0);
|
|
lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(badge, lv_color_hex(border), 0);
|
|
lv_obj_set_style_border_width(badge, 1, 0);
|
|
lv_obj_set_style_pad_all(badge, 0, 0);
|
|
lv_obj_clear_flag(badge, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
lv_obj_t *lbl = lv_label_create(badge);
|
|
lv_label_set_text(lbl, sym);
|
|
lv_obj_set_style_text_color(lbl, lv_color_hex(txt), 0);
|
|
#if LV_FONT_MONTSERRAT_12
|
|
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0);
|
|
#else
|
|
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
|
|
#endif
|
|
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
p = end + 1;
|
|
} else {
|
|
p++;
|
|
}
|
|
}
|
|
|
|
return sym_count * badge_sz + (sym_count - 1) * gap;
|
|
}
|
|
|
|
/* 8. Card View Screen */
|
|
static void view_card(void)
|
|
{
|
|
char json_path[72];
|
|
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", s_uid);
|
|
|
|
char *buf = NULL;
|
|
size_t len = 0;
|
|
if (storage_read_text(json_path, &buf, &len) != ESP_OK || buf == NULL) {
|
|
free(buf);
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
lv_obj_t *err = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(err, "CARD DATA MISSING");
|
|
lv_obj_set_style_text_font(err, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(err, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_align(err, LV_ALIGN_CENTER, 0, 0);
|
|
lvgl_port_unlock();
|
|
return;
|
|
}
|
|
cJSON *root = cJSON_Parse(buf);
|
|
free(buf);
|
|
if (root == NULL) {
|
|
remove(json_path);
|
|
if (s_card_name[0] != '\0' && s_uid[0] != '\0') {
|
|
scryfall_fetch_request(s_card_name, s_uid);
|
|
}
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
lv_obj_t *err = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(err, "RE-FETCHING CARD...");
|
|
lv_obj_set_style_text_font(err, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(err, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(err, LV_ALIGN_CENTER, 0, 0);
|
|
lvgl_port_unlock();
|
|
return;
|
|
}
|
|
|
|
const char *name = get_json_str(root, "name", s_card_name[0] ? s_card_name : "?");
|
|
if (s_card_name[0] != '\0' && name[0] != '\0' && strcasecmp(name, s_card_name) != 0) {
|
|
/* Stale cache entry from a previous card! Purge and fetch fresh */
|
|
ESP_LOGW(TAG, "Cached card '%s' != mapped card '%s' for %s, re-fetching...", name, s_card_name, s_uid);
|
|
cJSON_Delete(root);
|
|
remove(json_path);
|
|
char jpg_path[72];
|
|
snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", s_uid);
|
|
remove(jpg_path);
|
|
s_waiting_fetch = true;
|
|
view_fetching(s_card_name);
|
|
scryfall_fetch_request(s_card_name, s_uid);
|
|
return;
|
|
}
|
|
|
|
const char *mana = get_json_str(root, "mana_cost", "");
|
|
const char *type = get_json_str(root, "type", "");
|
|
const char *rarity = get_json_str(root, "rarity", "");
|
|
const char *power = get_json_str(root, "power", "");
|
|
const char *tough = get_json_str(root, "toughness", "");
|
|
const char *loyalty = get_json_str(root, "loyalty", "");
|
|
const char *oracle = get_json_str(root, "oracle", "");
|
|
const char *price = get_json_str(root, "price_usd", "");
|
|
|
|
if (strstr(type, "Legendary") != NULL || strcasecmp(rarity, "mythic") == 0) {
|
|
buzzer_play_sound(BUZZER_SOUND_LEGENDARY);
|
|
}
|
|
|
|
bool has_art = load_art_crop(s_uid);
|
|
|
|
lv_coord_t prev_scroll = 0;
|
|
if (s_card_scroll_cont != NULL) {
|
|
prev_scroll = lv_obj_get_scroll_y(s_card_scroll_cont);
|
|
}
|
|
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
/* Card Box Container */
|
|
lv_obj_t *card_box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(card_box, DISP_W - 12, DISP_H - 12);
|
|
lv_obj_align(card_box, LV_ALIGN_CENTER, 0, 0);
|
|
lv_obj_set_style_bg_color(card_box, lv_color_hex(0x181818), 0);
|
|
lv_obj_set_style_border_color(card_box, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_set_style_border_width(card_box, 2, 0);
|
|
lv_obj_set_style_radius(card_box, 6, 0);
|
|
lv_obj_set_style_pad_all(card_box, 6, 0);
|
|
lv_obj_clear_flag(card_box, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_scrollbar_mode(card_box, LV_SCROLLBAR_MODE_OFF);
|
|
|
|
/* Pinned Header: Mana Badges */
|
|
int mana_w = 0;
|
|
if (mana[0]) {
|
|
mana_w = render_mana_symbols(card_box, mana);
|
|
}
|
|
|
|
/* Pinned Header: Name */
|
|
lv_obj_t *name_lbl = lv_label_create(card_box);
|
|
lv_label_set_text(name_lbl, name);
|
|
lv_obj_set_style_text_font(name_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(name_lbl, lv_color_hex(0xFFCC00), 0);
|
|
int name_max_w = (mana_w > 0) ? (DISP_W - 36 - mana_w - 6) : (DISP_W - 36);
|
|
if (name_max_w < 60) name_max_w = 60;
|
|
lv_obj_set_width(name_lbl, name_max_w);
|
|
lv_label_set_long_mode(name_lbl, LV_LABEL_LONG_DOT);
|
|
lv_obj_align(name_lbl, LV_ALIGN_TOP_LEFT, 0, 0);
|
|
|
|
/* Pinned Footer: P/T or Loyalty */
|
|
if (power[0] || tough[0]) {
|
|
lv_obj_t *pt_lbl = lv_label_create(card_box);
|
|
lv_label_set_text_fmt(pt_lbl, "%s / %s", power, tough);
|
|
lv_obj_set_style_text_font(pt_lbl, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(pt_lbl, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(pt_lbl, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
|
|
} else if (loyalty[0]) {
|
|
lv_obj_t *loy_lbl = lv_label_create(card_box);
|
|
lv_label_set_text_fmt(loy_lbl, "[%s]", loyalty);
|
|
lv_obj_set_style_text_font(loy_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(loy_lbl, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(loy_lbl, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
|
|
}
|
|
|
|
/* Pinned Footer: Price */
|
|
if (price[0]) {
|
|
lv_obj_t *pr_lbl = lv_label_create(card_box);
|
|
lv_label_set_text_fmt(pr_lbl, "$%s", price);
|
|
lv_obj_set_style_text_font(pr_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(pr_lbl, lv_color_hex(0xE040FB), 0);
|
|
lv_obj_align(pr_lbl, LV_ALIGN_BOTTOM_LEFT, 0, 0);
|
|
}
|
|
|
|
/* Middle Scrollable Container (Art + Type + Oracle Text) */
|
|
s_card_scroll_cont = lv_obj_create(card_box);
|
|
lv_obj_set_size(s_card_scroll_cont, lv_pct(100), 250);
|
|
lv_obj_set_pos(s_card_scroll_cont, 0, 22);
|
|
lv_obj_set_style_bg_opa(s_card_scroll_cont, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_border_width(s_card_scroll_cont, 0, 0);
|
|
lv_obj_set_style_pad_all(s_card_scroll_cont, 0, 0);
|
|
lv_obj_set_style_pad_right(s_card_scroll_cont, 6, 0);
|
|
lv_obj_set_style_pad_bottom(s_card_scroll_cont, 24, 0);
|
|
lv_obj_set_scrollbar_mode(s_card_scroll_cont, LV_SCROLLBAR_MODE_AUTO);
|
|
lv_obj_set_scroll_dir(s_card_scroll_cont, LV_DIR_VER);
|
|
lv_obj_set_style_bg_color(s_card_scroll_cont, lv_color_hex(COLOR_GOLD), LV_PART_SCROLLBAR);
|
|
lv_obj_set_style_bg_opa(s_card_scroll_cont, LV_OPA_70, LV_PART_SCROLLBAR);
|
|
lv_obj_set_style_width(s_card_scroll_cont, 4, LV_PART_SCROLLBAR);
|
|
|
|
int cur_y = 0;
|
|
|
|
/* Art Crop Image */
|
|
if (has_art) {
|
|
lv_obj_t *img = lv_img_create(s_card_scroll_cont);
|
|
lv_img_set_src(img, &s_art_img_dsc);
|
|
lv_obj_align(img, LV_ALIGN_TOP_MID, 0, cur_y);
|
|
lv_obj_set_style_border_color(img, lv_color_hex(0x8B6508), 0);
|
|
lv_obj_set_style_border_width(img, 1, 0);
|
|
lv_obj_set_style_radius(img, 3, 0);
|
|
cur_y += s_art_img_dsc.header.h + 4;
|
|
} else if (scryfall_is_fetching_image(s_uid)) {
|
|
/* Compact collapsible banner while art is downloading in background */
|
|
lv_obj_t *banner = lv_obj_create(s_card_scroll_cont);
|
|
lv_obj_set_size(banner, DISP_W - 32, 28);
|
|
lv_obj_align(banner, LV_ALIGN_TOP_MID, 0, cur_y);
|
|
lv_obj_set_style_bg_color(banner, lv_color_hex(0x222222), 0);
|
|
lv_obj_set_style_border_color(banner, lv_color_hex(COLOR_GOLD), 0);
|
|
lv_obj_set_style_border_width(banner, 1, 0);
|
|
lv_obj_set_style_radius(banner, 4, 0);
|
|
lv_obj_set_style_pad_all(banner, 2, 0);
|
|
lv_obj_clear_flag(banner, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
lv_obj_t *blbl = lv_label_create(banner);
|
|
lv_label_set_text(blbl, "DOWNLOADING ART...");
|
|
lv_obj_set_style_text_font(blbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(blbl, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(blbl, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
cur_y += 32;
|
|
}
|
|
|
|
/* Type */
|
|
lv_obj_t *type_lbl = lv_label_create(s_card_scroll_cont);
|
|
lv_label_set_text(type_lbl, type);
|
|
lv_obj_set_style_text_font(type_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(type_lbl, lv_color_white(), 0);
|
|
lv_obj_set_width(type_lbl, DISP_W - 32);
|
|
lv_label_set_long_mode(type_lbl, LV_LABEL_LONG_WRAP);
|
|
lv_obj_align(type_lbl, LV_ALIGN_TOP_LEFT, 0, cur_y);
|
|
lv_obj_update_layout(type_lbl);
|
|
cur_y += lv_obj_get_height(type_lbl) + 4;
|
|
|
|
/* Oracle text */
|
|
if (oracle[0]) {
|
|
lv_obj_t *oracle_lbl = lv_label_create(s_card_scroll_cont);
|
|
lv_label_set_text(oracle_lbl, oracle);
|
|
lv_label_set_long_mode(oracle_lbl, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(oracle_lbl, DISP_W - 36);
|
|
lv_obj_set_style_text_font(oracle_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(oracle_lbl, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(oracle_lbl, LV_ALIGN_TOP_LEFT, 0, cur_y);
|
|
}
|
|
|
|
if (prev_scroll > 0) {
|
|
lv_obj_scroll_to_y(s_card_scroll_cont, prev_scroll, LV_ANIM_OFF);
|
|
}
|
|
|
|
lvgl_port_unlock();
|
|
cJSON_Delete(root);
|
|
}
|
|
|
|
static void view_unmapped(const char *uid)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
lv_obj_t *title = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(title, "UNMAPPED CARD");
|
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 30);
|
|
|
|
lv_obj_t *u_lbl = lv_label_create(lv_scr_act());
|
|
lv_label_set_text_fmt(u_lbl, "UID: %s", uid);
|
|
lv_obj_set_style_text_font(u_lbl, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(u_lbl, lv_color_hex(0x00E5FF), 0);
|
|
lv_obj_align(u_lbl, LV_ALIGN_TOP_MID, 0, 70);
|
|
|
|
lv_obj_t *sub = lv_label_create(lv_scr_act());
|
|
lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(sub, lv_color_white(), 0);
|
|
|
|
lv_obj_t *ip = lv_label_create(lv_scr_act());
|
|
lv_obj_set_style_text_font(ip, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(ip, lv_color_hex(0x00E676), 0);
|
|
|
|
if (wifi_manager_connected()) {
|
|
lv_label_set_text(sub, "PAIR VIA WI-FI:");
|
|
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 105);
|
|
|
|
lv_label_set_text_fmt(ip, "http://%s", wifi_manager_get_ip());
|
|
lv_obj_align(ip, LV_ALIGN_TOP_MID, 0, 135);
|
|
|
|
lv_obj_t *alt = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(alt, "or Hotspot: MTG-Companion\n(http://192.168.4.1)");
|
|
lv_obj_set_style_text_font(alt, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(alt, lv_color_hex(0x888888), 0);
|
|
lv_obj_set_style_text_align(alt, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(alt, LV_ALIGN_TOP_MID, 0, 175);
|
|
} else {
|
|
lv_label_set_text(sub, "PAIR ON HOTSPOT:");
|
|
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 105);
|
|
|
|
lv_obj_t *ssid = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(ssid, "SSID: MTG-Companion");
|
|
lv_obj_set_style_text_font(ssid, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(ssid, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(ssid, LV_ALIGN_TOP_MID, 0, 135);
|
|
|
|
lv_label_set_text(ip, "http://192.168.4.1");
|
|
lv_obj_align(ip, LV_ALIGN_TOP_MID, 0, 165);
|
|
}
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "HOLD BUTTON: MAIN MENU");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -10);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
static void view_fetching(const char *name)
|
|
{
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
|
|
lv_obj_t *title = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(title, "FETCHING CARD...");
|
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(title, lv_color_hex(0xFFCC00), 0);
|
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 50);
|
|
|
|
lv_obj_t *cname = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(cname, name);
|
|
lv_label_set_long_mode(cname, LV_LABEL_LONG_DOT);
|
|
lv_obj_set_width(cname, DISP_W - 24);
|
|
lv_obj_set_style_text_align(cname, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_text_font(cname, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(cname, lv_color_white(), 0);
|
|
lv_obj_align(cname, LV_ALIGN_CENTER, 0, -20);
|
|
|
|
lv_obj_t *sub = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(sub, "Downloading Scryfall data\nand art illustration...");
|
|
lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(sub, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_set_style_text_align(sub, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(sub, LV_ALIGN_CENTER, 0, 20);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
static void handle_new_card(const char *uid)
|
|
{
|
|
snprintf(s_uid, sizeof(s_uid), "%s", uid);
|
|
rfid_manager_consume_card();
|
|
|
|
char *name = NULL;
|
|
esp_err_t ret = storage_manager_lookup_name(uid, &name);
|
|
if (ret != ESP_OK || name == NULL) {
|
|
wifi_manager_set_power_save(false);
|
|
app_set_view(APP_STATE_REGISTRATION);
|
|
view_unmapped(uid);
|
|
buzzer_chirp(392, 80);
|
|
free(name);
|
|
return;
|
|
}
|
|
|
|
snprintf(s_card_name, sizeof(s_card_name), "%s", name);
|
|
app_set_view(APP_STATE_CARD_VIEW);
|
|
|
|
buzzer_sound_rfid_tap();
|
|
|
|
char json_path[72];
|
|
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid);
|
|
bool need_fetch = true;
|
|
if (storage_file_exists(json_path)) {
|
|
char *chk = NULL;
|
|
size_t chk_len = 0;
|
|
if (storage_read_text(json_path, &chk, &chk_len) == ESP_OK && chk != NULL) {
|
|
cJSON *cj = cJSON_Parse(chk);
|
|
if (cj) {
|
|
cJSON *cn = cJSON_GetObjectItem(cj, "name");
|
|
if (cn && cn->valuestring && strcasecmp(cn->valuestring, name) == 0) {
|
|
need_fetch = false;
|
|
}
|
|
cJSON_Delete(cj);
|
|
}
|
|
free(chk);
|
|
}
|
|
}
|
|
|
|
if (!need_fetch) {
|
|
view_card();
|
|
} else {
|
|
s_waiting_fetch = true;
|
|
view_fetching(name);
|
|
scryfall_fetch_request(name, uid);
|
|
}
|
|
free(name);
|
|
}
|
|
|
|
static void handle_fetch_ready(void)
|
|
{
|
|
const char *ready_uid = scryfall_last_result_uid();
|
|
if (ready_uid != NULL && strcmp(ready_uid, s_uid) == 0) {
|
|
bool success = scryfall_last_result_success();
|
|
scryfall_consume_result();
|
|
s_waiting_fetch = false;
|
|
if (success) {
|
|
view_card();
|
|
} else {
|
|
buzzer_chirp(220, 150);
|
|
|
|
lvgl_port_lock(0);
|
|
ui_clean_screen();
|
|
ui_draw_header("FETCH FAILED", NULL);
|
|
|
|
lv_obj_t *box = lv_obj_create(lv_scr_act());
|
|
lv_obj_set_size(box, DISP_W - 24, 180);
|
|
lv_obj_align(box, LV_ALIGN_CENTER, 0, -10);
|
|
lv_obj_set_style_bg_color(box, lv_color_hex(0x281010), 0);
|
|
lv_obj_set_style_border_color(box, lv_color_hex(0xFF1744), 0);
|
|
lv_obj_set_style_border_width(box, 2, 0);
|
|
lv_obj_set_style_radius(box, 8, 0);
|
|
lv_obj_set_style_pad_all(box, 8, 0);
|
|
|
|
lv_obj_t *cname = lv_label_create(box);
|
|
lv_label_set_text(cname, s_card_name);
|
|
lv_obj_set_style_text_font(cname, &lv_font_montserrat_20, 0);
|
|
lv_obj_set_style_text_color(cname, lv_color_hex(0xFFCC00), 0);
|
|
lv_label_set_long_mode(cname, LV_LABEL_LONG_DOT);
|
|
lv_obj_set_width(cname, DISP_W - 44);
|
|
lv_obj_align(cname, LV_ALIGN_TOP_MID, 0, 10);
|
|
|
|
lv_obj_t *sub = lv_label_create(box);
|
|
lv_label_set_text(sub, "Could not fetch card from\nScryfall. Check Wi-Fi STA\nor card spelling online.\n\nCLICK: RETRY");
|
|
lv_obj_set_style_text_font(sub, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(sub, lv_color_white(), 0);
|
|
lv_obj_set_style_text_align(sub, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_align(sub, LV_ALIGN_CENTER, 0, 20);
|
|
|
|
lv_obj_t *hint = lv_label_create(lv_scr_act());
|
|
lv_label_set_text(hint, "HOLD: MAIN MENU");
|
|
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_AMBER), 0);
|
|
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
|
|
lvgl_port_unlock();
|
|
}
|
|
} else {
|
|
scryfall_consume_result();
|
|
}
|
|
}
|
|
|
|
static void handle_image_ready(void)
|
|
{
|
|
const char *img_uid = scryfall_last_image_uid();
|
|
scryfall_consume_image();
|
|
|
|
/* If the user is currently viewing this exact card, refresh view_card so art replaces the banner */
|
|
if (app_get_view() == APP_STATE_CARD_VIEW && img_uid != NULL && strcmp(img_uid, s_uid) == 0) {
|
|
ESP_LOGI(TAG, "Background art arrived for %s, updating card view", img_uid);
|
|
view_card();
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Encoder event dispatcher */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void handle_encoder_click(void)
|
|
{
|
|
app_state_t cur = app_get_view();
|
|
switch (cur) {
|
|
case APP_STATE_MENU:
|
|
buzzer_chirp(988, 40);
|
|
switch (s_menu_idx) {
|
|
case 0:
|
|
app_set_view(APP_STATE_SCANNER);
|
|
view_scanner();
|
|
break;
|
|
case 1:
|
|
app_set_view(APP_STATE_LIFE_COUNTER);
|
|
view_life();
|
|
break;
|
|
case 2:
|
|
app_set_view(APP_STATE_DICE_ROLLER);
|
|
view_dice();
|
|
break;
|
|
case 3:
|
|
app_set_view(APP_STATE_COMMANDER_TRACKER);
|
|
view_commander_tax();
|
|
break;
|
|
case 4:
|
|
app_set_view(APP_STATE_TOKEN_SPAWNER);
|
|
s_token_edit_count = false;
|
|
view_token_spawner();
|
|
break;
|
|
case 5:
|
|
app_set_view(APP_STATE_SETTINGS_MENU);
|
|
s_settings_idx = 0;
|
|
view_settings_menu();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_SETTINGS_MENU:
|
|
buzzer_chirp(988, 40);
|
|
switch (s_settings_idx) {
|
|
case 0: /* < BACK TO MAIN MENU */
|
|
app_set_view(APP_STATE_MENU);
|
|
view_menu();
|
|
break;
|
|
case 1: { /* Audio profile inline toggle */
|
|
audio_profile_t next_prof = (buzzer_get_profile() + 1) % 3;
|
|
buzzer_set_profile(next_prof);
|
|
if (next_prof == AUDIO_PROFILE_CHIPTUNE) {
|
|
buzzer_chirp(1046, 50);
|
|
} else if (next_prof == AUDIO_PROFILE_TACTILE) {
|
|
buzzer_chirp(880, 15);
|
|
}
|
|
view_settings_menu();
|
|
break;
|
|
}
|
|
case 2: /* DECK STORAGE */
|
|
app_set_view(APP_STATE_STORAGE_MANAGER);
|
|
s_storage_confirm_dialog = false;
|
|
s_storage_option_idx = 0;
|
|
view_storage();
|
|
break;
|
|
case 3: /* DEVICE & WIFI INFO */
|
|
app_set_view(APP_STATE_DEVICE_INFO);
|
|
view_device_info();
|
|
break;
|
|
case 4: /* UPDATE FIRMWARE */
|
|
app_set_view(APP_STATE_OTA_UPDATE);
|
|
buzzer_chirp(988, 40);
|
|
ota_manager_check_update_async();
|
|
view_ota();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_OTA_UPDATE: {
|
|
ota_status_t st = ota_manager_get_status();
|
|
if (st == OTA_STATUS_UPDATE_AVAILABLE) {
|
|
buzzer_chirp(1046, 100);
|
|
ota_manager_start_update_async();
|
|
view_ota();
|
|
} else if (st == OTA_STATUS_UP_TO_DATE || st == OTA_STATUS_FAILED) {
|
|
ota_manager_reset();
|
|
app_set_view(APP_STATE_SETTINGS_MENU);
|
|
view_settings_menu();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case APP_STATE_LIFE_COUNTER:
|
|
s_life_mode = (s_life_mode + 1) % 3;
|
|
buzzer_chirp(880, 30);
|
|
view_life();
|
|
break;
|
|
|
|
case APP_STATE_DICE_ROLLER:
|
|
roll_current_dice();
|
|
break;
|
|
|
|
case APP_STATE_COMMANDER_TRACKER:
|
|
s_tax_edit_turns = !s_tax_edit_turns;
|
|
buzzer_chirp(880, 30);
|
|
view_commander_tax();
|
|
break;
|
|
|
|
case APP_STATE_TOKEN_SPAWNER:
|
|
s_token_edit_count = !s_token_edit_count;
|
|
buzzer_chirp(880, 30);
|
|
view_token_spawner();
|
|
break;
|
|
|
|
case APP_STATE_STORAGE_MANAGER:
|
|
if (!s_storage_confirm_dialog) {
|
|
if (s_storage_option_idx == 0) {
|
|
s_storage_confirm_dialog = true;
|
|
buzzer_chirp(587, 60);
|
|
view_storage();
|
|
} else {
|
|
app_set_view(APP_STATE_SETTINGS_MENU);
|
|
view_settings_menu();
|
|
}
|
|
} else {
|
|
if (s_storage_option_idx == 0) {
|
|
storage_manager_clear_all_cards(true);
|
|
s_card_name[0] = '\0';
|
|
s_uid[0] = '\0';
|
|
s_waiting_fetch = false;
|
|
buzzer_chirp(1046, 100);
|
|
s_storage_confirm_dialog = false;
|
|
view_storage();
|
|
} else {
|
|
s_storage_confirm_dialog = false;
|
|
s_storage_option_idx = 0;
|
|
view_storage();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_DEVICE_INFO:
|
|
encoder_set_steps_per_detent(4);
|
|
buzzer_chirp(880, 40);
|
|
app_set_view(APP_STATE_SETTINGS_MENU);
|
|
view_settings_menu();
|
|
break;
|
|
|
|
case APP_STATE_SCANNER:
|
|
case APP_STATE_CARD_VIEW:
|
|
case APP_STATE_REGISTRATION:
|
|
if (cur == APP_STATE_REGISTRATION) {
|
|
wifi_manager_set_power_save(true);
|
|
}
|
|
if (cur == APP_STATE_CARD_VIEW && s_card_name[0] != '\0' && !s_waiting_fetch) {
|
|
char jp[72];
|
|
snprintf(jp, sizeof(jp), STORAGE_CARDS_DIR "/%s.json", s_uid);
|
|
if (!storage_file_exists(jp)) {
|
|
/* Retry fetch */
|
|
s_waiting_fetch = true;
|
|
view_fetching(s_card_name);
|
|
scryfall_fetch_request(s_card_name, s_uid);
|
|
break;
|
|
}
|
|
}
|
|
app_set_view(APP_STATE_LIFE_COUNTER);
|
|
view_life();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void handle_encoder_rotation(int32_t delta)
|
|
{
|
|
if (delta == 0) return;
|
|
|
|
app_state_t cur = app_get_view();
|
|
if (cur == APP_STATE_CARD_VIEW) {
|
|
if (s_card_scroll_cont != NULL) {
|
|
lvgl_port_lock(0);
|
|
lv_obj_scroll_by_bounded(s_card_scroll_cont, 0, -delta * 35, LV_ANIM_ON);
|
|
lvgl_port_unlock();
|
|
}
|
|
buzzer_sound_tick();
|
|
return;
|
|
}
|
|
|
|
int32_t dir = (delta > 0) ? 1 : -1;
|
|
int32_t count = (delta > 0) ? delta : -delta;
|
|
|
|
/* Apply one discrete action per tick */
|
|
for (int32_t i = 0; i < count; i++) {
|
|
switch (cur) {
|
|
case APP_STATE_MENU:
|
|
if (dir > 0) {
|
|
s_menu_idx = (s_menu_idx + 1) % MAIN_MENU_ITEM_COUNT;
|
|
} else {
|
|
s_menu_idx = (s_menu_idx - 1 + MAIN_MENU_ITEM_COUNT) % MAIN_MENU_ITEM_COUNT;
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_SETTINGS_MENU:
|
|
if (dir > 0) {
|
|
s_settings_idx = (s_settings_idx + 1) % SETTINGS_MENU_ITEM_COUNT;
|
|
} else {
|
|
s_settings_idx = (s_settings_idx - 1 + SETTINGS_MENU_ITEM_COUNT) % SETTINGS_MENU_ITEM_COUNT;
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_LIFE_COUNTER:
|
|
if (s_life_mode == LIFE_MODE_TOTAL) {
|
|
app_set_life(app_get_life() + dir);
|
|
if (app_get_life() < 0) app_set_life(0);
|
|
} else if (s_life_mode == LIFE_MODE_POISON) {
|
|
s_poison += dir;
|
|
if (s_poison < 0) s_poison = 0;
|
|
if (s_poison > 10) s_poison = 10;
|
|
} else if (s_life_mode == LIFE_MODE_CMD_DMG) {
|
|
s_cmd_dmg += dir;
|
|
if (s_cmd_dmg < 0) s_cmd_dmg = 0;
|
|
if (s_cmd_dmg > 21) s_cmd_dmg = 21;
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_DICE_ROLLER:
|
|
if (dir > 0) {
|
|
s_dice_mode = (s_dice_mode + 1) % 3;
|
|
} else {
|
|
s_dice_mode = (s_dice_mode - 1 + 3) % 3;
|
|
}
|
|
s_dice_rolled = false;
|
|
break;
|
|
|
|
case APP_STATE_COMMANDER_TRACKER:
|
|
if (!s_tax_edit_turns) {
|
|
s_cmd_casts += dir;
|
|
if (s_cmd_casts < 0) s_cmd_casts = 0;
|
|
if (s_cmd_casts > 50) s_cmd_casts = 50;
|
|
} else {
|
|
s_turn_count += dir;
|
|
if (s_turn_count < 1) s_turn_count = 1;
|
|
if (s_turn_count > 99) s_turn_count = 99;
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_TOKEN_SPAWNER:
|
|
if (!s_token_edit_count) {
|
|
s_token_idx = (s_token_idx + dir + TOKEN_COUNT) % TOKEN_COUNT;
|
|
} else {
|
|
s_tokens[s_token_idx].count += dir;
|
|
if (s_tokens[s_token_idx].count < 0) s_tokens[s_token_idx].count = 0;
|
|
if (s_tokens[s_token_idx].count > 99) s_tokens[s_token_idx].count = 99;
|
|
}
|
|
break;
|
|
|
|
case APP_STATE_STORAGE_MANAGER:
|
|
s_storage_option_idx = (s_storage_option_idx == 0) ? 1 : 0;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Redraw view once per rotation event */
|
|
switch (cur) {
|
|
case APP_STATE_MENU:
|
|
buzzer_sound_tick();
|
|
view_menu();
|
|
break;
|
|
|
|
case APP_STATE_SETTINGS_MENU:
|
|
buzzer_sound_tick();
|
|
view_settings_menu();
|
|
break;
|
|
|
|
case APP_STATE_LIFE_COUNTER:
|
|
if (dir > 0) {
|
|
buzzer_sound_life_gain();
|
|
} else {
|
|
buzzer_sound_life_loss();
|
|
}
|
|
view_life();
|
|
break;
|
|
|
|
case APP_STATE_DICE_ROLLER:
|
|
buzzer_sound_tick();
|
|
view_dice();
|
|
break;
|
|
|
|
case APP_STATE_COMMANDER_TRACKER:
|
|
buzzer_sound_tick();
|
|
view_commander_tax();
|
|
break;
|
|
|
|
case APP_STATE_TOKEN_SPAWNER:
|
|
buzzer_sound_tick();
|
|
view_token_spawner();
|
|
break;
|
|
|
|
case APP_STATE_STORAGE_MANAGER:
|
|
buzzer_sound_tick();
|
|
view_storage();
|
|
break;
|
|
|
|
case APP_STATE_OTA_UPDATE: {
|
|
ota_status_t st = ota_manager_get_status();
|
|
if (st == OTA_STATUS_UPDATE_AVAILABLE) {
|
|
ota_manager_reset();
|
|
app_set_view(APP_STATE_SETTINGS_MENU);
|
|
view_settings_menu();
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Task loop */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void ui_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
ESP_LOGI(TAG, "UI task on core %d", xPortGetCoreID());
|
|
|
|
encoder_init();
|
|
buzzer_init();
|
|
|
|
/* Start on Main Menu at boot */
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
app_set_view(APP_STATE_MENU);
|
|
view_menu();
|
|
|
|
#if CONFIG_MTG_OTA_AUTO_CHECK
|
|
static bool s_boot_ota_checked = false;
|
|
static uint32_t s_wifi_up_ms = 0;
|
|
#endif
|
|
|
|
while (1) {
|
|
/* 1. New RFID card - scans immediately anywhere (wakes if sleeping) */
|
|
if (rfid_manager_has_pending_card()) {
|
|
if (display_is_sleeping()) {
|
|
display_set_sleep(false);
|
|
}
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
handle_new_card(rfid_manager_last_uid());
|
|
}
|
|
|
|
/* 2. Async Scryfall cache result */
|
|
if (s_waiting_fetch && scryfall_has_pending_result()) {
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
handle_fetch_ready();
|
|
}
|
|
|
|
/* 2b. Async Scryfall card image ready */
|
|
if (scryfall_has_pending_image()) {
|
|
handle_image_ready();
|
|
}
|
|
|
|
/* 3. Button press & hold detection */
|
|
int sw_level = gpio_get_level(HW_ENC_SW_GPIO);
|
|
if (sw_level == 0) {
|
|
if (display_is_sleeping()) {
|
|
display_set_sleep(false);
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
buzzer_sound_tick();
|
|
s_btn_held_ms = 0;
|
|
s_btn_long_fired = true; /* Suppress click on release when waking */
|
|
} else {
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
s_btn_held_ms += 25;
|
|
if (s_btn_held_ms >= 750 && !s_btn_long_fired) {
|
|
s_btn_long_fired = true;
|
|
/* Long press: RETURN TO MAIN MENU from any screen */
|
|
buzzer_chirp(1046, 50);
|
|
vTaskDelay(pdMS_TO_TICKS(40));
|
|
buzzer_chirp(1318, 70);
|
|
if (app_get_view() == APP_STATE_REGISTRATION) {
|
|
wifi_manager_set_power_save(true);
|
|
}
|
|
app_set_view(APP_STATE_MENU);
|
|
view_menu();
|
|
}
|
|
}
|
|
} else {
|
|
if (s_btn_held_ms >= 35 && !s_btn_long_fired) {
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
handle_encoder_click();
|
|
}
|
|
s_btn_held_ms = 0;
|
|
s_btn_long_fired = false;
|
|
}
|
|
|
|
/* 4. Rotary encoder rotation */
|
|
if (encoder_has_delta()) {
|
|
int32_t delta = encoder_get_delta_and_reset();
|
|
if (display_is_sleeping()) {
|
|
display_set_sleep(false);
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
buzzer_sound_tick();
|
|
} else {
|
|
s_last_activity_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
handle_encoder_rotation(delta);
|
|
}
|
|
}
|
|
|
|
/* 5. Inactivity display sleep check (3 minutes) */
|
|
uint32_t now_ms = pdTICKS_TO_MS(xTaskGetTickCount());
|
|
if (!display_is_sleeping() && (now_ms - s_last_activity_ms >= UI_SLEEP_TIMEOUT_MS)) {
|
|
if (app_get_view() != APP_STATE_OTA_UPDATE || ota_manager_get_status() != OTA_STATUS_DOWNLOADING) {
|
|
display_set_sleep(true);
|
|
}
|
|
}
|
|
|
|
/* 5. OTA screen live refresh / status change */
|
|
if (app_get_view() == APP_STATE_OTA_UPDATE) {
|
|
ota_status_t cur_ota_st = ota_manager_get_status();
|
|
int cur_percent = ota_manager_get_progress();
|
|
if (cur_ota_st != s_last_ota_rendered_st) {
|
|
view_ota();
|
|
} else if (cur_ota_st == OTA_STATUS_DOWNLOADING && cur_percent != s_last_ota_percent) {
|
|
s_last_ota_percent = cur_percent;
|
|
size_t rb = 0, tb = 0;
|
|
ota_manager_get_bytes(&rb, &tb);
|
|
update_ota_progress(cur_percent, rb, tb);
|
|
}
|
|
}
|
|
|
|
#if CONFIG_MTG_OTA_AUTO_CHECK
|
|
/* 6. Automatic background OTA check on startup after Wi-Fi connects */
|
|
if (!s_boot_ota_checked && wifi_manager_connected()) {
|
|
s_wifi_up_ms += 25;
|
|
if (s_wifi_up_ms >= 4000) {
|
|
s_boot_ota_checked = true;
|
|
ESP_LOGI(TAG, "Triggering startup Gitea OTA check...");
|
|
ota_manager_check_update_async();
|
|
}
|
|
}
|
|
if (s_boot_ota_checked && ota_manager_get_status() == OTA_STATUS_UPDATE_AVAILABLE &&
|
|
(app_get_view() == APP_STATE_MENU || app_get_view() == APP_STATE_SETTINGS_MENU)) {
|
|
app_set_view(APP_STATE_OTA_UPDATE);
|
|
buzzer_chirp(1046, 120);
|
|
view_ota();
|
|
}
|
|
#endif
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(25));
|
|
}
|
|
}
|
|
|
|
esp_err_t ui_task_start(void)
|
|
{
|
|
xTaskCreatePinnedToCore(ui_task, "ui_task", UI_TASK_STACK, NULL, UI_TASK_PRIO, &s_ui_task, UI_CORE);
|
|
return ESP_OK;
|
|
} |