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
This commit is contained in:
Adolfo
2026-07-30 16:13:47 -04:00
parent 7ef8e272fc
commit d4a257a01c
6 changed files with 262 additions and 149 deletions
+100 -67
View File
@@ -76,8 +76,8 @@ void ui_update_status_locked(const char* msg){
void ui_update_header_locked(void){
if(!G.lbl_title && !G.lbl_season) return;
if(G.fetching){
if(G.lbl_season) lv_label_set_text(G.lbl_season, "Fetching...");
return;
// 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");
@@ -103,14 +103,14 @@ static void rebuild_episode_list_locked(void){
if(!G.ep_list_cont) return;
clear_container(G.ep_list_cont);
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;
}
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);
@@ -218,7 +218,8 @@ static void ep_action_cb(lv_event_t* 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 || G.fetching) 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);
@@ -304,74 +305,110 @@ static void close_btn_cb(lv_event_t* 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.fetching) return;
if(G.overlay){ close_overlay(); return; }
tt_lvgl_lock(portMAX_DELAY);
// Create overlay centered modal – parented to root if possible to avoid lingering on app background
lv_obj_t* ovl_parent = G.root ? G.root : lv_scr_act();
// 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, 240, 280);
lv_obj_center(G.overlay);
lv_obj_set_style_bg_color(G.overlay,lv_color_hex(0x222222),0);
lv_obj_set_style_radius(G.overlay,12,0);
lv_obj_set_style_border_width(G.overlay,1,0);
lv_obj_set_style_border_color(G.overlay, lv_color_hex(0x444444),0);
lv_obj_set_style_pad_all(G.overlay,8,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_CENTER, LV_FLEX_ALIGN_CENTER);
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);
lv_obj_t* title=lv_label_create(G.overlay);
// 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);
// Scrollable list container
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), 200);
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(0x1A1A1A),0);
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,4,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,4,0);
// Ensure scrollable
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), 38);
lv_obj_set_style_bg_color(btn, sn==G.cur_season ? lv_color_hex(0x2E7D32) : lv_color_hex(0x333333),0);
lv_obj_set_style_radius(btn,6,0);
lv_obj_set_style_pad_all(btn,6,0);
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[40];
char blabel[48];
if(total>0){
if(sn==G.cur_season) snprintf(blabel,sizeof(blabel),"S%02d %d/%d " LV_SYMBOL_OK, sn, cached, total);
else snprintf(blabel,sizeof(blabel),"S%02d %d/%d", sn, cached, total);
snprintf(blabel,sizeof(blabel),"Season %02d %d/%d on SD", sn, cached, total);
}else{
if(sn==G.cur_season) snprintf(blabel,sizeof(blabel),"S%02d " LV_SYMBOL_OK, sn);
else snprintf(blabel,sizeof(blabel),"S%02d", sn);
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_SMALL),0);
lv_obj_center(l1);
}
lv_obj_set_style_text_font(l1, lvgl_get_text_font(FONT_SIZE_DEFAULT),0);
lv_obj_set_flex_grow(l1,1);
lv_obj_t* bc=lv_btn_create(G.overlay);
lv_obj_set_size(bc, LV_PCT(60), 36);
lv_obj_set_style_bg_color(bc, lv_color_hex(0x444444),0);
lv_obj_t* lc=lv_label_create(bc);
lv_label_set_text(lc,"Close");
lv_obj_center(lc);
lv_obj_add_event_cb(bc,close_btn_cb,LV_EVENT_CLICKED,NULL);
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();
}
@@ -379,7 +416,6 @@ void btn_seasons_cb(lv_event_t* e){
void btn_dl_season_cb(lv_event_t* e){
(void)e;
if(G.downloading) return;
if(G.fetching) return;
if(G.ep_cnt==0) return;
int season = G.cur_season;
@@ -457,7 +493,7 @@ void build_ui(lv_obj_t* parent){
lv_obj_center(l_close);
lv_obj_add_event_cb(btn_close, btn_close_cb, LV_EVENT_CLICKED, NULL);
// Action row: Seasons + DL Missing
// 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);
@@ -479,16 +515,18 @@ void build_ui(lv_obj_t* parent){
lv_obj_center(ls);
lv_obj_add_event_cb(bs,btn_seasons_cb,LV_EVENT_CLICKED,NULL);
lv_obj_t* bd=lv_btn_create(row);
lv_obj_set_size(bd,LV_PCT(48),36);
G.btn_dl_missing=bd;
lv_obj_set_style_bg_color(bd, lv_color_hex(0x1E88E5), 0);
lv_obj_set_style_radius(bd,6,0);
lv_obj_t* ld=lv_label_create(bd);
lv_label_set_text(ld,"DL Missing");
lv_obj_set_style_text_font(ld, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_center(ld);
lv_obj_add_event_cb(bd,btn_dl_season_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);
@@ -510,13 +548,8 @@ void build_ui(lv_obj_t* parent){
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 at bottom
G.lbl_status=lv_label_create(root);
lv_label_set_text(G.lbl_status,"Fetching data...");
lv_obj_set_style_text_color(G.lbl_status,lv_color_hex(0x888888),0);
lv_obj_set_style_text_font(G.lbl_status, lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_width(G.lbl_status, LV_PCT(100));
lv_label_set_long_mode(G.lbl_status, LV_LABEL_LONG_DOT);
// Status bar removed per UX request – no help label
G.lbl_status=NULL;
// Legacy placeholders nulled
G.lbl_meta=NULL;