Login
1 branch 0 tags
Ben (Desktop/Arch) Fixed audiobooks c5958d5 1 month ago 34 Commits
moon / firmware / src / screens / audiobooks_screen.c
#include "audiobooks_screen.h"
#include "files_screen.h"
#include "../storage.h"
#include "../audio_player.h"
#include "../bookmark.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define MAX_ENTRIES 256
#define AUDIOBOOKS_ROOT "/Audiobooks"

static int entry_name_cmp(const void *a, const void *b) {
    const storage_entry_t *ea = a;
    const storage_entry_t *eb = b;
    return strcmp(ea->name, eb->name);
}

// 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;
    audiobooks_screen_state_t *state = ui_state.audiobooks;
    if (!state) return;

    if (strcmp(state->cwd, AUDIOBOOKS_ROOT) != 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_audiobooks_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);
    }
}

// Check if a directory contains any audio files
static bool dir_has_audio_files(const char *dir_path) {
    storage_entry_t *entries = malloc(MAX_ENTRIES * sizeof(storage_entry_t));
    if (!entries) return false;

    int count = storage_list_dir(dir_path, entries, MAX_ENTRIES);
    bool found = false;
    for (int i = 0; i < count && !found; i++) {
        if (entries[i].type == STORAGE_TYPE_FILE && is_audio_file(entries[i].name)) {
            found = true;
        }
    }
    free(entries);
    return found;
}

// 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;

    audiobooks_screen_state_t *state = ui_state.audiobooks;
    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) {
        if (dir_has_audio_files(path)) {
            // Directory with audio files: treat as multi-file audiobook
            strncpy(app_state.audiobook_dir, path, sizeof(app_state.audiobook_dir));
            app_state.audiobook_dir[sizeof(app_state.audiobook_dir) - 1] = '\0';

            // Find resume file from directory bookmark, fall back to first audio file
            char resume_file[STORAGE_MAX_NAME] = {0};
            bookmark_load_string(path, resume_file, sizeof(resume_file));

            // Build the file path to play
            char file_path[STORAGE_MAX_PATH + STORAGE_MAX_NAME];
            if (resume_file[0]) {
                snprintf(file_path, sizeof(file_path), "%s/%s", path, resume_file);
            } else {
                // Find first audio file (sorted)
                storage_entry_t *entries = malloc(MAX_ENTRIES * sizeof(storage_entry_t));
                if (!entries) return;
                int count = storage_list_dir(path, entries, MAX_ENTRIES);
                qsort(entries, (size_t)count, sizeof(storage_entry_t), entry_name_cmp);
                file_path[0] = '\0';
                for (int i = 0; i < count; i++) {
                    if (entries[i].type == STORAGE_TYPE_FILE && is_audio_file(entries[i].name)) {
                        snprintf(file_path, sizeof(file_path), "%s/%s", path, entries[i].name);
                        break;
                    }
                }
                free(entries);
                if (!file_path[0]) return;
            }

            if (audio_player_play(file_path)) {
                strncpy(app_state.last_played_path, file_path, sizeof(app_state.last_played_path));
                app_state.last_played_path[sizeof(app_state.last_played_path) - 1] = '\0';
                navigate_to(SCREEN_AUDIOBOOK_PLAYER);
            }
        } else {
            // No audio files: navigate into directory
            free_screen();
            ui_state = setup_audiobooks_screen(path, "");
        }
    } else if (is_audio_file(entry.name)) {
        // Single file: clear directory mode
        app_state.audiobook_dir[0] = '\0';
        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_AUDIOBOOK_PLAYER);
        }
    }
}

ui_state_t setup_audiobooks_screen(const char *cwd, const char *focus_item) {
    audiobooks_screen_state_t *state = calloc(1, sizeof(audiobooks_screen_state_t));

    // Store current directory (default to AUDIOBOOKS_ROOT 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, AUDIOBOOKS_ROOT);
    }

    // Create a list that fills the screen
    state->list = lv_list_create(lv_screen_active());
    lv_obj_set_style_pad_hor(state->list, 4, 0);
    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
    snprintf(string_buffer, sizeof(string_buffer), LV_SYMBOL_FILE " %.24s", state->cwd);
    string_buffer[sizeof(string_buffer)-1] = 0;
    bvs_list_add_header(state->list, string_buffer);

    lv_obj_t *focus_btn = NULL;
    if (!storage_is_mounted()) {
        lv_obj_t *error = bvs_list_add_button(state->list, LV_SYMBOL_WARNING, "Error reading sd card");
        (void)error;
    } 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 = bvs_list_add_button(state->list, LV_SYMBOL_WARNING, "Error reading dir");
            (void)error;
        } else if (count == 0) {
            free(entries);
            lv_obj_t *empty = bvs_list_add_button(state->list, LV_SYMBOL_WARNING, "Empty Directory");
            (void)empty;
        } else {
            for (int i = 0; i < count; i++) {
                if (entries[i].name[0] == '.') continue;
                lv_obj_t *btn = bvs_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 (!focus_btn) {
                    focus_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 = 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);

    // Focus: requested item > first entry > back button
    if (focus_btn) {
        lv_group_focus_obj(focus_btn);
    } else {
        lv_group_focus_obj(back_btn);
    }

    return (ui_state_t){.type = SCREEN_AUDIOBOOKS, .audiobooks = state};
}

void free_audiobooks_screen(audiobooks_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_audiobooks_screen(audiobooks_screen_state_t *state) {
    (void)state;  // LVGL handles updates via timer
}