Login
1 branch 0 tags
Ben (Desktop/Arch) Code cleanup, extracted playlist code from now_playing_screen e62119b 29 days ago 79 Commits
moon / src / ui.c
#include "ui.h"
#include <stdlib.h>
#include <string.h>
#include "lvgl.h"
#include "moon.h"
#include "playlist.h"
#include "storage.h"
#include "topbar.h"

char string_buffer[256];

// Pending screen transition state
static lv_obj_t* pending_screen;
static anim_dir_t pending_dir;

// Confirm dialog state
static confirm_cb_t confirm_callback;
static screen_type_t confirm_return_screen;
static char confirm_title[64];

struct confirm_screen_state {
	lv_obj_t* list;
};

static void button_focus_cb(lv_event_t* e) {
	lv_obj_t* btn = lv_event_get_target(e);
	if (!btn) {
		return;
	}
	lv_obj_t* label = lv_obj_get_child(btn, 0);
	if (!label) {
		return;
	}
	lv_event_code_t code = lv_event_get_code(e);

	if (code == LV_EVENT_FOCUSED) {
		lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR);
	} else {
		lv_label_set_long_mode(label, LV_LABEL_LONG_CLIP);
	}
}

lv_obj_t* bvs_list_add_button(lv_obj_t* list,
                              const void* icon,
                              const char* txt) {
	lv_obj_t* ret = lv_list_add_button(list, icon, txt);
	lv_obj_set_style_pad_hor(ret, 2, 0);
	lv_obj_set_style_pad_ver(ret, 1, 0);
	lv_obj_set_height(ret, 20);

	lv_obj_t* label = lv_obj_get_child(ret, 0);
	lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
	lv_obj_set_style_anim_duration(label, 12000, 0);

	lv_obj_add_event_cb(ret, button_focus_cb, LV_EVENT_FOCUSED, NULL);
	lv_obj_add_event_cb(ret, button_focus_cb, LV_EVENT_DEFOCUSED, NULL);

	return ret;
}

lv_obj_t* bvs_list_create(lv_obj_t* parent) {
	lv_obj_t* list = lv_list_create(parent);
	lv_obj_set_style_pad_hor(list, 2, 0);
	lv_obj_set_style_radius(list, 0, 0);
	lv_obj_set_style_border_side(list, 0, 0);
	lv_obj_set_pos(list, 0, TOPBAR_H);
	lv_obj_set_size(list, SCREEN_WIDTH, SCREEN_HEIGHT - TOPBAR_H);
	return list;
}

void bvs_list_clear(lv_obj_t* list) {
	if (!list) {
		return;
	}
	uint32_t count = lv_obj_get_child_count(list);
	for (uint32_t i = 0; i < count; i++) {
		lv_group_remove_obj(lv_obj_get_child(list, i));
	}
	lv_obj_delete(list);
}

void path_parent(const char* path,
                 char* parent,
                 size_t parent_size,
                 char* basename,
                 size_t basename_size) {
	strncpy(parent, path, parent_size);
	parent[parent_size - 1] = '\0';

	char* last_slash = strrchr(parent, '/');
	if (!last_slash || last_slash == parent) {
		strncpy(basename, parent + 1, basename_size);
		basename[basename_size - 1] = '\0';
		strcpy(parent, "/");
	} else {
		strncpy(basename, last_slash + 1, basename_size);
		basename[basename_size - 1] = '\0';
		*last_slash = '\0';
	}
}

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

int get_sorted_entries(const char* dir,
                       storage_entry_t* entries,
                       int max_entries) {
	int count = storage_list_dir(dir, entries, max_entries);
	if (count <= 0) {
		return 0;
	}

	qsort(entries, (size_t)count, sizeof(storage_entry_t), entry_name_cmp);

	int visible = 0;
	for (int i = 0; i < count; i++) {
		if (entries[i].name[0] == '.') {
			continue;
		}
		entries[visible++] = entries[i];
	}
	return visible;
}

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 = get_sorted_entries(dir, entries, STORAGE_MAX_DIR_ENTRIES);
	bool found = (index >= 0 && index < count);
	if (found) {
		*out = entries[index];
	}
	free(entries);
	return found;
}

