diff --git a/Apps/DiscoveryMountain/main/Source/app_context.h b/Apps/DiscoveryMountain/main/Source/app_context.h index 1763a3b..efff4e1 100644 --- a/Apps/DiscoveryMountain/main/Source/app_context.h +++ b/Apps/DiscoveryMountain/main/Source/app_context.h @@ -97,8 +97,15 @@ typedef struct { int dl_missing_total; int dl_missing_done; int dl_missing_failed; + + // Refresh button reference + lv_obj_t *btn_refresh; } AppCtx; +void trigger_manual_refresh(void); +void dm_preserve_season(int season); +int dm_consume_preserved_season(void); + extern AppCtx G; #ifdef __cplusplus diff --git a/Apps/DiscoveryMountain/main/Source/main.c b/Apps/DiscoveryMountain/main/Source/main.c index 42648ec..315019c 100644 --- a/Apps/DiscoveryMountain/main/Source/main.c +++ b/Apps/DiscoveryMountain/main/Source/main.c @@ -14,46 +14,42 @@ #include "esp_log.h" #include "esp_heap_caps.h" +// Preserved browsing season that survives G memset across external app launch/return +// This fixes season reset to 1 when returning from player/downloader where file read may race +static int s_preserved_season = -1; + +void dm_preserve_season(int season){ + if(season>0) s_preserved_season = season; +} +int dm_consume_preserved_season(void){ + int s = s_preserved_season; + s_preserved_season = -1; + return s; +} + static void fetch_task_fn(void* arg){ (void)arg; G.fetching=true; G.fetch_cancel_req=false; - ESP_LOGI(TAG,"fetch task start"); + ESP_LOGI(TAG,"fetch task start (manual refresh)"); - // Try load cached seasons/episodes – instant offline support - bool cache_loaded = load_cache(); - if(cache_loaded){ - ESP_LOGI(TAG,"Cache loaded: %d seasons, %d eps", G.season_cnt, G.ep_cnt); - // Restore cur idx from last_slug - if(strlen(G.last_slug)>0){ - for(int i=0;i0){ - s1=0; - G.cur_season=G.eps[0].season; - } - G.cur_ep_idx=s1; - } - // Show cached data immediately - G.fetching=false; - ui_rebuild_episode_list(); - tt_lvgl_lock(portMAX_DELAY); - if(G.lbl_status) lv_label_set_text(G.lbl_status,"Loaded from cache, refreshing..."); - tt_lvgl_unlock(); - G.fetching=true; - vTaskDelay(pdMS_TO_TICKS(200)); - } else { - ESP_LOGI(TAG,"No cache found, will fetch from network"); + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"Fetching from network..."); + tt_lvgl_unlock(); + + // Preserve user's current season selection across fetch + int preserved_season = G.cur_season; + char preserved_slug[96]={0}; + if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt){ + strncpy(preserved_slug, G.eps[G.cur_ep_idx].slug, sizeof(preserved_slug)-1); + } else if(strlen(G.last_slug)>0){ + strncpy(preserved_slug, G.last_slug, sizeof(preserved_slug)-1); } - // Network fetch – updates if possible, otherwise keeps cache bool fetch_ok = fetch_data(); if(G.fetch_cancel_req){ - ESP_LOGI(TAG,"fetch task canceled, exiting"); + ESP_LOGI(TAG,"fetch task canceled"); G.fetching=false; G.fetch_handle=NULL; vTaskDelete(NULL); @@ -64,60 +60,43 @@ static void fetch_task_fn(void* arg){ ESP_LOGI(TAG,"Network fetch ok: %d seasons, %d eps – saving cache", G.season_cnt, G.ep_cnt); save_cache(); } else { - if(cache_loaded){ - ESP_LOGW(TAG,"Network fetch failed, restoring cache"); - // fetch_data overwrote with fallback, restore cache - load_cache(); - } else { - ESP_LOGW(TAG,"Network fetch failed and no cache – using fallback"); - } + ESP_LOGW(TAG,"Network fetch failed"); + // Do not overwrite current data if fetch fails – keep existing cache in memory + // If we have no data at all, fetch_data already created fallback } - ESP_LOGI(TAG,"fetch done seasons=%d eps=%d cache=%d fetch_ok=%d",G.season_cnt,G.ep_cnt,cache_loaded,fetch_ok); - vTaskDelay(pdMS_TO_TICKS(300)); + ESP_LOGI(TAG,"fetch done seasons=%d eps=%d fetch_ok=%d", G.season_cnt, G.ep_cnt, fetch_ok); - // Resolve current episode index – preserve user season selection if cache was already shown - if(cache_loaded){ - // User may have changed season while network fetch was in progress – keep their selection if it still exists - int cur_s = G.cur_season; + // Restore preserved season if it still exists + if(preserved_season>0){ bool season_exists=false; - for(int i=0;i0){ - for(int i=0;i0){ - for(int i=0;i0){ + for(int i=0;i0){ + // Fallback to preserved slug or first + if(strlen(preserved_slug)>0){ + for(int i=0;i0){ G.cur_ep_idx=0; G.cur_season=G.eps[0].season; } } } else { - // First launch – use last_slug to restore position - if(strlen(G.last_slug)>0){ - for(int i=0;i0){ - s1=0; - G.cur_season=G.eps[0].season; - } - G.cur_ep_idx=s1; + if(G.cur_ep_idx<0 && G.ep_cnt>0){ + G.cur_ep_idx=0; + G.cur_season=G.eps[0].season; } } @@ -144,9 +123,8 @@ static void fetch_task_fn(void* arg){ tt_lvgl_lock(portMAX_DELAY); if(G.lbl_status){ if(G.ep_cnt>0){ - if(fetch_ok) lv_label_set_text(G.lbl_status, "Select episode to play / download"); - else if(cache_loaded) lv_label_set_text(G.lbl_status, "Offline, using cached data"); - else lv_label_set_text(G.lbl_status,"No network – limited data"); + if(fetch_ok) lv_label_set_text(G.lbl_status, "Refreshed – select episode"); + else lv_label_set_text(G.lbl_status, "Refresh failed – using cache"); } else { lv_label_set_text(G.lbl_status,"No data – check network"); } @@ -158,8 +136,37 @@ static void fetch_task_fn(void* arg){ vTaskDelete(NULL); } +void trigger_manual_refresh(void){ + if(G.fetching){ + ESP_LOGI(TAG,"refresh already in progress"); + return; + } + if(G.fetch_handle){ + ESP_LOGW(TAG,"fetch handle still present, not starting new"); + return; + } + G.fetch_cancel_req=false; + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + ESP_LOGI(TAG,"trigger_manual_refresh heap: internal=%d psram=%d", free_internal, free_psram); + BaseType_t res = xTaskCreate(fetch_task_fn,"dm_fetch",12288,NULL,5,&G.fetch_handle); + ESP_LOGI(TAG,"xTaskCreate manual refresh res=%d handle=%p", res, G.fetch_handle); + if(res != pdPASS){ + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"Failed to start refresh"); + tt_lvgl_unlock(); + G.fetch_handle=NULL; + } +} + static void onShow(AppHandle app, void* data, lv_obj_t* parent){ (void)app; (void)data; (void)parent; + // Check for preserved season from external app launch (survives G memset) + int preserved = dm_consume_preserved_season(); + if(preserved>0){ + ESP_LOGI(TAG,"onShow preserved season S%02d from previous launch", preserved); + } + memset(&G,0,sizeof(G)); G.pos_sec=0; G.total_sec=0; @@ -174,17 +181,67 @@ static void onShow(AppHandle app, void* data, lv_obj_t* parent){ G.dl_cancel_req=false; ensure_dir(); load_state(); + + // If we have a preserved season from launching player/downloader, it overrides file + if(preserved>0){ + G.cur_season = preserved; + ESP_LOGI(TAG,"onShow overriding cur_season with preserved S%02d", preserved); + } + build_ui(parent); + + // Instant cache load – no network, fully synchronous for fast startup + // NOTE: load_state() already loaded last_season (browsing season) and last_slug (last played) + // We want to preserve browsing season, NOT overwrite it with last_slug's season. + bool cache_loaded = load_cache(); + if(cache_loaded){ + ESP_LOGI(TAG,"Cache loaded instantly: %d seasons, %d eps browsing S%02d last_slug=%s preserved=%d", G.season_cnt, G.ep_cnt, G.cur_season, G.last_slug, preserved); + // Validate browsing season still exists, otherwise fallback to first season + bool season_exists=false; + for(int i=0;i0){ + for(int i=0;i0){ + ESP_LOGW(TAG,"Browsing season S%02d not found in cache, fallback to S%02d", G.cur_season, G.seasons[0].num); + G.cur_season = G.seasons[0].num; + season_exists=true; + } + } + // Resolve cur_ep_idx: prefer last_slug if it is within browsing season, otherwise first ep in browsing season + G.cur_ep_idx=-1; + if(strlen(G.last_slug)>0 && season_exists){ + for(int i=0;i0){ + G.cur_ep_idx=0; + G.cur_season=G.eps[0].season; + } + ui_rebuild_episode_list(); + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"Loaded from cache – tap Refresh to update"); + tt_lvgl_unlock(); + } else { + ESP_LOGI(TAG,"No cache at startup, cur_season S%02d preserved %d", G.cur_season, preserved); + tt_lvgl_lock(portMAX_DELAY); + if(G.lbl_status) lv_label_set_text(G.lbl_status,"No cache – tap Refresh"); + tt_lvgl_unlock(); + ui_rebuild_episode_list(); + } + + // Final safeguard: if preserved was set, ensure it is saved back to state file so next open keeps it + if(preserved>0){ + save_state(); + } + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); - ESP_LOGI(TAG, "onShow heap before fetch task: internal=%d psram=%d", free_internal, free_psram); - BaseType_t res = xTaskCreate(fetch_task_fn,"dm_fetch",12288,NULL,5,&G.fetch_handle); - ESP_LOGI(TAG, "xTaskCreate fetch_task res=%d handle=%p", res, G.fetch_handle); - if (res != pdPASS) { - tt_lvgl_lock(portMAX_DELAY); - if (G.lbl_status) lv_label_set_text(G.lbl_status, "Failed to create fetch task"); - tt_lvgl_unlock(); - } + ESP_LOGI(TAG, "onShow heap after cache load: internal=%d psram=%d cache=%d cur_season=%d", free_internal, free_psram, cache_loaded, G.cur_season); } static void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData){ @@ -419,6 +476,7 @@ static void onHide(AppHandle app, void* data){ G.btn_play = NULL; G.btn_seasons = NULL; G.btn_dl_missing = NULL; + G.btn_refresh = NULL; G.season_list_cont = NULL; G.bar = NULL; } diff --git a/Apps/DiscoveryMountain/main/Source/player.c b/Apps/DiscoveryMountain/main/Source/player.c index 8d1a419..80efab6 100644 --- a/Apps/DiscoveryMountain/main/Source/player.c +++ b/Apps/DiscoveryMountain/main/Source/player.c @@ -37,6 +37,14 @@ static bool try_external_downloader(int idx) { char fpath[300]; make_sd_path(fpath, sizeof(fpath), ep->slug); + // Preserve browsing season so we return to same season after downloader closes – survive G memset via static + dm_preserve_season(ep->season); + int saved_season = G.cur_season; + G.cur_season = ep->season; + save_state(); + G.cur_ep_idx = idx; + (void)saved_season; + tt_lvgl_lock(portMAX_DELAY); close_overlay(); if(G.dl_overlay) hide_dl_overlay(); @@ -51,7 +59,7 @@ static bool try_external_downloader(int idx) { tt_bundle_put_string(b, "path", fpath); tt_bundle_put_bool(b, "override", false); - ESP_LOGI(TAG, "Starting external SdDownloader for %s -> %s", ep->slug, fpath); + ESP_LOGI(TAG, "Starting external SdDownloader for %s -> %s season %d", ep->slug, fpath, G.cur_season); G.pending_dl_idx = idx; tt_lvgl_lock(portMAX_DELAY); @@ -118,6 +126,10 @@ bool download_missing_season_start(int season){ G.dl_missing_total=missing; G.dl_missing_done=0; G.dl_missing_failed=0; + // Preserve season in persisted state and static before leaving + dm_preserve_season(season); + G.cur_season = season; + save_state(); int first = find_next_missing_in_season(season); if(first==-1){ G.dl_missing_active=false; @@ -173,11 +185,12 @@ void start_idx_internal(int idx){ tt_bundle_put_int32(b, "pos_sec", saved); tt_bundle_put_int32(b, "position", saved); tt_bundle_put_int32(b, "volume", G.volume); - ESP_LOGI(TAG, "Launching Mp3Player for %s pos %d", fpath, saved); + ESP_LOGI(TAG, "Launching Mp3Player for %s pos %d season %d", fpath, saved, ep->season); G.pending_dl_idx = idx; G.cur_ep_idx=idx; G.cur_season=ep->season; strncpy(G.cur_title,ep->title,sizeof(G.cur_title)-1); strncpy(G.last_slug,ep->slug,sizeof(G.last_slug)-1); + dm_preserve_season(ep->season); save_state(); tt_app_start_with_bundle("one.tactility.mp3player", b); return; diff --git a/Apps/DiscoveryMountain/main/Source/storage.c b/Apps/DiscoveryMountain/main/Source/storage.c index 5f2f41e..84ff9ad 100644 --- a/Apps/DiscoveryMountain/main/Source/storage.c +++ b/Apps/DiscoveryMountain/main/Source/storage.c @@ -151,7 +151,8 @@ void load_state(){ if(lsn&&cJSON_IsNumber(lsn)) G.cur_season=lsn->valueint; if(ls&&cJSON_IsString(ls)){ strncpy(G.last_slug, ls->valuestring, sizeof(G.last_slug)-1); - for(int i=0;ivaluestring)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; } + // Do NOT overwrite cur_season here based on slug – browsing season is independent + // cur_ep_idx will be resolved after cache load in onShow } cJSON* poss=cJSON_GetObjectItem(r,"positions"); if(poss && cJSON_IsObject(poss)){ diff --git a/Apps/DiscoveryMountain/main/Source/ui.c b/Apps/DiscoveryMountain/main/Source/ui.c index 1d07427..ef8b968 100644 --- a/Apps/DiscoveryMountain/main/Source/ui.c +++ b/Apps/DiscoveryMountain/main/Source/ui.c @@ -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;i0){ - 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; diff --git a/Apps/DiscoveryMountain/main/Source/ui.h b/Apps/DiscoveryMountain/main/Source/ui.h index 59693d6..7544319 100644 --- a/Apps/DiscoveryMountain/main/Source/ui.h +++ b/Apps/DiscoveryMountain/main/Source/ui.h @@ -14,6 +14,7 @@ void btn_prev_cb(lv_event_t* e); void btn_next_cb(lv_event_t* e); void btn_seasons_cb(lv_event_t* e); void btn_dl_season_cb(lv_event_t* e); +void btn_refresh_cb(lv_event_t* e); void close_overlay(void); void dropdown_cb(lv_event_t* e);