Login
1 branch 0 tags
Ben (Desktop/Arch) Improved colors 19d7aaa 1 month ago 30 Commits
moon / firmware / src / screens / audiobook_player_screen.c
#include "audiobook_player_screen.h"
#include "audiobooks_screen.h"
#include "files_screen.h"
#include "../audio_player.h"
#include "../bookmark.h"
#include "../storage.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

static void format_time(uint32_t ms, char *buf, size_t buf_size) {
    uint32_t total_sec = ms / 1000;
    uint32_t hours = total_sec / 3600;
    uint32_t mins = (total_sec % 3600) / 60;
    uint32_t secs = total_sec % 60;

    if (hours > 0) {
        snprintf(buf, buf_size, "%u:%02u:%02u", (unsigned)hours, (unsigned)mins, (unsigned)secs);
    } else {
        snprintf(buf, buf_size, "%u:%02u", (unsigned)mins, (unsigned)secs);
    }
}

static int filename_cmp(const void *a, const void *b) {
    return strcmp((const char *)a, (const char *)b);
}

// Load sorted audio files from directory into state
static void load_directory_files(audiobook_player_screen_state_t *state) {
    storage_entry_t *entries = malloc(MAX_AUDIOBOOK_FILES * sizeof(storage_entry_t));
    if (!entries) return;

    int count = storage_list_dir(state->dir, entries, MAX_AUDIOBOOK_FILES);
    state->file_count = 0;

    for (int i = 0; i < count && state->file_count < MAX_AUDIOBOOK_FILES; i++) {
        if (entries[i].type == STORAGE_TYPE_FILE && is_audio_file(entries[i].name)) {
            strncpy(state->files[state->file_count], entries[i].name, STORAGE_MAX_NAME - 1);
            state->files[state->file_count][STORAGE_MAX_NAME - 1] = '\0';
            state->file_count++;
        }
    }
    free(entries);

    qsort(state->files, (size_t)state->file_count, STORAGE_MAX_NAME, filename_cmp);
}

// Find index of filename in sorted file list, returns -1 if not found
static int find_file_index(audiobook_player_screen_state_t *state, const char *filename) {
    for (int i = 0; i < state->file_count; i++) {
        if (strcmp(state->files[i], filename) == 0) {
            return i;
        }
    }
    return -1;
}

// Save both directory and file bookmarks
static void save_bookmarks(audiobook_player_screen_state_t *state) {
    uint32_t pos = audio_player_get_position_ms();
    if (pos > 0 && app_state.last_played_path[0]) {
        bookmark_save(app_state.last_played_path, pos);
    }
    if (state->dir[0] && state->current_file_index >= 0 &&
        state->current_file_index < state->file_count) {
        bookmark_save_string(state->dir, state->files[state->current_file_index]);
    }
}

// Update chapter label text
static void update_chapter_label(audiobook_player_screen_state_t *state) {
    if (state->chapter_label && state->file_count > 0) {
        snprintf(string_buffer, sizeof(string_buffer), "%d / %d",
                 state->current_file_index + 1, state->file_count);
        lv_label_set_text(state->chapter_label, string_buffer);
    }
}

// Advance to next file in directory. Returns false if no more files.
static bool play_next_file(audiobook_player_screen_state_t *state) {
    int next = state->current_file_index + 1;
    if (next >= state->file_count) {
        return false;
    }

    // Remove completed file's bookmark
    if (app_state.last_played_path[0]) {
        bookmark_remove(app_state.last_played_path);
    }

    // Build path for next file
    char file_path[STORAGE_MAX_PATH + STORAGE_MAX_NAME];
    snprintf(file_path, sizeof(file_path), "%s/%s", state->dir, state->files[next]);

    if (!audio_player_play(file_path)) {
        return false;
    }

    state->current_file_index = next;
    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';

    // Save directory bookmark pointing to new file
    bookmark_save_string(state->dir, state->files[next]);

    // Reset seek state for new file
    state->bookmark_ms = bookmark_load(app_state.last_played_path);
    state->bookmark_applied = false;

    // Update UI
    const char *title = audio_player_get_title();
    lv_label_set_text(state->title_label, title ? title : "Unknown");
    update_chapter_label(state);

    return true;
}

