d4a257a01c
- 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
588 lines
21 KiB
C
588 lines
21 KiB
C
#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 */ }
|