text/plain
•
5.88 KB
•
228 lines
#include "podcast_feed_screen.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../podcast_xml.h"
#include "../storage.h"
#include "../topbar.h"
#define PODCASTS_OPML "/Podcasts/podcasts.xml"
static void cancel_event_cb(lv_event_t* e) {
(void)e;
navigate_back(SCREEN_PODCASTS);
}
// Look up feed URL from OPML by matching directory basename
static bool lookup_feed_url(const char* dir,
char* out_title,
size_t title_size,
char* out_url,
size_t url_size) {
const char* basename = strrchr(dir, '/');
basename = (basename && basename[1]) ? basename + 1 : dir;
podcast_feed_t* feeds = malloc(PODCAST_MAX_FEEDS * sizeof(podcast_feed_t));
if (!feeds) {
return false;
}
int count = podcast_parse_opml(PODCASTS_OPML, feeds, PODCAST_MAX_FEEDS);
// Match by sanitizing each feed title and comparing to basename
for (int i = 0; i < count; i++) {
char sanitized[65];
podcast_sanitize_title(feeds[i].title, sanitized, sizeof(sanitized));
if (strcmp(sanitized, basename) == 0) {
snprintf(out_title, title_size, "%s", feeds[i].title);
snprintf(out_url, url_size, "%s", feeds[i].url);
free(feeds);
return true;
}
}
free(feeds);
return false;
}
static void sync_click_cb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
podcast_feed_screen_state_t* state = ui_state.podcast_feed;
if (!state) {
return;
}
// Look up feed URL from OPML
if (!lookup_feed_url(state->dir, app_state.podcast_title,
sizeof(app_state.podcast_title), app_state.podcast_url,
sizeof(app_state.podcast_url))) {
return;
}
navigate_to(SCREEN_PODCAST_SYNC);
}
static void back_event_cb(lv_event_t* e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
cancel_event_cb(e);
}
}
static void episode_click_cb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
return;
}
podcast_feed_screen_state_t* state = ui_state.podcast_feed;
if (!state) {
return;
}
int target = (int)(intptr_t)lv_event_get_user_data(e);
// Get sorted entries to find episode dir by directory index
storage_entry_t* entries =
malloc(STORAGE_MAX_DIR_ENTRIES * sizeof(storage_entry_t));
if (!entries) {
return;
}
int count =
get_sorted_entries(state->dir, entries, STORAGE_MAX_DIR_ENTRIES);
// Find the nth directory entry (matching how buttons were created)
int dir_index = 0;
int found = -1;
for (int i = 0; i < count; i++) {
if (entries[i].type != STORAGE_TYPE_DIR) {
continue;
}
if (dir_index == target) {
found = i;
break;
}
dir_index++;
}
if (found < 0) {
free(entries);
return;
}
// Build episode directory path and store in app_state
int n = snprintf(app_state.podcast_episode_dir,
sizeof(app_state.podcast_episode_dir), "%s/%s", state->dir,
entries[found].name);
free(entries);
if (n <= 0 || (size_t)n >= sizeof(app_state.podcast_episode_dir)) {
return;
}
navigate_to(SCREEN_PODCAST_EPISODE);
}
ui_state_t setup_podcast_feed_screen(lv_obj_t* parent,
const char* dir,
const char* focus_item) {
podcast_feed_screen_state_t* state =
calloc(1, sizeof(podcast_feed_screen_state_t));
if (!state) {
return (ui_state_t){0};
}
if (dir && dir[0]) {
strncpy(state->dir, dir, sizeof(state->dir) - 1);
state->dir[sizeof(state->dir) - 1] = '\0';
}
state->list = bvs_list_create(parent);
// Set topbar title to directory basename
const char* basename = strrchr(state->dir, '/');
basename = (basename && basename[1]) ? basename + 1 : "Episodes";
topbar_set_title(basename);
lv_obj_t* focus_btn = NULL;
// Sync button for this feed
lv_obj_t* sync_btn = bvs_list_add_nav_button(
state->list, NULL, "Sync now", sync_click_cb, NULL, cancel_event_cb);
focus_btn = sync_btn;
storage_entry_t* entries =
malloc(STORAGE_MAX_DIR_ENTRIES * sizeof(storage_entry_t));
int count = entries ? get_sorted_entries(state->dir, entries,
STORAGE_MAX_DIR_ENTRIES)
: 0;
// Only show directories (episode dirs), skip regular files like feed.xml
// Limit to latest 16 episodes to avoid exhausting LVGL memory pool
#define MAX_EPISODE_BUTTONS 16
int btn_index = 0;
for (int i = 0; i < count && btn_index < MAX_EPISODE_BUTTONS; i++) {
if (entries[i].type != STORAGE_TYPE_DIR) {
continue;
}
// Try to read title.txt for a human-readable label
char title_path[STORAGE_MAX_PATH];
char title[128];
const char* label = entries[i].name;
int tp = snprintf(title_path, sizeof(title_path), "%s/%s/title.txt",
state->dir, entries[i].name);
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 (n > 0) {
label = title;
}
}
}
lv_obj_t* btn =
bvs_list_add_nav_button(state->list, NULL, label, episode_click_cb,
(void*)(intptr_t)btn_index, cancel_event_cb);
if (!focus_btn) {
focus_btn = btn;
}
if (focus_item && focus_item[0] &&
strcmp(entries[i].name, focus_item) == 0) {
focus_btn = btn;
}
btn_index++;
}
if (btn_index == 0) {
bvs_list_add_button(state->list, NULL, "No episodes");
}
free(entries);
// Back button
lv_obj_t* back_btn = bvs_list_add_nav_button(
state->list, NULL, "Back", back_event_cb, NULL, cancel_event_cb);
lv_group_focus_obj(focus_btn ? focus_btn : back_btn);
return (ui_state_t){.type = SCREEN_PODCAST_FEED, .podcast_feed = state};
}
void free_podcast_feed_screen(podcast_feed_screen_state_t* state) {
if (!state) {
return;
}
free(state);
}
void update_podcast_feed_screen(podcast_feed_screen_state_t* state) {
(void)state;
}