// Navigate back to audiobooks screen, restoring directory from last played path
static void navigate_to_audiobooks(void) {
    char dir[STORAGE_MAX_PATH];
    char focus_name[STORAGE_MAX_NAME];

    if (app_state.audiobook_dir[0]) {
        // Directory mode: navigate to parent of audiobook dir, focus the dir entry
        strncpy(dir, app_state.audiobook_dir, sizeof(dir));
        dir[sizeof(dir) - 1] = '\0';

        char *last_slash = strrchr(dir, '/');
        if (last_slash && last_slash != dir) {
            strncpy(focus_name, last_slash + 1, sizeof(focus_name));
            focus_name[sizeof(focus_name) - 1] = '\0';
            *last_slash = '\0';
        } else if (last_slash == dir) {
            strncpy(focus_name, dir + 1, sizeof(focus_name));
            focus_name[sizeof(focus_name) - 1] = '\0';
            strcpy(dir, "/");
        } else {
            strcpy(dir, "/Audiobooks");
            focus_name[0] = '\0';
        }
    } else if (app_state.last_played_path[0] != '\0') {
        strncpy(dir, app_state.last_played_path, sizeof(dir));
        dir[sizeof(dir) - 1] = '\0';

        char *last_slash = strrchr(dir, '/');
        if (last_slash && last_slash != dir) {
            strncpy(focus_name, last_slash + 1, sizeof(focus_name));
            focus_name[sizeof(focus_name) - 1] = '\0';
            *last_slash = '\0';
        } else if (last_slash == dir) {
            strncpy(focus_name, dir + 1, sizeof(focus_name));
            focus_name[sizeof(focus_name) - 1] = '\0';
            strcpy(dir, "/");
        } else {
            strcpy(dir, "/Audiobooks");
            focus_name[0] = '\0';
        }
    } else {
        strcpy(dir, "/Audiobooks");
        focus_name[0] = '\0';
    }

    free_screen();
    ui_state = setup_audiobooks_screen(dir, focus_name);
}

static void cancel_event_cb(lv_event_t *e) {
    (void)e;
    audiobook_player_screen_state_t *state = ui_state.audiobook_player;
    if (state) {
        save_bookmarks(state);
    }
    audio_player_stop();
    navigate_to_audiobooks();
}

static void play_btn_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);
    if (code == LV_EVENT_CLICKED) {
        audio_state_t astate = audio_player_get_state();
        if (astate == AUDIO_STATE_PLAYING) {
            audiobook_player_screen_state_t *state = ui_state.audiobook_player;
            if (state) {
                save_bookmarks(state);
            }
            audio_player_pause();
        } else if (astate == AUDIO_STATE_PAUSED) {
            audio_player_resume();
        }
    }
}

static void stop_btn_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);
    if (code == LV_EVENT_CLICKED) {
        audiobook_player_screen_state_t *state = ui_state.audiobook_player;
        if (state) {
            save_bookmarks(state);
        }
        audio_player_stop();
        navigate_to_audiobooks();
    }
}

