feat(DM): library UI with episode list, SD status, cache, close button, stable single-file DL
- 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...'
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
#include "player.h"
|
||||
#include "storage.h"
|
||||
#include "ui.h"
|
||||
#include "download.h"
|
||||
|
||||
#include <tt_lvgl.h>
|
||||
#include <tt_app.h>
|
||||
#include <tt_bundle.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
void wait_play_exit(void) {
|
||||
// No internal playback anymore, nothing to wait
|
||||
}
|
||||
|
||||
void stop_playback_sync(void) {
|
||||
// No internal playback, nothing to stop
|
||||
G.is_playing = false;
|
||||
G.is_paused = false;
|
||||
}
|
||||
|
||||
static bool try_external_downloader(int idx) {
|
||||
if(idx<0||idx>=G.ep_cnt) return false;
|
||||
EpInfo* ep=&G.eps[idx];
|
||||
if (strlen(ep->audio_url) < 8) {
|
||||
ESP_LOGW(TAG, "Skipping DL for %s: empty URL", ep->slug);
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
if (G.lbl_status) lv_label_set_text(G.lbl_status, "No URL – fetch failed?");
|
||||
tt_lvgl_unlock();
|
||||
return false;
|
||||
}
|
||||
char fpath[300];
|
||||
make_sd_path(fpath, sizeof(fpath), ep->slug);
|
||||
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
close_overlay();
|
||||
if(G.dl_overlay) hide_dl_overlay();
|
||||
tt_lvgl_unlock();
|
||||
|
||||
BundleHandle b = tt_bundle_alloc();
|
||||
if (!b) {
|
||||
ESP_LOGW(TAG, "Failed to alloc bundle for external DL");
|
||||
return false;
|
||||
}
|
||||
tt_bundle_put_string(b, "url", ep->audio_url);
|
||||
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);
|
||||
G.pending_dl_idx = idx;
|
||||
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
if (G.lbl_status) lv_label_set_text(G.lbl_status, "Starting downloader...");
|
||||
tt_lvgl_unlock();
|
||||
|
||||
tt_app_start_with_bundle("one.tactility.sddownloader", b);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool try_external_downloader_batch(int season) {
|
||||
// Reverted to single-file mode – batch array caused crashes in SdDownloader
|
||||
// Now just download first missing episode in season
|
||||
int first_missing = -1;
|
||||
for(int i=0;i<G.ep_cnt;i++){
|
||||
if(G.eps[i].season == season && !G.eps[i].seen && strlen(G.eps[i].audio_url)>=8){
|
||||
first_missing = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(first_missing==-1){
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Season already cached");
|
||||
tt_lvgl_unlock();
|
||||
return true;
|
||||
}
|
||||
ESP_LOGI(TAG, "DL Missing (single-file revert) – downloading first missing S%02d idx %d", season, first_missing);
|
||||
return try_external_downloader(first_missing);
|
||||
}
|
||||
|
||||
void start_idx_internal(int idx){
|
||||
if(idx<0||idx>=G.ep_cnt) return;
|
||||
EpInfo* ep=&G.eps[idx];
|
||||
char fpath[300];
|
||||
make_sd_path(fpath, sizeof(fpath), ep->slug);
|
||||
|
||||
if(!has_slug(ep->slug)){
|
||||
ESP_LOGW(TAG, "start_idx_internal called but file missing %s", ep->slug);
|
||||
start_idx(idx);
|
||||
return;
|
||||
}
|
||||
|
||||
int saved = 0;
|
||||
if(G.cur_ep_idx!=idx){
|
||||
saved = get_saved_pos_for_slug(ep->slug);
|
||||
if(saved>5){
|
||||
ESP_LOGI(TAG,"Restoring saved pos %d for %s", saved, ep->slug);
|
||||
}
|
||||
} else {
|
||||
saved = G.pos_sec;
|
||||
}
|
||||
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
close_overlay();
|
||||
if(G.dl_overlay) hide_dl_overlay();
|
||||
tt_lvgl_unlock();
|
||||
|
||||
BundleHandle b = tt_bundle_alloc();
|
||||
if(b){
|
||||
tt_bundle_put_string(b, "file", fpath);
|
||||
tt_bundle_put_string(b, "path", fpath);
|
||||
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);
|
||||
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);
|
||||
save_state();
|
||||
tt_app_start_with_bundle("one.tactility.mp3player", b);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Failed to alloc bundle for Mp3Player");
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Failed to launch player");
|
||||
tt_lvgl_unlock();
|
||||
}
|
||||
|
||||
void start_idx(int idx){
|
||||
if(idx<0||idx>=G.ep_cnt) return;
|
||||
EpInfo* ep=&G.eps[idx];
|
||||
if(!has_slug(ep->slug)){
|
||||
// File not on SD, download via external downloader
|
||||
if (try_external_downloader(idx)) {
|
||||
return;
|
||||
}
|
||||
// Fallback: show message, cannot download internally anymore
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
if(G.lbl_status) lv_label_set_text(G.lbl_status,"Install SdDownloader");
|
||||
tt_lvgl_unlock();
|
||||
return;
|
||||
}
|
||||
start_idx_internal(idx);
|
||||
}
|
||||
|
||||
void select_ep(int idx){
|
||||
if(idx<0||idx>=G.ep_cnt) return;
|
||||
if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt && G.cur_ep_idx!=idx){
|
||||
save_current_ep_pos();
|
||||
}
|
||||
G.cur_ep_idx=idx;
|
||||
EpInfo* ep=&G.eps[idx];
|
||||
G.cur_season=ep->season;
|
||||
strncpy(G.last_slug, ep->slug, sizeof(G.last_slug)-1);
|
||||
|
||||
int saved = get_saved_pos_for_slug(ep->slug);
|
||||
G.pos_sec=saved;
|
||||
G.last_pct=-1;
|
||||
G.total_sec=0;
|
||||
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
if(G.lbl_title) lv_label_set_text(G.lbl_title,ep->title);
|
||||
if(G.lbl_meta){
|
||||
char b[64];
|
||||
if(saved>5){
|
||||
char tbuf[16];
|
||||
fmt_time(tbuf,saved);
|
||||
snprintf(b,sizeof(b),"S%02d E%02d %s • %s",ep->season,ep->ep_num,ep->seen?"*":"",tbuf);
|
||||
} else {
|
||||
snprintf(b,sizeof(b),"S%02d E%02d %s",ep->season,ep->ep_num,ep->seen?"*":"");
|
||||
}
|
||||
lv_label_set_text(G.lbl_meta,b);
|
||||
}
|
||||
if(G.lbl_status){
|
||||
if(saved>5) lv_label_set_text(G.lbl_status,"Resume – tap Play");
|
||||
else lv_label_set_text(G.lbl_status,ep->seen?"On SD – tap Play":"Tap Play to download");
|
||||
}
|
||||
if(G.bar){
|
||||
lv_bar_set_value(G.bar,0,LV_ANIM_OFF);
|
||||
}
|
||||
if(G.lbl_time){
|
||||
if(saved>5){
|
||||
char a[16];
|
||||
fmt_time(a,saved);
|
||||
char buf[40];
|
||||
snprintf(buf,sizeof(buf),"%s / --:--",a);
|
||||
lv_label_set_text(G.lbl_time,buf);
|
||||
} else {
|
||||
lv_label_set_text(G.lbl_time,"0:00 / --:--");
|
||||
}
|
||||
}
|
||||
tt_lvgl_unlock();
|
||||
save_state();
|
||||
}
|
||||
|
||||
void go_next(){
|
||||
if(G.ep_cnt==0) return;
|
||||
save_current_ep_pos();
|
||||
save_state();
|
||||
int n=G.cur_ep_idx+1;
|
||||
if(n>=G.ep_cnt) n=0;
|
||||
select_ep(n);
|
||||
// Do not auto-play next, per user request: no autoplay
|
||||
}
|
||||
|
||||
void go_prev(){
|
||||
if(G.ep_cnt==0) return;
|
||||
if(G.pos_sec>5){
|
||||
if(G.cur_ep_idx>=0){
|
||||
set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, 0);
|
||||
save_state();
|
||||
}
|
||||
G.pos_sec=0;
|
||||
int cur=G.cur_ep_idx;
|
||||
if(cur<0) cur=0;
|
||||
select_ep(cur);
|
||||
return;
|
||||
}
|
||||
save_current_ep_pos();
|
||||
save_state();
|
||||
int p=G.cur_ep_idx-1;
|
||||
if(p<0) p=G.ep_cnt-1;
|
||||
select_ep(p);
|
||||
}
|
||||
|
||||
// Called from UI for season batch download
|
||||
bool download_season(int season) {
|
||||
return try_external_downloader_batch(season);
|
||||
}
|
||||
Reference in New Issue
Block a user