void show_queue_dialog(const char* path,
                       playlist_type_t type,
                       screen_type_t return_screen,
                       const char* cwd,
                       const char* focus_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 = type;
	app_state.return_screen = return_screen;
	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, focus_name, sizeof(app_state.return_focus));
	app_state.return_focus[sizeof(app_state.return_focus) - 1] = '\0';
	navigate_to(SCREEN_QUEUE_DIALOG);
}

lv_obj_t* bvs_list_add_nav_button(lv_obj_t* list,
                                  const void* icon,
                                  const char* txt,
                                  lv_event_cb_t click_cb,
                                  void* click_data,
                                  lv_event_cb_t cancel_cb) {
	lv_obj_t* btn = bvs_list_add_button(list, icon, txt);
	lv_obj_add_event_cb(btn, click_cb, LV_EVENT_CLICKED, click_data);
	lv_obj_add_event_cb(btn, cancel_cb, LV_EVENT_CANCEL, NULL);
	lv_group_add_obj(lv_group_get_default(), btn);
	return btn;
}

// --- Navigation ---

lv_obj_t* navigate_prepare(anim_dir_t dir) {
	free_screen();
	lv_obj_t* scr = lv_obj_create(NULL);
	pending_screen = scr;
	pending_dir = dir;
	return scr;
}

void navigate_commit(void) {
	if (!pending_screen) {
		return;
	}

	lv_obj_t* scr = pending_screen;
	anim_dir_t dir = pending_dir;
	pending_screen = NULL;

	if (dir == ANIM_FORWARD) {
		lv_screen_load_anim(scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, ANIM_DURATION_MS,
		                    0, true);
	} else if (dir == ANIM_BACK) {
		lv_screen_load_anim(scr, LV_SCR_LOAD_ANIM_MOVE_RIGHT, ANIM_DURATION_MS,
		                    0, true);
	} else {
		lv_screen_load_anim(scr, LV_SCR_LOAD_ANIM_NONE, 0, 0, true);
	}
}

// --- Confirm dialog screen ---

static void confirm_ok_cb(lv_event_t* e) {
	if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
		return;
	}
	confirm_cb_t cb = confirm_callback;
	screen_type_t ret = confirm_return_screen;
	if (cb) {
		cb();
	}
	navigate_back(ret);
}

static void confirm_cancel_cb(lv_event_t* e) {
	(void)e;
	navigate_back(confirm_return_screen);
}

static void confirm_cancel_click_cb(lv_event_t* e) {
	if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
		confirm_cancel_cb(e);
	}
}

ui_state_t setup_confirm_screen(lv_obj_t* parent) {
	confirm_screen_state_t* state = calloc(1, sizeof(confirm_screen_state_t));

	topbar_set_title(confirm_title);

	state->list = bvs_list_create(parent);

	lv_obj_t* ok_btn = bvs_list_add_button(state->list, NULL, "Confirm");
	lv_obj_add_event_cb(ok_btn, confirm_ok_cb, LV_EVENT_CLICKED, NULL);
	lv_obj_add_event_cb(ok_btn, confirm_cancel_cb, LV_EVENT_CANCEL, NULL);
	lv_obj_set_style_bg_opa(ok_btn, LV_OPA_COVER, 0);

	lv_obj_t* cancel_btn = bvs_list_add_button(state->list, NULL, "Cancel");
	lv_obj_add_event_cb(cancel_btn, confirm_cancel_click_cb, LV_EVENT_CLICKED,
	                    NULL);
	lv_obj_add_event_cb(cancel_btn, confirm_cancel_cb, LV_EVENT_CANCEL, NULL);

	lv_group_add_obj(lv_group_get_default(), ok_btn);
	lv_group_add_obj(lv_group_get_default(), cancel_btn);
	lv_group_focus_obj(ok_btn);

	return (ui_state_t){.type = SCREEN_CONFIRM, .confirm = state};
}

void free_confirm_screen(confirm_screen_state_t* state) {
	if (!state) {
		return;
	}
	free(state);
}

void navigate_to_confirm(const char* title,
                         confirm_cb_t on_confirm,
                         screen_type_t return_screen) {
	confirm_callback = on_confirm;
	confirm_return_screen = return_screen;
	strncpy(confirm_title, title, sizeof(confirm_title));
	confirm_title[sizeof(confirm_title) - 1] = '\0';
	navigate_to(SCREEN_CONFIRM);
}