Login
1 branch 0 tags
Ben (Desktop/Arch) Navigation/metadata 7c7ae19 1 month ago 20 Commits
moon / firmware / src / screens / files_screen.c
#include "files_screen.h"
#include "../storage.h"
#include "../audio_player.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define MAX_ENTRIES 32

// 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");
}

// Forward declarations
static void navigate_up(files_screen_state_t *state);

static void cancel_event_cb(lv_event_t *e) {
    files_screen_state_t *state = lv_event_get_user_data(e);
    if (state && strcmp(state->cwd, "/") != 0) {
        navigate_up(state);
    } else {
        navigate_to(SCREEN_HOME);
    }
}

static void back_event_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);
    if (code == LV_EVENT_CLICKED) {
        files_screen_state_t *state = lv_event_get_user_data(e);
        if (state && strcmp(state->cwd, "/") != 0) {
            navigate_up(state);
        } else {
            navigate_to(SCREEN_HOME);
        }
    }
}

static void file_click_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);
    if (code != LV_EVENT_CLICKED) return;

    files_screen_state_t *state = lv_event_get_user_data(e);
    if (!state) return;

    lv_obj_t *btn = lv_event_get_target(e);
    // Get the label child (second child after the icon)
    lv_obj_t *label = lv_obj_get_child(btn, 1);
    if (!label) return;

    const char *filename = lv_label_get_text(label);
    if (!filename) return;

    if (is_audio_file(filename)) {
        // Build full path using current directory
        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 ? "" : "/",
                         filename);

        if (n > 0 && (size_t)n < sizeof(path) && audio_player_play(path)) {
            // Store path for navigation back from player
            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);
        }
    }
}

static void dir_click_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);
    if (code != LV_EVENT_CLICKED) return;

    files_screen_state_t *state = lv_event_get_user_data(e);
    if (!state) return;

    lv_obj_t *btn = lv_event_get_target(e);
    lv_obj_t *label = lv_obj_get_child(btn, 1);
    if (!label) return;

    const char *dirname = lv_label_get_text(label);
    if (!dirname) return;

    // Build new path
    char new_path[STORAGE_MAX_PATH];
    bool at_root = (strcmp(state->cwd, "/") == 0);
    int n = snprintf(new_path, sizeof(new_path), "%s%s%s",
                     state->cwd,
                     at_root ? "" : "/",
                     dirname);

    // Only navigate if path fits
    if (n > 0 && (size_t)n < sizeof(new_path)) {
        free_screen();
        ui_state = setup_files_screen(new_path, "");
    }
}

static void navigate_up(files_screen_state_t *state) {
    // Extract parent path and current directory name
    char parent[STORAGE_MAX_PATH];
    char current_dir[STORAGE_MAX_NAME];

    strncpy(parent, state->cwd, sizeof(parent));
    parent[sizeof(parent) - 1] = '\0';

    // Find last slash
    char *last_slash = strrchr(parent, '/');
    if (!last_slash || last_slash == parent) {
        // We're at root or one level deep - go to root
        strncpy(current_dir, parent + 1, sizeof(current_dir));
        current_dir[sizeof(current_dir) - 1] = '\0';
        strcpy(parent, "/");
    } else {
        // Extract current directory name and truncate parent
        strncpy(current_dir, last_slash + 1, sizeof(current_dir));
        current_dir[sizeof(current_dir) - 1] = '\0';
        *last_slash = '\0';
    }

    // Reinitialize screen at parent, focusing on the directory we came from
    free_screen();
    ui_state = setup_files_screen(parent, current_dir);
}

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[MAX_ENTRIES];
        int count = storage_list_dir(state->cwd, entries, MAX_ENTRIES);

        if (count < 0) {
            lv_obj_t *error_label = lv_list_add_text(state->list, "Error reading directory");
            (void)error_label;
        } else if (count == 0) {
            lv_obj_t *empty_label = lv_list_add_text(state->list, "(empty)");
            (void)empty_label;
        } else {
            for (int i = 0; i < count; i++) {
                const char *icon;
                bool is_dir = (entries[i].type == STORAGE_TYPE_DIR);

                if (is_dir) {
                    icon = LV_SYMBOL_DIRECTORY;
                } else if (is_audio_file(entries[i].name)) {
                    icon = LV_SYMBOL_AUDIO;
                } else {
                    icon = LV_SYMBOL_FILE;
                }

                lv_obj_t *btn = lv_list_add_button(state->list, icon, entries[i].name);
                lv_obj_add_event_cb(btn, cancel_event_cb, LV_EVENT_CANCEL, state);

                if (is_dir) {
                    lv_obj_add_event_cb(btn, dir_click_cb, LV_EVENT_CLICKED, state);
                } else {
                    lv_obj_add_event_cb(btn, file_click_cb, LV_EVENT_CLICKED, state);
                }

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

    // Add Back button
    lv_obj_t *back_btn = lv_list_add_button(state->list, LV_SYMBOL_LEFT, "Back");
    lv_obj_add_event_cb(back_btn, back_event_cb, LV_EVENT_CLICKED, state);
    lv_obj_add_event_cb(back_btn, cancel_event_cb, LV_EVENT_CANCEL, state);
    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
}