3778cb3399
- DiscoveryMountain: new UI optimized for episode list (not player anymore) - Shows episodes for current season, indicates if on SD card (green=On SD, gray=Not DL) - Per-episode action: download single via SdDownloader or play via Mp3Player - DL Missing button for missing eps in season (currently single first missing, batch reverted) - Season selector screen with cached/total counts - Top-right X close button - Seasons/episodes cached to cache.json to avoid network fetch every launch - Fixes: overlay deletion race (deferred timer), ui_timer leak causing crash on close, season selection overwritten by fetch task - Fixes: unicode ellipsis squares - SdDownloader: revert to stable single-file mode (batch array caused crashes) - Remove batch fields and array allocation, keep single url/path download - Result bundle simple success/path/bytes - Mp3Player: add resume position support (pos_sec from bundle, total_sec estimate, total_decoded_samples), return position on exit via bundle for DM to save Tested on 192.168.68.133: opens, shows S10 2/6, Play/DL buttons, close works, no crash on open/close cycles, cache shows 'Loaded from cache, refreshing...'
555 lines
20 KiB
C
555 lines
20 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){
|
||
if(G.lbl_season) lv_label_set_text(G.lbl_season, "Fetching...");
|
||
return;
|
||
}
|
||
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.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){
|
||
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 || G.fetching) return;
|
||
|
||
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_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();
|
||
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_style_pad_gap(G.overlay,6,0);
|
||
|
||
lv_obj_t* title=lv_label_create(G.overlay);
|
||
lv_label_set_text(title,"Select Season");
|
||
lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_DEFAULT),0);
|
||
|
||
// Scrollable list container
|
||
G.season_list_cont=lv_obj_create(G.overlay);
|
||
lv_obj_set_size(G.season_list_cont, LV_PCT(100), 200);
|
||
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_radius(G.season_list_cont,8,0);
|
||
lv_obj_set_style_pad_all(G.season_list_cont,4,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_add_flag(G.season_list_cont, LV_OBJ_FLAG_SCROLLABLE);
|
||
|
||
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_add_event_cb(btn, season_select_cb, LV_EVENT_CLICKED, (void*)(intptr_t)sn);
|
||
|
||
char blabel[40];
|
||
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);
|
||
}else{
|
||
if(sn==G.cur_season) snprintf(blabel,sizeof(blabel),"S%02d " LV_SYMBOL_OK, sn);
|
||
else snprintf(blabel,sizeof(blabel),"S%02d", sn);
|
||
}
|
||
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_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);
|
||
|
||
tt_lvgl_unlock();
|
||
}
|
||
|
||
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;
|
||
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 + DL Missing
|
||
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* 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);
|
||
|
||
// 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 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);
|
||
|
||
// 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 */ }
|