ui_state_t setup_audiobook_player_screen(void) {
    audiobook_player_screen_state_t *state = calloc(1, sizeof(audiobook_player_screen_state_t));

    // Directory mode setup
    if (app_state.audiobook_dir[0]) {
        strncpy(state->dir, app_state.audiobook_dir, sizeof(state->dir));
        state->dir[sizeof(state->dir) - 1] = '\0';
        load_directory_files(state);

        // Find current file index from last_played_path
        const char *filename = strrchr(app_state.last_played_path, '/');
        filename = filename ? filename + 1 : app_state.last_played_path;
        state->current_file_index = find_file_index(state, filename);
        if (state->current_file_index < 0) {
            state->current_file_index = 0;
        }
    }

    // Load bookmark for current file
    state->bookmark_ms = bookmark_load(app_state.last_played_path);
    state->bookmark_applied = false;

    // Main container
    state->container = lv_obj_create(lv_screen_active());
    lv_obj_set_size(state->container, LV_PCT(100), LV_PCT(100));
    lv_obj_set_flex_flow(state->container, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(state->container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_set_style_pad_all(state->container, 8, 0);
    lv_obj_set_style_pad_gap(state->container, 6, 0);
    lv_obj_clear_flag(state->container, LV_OBJ_FLAG_SCROLLABLE);

    // Title label (scrolling) - shows "Artist - Title" or filename
    state->title_label = lv_label_create(state->container);
    const char *title = audio_player_get_title();
    lv_label_set_text(state->title_label, title ? title : "Unknown");
    lv_label_set_long_mode(state->title_label, LV_LABEL_LONG_SCROLL_CIRCULAR);
    lv_obj_set_width(state->title_label, LV_PCT(95));

    // Chapter label for directory mode (e.g., "3 / 12")
    if (state->dir[0] && state->file_count > 0) {
        state->chapter_label = lv_label_create(state->container);
        lv_obj_set_style_text_color(state->chapter_label, lv_color_hex(0xAAAAAA), 0);
        update_chapter_label(state);
    }

    // Time row container (position on left, duration on right)
    lv_obj_t *time_cont = lv_obj_create(state->container);
    lv_obj_set_size(time_cont, 144, LV_SIZE_CONTENT);
    lv_obj_set_flex_flow(time_cont, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(time_cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_set_style_pad_all(time_cont, 0, 0);
    lv_obj_set_style_border_width(time_cont, 0, 0);
    lv_obj_set_style_bg_opa(time_cont, LV_OPA_TRANSP, 0);

    // Position label (left)
    state->position_label = lv_label_create(time_cont);
    lv_label_set_text(state->position_label, "0:00");
    lv_obj_set_style_text_color(state->position_label, lv_color_hex(0xAAAAAA), 0);

    // Duration label (right)
    state->duration_label = lv_label_create(time_cont);
    lv_label_set_text(state->duration_label, "0:00");
    lv_obj_set_style_text_color(state->duration_label, lv_color_hex(0xAAAAAA), 0);

    // Progress bar
    state->progress_bar = lv_bar_create(state->container);
    lv_obj_set_size(state->progress_bar, 144, 6);
    lv_bar_set_range(state->progress_bar, 0, 100);
    lv_bar_set_value(state->progress_bar, 0, LV_ANIM_OFF);
    lv_obj_set_style_bg_color(state->progress_bar, lv_color_hex(0x444444), LV_PART_MAIN);
    lv_obj_set_style_bg_color(state->progress_bar, lv_color_hex(0xE91E63), LV_PART_INDICATOR);

    // Button container
    lv_obj_t *btn_cont = lv_obj_create(state->container);
    lv_obj_set_size(btn_cont, LV_PCT(100), LV_SIZE_CONTENT);
    lv_obj_set_flex_flow(btn_cont, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(btn_cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_set_style_pad_all(btn_cont, 4, 0);
    lv_obj_set_style_border_width(btn_cont, 0, 0);
    lv_obj_set_style_bg_opa(btn_cont, LV_OPA_TRANSP, 0);

    // Play/Pause button
    state->play_btn = lv_button_create(btn_cont);
    lv_obj_set_size(state->play_btn, 24, 24);
    lv_obj_t *play_label = lv_label_create(state->play_btn);
    lv_label_set_text(play_label, LV_SYMBOL_PAUSE);
    lv_obj_center(play_label);
    lv_obj_add_event_cb(state->play_btn, play_btn_cb, LV_EVENT_CLICKED, state);
    lv_obj_add_event_cb(state->play_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
    lv_group_add_obj(lv_group_get_default(), state->play_btn);

    // Stop button
    state->stop_btn = lv_button_create(btn_cont);
    lv_obj_set_size(state->stop_btn, 24, 24);
    lv_obj_t *stop_label = lv_label_create(state->stop_btn);
    lv_label_set_text(stop_label, LV_SYMBOL_STOP);
    lv_obj_center(stop_label);
    lv_obj_add_event_cb(state->stop_btn, stop_btn_cb, LV_EVENT_CLICKED, NULL);
    lv_obj_add_event_cb(state->stop_btn, cancel_event_cb, LV_EVENT_CANCEL, NULL);
    lv_group_add_obj(lv_group_get_default(), state->stop_btn);

    // Focus play button by default
    lv_group_focus_obj(state->play_btn);

    return (ui_state_t){.type = SCREEN_AUDIOBOOK_PLAYER, .audiobook_player = state};
}

void free_audiobook_player_screen(audiobook_player_screen_state_t *state) {
    if (!state) return;

    lv_group_remove_obj(state->play_btn);
    lv_group_remove_obj(state->stop_btn);

    lv_obj_delete(state->container);
    free(state);
}

void update_audiobook_player_screen(audiobook_player_screen_state_t *state) {
    if (!state) return;

    audio_state_t audio_state = audio_player_get_state();

    // Apply bookmark seek on first frame where player is playing
    if (!state->bookmark_applied && state->bookmark_ms > 0 &&
        audio_state == AUDIO_STATE_PLAYING) {
        state->bookmark_applied = true;
        if (!audio_player_seek_ms(state->bookmark_ms)) {
            // Seek not supported - show brief message but continue playing
            lv_label_set_text(state->title_label, "Resume not available");
        }
    }

    // Detect playback completion (player stopped without user action)
    if (audio_state == AUDIO_STATE_STOPPED && state->bookmark_applied) {
        if (state->dir[0] && play_next_file(state)) {
            // Advanced to next file in directory
            return;
        }
        // Single file mode or last file done: remove bookmarks and navigate back
        if (app_state.last_played_path[0]) {
            bookmark_remove(app_state.last_played_path);
        }
        if (state->dir[0]) {
            bookmark_remove(state->dir);
        }
        navigate_to_audiobooks();
        return;
    }

    // Update play/pause button icon
    lv_obj_t *play_label = lv_obj_get_child(state->play_btn, 0);
    if (audio_state == AUDIO_STATE_PLAYING || audio_state == AUDIO_STATE_PAUSED) {
        lv_label_set_text(play_label, audio_state == AUDIO_STATE_PLAYING ? LV_SYMBOL_PAUSE : LV_SYMBOL_PLAY);
    } else {
        lv_label_set_text(play_label, LV_SYMBOL_PLAY);
    }

    // Update progress bar and time labels
    uint32_t position_ms = audio_player_get_position_ms();
    uint32_t duration_ms = audio_player_get_duration_ms();
    uint8_t progress = audio_player_get_progress();

    lv_bar_set_value(state->progress_bar, progress, LV_ANIM_OFF);

    char time_buf[16];
    format_time(position_ms, time_buf, sizeof(time_buf));
    lv_label_set_text(state->position_label, time_buf);

    if (duration_ms > 0) {
        format_time(duration_ms, time_buf, sizeof(time_buf));
        lv_label_set_text(state->duration_label, time_buf);
    } else {
        lv_label_set_text(state->duration_label, "--:--");
    }
}