#include "TodoList.h" #include #include #include #include #include #include #include #include #include #include #include #include static constexpr const char* SAVE_FILENAME = "todos.txt"; /* File-scope instance pointer for index-based callbacks */ static TodoList* g_instance = nullptr; static AppHandle s_appHandle = nullptr; /* ── Helpers ──────────────────────────────────────────────────────── */ static bool getSaveFilePath(char* buf, size_t bufSize) { if (!s_appHandle) return false; size_t size = bufSize; tt_app_get_user_data_child_path(s_appHandle, SAVE_FILENAME, buf, &size); return size > 0; } static bool ensureDir() { if (!s_appHandle) return false; char dir[256]; size_t size = sizeof(dir); tt_app_get_user_data_path(s_appHandle, dir, &size); if (size == 0) return false; for (char* p = dir + 1; *p; ++p) { if (*p == '/') { *p = '\0'; mkdir(dir, 0755); *p = '/'; } } mkdir(dir, 0755); return true; } static uint32_t getToolbarHeight(UiDensity uiDensity) { if (uiDensity == LVGL_UI_DENSITY_COMPACT) { return lvgl_get_text_font_height(FONT_SIZE_DEFAULT) * 1.4f; } else { return lvgl_get_text_font_height(FONT_SIZE_LARGE) * 2.2f; } } static uint32_t getActionIconPadding(UiDensity uiDensity) { auto toolbar_height = getToolbarHeight(uiDensity); return (uiDensity != LVGL_UI_DENSITY_COMPACT) ? (uint32_t)(toolbar_height * 0.2f) : 8; } /* ── Persistence ──────────────────────────────────────────────────── */ void TodoList::saveTodos() { if (!ensureDir()) return; char savePath[256]; if (!getSaveFilePath(savePath, sizeof(savePath))) return; auto lock = tt_lock_alloc_for_path(savePath); if (!lock) return; if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) { FILE* f = fopen(savePath, "w"); if (f) { for (int i = 0; i < count; i++) { fprintf(f, "%c %s\n", items[i].done ? '+' : '-', items[i].text); } fclose(f); } tt_lock_release(lock); } tt_lock_free(lock); } void TodoList::loadTodos() { char savePath[256]; if (!getSaveFilePath(savePath, sizeof(savePath))) return; auto lock = tt_lock_alloc_for_path(savePath); if (!lock) return; if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) { count = 0; FILE* f = fopen(savePath, "r"); if (f) { char line[MAX_TEXT_LEN + 4]; while (count < MAX_TODOS && fgets(line, sizeof(line), f)) { size_t len = strlen(line); while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { line[--len] = '\0'; } if (len < 3 || line[1] != ' ') continue; TodoItem* item = &items[count]; item->done = (line[0] == '+'); strncpy(item->text, &line[2], MAX_TEXT_LEN - 1); item->text[MAX_TEXT_LEN - 1] = '\0'; count++; } fclose(f); } tt_lock_release(lock); } tt_lock_free(lock); } /* ── UI Helpers ───────────────────────────────────────────────────── */ void TodoList::updateCountLabel() { if (!countLabel) return; int pending = 0; for (int i = 0; i < count; i++) { if (!items[i].done) pending++; } if (pending > 0) { lv_label_set_text_fmt(countLabel, "%d left", pending); } else if (count > 0) { lv_label_set_text(countLabel, "All done!"); } else { lv_label_set_text(countLabel, "No tasks"); } } void TodoList::addItem(const char* text) { if (!text || !text[0]) return; if (count >= MAX_TODOS) return; while (*text == ' ') text++; if (!*text) return; TodoItem* item = &items[count]; item->done = false; strncpy(item->text, text, MAX_TEXT_LEN - 1); item->text[MAX_TEXT_LEN - 1] = '\0'; size_t len = strlen(item->text); while (len > 0 && item->text[len - 1] == ' ') { item->text[--len] = '\0'; } count++; saveTodos(); rebuildList(); } void TodoList::scheduleRebuild() { if (rebuildPending) return; rebuildPending = true; rebuildTimer = lv_timer_create(onDeferredRebuild, 0, this); if (!rebuildTimer) { rebuildPending = false; return; } lv_timer_set_repeat_count(rebuildTimer, 1); } void TodoList::onDeferredRebuild(lv_timer_t* timer) { TodoList* self = static_cast(lv_timer_get_user_data(timer)); if (self) { self->rebuildTimer = nullptr; self->rebuildPending = false; self->rebuildList(); } } void TodoList::rebuildList() { if (!list) return; lv_obj_clean(list); auto ui_density = lvgl_get_ui_density(); auto toolbar_height = getToolbarHeight(ui_density); auto icon_padding = getActionIconPadding(ui_density); if (count == 0) { lv_list_add_text(list, "No tasks yet. Add one below!"); } for (int i = 0; i < count; i++) { TodoItem* item = &items[i]; char display[MAX_TEXT_LEN + 8]; snprintf(display, sizeof(display), "%s %s", item->done ? LV_SYMBOL_OK : LV_SYMBOL_DUMMY, item->text); lv_obj_t* btn = lv_list_add_button(list, NULL, display); if (item->done) { lv_obj_set_style_text_opa(btn, LV_OPA_50, LV_PART_MAIN); } lv_obj_add_event_cb(btn, onItemClicked, LV_EVENT_SHORT_CLICKED, (void*)(intptr_t)i); lv_obj_t* delBtn = lv_button_create(btn); lv_obj_set_size(delBtn, toolbar_height - icon_padding, toolbar_height - icon_padding); lv_obj_align(delBtn, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_set_style_bg_color(delBtn, lv_palette_main(LV_PALETTE_RED), LV_PART_MAIN); lv_obj_set_style_pad_all(delBtn, 0, LV_PART_MAIN); lv_obj_set_style_shadow_width(delBtn, 0, LV_PART_MAIN); lv_obj_t* delIcon = lv_label_create(delBtn); lv_label_set_text(delIcon, LV_SYMBOL_CLOSE); lv_obj_center(delIcon); lv_obj_add_event_cb(delBtn, onDeleteClicked, LV_EVENT_CLICKED, (void*)(intptr_t)i); } updateCountLabel(); } /* ── Callbacks ────────────────────────────────────────────────────── */ void TodoList::onItemClicked(lv_event_t* e) { if (!g_instance) return; int idx = (int)(intptr_t)lv_event_get_user_data(e); if (idx < 0 || idx >= g_instance->count) return; g_instance->items[idx].done = !g_instance->items[idx].done; g_instance->saveTodos(); g_instance->scheduleRebuild(); } void TodoList::onDeleteClicked(lv_event_t* e) { if (!g_instance) return; int idx = (int)(intptr_t)lv_event_get_user_data(e); if (idx < 0 || idx >= g_instance->count) return; for (int i = idx; i < g_instance->count - 1; i++) { g_instance->items[i] = g_instance->items[i + 1]; } g_instance->count--; g_instance->saveTodos(); g_instance->scheduleRebuild(); } void TodoList::onAddClicked(lv_event_t* e) { if (!g_instance || !g_instance->inputTa) return; const char* text = lv_textarea_get_text(g_instance->inputTa); g_instance->addItem(text); lv_textarea_set_text(g_instance->inputTa, ""); } void TodoList::onInputReady(lv_event_t* e) { onAddClicked(e); } void TodoList::onClearDoneClicked(lv_event_t* e) { if (!g_instance) return; int write = 0; for (int read = 0; read < g_instance->count; read++) { if (!g_instance->items[read].done) { if (write != read) { g_instance->items[write] = g_instance->items[read]; } write++; } } g_instance->count = write; g_instance->saveTodos(); g_instance->scheduleRebuild(); } /* ── Lifecycle ────────────────────────────────────────────────────── */ void TodoList::onShow(AppHandle app, lv_obj_t* parent) { g_instance = this; s_appHandle = app; loadTodos(); lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); /* Toolbar */ lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_t* countWrapper = lv_obj_create(toolbar); lv_obj_set_size(countWrapper, LV_SIZE_CONTENT, LV_PCT(100)); lv_obj_set_style_pad_top(countWrapper, 4, LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_left(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_right(countWrapper, 4, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_column(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(countWrapper, 0, LV_STATE_DEFAULT); lv_obj_remove_flag(countWrapper, LV_OBJ_FLAG_SCROLLABLE); countLabel = lv_label_create(countWrapper); lv_obj_set_size(countLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_align(countLabel, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(countLabel, LV_TEXT_ALIGN_LEFT, LV_STATE_DEFAULT); lv_obj_set_style_text_font(countLabel, lv_font_get_default(), 0); lv_obj_set_style_text_color(countLabel, lv_palette_main(LV_PALETTE_CYAN), LV_PART_MAIN); auto ui_density = lvgl_get_ui_density(); auto toolbar_height = getToolbarHeight(ui_density); auto icon_padding = getActionIconPadding(ui_density); lv_obj_t* btnWrapper = lv_obj_create(toolbar); lv_obj_set_width(btnWrapper, LV_SIZE_CONTENT); lv_obj_set_flex_flow(btnWrapper, LV_FLEX_FLOW_ROW); lv_obj_set_style_pad_all(btnWrapper, icon_padding / 2, LV_STATE_DEFAULT); lv_obj_set_style_pad_column(btnWrapper, 4, LV_STATE_DEFAULT); lv_obj_set_style_border_width(btnWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(btnWrapper, 0, LV_STATE_DEFAULT); lv_obj_t* clearBtn = lv_button_create(btnWrapper); lv_obj_set_size(clearBtn, toolbar_height - icon_padding, toolbar_height - icon_padding); lv_obj_set_style_pad_all(clearBtn, 0, LV_STATE_DEFAULT); lv_obj_align(clearBtn, LV_ALIGN_CENTER, 0, 0); lv_obj_add_event_cb(clearBtn, onClearDoneClicked, LV_EVENT_CLICKED, nullptr); lv_obj_t* clearIcon = lv_label_create(clearBtn); lv_label_set_text(clearIcon, LV_SYMBOL_TRASH); lv_obj_align(clearIcon, LV_ALIGN_CENTER, 0, 0); /* Container */ lv_obj_t* cont = lv_obj_create(parent); lv_obj_set_width(cont, LV_PCT(100)); lv_obj_set_flex_grow(cont, 1); lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(cont, 4, 0); lv_obj_set_style_pad_gap(cont, 4, 0); lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_border_width(cont, 0, 0); /* Scrollable list */ list = lv_list_create(cont); lv_obj_set_width(list, LV_PCT(100)); lv_obj_set_flex_grow(list, 1); /* Input row */ inputRow = lv_obj_create(cont); lv_obj_set_size(inputRow, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_flex_flow(inputRow, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(inputRow, 0, 0); lv_obj_set_style_pad_gap(inputRow, 4, 0); lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_border_width(inputRow, 0, 0); inputTa = lv_textarea_create(inputRow); lv_textarea_set_placeholder_text(inputTa, "New task..."); lv_textarea_set_one_line(inputTa, true); lv_obj_set_flex_grow(inputTa, 1); lv_obj_set_style_text_font(inputTa, lv_font_get_default(), 0); lv_obj_add_event_cb(inputTa, onInputReady, LV_EVENT_READY, nullptr); lv_obj_t* addBtn = lv_button_create(inputRow); lv_obj_t* addLbl = lv_label_create(addBtn); lv_label_set_text(addLbl, LV_SYMBOL_PLUS); lv_obj_add_event_cb(addBtn, onAddClicked, LV_EVENT_CLICKED, nullptr); rebuildList(); } void TodoList::onHide(AppHandle app) { if (rebuildTimer) { lv_timer_delete(rebuildTimer); rebuildTimer = nullptr; } rebuildPending = false; list = nullptr; inputRow = nullptr; inputTa = nullptr; countLabel = nullptr; g_instance = nullptr; s_appHandle = nullptr; }