text/plain
•
8.12 KB
•
297 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 "../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;
}
// Read enclosure URL from url.txt in episode dir
static bool read_url_file(const char* dir, char* out, size_t out_size) {
char path[STORAGE_MAX_PATH];
int pn = snprintf(path, sizeof(path), "%s/url.txt", dir);
if (pn <= 0 || (size_t)pn >= sizeof(path)) {
return false;
}
storage_file_t f = storage_open(path, "r");
if (!f) {
return false;
}
size_t n = storage_read(f, out, out_size - 1);
storage_close(f);
out[n] = '\0';
// Trim trailing whitespace
while (n > 0 &&
(out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ')) {
out[--n] = '\0';
}
return n > 0;
}
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;
}
// Extract filename from URL (last path segment, strip query params)
static void url_filename(const char* url, char* out, size_t out_size) {
const char* last_slash = strrchr(url, '/');
const char* start = last_slash ? last_slash + 1 : url;
const char* query = strchr(start, '?');
size_t len = query ? (size_t)(query - start) : strlen(start);
if (len >= out_size) {
len = out_size - 1;
}
memcpy(out, start, len);
out[len] = '\0';
}
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);
// Set topbar title from title.txt, fall back to dir basename
char title_path[STORAGE_MAX_PATH];
char title[128] = {0};
int tp =
snprintf(title_path, sizeof(title_path), "%s/title.txt", state->dir);
if (tp > 0 && (size_t)tp < sizeof(title_path)) {
storage_file_t tf = storage_open(title_path, "r");
if (tf) {
size_t n = storage_read(tf, title, sizeof(title) - 1);
storage_close(tf);
title[n] = '\0';
}
}
if (title[0]) {
topbar_set_title(title);
} else {
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;
}
}
// Read enclosure URL
read_url_file(state->dir, state->enclosure_url,
sizeof(state->enclosure_url));
state->list = bvs_list_create(parent);
if (state->has_audio) {
lv_obj_t* play_btn = bvs_list_add_button(state->list, NULL, "Play now");
lv_obj_add_event_cb(play_btn, play_now_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(play_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
lv_group_add_obj(lv_group_get_default(), play_btn);
lv_obj_t* queue_btn =
bvs_list_add_button(state->list, NULL, "Add to queue");
lv_obj_add_event_cb(queue_btn, add_to_queue_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(queue_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
lv_group_add_obj(lv_group_get_default(), queue_btn);
lv_obj_t* del_btn =
bvs_list_add_button(state->list, NULL, "Delete download");
lv_obj_add_event_cb(del_btn, delete_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(del_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
lv_group_add_obj(lv_group_get_default(), del_btn);
lv_group_focus_obj(play_btn);
} else {
if (state->enclosure_url[0]) {
lv_obj_t* dl_btn =
bvs_list_add_button(state->list, NULL, "Download");
lv_obj_add_event_cb(dl_btn, download_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(dl_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
lv_group_add_obj(lv_group_get_default(), dl_btn);
lv_group_focus_obj(dl_btn);
} else {
lv_obj_t* msg =
bvs_list_add_button(state->list, NULL, "No URL found");
(void)msg;
}
}
lv_obj_t* back_btn = bvs_list_add_button(state->list, NULL, "Back");
lv_obj_add_event_cb(back_btn, back_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(back_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
lv_group_add_obj(lv_group_get_default(), back_btn);
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());
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
char audio_name[64];
url_filename(state->enclosure_url, audio_name, sizeof(audio_name));
if (audio_name[0] == '\0') {
snprintf(audio_name, sizeof(audio_name), "audio.mp3");
}
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();
}