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:
@@ -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;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; }
|
||||
}
|
||||
if(G.cur_ep_idx<0){
|
||||
int s1=-1;
|
||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season){ s1=i; break; }
|
||||
if(s1==-1 && G.ep_cnt>0){
|
||||
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;i<G.season_cnt;i++) if(G.seasons[i].num==cur_s) { season_exists=true; break; }
|
||||
if(!season_exists){
|
||||
// Fallback to last_slug or first season
|
||||
if(strlen(G.last_slug)>0){
|
||||
for(int i=0;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; season_exists=true; break; }
|
||||
}
|
||||
}
|
||||
for(int i=0;i<G.season_cnt;i++) if(G.seasons[i].num==preserved_season){ season_exists=true; break; }
|
||||
if(season_exists){
|
||||
// Keep cur_season, find best idx within it
|
||||
G.cur_season = preserved_season;
|
||||
// try to restore slug within preserved season
|
||||
int best=-1;
|
||||
if(strlen(G.last_slug)>0){
|
||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==cur_s && strcmp(G.eps[i].slug,G.last_slug)==0){ best=i; break; }
|
||||
if(strlen(preserved_slug)>0){
|
||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==preserved_season && strcmp(G.eps[i].slug,preserved_slug)==0){ best=i; break; }
|
||||
}
|
||||
if(best==-1){
|
||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==cur_s){ best=i; break; }
|
||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==preserved_season){ best=i; break; }
|
||||
}
|
||||
if(best!=-1) G.cur_ep_idx=best;
|
||||
strncpy(G.last_slug, preserved_slug, sizeof(G.last_slug)-1);
|
||||
} else {
|
||||
// Season no longer exists, fallback to first
|
||||
if(G.ep_cnt>0){
|
||||
// Fallback to preserved slug or first
|
||||
if(strlen(preserved_slug)>0){
|
||||
for(int i=0;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,preserved_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; }
|
||||
}
|
||||
if(G.cur_ep_idx<0 && G.ep_cnt>0){
|
||||
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;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; G.cur_season=G.eps[i].season; break; }
|
||||
}
|
||||
if(G.cur_ep_idx<0){
|
||||
int s1=-1;
|
||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season){ s1=i; break; }
|
||||
if(s1==-1 && G.ep_cnt>0){
|
||||
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;i<G.season_cnt;i++) if(G.seasons[i].num==G.cur_season){ season_exists=true; break; }
|
||||
if(!season_exists){
|
||||
// If we had a preserved season but it's not in seasons list, maybe list incomplete – try preserved again
|
||||
if(preserved>0){
|
||||
for(int i=0;i<G.season_cnt;i++) if(G.seasons[i].num==preserved){ season_exists=true; G.cur_season=preserved; break; }
|
||||
}
|
||||
if(!season_exists && G.season_cnt>0){
|
||||
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;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season && strcmp(G.eps[i].slug,G.last_slug)==0){ G.cur_ep_idx=i; break; }
|
||||
}
|
||||
if(G.cur_ep_idx<0){
|
||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season){ G.cur_ep_idx=i; break; }
|
||||
}
|
||||
if(G.cur_ep_idx<0 && G.ep_cnt>0){
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user