Login
1 branch 0 tags
Ben (Desktop/Arch) Wifi/Podcasts on the ESP32!!!! 90acdde 1 month ago 77 Commits
moon / src / screens / files_screen.c
#include "files_screen.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../audio_player.h"
#include "../playlist.h"
#include "../storage.h"
#include "../topbar.h"

// Case-insensitive extension check helper
static bool ext_match(const char* ext, const char* pattern) {
	while (*pattern) {
		if ((tolower(*ext++) != tolower(*pattern++))) {
			return false;
		}
	}
	return !(*ext);
}

// Check if filename is a supported audio file
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, ".m4b") || ext_match(dot, ".flac") ||
	       ext_match(dot, ".ogg") ||
	       ext_match(dot, ".opu") ||  // 8.3 Filenames...
	       ext_match(dot, ".fla") || ext_match(dot, ".opus");
}

// 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(STORAGE_MAX_DIR_ENTRIES * sizeof(storage_entry_t));
	if (!entries) {
		return false;
	}

	int count = storage_list_dir(dir, entries, STORAGE_MAX_DIR_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) {
		char parent[STORAGE_MAX_PATH];
		char current_dir[STORAGE_MAX_NAME];
		path_parent(state->cwd, parent, sizeof(parent), current_dir,
		            sizeof(current_dir));
		lv_obj_t* scr = navigate_prepare(ANIM_BACK);
		ui_state = setup_files_screen(scr, parent, current_dir);
		navigate_commit();
	} else {
		navigate_back(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);
	}
}

// Helper to store return context and navigate to queue dialog
static void show_queue_dialog(const char* path,
                              const char* cwd,
                              const char* entry_name) {
	strncpy(app_state.pending_path, path, sizeof(app_state.pending_path));
	app_state.pending_path[sizeof(app_state.pending_path) - 1] = '\0';
	app_state.pending_type = PLAYLIST_MUSIC;
	app_state.return_screen = SCREEN_FILES;
	strncpy(app_state.return_cwd, cwd, sizeof(app_state.return_cwd));
	app_state.return_cwd[sizeof(app_state.return_cwd) - 1] = '\0';
	strncpy(app_state.return_focus, entry_name, sizeof(app_state.return_focus));
	app_state.return_focus[sizeof(app_state.return_focus) - 1] = '\0';
	navigate_to(SCREEN_QUEUE_DIALOG);
}

// 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
		lv_obj_t* scr = navigate_prepare(ANIM_FORWARD);
		ui_state = setup_files_screen(scr, path, "");
		navigate_commit();
	} else if (is_audio_file(entry.name)) {
		if (playlist_is_empty()) {
			playlist_add(path, PLAYLIST_MUSIC);
			playlist_set_index(0);
			playlist_save();
			navigate_to(SCREEN_NOW_PLAYING);
		} else {
			// Playlist has entries: show queue dialog
			show_queue_dialog(path, state->cwd, entry.name);
		}
	}
}

const void* storage_entry_get_icon(const storage_entry_t* entry) {
	if (entry->type == STORAGE_TYPE_DIR) {
		return LV_SYMBOL_DIRECTORY;
	}
	if (is_audio_file(entry->name)) {
		return LV_SYMBOL_AUDIO;
	}
	return LV_SYMBOL_FILE;
}

ui_state_t setup_files_screen(lv_obj_t* parent,
                              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, "/");
	}

	state->list = bvs_list_create(parent);

	// Set topbar title to directory basename
	const char* basename = strrchr(state->cwd, '/');
	basename = (basename && basename[1]) ? basename + 1 : "Files";
	topbar_set_title(basename);

	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(STORAGE_MAX_DIR_ENTRIES * sizeof(storage_entry_t));
		int count = entries ? storage_list_dir(state->cwd, entries,
		                                       STORAGE_MAX_DIR_ENTRIES)
		                    : -1;

		if (count < 0) {
			lv_obj_t* error =
			    bvs_list_add_button(state->list, NULL, "Error reading dir");
			(void)error;
		} else if (count == 0) {
			lv_obj_t* empty =
			    bvs_list_add_button(state->list, NULL, "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_FILES, .files = state};
}

void free_files_screen(files_screen_state_t* state) {
	if (!state) {
		return;
	}
	free(state);
}

void update_files_screen(files_screen_state_t* state) {
	(void)state;  // LVGL handles updates via timer
}