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:
@@ -97,8 +97,15 @@ typedef struct {
|
|||||||
int dl_missing_total;
|
int dl_missing_total;
|
||||||
int dl_missing_done;
|
int dl_missing_done;
|
||||||
int dl_missing_failed;
|
int dl_missing_failed;
|
||||||
|
|
||||||
|
// Refresh button reference
|
||||||
|
lv_obj_t *btn_refresh;
|
||||||
} AppCtx;
|
} AppCtx;
|
||||||
|
|
||||||
|
void trigger_manual_refresh(void);
|
||||||
|
void dm_preserve_season(int season);
|
||||||
|
int dm_consume_preserved_season(void);
|
||||||
|
|
||||||
extern AppCtx G;
|
extern AppCtx G;
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -14,46 +14,42 @@
|
|||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_heap_caps.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){
|
static void fetch_task_fn(void* arg){
|
||||||
(void)arg;
|
(void)arg;
|
||||||
G.fetching=true;
|
G.fetching=true;
|
||||||
G.fetch_cancel_req=false;
|
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
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
bool cache_loaded = load_cache();
|
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Fetching from network...");
|
||||||
if(cache_loaded){
|
tt_lvgl_unlock();
|
||||||
ESP_LOGI(TAG,"Cache loaded: %d seasons, %d eps", G.season_cnt, G.ep_cnt);
|
|
||||||
// Restore cur idx from last_slug
|
// Preserve user's current season selection across fetch
|
||||||
if(strlen(G.last_slug)>0){
|
int preserved_season = G.cur_season;
|
||||||
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; }
|
char preserved_slug[96]={0};
|
||||||
}
|
if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt){
|
||||||
if(G.cur_ep_idx<0){
|
strncpy(preserved_slug, G.eps[G.cur_ep_idx].slug, sizeof(preserved_slug)-1);
|
||||||
int s1=-1;
|
} else if(strlen(G.last_slug)>0){
|
||||||
for(int i=0;i<G.ep_cnt;i++) if(G.eps[i].season==G.cur_season){ s1=i; break; }
|
strncpy(preserved_slug, G.last_slug, sizeof(preserved_slug)-1);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network fetch – updates if possible, otherwise keeps cache
|
|
||||||
bool fetch_ok = fetch_data();
|
bool fetch_ok = fetch_data();
|
||||||
|
|
||||||
if(G.fetch_cancel_req){
|
if(G.fetch_cancel_req){
|
||||||
ESP_LOGI(TAG,"fetch task canceled, exiting");
|
ESP_LOGI(TAG,"fetch task canceled");
|
||||||
G.fetching=false;
|
G.fetching=false;
|
||||||
G.fetch_handle=NULL;
|
G.fetch_handle=NULL;
|
||||||
vTaskDelete(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);
|
ESP_LOGI(TAG,"Network fetch ok: %d seasons, %d eps – saving cache", G.season_cnt, G.ep_cnt);
|
||||||
save_cache();
|
save_cache();
|
||||||
} else {
|
} else {
|
||||||
if(cache_loaded){
|
ESP_LOGW(TAG,"Network fetch failed");
|
||||||
ESP_LOGW(TAG,"Network fetch failed, restoring cache");
|
// Do not overwrite current data if fetch fails – keep existing cache in memory
|
||||||
// fetch_data overwrote with fallback, restore cache
|
// If we have no data at all, fetch_data already created fallback
|
||||||
load_cache();
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG,"Network fetch failed and no cache – using 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);
|
ESP_LOGI(TAG,"fetch done seasons=%d eps=%d fetch_ok=%d", G.season_cnt, G.ep_cnt, fetch_ok);
|
||||||
vTaskDelay(pdMS_TO_TICKS(300));
|
|
||||||
|
|
||||||
// Resolve current episode index – preserve user season selection if cache was already shown
|
// Restore preserved season if it still exists
|
||||||
if(cache_loaded){
|
if(preserved_season>0){
|
||||||
// User may have changed season while network fetch was in progress – keep their selection if it still exists
|
|
||||||
int cur_s = G.cur_season;
|
|
||||||
bool season_exists=false;
|
bool season_exists=false;
|
||||||
for(int i=0;i<G.season_cnt;i++) if(G.seasons[i].num==cur_s) { 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){
|
|
||||||
// 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(season_exists){
|
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;
|
int best=-1;
|
||||||
if(strlen(G.last_slug)>0){
|
if(strlen(preserved_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; }
|
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){
|
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;
|
if(best!=-1) G.cur_ep_idx=best;
|
||||||
|
strncpy(G.last_slug, preserved_slug, sizeof(G.last_slug)-1);
|
||||||
} else {
|
} else {
|
||||||
// Season no longer exists, fallback to first
|
// Fallback to preserved slug or first
|
||||||
if(G.ep_cnt>0){
|
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_ep_idx=0;
|
||||||
G.cur_season=G.eps[0].season;
|
G.cur_season=G.eps[0].season;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// First launch – use last_slug to restore position
|
if(G.cur_ep_idx<0 && G.ep_cnt>0){
|
||||||
if(strlen(G.last_slug)>0){
|
G.cur_ep_idx=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; }
|
G.cur_season=G.eps[0].season;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,9 +123,8 @@ static void fetch_task_fn(void* arg){
|
|||||||
tt_lvgl_lock(portMAX_DELAY);
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
if(G.lbl_status){
|
if(G.lbl_status){
|
||||||
if(G.ep_cnt>0){
|
if(G.ep_cnt>0){
|
||||||
if(fetch_ok) lv_label_set_text(G.lbl_status, "Select episode to play / download");
|
if(fetch_ok) lv_label_set_text(G.lbl_status, "Refreshed – select episode");
|
||||||
else if(cache_loaded) lv_label_set_text(G.lbl_status, "Offline, using cached data");
|
else lv_label_set_text(G.lbl_status, "Refresh failed – using cache");
|
||||||
else lv_label_set_text(G.lbl_status,"No network – limited data");
|
|
||||||
} else {
|
} else {
|
||||||
lv_label_set_text(G.lbl_status,"No data – check network");
|
lv_label_set_text(G.lbl_status,"No data – check network");
|
||||||
}
|
}
|
||||||
@@ -158,8 +136,37 @@ static void fetch_task_fn(void* arg){
|
|||||||
vTaskDelete(NULL);
|
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){
|
static void onShow(AppHandle app, void* data, lv_obj_t* parent){
|
||||||
(void)app; (void)data; (void)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));
|
memset(&G,0,sizeof(G));
|
||||||
G.pos_sec=0;
|
G.pos_sec=0;
|
||||||
G.total_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;
|
G.dl_cancel_req=false;
|
||||||
ensure_dir();
|
ensure_dir();
|
||||||
load_state();
|
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);
|
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_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||||
size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
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);
|
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);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData){
|
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_play = NULL;
|
||||||
G.btn_seasons = NULL;
|
G.btn_seasons = NULL;
|
||||||
G.btn_dl_missing = NULL;
|
G.btn_dl_missing = NULL;
|
||||||
|
G.btn_refresh = NULL;
|
||||||
G.season_list_cont = NULL;
|
G.season_list_cont = NULL;
|
||||||
G.bar = NULL;
|
G.bar = NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ static bool try_external_downloader(int idx) {
|
|||||||
char fpath[300];
|
char fpath[300];
|
||||||
make_sd_path(fpath, sizeof(fpath), ep->slug);
|
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);
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
close_overlay();
|
close_overlay();
|
||||||
if(G.dl_overlay) hide_dl_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_string(b, "path", fpath);
|
||||||
tt_bundle_put_bool(b, "override", false);
|
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;
|
G.pending_dl_idx = idx;
|
||||||
|
|
||||||
tt_lvgl_lock(portMAX_DELAY);
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
@@ -118,6 +126,10 @@ bool download_missing_season_start(int season){
|
|||||||
G.dl_missing_total=missing;
|
G.dl_missing_total=missing;
|
||||||
G.dl_missing_done=0;
|
G.dl_missing_done=0;
|
||||||
G.dl_missing_failed=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);
|
int first = find_next_missing_in_season(season);
|
||||||
if(first==-1){
|
if(first==-1){
|
||||||
G.dl_missing_active=false;
|
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, "pos_sec", saved);
|
||||||
tt_bundle_put_int32(b, "position", saved);
|
tt_bundle_put_int32(b, "position", saved);
|
||||||
tt_bundle_put_int32(b, "volume", G.volume);
|
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.pending_dl_idx = idx;
|
||||||
G.cur_ep_idx=idx; G.cur_season=ep->season;
|
G.cur_ep_idx=idx; G.cur_season=ep->season;
|
||||||
strncpy(G.cur_title,ep->title,sizeof(G.cur_title)-1);
|
strncpy(G.cur_title,ep->title,sizeof(G.cur_title)-1);
|
||||||
strncpy(G.last_slug,ep->slug,sizeof(G.last_slug)-1);
|
strncpy(G.last_slug,ep->slug,sizeof(G.last_slug)-1);
|
||||||
|
dm_preserve_season(ep->season);
|
||||||
save_state();
|
save_state();
|
||||||
tt_app_start_with_bundle("one.tactility.mp3player", b);
|
tt_app_start_with_bundle("one.tactility.mp3player", b);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ void load_state(){
|
|||||||
if(lsn&&cJSON_IsNumber(lsn)) G.cur_season=lsn->valueint;
|
if(lsn&&cJSON_IsNumber(lsn)) G.cur_season=lsn->valueint;
|
||||||
if(ls&&cJSON_IsString(ls)){
|
if(ls&&cJSON_IsString(ls)){
|
||||||
strncpy(G.last_slug, ls->valuestring, sizeof(G.last_slug)-1);
|
strncpy(G.last_slug, ls->valuestring, sizeof(G.last_slug)-1);
|
||||||
for(int i=0;i<G.ep_cnt;i++) if(strcmp(G.eps[i].slug,ls->valuestring)==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");
|
cJSON* poss=cJSON_GetObjectItem(r,"positions");
|
||||||
if(poss && cJSON_IsObject(poss)){
|
if(poss && cJSON_IsObject(poss)){
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ void ui_update_status_locked(const char* msg){
|
|||||||
void ui_update_header_locked(void){
|
void ui_update_header_locked(void){
|
||||||
if(!G.lbl_title && !G.lbl_season) return;
|
if(!G.lbl_title && !G.lbl_season) return;
|
||||||
if(G.fetching){
|
if(G.fetching){
|
||||||
if(G.lbl_season) lv_label_set_text(G.lbl_season, "Fetching...");
|
// Keep showing current season stats instead of blocking header
|
||||||
return;
|
// status bar already says fetching
|
||||||
}
|
}
|
||||||
if(G.season_cnt==0){
|
if(G.season_cnt==0){
|
||||||
if(G.lbl_season) lv_label_set_text(G.lbl_season, "No seasons");
|
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;
|
if(!G.ep_list_cont) return;
|
||||||
clear_container(G.ep_list_cont);
|
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.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_obj_t* lbl=lv_label_create(G.ep_list_cont);
|
||||||
lv_label_set_text(lbl,"No episodes");
|
lv_label_set_text(lbl,"No episodes");
|
||||||
lv_obj_set_style_text_color(lbl, lv_color_hex(0xAAAAAA),0);
|
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?
|
// Fallback: try event target's user data via current target?
|
||||||
// lv_event_get_user_data should already give us the idx
|
// lv_event_get_user_data should already give us the idx
|
||||||
if(idx<0 || idx>=G.ep_cnt) return;
|
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];
|
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);
|
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();
|
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 btn_seasons_cb(lv_event_t* e){
|
||||||
(void)e;
|
(void)e;
|
||||||
if(G.downloading) return;
|
if(G.downloading) return;
|
||||||
if(G.fetching) return;
|
|
||||||
if(G.overlay){ close_overlay(); return; }
|
if(G.overlay){ close_overlay(); return; }
|
||||||
tt_lvgl_lock(portMAX_DELAY);
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
// Create overlay centered modal – parented to root if possible to avoid lingering on app background
|
// Full-screen season selector – parent to active screen to cover whole 240x320, not just root content
|
||||||
lv_obj_t* ovl_parent = G.root ? G.root : lv_scr_act();
|
lv_obj_t* ovl_parent = lv_scr_act();
|
||||||
G.overlay=lv_obj_create(ovl_parent);
|
G.overlay=lv_obj_create(ovl_parent);
|
||||||
lv_obj_set_size(G.overlay, 240, 280);
|
lv_obj_set_size(G.overlay, LV_PCT(100), LV_PCT(100));
|
||||||
lv_obj_center(G.overlay);
|
lv_obj_set_pos(G.overlay, 0, 0);
|
||||||
lv_obj_set_style_bg_color(G.overlay,lv_color_hex(0x222222),0);
|
lv_obj_set_style_bg_color(G.overlay, lv_color_hex(0x111111),0);
|
||||||
lv_obj_set_style_radius(G.overlay,12,0);
|
lv_obj_set_style_bg_opa(G.overlay, LV_OPA_COVER,0);
|
||||||
lv_obj_set_style_border_width(G.overlay,1,0);
|
lv_obj_set_style_radius(G.overlay,0,0);
|
||||||
lv_obj_set_style_border_color(G.overlay, lv_color_hex(0x444444),0);
|
lv_obj_set_style_border_width(G.overlay,0,0);
|
||||||
lv_obj_set_style_pad_all(G.overlay,8,0);
|
lv_obj_set_style_pad_all(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_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
||||||
lv_obj_set_style_pad_gap(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_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_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);
|
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_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_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_flex_flow(G.season_list_cont, LV_FLEX_FLOW_COLUMN);
|
||||||
lv_obj_set_style_pad_gap(G.season_list_cont,4,0);
|
lv_obj_set_style_pad_gap(G.season_list_cont,6,0);
|
||||||
// Ensure scrollable
|
|
||||||
lv_obj_add_flag(G.season_list_cont, LV_OBJ_FLAG_SCROLLABLE);
|
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++){
|
for(int i=0;i<G.season_cnt;i++){
|
||||||
int sn=G.seasons[i].num;
|
int sn=G.seasons[i].num;
|
||||||
int total,cached;
|
int total,cached;
|
||||||
get_season_stats(sn,&total,&cached);
|
get_season_stats(sn,&total,&cached);
|
||||||
lv_obj_t* btn=lv_btn_create(G.season_list_cont);
|
lv_obj_t* btn=lv_btn_create(G.season_list_cont);
|
||||||
lv_obj_set_size(btn, LV_PCT(100), 38);
|
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(0x333333),0);
|
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,6,0);
|
lv_obj_set_style_radius(btn,8,0);
|
||||||
lv_obj_set_style_pad_all(btn,6,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_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(total>0){
|
||||||
if(sn==G.cur_season) snprintf(blabel,sizeof(blabel),"S%02d %d/%d " LV_SYMBOL_OK, sn, cached, total);
|
snprintf(blabel,sizeof(blabel),"Season %02d %d/%d on SD", sn, cached, total);
|
||||||
else snprintf(blabel,sizeof(blabel),"S%02d %d/%d", sn, cached, total);
|
|
||||||
}else{
|
}else{
|
||||||
if(sn==G.cur_season) snprintf(blabel,sizeof(blabel),"S%02d " LV_SYMBOL_OK, sn);
|
snprintf(blabel,sizeof(blabel),"Season %02d %d eps", sn, total);
|
||||||
else snprintf(blabel,sizeof(blabel),"S%02d", sn);
|
|
||||||
}
|
}
|
||||||
lv_obj_t* l1=lv_label_create(btn);
|
lv_obj_t* l1=lv_label_create(btn);
|
||||||
lv_label_set_text(l1, blabel);
|
lv_label_set_text(l1, blabel);
|
||||||
lv_obj_set_style_text_font(l1, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
lv_obj_set_style_text_font(l1, lvgl_get_text_font(FONT_SIZE_DEFAULT),0);
|
||||||
lv_obj_center(l1);
|
lv_obj_set_flex_grow(l1,1);
|
||||||
}
|
|
||||||
|
|
||||||
lv_obj_t* bc=lv_btn_create(G.overlay);
|
if(sn==G.cur_season){
|
||||||
lv_obj_set_size(bc, LV_PCT(60), 36);
|
lv_obj_t* lcheck=lv_label_create(btn);
|
||||||
lv_obj_set_style_bg_color(bc, lv_color_hex(0x444444),0);
|
lv_label_set_text(lcheck, LV_SYMBOL_OK);
|
||||||
lv_obj_t* lc=lv_label_create(bc);
|
lv_obj_set_style_text_color(lcheck, lv_color_hex(0x4CAF50),0);
|
||||||
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();
|
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 btn_dl_season_cb(lv_event_t* e){
|
||||||
(void)e;
|
(void)e;
|
||||||
if(G.downloading) return;
|
if(G.downloading) return;
|
||||||
if(G.fetching) return;
|
|
||||||
if(G.ep_cnt==0) return;
|
if(G.ep_cnt==0) return;
|
||||||
|
|
||||||
int season = G.cur_season;
|
int season = G.cur_season;
|
||||||
@@ -457,7 +493,7 @@ void build_ui(lv_obj_t* parent){
|
|||||||
lv_obj_center(l_close);
|
lv_obj_center(l_close);
|
||||||
lv_obj_add_event_cb(btn_close, btn_close_cb, LV_EVENT_CLICKED, NULL);
|
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_t* row=lv_obj_create(root);
|
||||||
lv_obj_set_size(row,LV_PCT(100),44);
|
lv_obj_set_size(row,LV_PCT(100),44);
|
||||||
lv_obj_set_flex_flow(row,LV_FLEX_FLOW_ROW);
|
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_center(ls);
|
||||||
lv_obj_add_event_cb(bs,btn_seasons_cb,LV_EVENT_CLICKED,NULL);
|
lv_obj_add_event_cb(bs,btn_seasons_cb,LV_EVENT_CLICKED,NULL);
|
||||||
|
|
||||||
lv_obj_t* bd=lv_btn_create(row);
|
lv_obj_t* br=lv_btn_create(row);
|
||||||
lv_obj_set_size(bd,LV_PCT(48),36);
|
lv_obj_set_size(br,LV_PCT(48),36);
|
||||||
G.btn_dl_missing=bd;
|
G.btn_refresh=br;
|
||||||
lv_obj_set_style_bg_color(bd, lv_color_hex(0x1E88E5), 0);
|
lv_obj_set_style_bg_color(br, lv_color_hex(0x444444), 0);
|
||||||
lv_obj_set_style_radius(bd,6,0);
|
lv_obj_set_style_radius(br,6,0);
|
||||||
lv_obj_t* ld=lv_label_create(bd);
|
lv_obj_t* lr=lv_label_create(br);
|
||||||
lv_label_set_text(ld,"DL Missing");
|
lv_label_set_text(lr, LV_SYMBOL_REFRESH " Refresh");
|
||||||
lv_obj_set_style_text_font(ld, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
lv_obj_set_style_text_font(lr, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
lv_obj_center(ld);
|
lv_obj_center(lr);
|
||||||
lv_obj_add_event_cb(bd,btn_dl_season_cb,LV_EVENT_CLICKED,NULL);
|
lv_obj_add_event_cb(br,btn_refresh_cb,LV_EVENT_CLICKED,NULL);
|
||||||
|
|
||||||
|
G.btn_dl_missing=NULL;
|
||||||
|
|
||||||
// Episode list container – fills remaining space
|
// Episode list container – fills remaining space
|
||||||
G.ep_list_cont=lv_obj_create(root);
|
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_color(hint, lv_color_hex(0x888888),0);
|
||||||
lv_obj_set_style_text_font(hint, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
lv_obj_set_style_text_font(hint, lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||||
|
|
||||||
// Status bar at bottom
|
// Status bar removed per UX request – no help label
|
||||||
G.lbl_status=lv_label_create(root);
|
G.lbl_status=NULL;
|
||||||
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
|
// Legacy placeholders nulled
|
||||||
G.lbl_meta=NULL;
|
G.lbl_meta=NULL;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ void btn_prev_cb(lv_event_t* e);
|
|||||||
void btn_next_cb(lv_event_t* e);
|
void btn_next_cb(lv_event_t* e);
|
||||||
void btn_seasons_cb(lv_event_t* e);
|
void btn_seasons_cb(lv_event_t* e);
|
||||||
void btn_dl_season_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 close_overlay(void);
|
||||||
void dropdown_cb(lv_event_t* e);
|
void dropdown_cb(lv_event_t* e);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user