text/plain
•
6.80 KB
•
259 lines
#include "podcast_episode_screen.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../audio_player.h"
#include "../network.h"
#include "../playlist.h"
#include "../podcast_xml.h"
#include "../storage.h"
#include "../topbar.h"
#include "files_screen.h"
#include "podcast_feed_screen.h"
// Find first audio file in a directory
static bool find_audio_file(const char* dir, char* out, size_t out_size) {
storage_entry_t entries[STORAGE_MAX_DIR_ENTRIES];
int count = storage_list_dir(dir, entries, STORAGE_MAX_DIR_ENTRIES);
for (int i = 0; i < count; i++) {
if (entries[i].type == STORAGE_TYPE_FILE &&
is_audio_file(entries[i].name)) {
size_t len = strlen(entries[i].name);
if (len >= out_size) {
len = out_size - 1;
}
memcpy(out, entries[i].name, len);
out[len] = '\0';
return true;
}
}
return false;
}
static void go_back(void) {
lv_obj_t* scr = navigate_prepare(ANIM_BACK);
ui_state = setup_podcast_feed_screen(scr, app_state.podcast_dir, "");
navigate_commit();
}
static void cancel_event_cb(lv_event_t* e) {
(void)e;
go_back();
}
static void back_event_cb(lv_event_t* e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
go_back();
}
}
static void play_now_cb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
podcast_episode_screen_state_t* state = ui_state.podcast_episode;
if (!state || !state->has_audio) {
return;
}
audio_player_stop();
playlist_clear();
playlist_add(state->audio_path, PLAYLIST_MUSIC);
playlist_set_index(0);
playlist_save();
navigate_to(SCREEN_NOW_PLAYING);
}
static void add_to_queue_cb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
podcast_episode_screen_state_t* state = ui_state.podcast_episode;
if (!state || !state->has_audio) {
return;
}
playlist_add(state->audio_path, PLAYLIST_MUSIC);
playlist_save();
go_back();
}
static void delete_cb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
podcast_episode_screen_state_t* state = ui_state.podcast_episode;
if (!state || !state->has_audio) {
return;
}
storage_remove(state->audio_path);
// Refresh screen
lv_obj_t* scr = navigate_prepare(ANIM_NONE);
ui_state = setup_podcast_episode_screen(scr);
navigate_commit();
}
static void download_cb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
podcast_episode_screen_state_t* state = ui_state.podcast_episode;
if (!state || state->has_audio || state->downloading) {
return;
}
if (state->enclosure_url[0] == '\0') {
return;
}
// Set downloading flag — actual download happens in update
state->downloading = true;
}
// Build "episode.ext" from URL, defaulting to episode.mp3
static void episode_filename(const char* url, char* out, size_t out_size) {
// Find last dot in URL path (before ? or #)
const char* dot = NULL;
for (const char* p = url; *p && *p != '?' && *p != '#'; p++) {
if (*p == '.') {
dot = p;
}
}
// Extract extension, lowercased
char ext[8] = {0};
if (dot) {
size_t i = 0;
for (const char* p = dot;
*p && *p != '?' && *p != '#' && i < sizeof(ext) - 1; p++) {
ext[i++] = (*p >= 'A' && *p <= 'Z') ? (*p + 32) : *p;
}
ext[i] = '\0';
}
snprintf(out, out_size, "episode%s", ext[0] ? ext : ".mp3");
}
ui_state_t setup_podcast_episode_screen(lv_obj_t* parent) {
podcast_episode_screen_state_t* state =
calloc(1, sizeof(podcast_episode_screen_state_t));
snprintf(state->dir, sizeof(state->dir), "%s",
app_state.podcast_episode_dir);
// Parse entry.xml for episode metadata
char entry_path[STORAGE_MAX_PATH + 16];
snprintf(entry_path, sizeof(entry_path), "%s/entry.xml", state->dir);
podcast_episode_t parsed = {0};
if (podcast_parse_entry(entry_path, &parsed)) {
if (parsed.title[0]) {
topbar_set_title(parsed.title);
}
snprintf(state->enclosure_url, sizeof(state->enclosure_url), "%s",
parsed.enclosure_url);
}
if (!parsed.title[0]) {
const char* basename = strrchr(state->dir, '/');
basename = (basename && basename[1]) ? basename + 1 : "Episode";
topbar_set_title(basename);
}
// Check for existing audio
char audio_name[STORAGE_MAX_NAME];
if (find_audio_file(state->dir, audio_name, sizeof(audio_name))) {
int pn = snprintf(state->audio_path, sizeof(state->audio_path), "%s/%s",
state->dir, audio_name);
if (pn > 0 && (size_t)pn < sizeof(state->audio_path)) {
state->has_audio = true;
}
}
state->list = bvs_list_create(parent);
if (state->has_audio) {
lv_obj_t* play_btn = bvs_list_add_nav_button(
state->list, NULL, "Play now", play_now_cb, NULL, cancel_event_cb);
bvs_list_add_nav_button(state->list, NULL, "Add to queue",
add_to_queue_cb, NULL, cancel_event_cb);
bvs_list_add_nav_button(state->list, NULL, "Delete download", delete_cb,
NULL, cancel_event_cb);
lv_group_focus_obj(play_btn);
} else {
if (state->enclosure_url[0]) {
lv_obj_t* dl_btn =
bvs_list_add_nav_button(state->list, NULL, "Download",
download_cb, NULL, cancel_event_cb);
lv_group_focus_obj(dl_btn);
} else {
bvs_list_add_button(state->list, NULL, "No URL found");
}
}
bvs_list_add_nav_button(state->list, NULL, "Back", back_event_cb, NULL,
cancel_event_cb);
return (ui_state_t){.type = SCREEN_PODCAST_EPISODE,
.podcast_episode = state};
}
void free_podcast_episode_screen(podcast_episode_screen_state_t* state) {
if (!state) {
return;
}
free(state);
}
void update_podcast_episode_screen(podcast_episode_screen_state_t* state) {
if (!state || !state->downloading) {
return;
}
state->downloading = false;
// Update UI to show downloading status
bvs_list_clear(state->list);
lv_group_remove_all_objs(lv_group_get_default());
state->list = bvs_list_create(lv_screen_active());
lv_obj_t* msg = bvs_list_add_button(state->list, NULL, "Downloading...");
lv_group_add_obj(lv_group_get_default(), msg);
lv_group_focus_obj(msg);
// Force LVGL to render the "Downloading..." label before blocking
lv_refr_now(lv_display_get_default());
// Build audio path from URL extension
char audio_name[32];
episode_filename(state->enclosure_url, audio_name, sizeof(audio_name));
char audio_path[STORAGE_MAX_PATH];
int n = snprintf(audio_path, sizeof(audio_path), "%s/%s", state->dir,
audio_name);
if (n <= 0 || (size_t)n >= sizeof(audio_path)) {
return;
}
network_set_enabled(true);
bool ok = network_download_file(state->enclosure_url, audio_path);
network_set_enabled(false);
if (ok) {
state->has_audio = true;
snprintf(state->audio_path, sizeof(state->audio_path), "%s",
audio_path);
}
// Refresh screen to show updated buttons
lv_obj_t* scr = navigate_prepare(ANIM_NONE);
ui_state = setup_podcast_episode_screen(scr);
navigate_commit();
}