Files
tactility_apps/Apps/DiscoveryMountain/main/Source/ui.c
T
Adolfo d4a257a01c feat(DM): instant cache load, manual refresh, full-screen season selector, season persistence fix
- Cache loads synchronously on onShow for instant startup, no auto fetch
- Added manual Refresh button, removed auto refresh that blocked UI
- Made UI non-blocking during fetch (episode list usable while fetching)
- Fixed season loss after player/downloader by preserving season in static s_preserved_season surviving G memset
- Season selector now full-screen overlay parented to lv_scr_act()
- Removed DL Missing batch button and bottom help/status label per UX request
- load_state no longer overwrites browsing season with last_slug season
2026-07-30 16:13:47 -04:00

588 lines
21 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "ui.h"
#include "storage.h"
#include "player.h"
#include "download.h"
#include <tt_lvgl.h>
#include <tt_app.h>
#include <tt_bundle.h>
#include <tactility/lvgl_fonts.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "esp_log.h"
#include "esp_heap_caps.h"
void close_overlay(){
if(G.overlay){
lv_obj_delete(G.overlay);
G.overlay=NULL;
G.dropdown=NULL;
G.season_list_cont=NULL;
}
}
static void overlay_close_timer_cb(lv_timer_t* t){
close_overlay();
lv_timer_delete(t);
}
static void close_overlay_safe(void){
// Defer deletion to avoid deleting ancestor of current event target (season buttons inside overlay)
lv_timer_create(overlay_close_timer_cb, 20, NULL);
}
// Forward decls
static void ep_action_cb(lv_event_t* e);
static void season_select_cb(lv_event_t* e);
static void rebuild_episode_list_locked(void);
static void clear_container(lv_obj_t* cont){
if(!cont) return;
uint32_t child_cnt = lv_obj_get_child_cnt(cont);
while(child_cnt>0){
lv_obj_t* child = lv_obj_get_child(cont, 0);
if(child) lv_obj_delete(child);
else break;
child_cnt = lv_obj_get_child_cnt(cont);
}
}
static void btn_close_cb(lv_event_t* e){
(void)e;
if(G.overlay){
close_overlay();
return;
}
tt_app_stop();
}
static void get_season_stats(int season, int* total, int* cached){
int t=0,c=0;
for(int i=0;i<G.ep_cnt;i++){
if(G.eps[i].season==season){
t++;
if(G.eps[i].seen) c++;
}
}
if(total) *total=t;
if(cached) *cached=c;
}
void ui_update_status_locked(const char* msg){
if(G.lbl_status && msg) lv_label_set_text(G.lbl_status, msg);
}
void ui_update_header_locked(void){
if(!G.lbl_title && !G.lbl_season) return;
if(G.fetching){
// Keep showing current season stats instead of blocking header
// status bar already says fetching
}
if(G.season_cnt==0){
if(G.lbl_season) lv_label_set_text(G.lbl_season, "No seasons");
return;
}
int total=0,cached=0;
get_season_stats(G.cur_season,&total,&cached);
char buf[64];
if(total>0){
snprintf(buf,sizeof(buf),"S%02d • %d/%d on SD • %d eps", G.cur_season, cached, total, total);
}else{
snprintf(buf,sizeof(buf),"S%02d • %d eps", G.cur_season, total);
}
if(G.lbl_season) lv_label_set_text(G.lbl_season, buf);
if(G.lbl_title){
// Keep app name
// optionally update title to count
lv_label_set_text(G.lbl_title, "Discovery Mountain");
}
}
static void rebuild_episode_list_locked(void){
if(!G.ep_list_cont) return;
clear_container(G.ep_list_cont);
if(G.ep_cnt==0){
if(G.fetching){
lv_obj_t* lbl=lv_label_create(G.ep_list_cont);
lv_label_set_text(lbl,"Fetching episodes...");
lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0);
lv_obj_align(lbl,LV_ALIGN_TOP_MID,0,20);
return;
}
lv_obj_t* lbl=lv_label_create(G.ep_list_cont);
lv_label_set_text(lbl,"No episodes");
lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0);
return;
}
// Count episodes for cur season
int count=0;
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season) count++;
if(count==0){
lv_obj_t* lbl=lv_label_create(G.ep_list_cont);
lv_label_set_text(lbl,"No episodes in season");
lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0);
// Also show all if fallback?
// list first few from all
return;
}
for(int i=0;i<G.ep_cnt;i++){
if(G.eps[i].season!=G.cur_season) continue;
EpInfo* ep=&G.eps[i];
// Row container – optimized for low internal heap (4 objs per ep)
lv_obj_t* row=lv_obj_create(G.ep_list_cont);
lv_obj_set_size(row, LV_PCT(100), 50);
lv_obj_set_style_bg_color(row, ep->seen ? lv_color_hex(0x1E2A22) : lv_color_hex(0x222222),0);
lv_obj_set_style_bg_opa(row, LV_OPA_COVER,0);
lv_obj_set_style_radius(row,8,0);
lv_obj_set_style_pad_all(row,6,0);
lv_obj_set_style_border_width(row,1,0);
lv_obj_set_style_border_color(row, ep->seen ? lv_color_hex(0x2E7D32) : lv_color_hex(0x333333),0);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_pad_gap(row,6,0);
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(row, ep_action_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
// Left single label with title (truncated) – we embed meta as second line via \n to save objects
lv_obj_t* ltitle=lv_label_create(row);
lv_obj_set_flex_grow(ltitle,1);
// Truncate title to ~26 chars for display
char short_title[30];
strncpy(short_title, ep->title, sizeof(short_title)-1);
short_title[sizeof(short_title)-1]=0;
if(strlen(ep->title) > 26){
short_title[23]='.';
short_title[24]='.';
short_title[25]='.';
short_title[26]=0;
}
char line1[64];
snprintf(line1,sizeof(line1),"E%02d: %s", ep->ep_num, short_title);
// Second line status
char line2[32];
int saved = get_saved_pos_for_slug(ep->slug);
if(ep->seen){
if(saved>5){
char tb[16]; fmt_time(tb,saved);
snprintf(line2,sizeof(line2),"On SD %s", tb);
}else{
snprintf(line2,sizeof(line2),"On SD");
}
}else{
snprintf(line2,sizeof(line2),"Not DL");
}
char tbuf[96];
snprintf(tbuf,sizeof(tbuf),"%s\n%s", line1, line2);
lv_label_set_text(ltitle, tbuf);
lv_obj_set_width(ltitle, LV_PCT(100));
lv_label_set_long_mode(ltitle, LV_LABEL_LONG_DOT);
lv_obj_set_style_text_font(ltitle, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(ltitle, ep->seen ? lv_color_hex(0xE0E0E0) : lv_color_hex(0xCCCCCC),0);
// Right side: button
lv_obj_t* btn=lv_btn_create(row);
lv_obj_set_size(btn, 52, 34);
if(ep->seen){
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2E7D32),0);
}else{
lv_obj_set_style_bg_color(btn, lv_color_hex(0x1E88E5),0);
}
lv_obj_set_style_radius(btn,6,0);
lv_obj_t* bl=lv_label_create(btn);
lv_label_set_text(bl, ep->seen ? LV_SYMBOL_PLAY : LV_SYMBOL_DOWNLOAD);
lv_obj_center(bl);
lv_obj_add_event_cb(btn, ep_action_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
}
// Scroll to top after rebuild
lv_obj_scroll_to_y(G.ep_list_cont, 0, LV_ANIM_OFF);
}
void ui_rebuild_episode_list(void){
tt_lvgl_lock(portMAX_DELAY);
if(G.ep_list_cont) rebuild_episode_list_locked();
ui_update_header_locked();
tt_lvgl_unlock();
}
static void ep_action_cb(lv_event_t* e){
if(lv_event_get_code(e)!=LV_EVENT_CLICKED) return;
// Retrieve idx from user data
int idx = (int)(intptr_t)lv_event_get_user_data(e);
// Fallback: try event target's user data via current target?
// lv_event_get_user_data should already give us the idx
if(idx<0 || idx>=G.ep_cnt) return;
if(G.downloading) return;
// Allow interaction even while fetching – cache remains usable
EpInfo* ep=&G.eps[idx];
ESP_LOGI(TAG,"ep_action idx %d S%02dE%02d seen %d", idx, ep->season, ep->ep_num, ep->seen);
if(ep->season != G.cur_season){
// Shouldn't happen but update cur season to match
G.cur_season = ep->season;
}
// Use start_idx which handles both DL and Play
tt_lvgl_lock(portMAX_DELAY);
if(G.lbl_status){
if(ep->seen) lv_label_set_text(G.lbl_status,"Starting player...");
else lv_label_set_text(G.lbl_status,"Starting downloader...");
}
tt_lvgl_unlock();
start_idx(idx);
}
static void season_select_cb(lv_event_t* e){
if(lv_event_get_code(e)!=LV_EVENT_CLICKED) return;
int season_num = (int)(intptr_t)lv_event_get_user_data(e);
if(season_num<=0) return;
ESP_LOGI(TAG,"Season selected S%02d", season_num);
// Defer overlay deletion – deleting ancestor during event is unsafe
close_overlay_safe();
G.cur_season = season_num;
// Save state immediately
tt_lvgl_lock(portMAX_DELAY);
ui_update_header_locked();
rebuild_episode_list_locked();
if(G.lbl_status){
char buf[40];
snprintf(buf,sizeof(buf),"Switched to S%02d", season_num);
lv_label_set_text(G.lbl_status, buf);
}
tt_lvgl_unlock();
save_state();
// Jump to first unseen or first in season
int first=-1, first_unseen=-1;
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==season_num){
if(first==-1) first=i;
if(!G.eps[i].seen && first_unseen==-1) first_unseen=i;
}
int tgt = first_unseen!=-1?first_unseen:first;
if(tgt!=-1){
// Optional: select ep updates title but we already rebuilt list; keep cur_ep_idx
G.cur_ep_idx = tgt;
strncpy(G.last_slug, G.eps[tgt].slug, sizeof(G.last_slug)-1);
save_state();
}
}
void dropdown_cb(lv_event_t* e){
if(lv_event_get_code(e)!=LV_EVENT_VALUE_CHANGED) return;
lv_obj_t* dd = (lv_obj_t*)lv_event_get_target(e);
int sel=lv_dropdown_get_selected(dd);
if(sel<0||sel>=G.season_cnt) return;
int sn=G.seasons[sel].num;
close_overlay_safe();
int first=-1, first_unseen=-1;
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==sn){
if(first==-1) first=i;
if(!G.eps[i].seen && first_unseen==-1) first_unseen=i;
}
int tgt=first_unseen!=-1?first_unseen:first;
if(tgt!=-1){
G.cur_season=sn;
G.cur_ep_idx=tgt;
strncpy(G.last_slug, G.eps[tgt].slug, sizeof(G.last_slug)-1);
tt_lvgl_lock(portMAX_DELAY);
ui_update_header_locked();
rebuild_episode_list_locked();
tt_lvgl_unlock();
save_state();
}
}
static void close_btn_cb(lv_event_t* e){
(void)e;
close_overlay_safe();
}
void btn_refresh_cb(lv_event_t* e){
(void)e;
if(G.downloading) return;
if(G.overlay){ close_overlay(); }
if(G.fetching){
tt_lvgl_lock(portMAX_DELAY);
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Already refreshing...");
tt_lvgl_unlock();
return;
}
ESP_LOGI(TAG,"Manual refresh pressed");
trigger_manual_refresh();
}
void btn_seasons_cb(lv_event_t* e){
(void)e;
if(G.downloading) return;
if(G.overlay){ close_overlay(); return; }
tt_lvgl_lock(portMAX_DELAY);
// Full-screen season selector – parent to active screen to cover whole 240x320, not just root content
lv_obj_t* ovl_parent = lv_scr_act();
G.overlay=lv_obj_create(ovl_parent);
lv_obj_set_size(G.overlay, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(G.overlay, 0, 0);
lv_obj_set_style_bg_color(G.overlay, lv_color_hex(0x111111),0);
lv_obj_set_style_bg_opa(G.overlay, LV_OPA_COVER,0);
lv_obj_set_style_radius(G.overlay,0,0);
lv_obj_set_style_border_width(G.overlay,0,0);
lv_obj_set_style_pad_all(G.overlay,6,0);
lv_obj_set_style_pad_gap(G.overlay,6,0);
lv_obj_set_flex_flow(G.overlay, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(G.overlay, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_remove_flag(G.overlay, LV_OBJ_FLAG_SCROLLABLE);
// Header row: title + close
lv_obj_t* hdr=lv_obj_create(G.overlay);
lv_obj_set_size(hdr, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(hdr, LV_OPA_TRANSP,0);
lv_obj_set_style_border_width(hdr,0,0);
lv_obj_set_style_pad_all(hdr,2,0);
lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(hdr, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* title=lv_label_create(hdr);
lv_label_set_text(title,"Select Season");
lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_DEFAULT),0);
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF),0);
lv_obj_set_flex_grow(title,1);
lv_obj_t* bc=lv_btn_create(hdr);
lv_obj_set_size(bc, 40, 36);
lv_obj_set_style_bg_color(bc, lv_color_hex(0x333333),0);
lv_obj_set_style_radius(bc,6,0);
lv_obj_t* lc=lv_label_create(bc);
lv_label_set_text(lc, LV_SYMBOL_CLOSE);
lv_obj_set_style_text_font(lc, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_center(lc);
lv_obj_add_event_cb(bc,close_btn_cb,LV_EVENT_CLICKED,NULL);
// Scrollable list container fills rest
G.season_list_cont=lv_obj_create(G.overlay);
lv_obj_set_size(G.season_list_cont, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_grow(G.season_list_cont,1);
lv_obj_set_style_bg_color(G.season_list_cont, lv_color_hex(0x0E0E0E),0);
lv_obj_set_style_radius(G.season_list_cont,8,0);
lv_obj_set_style_pad_all(G.season_list_cont,6,0);
lv_obj_set_flex_flow(G.season_list_cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_gap(G.season_list_cont,6,0);
lv_obj_add_flag(G.season_list_cont, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(G.season_list_cont, LV_SCROLLBAR_MODE_AUTO);
for(int i=0;i<G.season_cnt;i++){
int sn=G.seasons[i].num;
int total,cached;
get_season_stats(sn,&total,&cached);
lv_obj_t* btn=lv_btn_create(G.season_list_cont);
lv_obj_set_size(btn, LV_PCT(100), 52);
lv_obj_set_style_bg_color(btn, sn==G.cur_season ? lv_color_hex(0x2E7D32) : lv_color_hex(0x222222),0);
lv_obj_set_style_radius(btn,8,0);
lv_obj_set_style_pad_all(btn,10,0);
lv_obj_set_style_border_width(btn,1,0);
lv_obj_set_style_border_color(btn, sn==G.cur_season ? lv_color_hex(0x4CAF50) : lv_color_hex(0x333333),0);
lv_obj_add_event_cb(btn, season_select_cb, LV_EVENT_CLICKED, (void*)(intptr_t)sn);
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
char blabel[48];
if(total>0){
snprintf(blabel,sizeof(blabel),"Season %02d %d/%d on SD", sn, cached, total);
}else{
snprintf(blabel,sizeof(blabel),"Season %02d %d eps", sn, total);
}
lv_obj_t* l1=lv_label_create(btn);
lv_label_set_text(l1, blabel);
lv_obj_set_style_text_font(l1, lvgl_get_text_font(FONT_SIZE_DEFAULT),0);
lv_obj_set_flex_grow(l1,1);
if(sn==G.cur_season){
lv_obj_t* lcheck=lv_label_create(btn);
lv_label_set_text(lcheck, LV_SYMBOL_OK);
lv_obj_set_style_text_color(lcheck, lv_color_hex(0x4CAF50),0);
}
}
tt_lvgl_unlock();
}
void btn_dl_season_cb(lv_event_t* e){
(void)e;
if(G.downloading) return;
if(G.ep_cnt==0) return;
int season = G.cur_season;
ESP_LOGI(TAG, "DL Missing S%02d pressed", season);
tt_lvgl_lock(portMAX_DELAY);
close_overlay();
if(G.dl_overlay) hide_dl_overlay();
if(G.lbl_status){
char buf[40];
snprintf(buf,sizeof(buf),"Checking S%02d missing...", season);
lv_label_set_text(G.lbl_status, buf);
}
tt_lvgl_unlock();
if (!download_season(season)) {
tt_lvgl_lock(portMAX_DELAY);
if(G.lbl_status) lv_label_set_text(G.lbl_status,"DL Season failed – no missing?");
tt_lvgl_unlock();
}
}
void build_ui(lv_obj_t* parent){
tt_lvgl_lock(portMAX_DELAY);
lv_obj_t* root = lv_obj_create(parent);
G.root = root;
lv_obj_set_size(root,LV_PCT(100),LV_PCT(100));
lv_obj_set_style_bg_color(root,lv_color_hex(0x111111),0);
lv_obj_set_style_pad_all(root,6,0);
lv_obj_set_style_pad_gap(root,6,0);
lv_obj_set_style_border_width(root,0,0);
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE);
// Header container
lv_obj_t* header=lv_obj_create(root);
lv_obj_set_size(header, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(header, LV_OPA_TRANSP,0);
lv_obj_set_style_border_width(header,0,0);
lv_obj_set_style_pad_all(header,2,0);
lv_obj_set_style_pad_gap(header,6,0);
lv_obj_set_flex_flow(header, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(header, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(header, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* header_left=lv_obj_create(header);
lv_obj_set_size(header_left, LV_PCT(80), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(header_left, LV_OPA_TRANSP,0);
lv_obj_set_style_border_width(header_left,0,0);
lv_obj_set_style_pad_all(header_left,0,0);
lv_obj_set_flex_flow(header_left, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_gap(header_left,2,0);
lv_obj_remove_flag(header_left, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_grow(header_left,1);
G.lbl_title=lv_label_create(header_left);
lv_label_set_text(G.lbl_title,"Discovery Mountain");
lv_obj_set_style_text_font(G.lbl_title, lvgl_get_text_font(FONT_SIZE_DEFAULT), 0);
lv_obj_set_style_text_color(G.lbl_title, lv_color_hex(0xFFFFFF),0);
G.lbl_season=lv_label_create(header_left);
lv_label_set_text(G.lbl_season,"Loading seasons...");
lv_obj_set_style_text_font(G.lbl_season, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(G.lbl_season,lv_color_hex(0xAAAAAA),0);
// Close button top right
lv_obj_t* btn_close=lv_btn_create(header);
lv_obj_set_size(btn_close, 36, 32);
lv_obj_set_style_bg_color(btn_close, lv_color_hex(0x333333),0);
lv_obj_set_style_radius(btn_close,6,0);
lv_obj_t* l_close=lv_label_create(btn_close);
lv_label_set_text(l_close, LV_SYMBOL_CLOSE);
lv_obj_set_style_text_font(l_close, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_center(l_close);
lv_obj_add_event_cb(btn_close, btn_close_cb, LV_EVENT_CLICKED, NULL);
// Action row: Seasons + Refresh (DL Missing removed per UX simplification)
lv_obj_t* row=lv_obj_create(root);
lv_obj_set_size(row,LV_PCT(100),44);
lv_obj_set_flex_flow(row,LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row,LV_FLEX_ALIGN_SPACE_EVENLY,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_bg_opa(row,LV_OPA_TRANSP,0);
lv_obj_set_style_border_width(row,0,0);
lv_obj_set_style_pad_all(row,0,0);
lv_obj_set_style_pad_gap(row,8,0);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* bs=lv_btn_create(row);
lv_obj_set_size(bs,LV_PCT(48),36);
G.btn_seasons=bs;
lv_obj_set_style_bg_color(bs, lv_color_hex(0x333333),0);
lv_obj_set_style_radius(bs,6,0);
lv_obj_t* ls=lv_label_create(bs);
lv_label_set_text(ls,"Seasons");
lv_obj_set_style_text_font(ls, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_center(ls);
lv_obj_add_event_cb(bs,btn_seasons_cb,LV_EVENT_CLICKED,NULL);
lv_obj_t* br=lv_btn_create(row);
lv_obj_set_size(br,LV_PCT(48),36);
G.btn_refresh=br;
lv_obj_set_style_bg_color(br, lv_color_hex(0x444444), 0);
lv_obj_set_style_radius(br,6,0);
lv_obj_t* lr=lv_label_create(br);
lv_label_set_text(lr, LV_SYMBOL_REFRESH " Refresh");
lv_obj_set_style_text_font(lr, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_center(lr);
lv_obj_add_event_cb(br,btn_refresh_cb,LV_EVENT_CLICKED,NULL);
G.btn_dl_missing=NULL;
// Episode list container – fills remaining space
G.ep_list_cont=lv_obj_create(root);
lv_obj_set_size(G.ep_list_cont, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_grow(G.ep_list_cont, 1);
lv_obj_set_style_bg_color(G.ep_list_cont, lv_color_hex(0x0E0E0E),0);
lv_obj_set_style_bg_opa(G.ep_list_cont, LV_OPA_COVER,0);
lv_obj_set_style_radius(G.ep_list_cont,8,0);
lv_obj_set_style_pad_all(G.ep_list_cont,4,0);
lv_obj_set_style_pad_gap(G.ep_list_cont,4,0);
lv_obj_set_flex_flow(G.ep_list_cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(G.ep_list_cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
// Ensure scrollable
lv_obj_add_flag(G.ep_list_cont, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(G.ep_list_cont, LV_SCROLLBAR_MODE_AUTO);
lv_obj_t* hint=lv_label_create(G.ep_list_cont);
lv_label_set_text(hint,"Loading episodes...");
lv_obj_set_style_text_color(hint, lv_color_hex(0x888888),0);
lv_obj_set_style_text_font(hint, lvgl_get_text_font(FONT_SIZE_SMALL),0);
// Status bar removed per UX request – no help label
G.lbl_status=NULL;
// Legacy placeholders nulled
G.lbl_meta=NULL;
G.bar=NULL;
G.lbl_time=NULL;
G.btn_play=NULL;
G.dl_overlay=NULL;
G.ui_timer_handle=NULL;
G.ui_timer_handle = lv_timer_create(ui_timer,300,NULL);
tt_lvgl_unlock();
}
void ui_deinit(void){
if(G.ui_timer_handle){
lv_timer_delete(G.ui_timer_handle);
G.ui_timer_handle=NULL;
}
}
void ui_timer(lv_timer_t* t){
(void)t;
// If app is closing (root null) skip
if(!G.root) return;
if(G.fetching){
return;
}
if(G.downloading && G.dl_overlay){
update_dl_overlay_ui();
}
}
void btn_play_cb(lv_event_t* e){ (void)e; /* legacy, not used */ }
void btn_prev_cb(lv_event_t* e){ (void)e; /* legacy */ }
void btn_next_cb(lv_event_t* e){ (void)e; /* legacy */ }