text/plain
•
7.40 KB
•
226 lines
#include "files_screen.h"
#include "../storage.h"
#include "../audio_player.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define MAX_ENTRIES 256
// Case-insensitive extension check helper
static bool ext_match(const char *ext, const char *pattern) {
while (*pattern) {
char c = *ext++;
char p = *pattern++;
// Convert to lowercase for comparison
if (c >= 'A' && c <= 'Z') c += 32;
if (p >= 'A' && p <= 'Z') p += 32;
if (c != p) return false;
}
return true;
}
// Check if filename is a supported audio file
static bool is_audio_file(const char *name) {
size_t len = strlen(name);
if (len < 4) return false;
// Find last dot
const char *dot = strrchr(name, '.');
if (!dot || dot == name) return false;
// Check supported extensions
return ext_match(dot, ".mp3") ||
ext_match(dot, ".wav") ||
ext_match(dot, ".aac") ||
ext_match(dot, ".m4a") ||
ext_match(dot, ".flac") ||
ext_match(dot, ".ogg");
}
// Get entry at index by re-reading directory (FAT32 is deterministic)
static bool get_entry_at_index(const char *dir, int index, storage_entry_t *out) {
storage_entry_t *entries = malloc(MAX_ENTRIES * sizeof(storage_entry_t));
if (!entries) return false;
int count = storage_list_dir(dir, entries, MAX_ENTRIES);
bool found = (index >= 0 && index < count);
if (found) {
*out = entries[index];
}
free(entries);
return found;
}
// Cancel goes up one directory or to home
static void cancel_event_cb(lv_event_t *e) {
(void)e;
files_screen_state_t *state = ui_state.files;
if (!state) return;
if (strcmp(state->cwd, "/") != 0) {
// Go up one directory
char parent[STORAGE_MAX_PATH];
char current_dir[STORAGE_MAX_NAME];
strncpy(parent, state->cwd, sizeof(parent));
parent[sizeof(parent) - 1] = '\0';
char *last_slash = strrchr(parent, '/');
if (!last_slash || last_slash == parent) {
strncpy(current_dir, parent + 1, sizeof(current_dir));
current_dir[sizeof(current_dir) - 1] = '\0';
strcpy(parent, "/");
} else {
strncpy(current_dir, last_slash + 1, sizeof(current_dir));
current_dir[sizeof(current_dir) - 1] = '\0';
*last_slash = '\0';
}
free_screen();
ui_state = setup_files_screen(parent, current_dir);
} else {
navigate_to(SCREEN_HOME);
}
}
// Back button click - same as cancel
static void back_event_cb(lv_event_t *e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
cancel_event_cb(e);
}
}
// Entry clicked - index stored in userdata
static void entry_click_cb(lv_event_t *e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
files_screen_state_t *state = ui_state.files;
if (!state) return;
int index = (int)(intptr_t)lv_event_get_user_data(e);
storage_entry_t entry;
if (!get_entry_at_index(state->cwd, index, &entry)) return;
// Build full path
char path[STORAGE_MAX_PATH];
bool at_root = (strcmp(state->cwd, "/") == 0);
int n = snprintf(path, sizeof(path), "%s%s%s",
state->cwd,
at_root ? "" : "/",
entry.name);
if (n <= 0 || (size_t)n >= sizeof(path)) return;
if (entry.type == STORAGE_TYPE_DIR) {
// Navigate into directory
free_screen();
ui_state = setup_files_screen(path, "");
} else if (is_audio_file(entry.name)) {
// Play audio file
if (audio_player_play(path)) {
strncpy(app_state.last_played_path, path, sizeof(app_state.last_played_path));
app_state.last_played_path[sizeof(app_state.last_played_path) - 1] = '\0';
navigate_to(SCREEN_PLAYER);
}
}
}
ui_state_t setup_files_screen(const char *cwd, const char *focus_item) {
files_screen_state_t *state = calloc(1, sizeof(files_screen_state_t));
// Store current directory (default to "/" if NULL or empty)
if (cwd && cwd[0] != '\0') {
strncpy(state->cwd, cwd, sizeof(state->cwd));
state->cwd[sizeof(state->cwd) - 1] = '\0';
} else {
strcpy(state->cwd, "/");
}
// Create a list that fills the screen
state->list = lv_list_create(lv_screen_active());
lv_obj_set_size(state->list, LV_PCT(100), LV_PCT(100));
lv_obj_center(state->list);
// Add list header with styled background - show current path
lv_obj_t *header = lv_list_add_text(state->list, state->cwd);
lv_obj_set_style_bg_color(header, lv_color_hex(0x0288D1), 0);
lv_obj_set_style_bg_opa(header, LV_OPA_COVER, 0);
lv_obj_set_style_text_color(header, lv_color_hex(0xFFFFFF), 0);
lv_obj_t *first_btn = NULL;
lv_obj_t *focus_btn = NULL;
if (!storage_is_mounted()) {
lv_obj_t *error_label = lv_list_add_text(state->list, "SD card not mounted");
(void)error_label;
} else {
// List current directory contents
storage_entry_t *entries = malloc(MAX_ENTRIES * sizeof(storage_entry_t));
int count = entries ? storage_list_dir(state->cwd, entries, MAX_ENTRIES) : -1;
if (count < 0) {
free(entries);
lv_obj_t *error_label = lv_list_add_text(state->list, "Error reading directory");
(void)error_label;
} else if (count == 0) {
free(entries);
lv_obj_t *empty_label = lv_list_add_text(state->list, "(empty)");
(void)empty_label;
} else {
for (int i = 0; i < count; i++) {
lv_obj_t *btn = lv_list_add_button(state->list, NULL, entries[i].name);
// Store index as userdata, use single callback for all entries
lv_obj_add_event_cb(btn, entry_click_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
lv_obj_add_event_cb(btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
lv_group_add_obj(lv_group_get_default(), btn);
if (!first_btn) {
first_btn = btn;
}
// Check if this is the item to focus
if (focus_item && focus_item[0] != '\0' &&
strcmp(entries[i].name, focus_item) == 0) {
focus_btn = btn;
}
}
free(entries);
}
}
// Add Back button
lv_obj_t *back_btn = lv_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);
// Focus: requested item > first entry > back button
if (focus_btn) {
lv_group_focus_obj(focus_btn);
} else if (first_btn) {
lv_group_focus_obj(first_btn);
} else {
lv_group_focus_obj(back_btn);
}
return (ui_state_t){.type = SCREEN_FILES, .files = state};
}
void free_files_screen(files_screen_state_t *state) {
if (!state) return;
// Get all children from list and remove from group
uint32_t child_count = lv_obj_get_child_count(state->list);
for (uint32_t i = 0; i < child_count; i++) {
lv_obj_t *child = lv_obj_get_child(state->list, i);
lv_group_remove_obj(child);
}
lv_obj_delete(state->list);
free(state);
}
void update_files_screen(files_screen_state_t *state) {
(void)state; // LVGL handles updates via timer
}