From 6dc8e6d222c436beacad89c86c902e56ee308a42 Mon Sep 17 00:00:00 2001 From: Revers-BR Date: Fri, 6 Feb 2026 13:43:40 -0300 Subject: [PATCH 1/3] Implement pagination for file entries in View class --- Tactility/Private/Tactility/app/files/View.h | 6 +- Tactility/Source/app/files/View.cpp | 61 ++++++++++++++++---- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/files/View.h index 00aa34331..6236659fb 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/files/View.h @@ -11,6 +11,10 @@ namespace tt::app::files { class View final { std::shared_ptr state; + + size_t current_start_index = 0; + size_t last_loaded_index = 0; + const size_t MAX_BATCH = 50; lv_obj_t* dir_entry_list = nullptr; lv_obj_t* action_list = nullptr; @@ -33,7 +37,7 @@ class View final { explicit View(const std::shared_ptr& state) : state(state) {} void init(const AppContext& appContext, lv_obj_t* parent); - void update(); + void update(size_t start_index = 0); void onNavigateUpPressed(); void onDirEntryPressed(uint32_t index); diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 49e418952..d1daeec92 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -289,25 +289,62 @@ void View::showActionsForFile() { lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN); } -void View::update() { +void View::update(size_t start_index) { + const bool is_root = (state->getCurrentPath() == "/"); + auto scoped_lockable = lvgl::getSyncLock()->asScopedLock(); - if (scoped_lockable.lock(lvgl::defaultLockTime)) { - lv_obj_clean(dir_entry_list); + if (!scoped_lockable.lock(lvgl::defaultLockTime)) { + LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl"); + return; + } - state->withEntries([this](const std::vector& entries) { - for (auto entry : entries) { - LOGGER.debug("Entry: {} {}", entry.d_name, entry.d_type); - createDirEntryWidget(dir_entry_list, entry); + lv_obj_clean(dir_entry_list); + + current_start_index = start_index; + + state->withEntries([this, is_root](const std::vector& entries) { + size_t total_entries = entries.size(); + size_t count = 0; + + if (!is_root && current_start_index > 0) { + auto* back_btn = lv_list_add_btn(dir_entry_list, LV_SYMBOL_LEFT, "Back"); + lv_obj_add_event_cb(back_btn, [](lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + size_t new_index = (view->current_start_index >= view->MAX_BATCH) ? + view->current_start_index - view->MAX_BATCH : 0; + view->update(new_index); + }, LV_EVENT_SHORT_CLICKED, this); + } + + for (size_t i = current_start_index; i < total_entries; ++i) { + auto entry = entries[i]; + + createDirEntryWidget(dir_entry_list, entry); + count++; + + if (count >= MAX_BATCH) { + last_loaded_index = i + 1; + break; } - }); + } - if (state->getCurrentPath() == "/") { - lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + if (!is_root && last_loaded_index < total_entries) { + if (total_entries - current_start_index > MAX_BATCH) { + auto* next_btn = lv_list_add_btn(dir_entry_list, LV_SYMBOL_RIGHT, "Next"); + lv_obj_add_event_cb(next_btn, [](lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->update(view->last_loaded_index); + }, LV_EVENT_SHORT_CLICKED, this); + } } else { - lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + last_loaded_index = total_entries; } + }); + + if (is_root) { + lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); } else { - LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl"); + lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); } } From d7aaf386ff23033916a32cd176a601cfc2b8f37c Mon Sep 17 00:00:00 2001 From: Revers-BR Date: Fri, 6 Feb 2026 20:35:53 -0300 Subject: [PATCH 2/3] Add resolveDirentFromListIndex method and refactor entry handling in View class --- Tactility/Private/Tactility/app/files/View.h | 4 + Tactility/Source/app/files/View.cpp | 130 +++++++++++-------- 2 files changed, 77 insertions(+), 57 deletions(-) diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/files/View.h index 6236659fb..d69f3cc19 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/files/View.h @@ -49,6 +49,10 @@ class View final { void onDirEntryListScrollBegin(); void onResult(LaunchId launchId, Result result, std::unique_ptr bundle); void deinit(const AppContext& appContext); + +private: + + bool resolveDirentFromListIndex(int32_t list_index, dirent& out_entry); }; } diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index d1daeec92..fd30db031 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include @@ -10,11 +10,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include @@ -104,7 +104,7 @@ void View::viewFile(const std::string& path, const std::string& filename) { // install(filename); auto message = std::format("Do you want to install {}?", filename); installAppPath = processed_filepath; - auto choices = std::vector { "Yes", "No" }; + auto choices = std::vector {"Yes", "No"}; installAppLaunchId = alertdialog::start("Install?", message, choices); #endif } else if (isSupportedImageFile(filename)) { @@ -123,59 +123,72 @@ void View::viewFile(const std::string& path, const std::string& filename) { onNavigate(); } +bool View::resolveDirentFromListIndex(int32_t list_index, dirent& out_entry) { + const bool is_root = (state->getCurrentPath() == "/"); + const bool has_back = (!is_root && current_start_index > 0); + + if (has_back && list_index == 0) { + return false; // Back button + } + + const size_t adjusted_index = + current_start_index + static_cast(list_index) - (has_back ? 1 : 0); + + return state->getDirent(static_cast(adjusted_index), out_entry); +} + void View::onDirEntryPressed(uint32_t index) { dirent dir_entry; - if (state->getDirent(index, dir_entry)) { - LOGGER.info("Pressed {} {}", dir_entry.d_name, dir_entry.d_type); - state->setSelectedChildEntry(dir_entry.d_name); - using namespace tt::file; - switch (dir_entry.d_type) { - case TT_DT_DIR: - case TT_DT_CHR: - state->setEntriesForChildPath(dir_entry.d_name); - onNavigate(); - update(); - break; - case TT_DT_LNK: - LOGGER.warn("opening links is not supported"); - break; - case TT_DT_REG: - viewFile(state->getCurrentPath(), dir_entry.d_name); - onNavigate(); - break; - default: - // Assume it's a file - // TODO: Find a better way to identify a file - viewFile(state->getCurrentPath(), dir_entry.d_name); - onNavigate(); - break; - } + if (!resolveDirentFromListIndex(static_cast(index), dir_entry)) { + return; + } + + LOGGER.info("Pressed {} {}", dir_entry.d_name, dir_entry.d_type); + state->setSelectedChildEntry(dir_entry.d_name); + + using namespace tt::file; + switch (dir_entry.d_type) { + case TT_DT_DIR: + case TT_DT_CHR: + state->setEntriesForChildPath(dir_entry.d_name); + onNavigate(); + update(); + break; + + case TT_DT_LNK: + LOGGER.warn("opening links is not supported"); + break; + + default: + viewFile(state->getCurrentPath(), dir_entry.d_name); + onNavigate(); + break; } } void View::onDirEntryLongPressed(int32_t index) { dirent dir_entry; - if (state->getDirent(index, dir_entry)) { - LOGGER.info("Pressed {} {}", dir_entry.d_name, dir_entry.d_type); - state->setSelectedChildEntry(dir_entry.d_name); - using namespace file; - switch (dir_entry.d_type) { - case TT_DT_DIR: - case TT_DT_CHR: - showActionsForDirectory(); - break; - case TT_DT_LNK: - LOGGER.warn("Opening links is not supported"); - break; - case TT_DT_REG: - showActionsForFile(); - break; - default: - // Assume it's a file - // TODO: Find a better way to identify a file - showActionsForFile(); - break; - } + if (!resolveDirentFromListIndex(index, dir_entry)) { + return; + } + + LOGGER.info("Pressed {} {}", dir_entry.d_name, dir_entry.d_type); + state->setSelectedChildEntry(dir_entry.d_name); + + using namespace file; + switch (dir_entry.d_type) { + case TT_DT_DIR: + case TT_DT_CHR: + showActionsForDirectory(); + break; + + case TT_DT_LNK: + LOGGER.warn("Opening links is not supported"); + break; + + default: + showActionsForFile(); + break; } } @@ -251,7 +264,7 @@ void View::onDeletePressed() { LOGGER.info("Pending delete {}", file_path); state->setPendingAction(State::ActionDelete); std::string message = "Do you want to delete this?\n" + file_path; - const std::vector choices = { "Yes", "No" }; + const std::vector choices = {"Yes", "No"}; alertdialog::start("Are you sure?", message, choices); } @@ -304,6 +317,9 @@ void View::update(size_t start_index) { state->withEntries([this, is_root](const std::vector& entries) { size_t total_entries = entries.size(); + if (current_start_index > total_entries) { + current_start_index = total_entries; + } size_t count = 0; if (!is_root && current_start_index > 0) { @@ -312,8 +328,7 @@ void View::update(size_t start_index) { auto* view = static_cast(lv_event_get_user_data(event)); size_t new_index = (view->current_start_index >= view->MAX_BATCH) ? view->current_start_index - view->MAX_BATCH : 0; - view->update(new_index); - }, LV_EVENT_SHORT_CLICKED, this); + view->update(new_index); }, LV_EVENT_SHORT_CLICKED, this); } for (size_t i = current_start_index; i < total_entries; ++i) { @@ -323,18 +338,19 @@ void View::update(size_t start_index) { count++; if (count >= MAX_BATCH) { - last_loaded_index = i + 1; break; } } + last_loaded_index = std::min(current_start_index + count, total_entries); + if (!is_root && last_loaded_index < total_entries) { - if (total_entries - current_start_index > MAX_BATCH) { + if (total_entries > current_start_index && ++ (total_entries - current_start_index) > MAX_BATCH) { auto* next_btn = lv_list_add_btn(dir_entry_list, LV_SYMBOL_RIGHT, "Next"); lv_obj_add_event_cb(next_btn, [](lv_event_t* event) { auto* view = static_cast(lv_event_get_user_data(event)); - view->update(view->last_loaded_index); - }, LV_EVENT_SHORT_CLICKED, this); + view->update(view->last_loaded_index); }, LV_EVENT_SHORT_CLICKED, this); } } else { last_loaded_index = total_entries; @@ -513,4 +529,4 @@ void View::deinit(const AppContext& appContext) { lv_obj_remove_event_cb(dir_entry_list, dirEntryListScrollBeginCallback); } -} +} // namespace tt::app::files From 5eddbdeb4ce016e0727e5f49008cfba8afabacd3 Mon Sep 17 00:00:00 2001 From: Revers-BR Date: Fri, 6 Feb 2026 20:51:17 -0300 Subject: [PATCH 3/3] Fix current_start_index calculation in View::update to prevent out-of-bounds access --- Tactility/Source/app/files/View.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index fd30db031..38bad9e61 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -317,8 +317,10 @@ void View::update(size_t start_index) { state->withEntries([this, is_root](const std::vector& entries) { size_t total_entries = entries.size(); - if (current_start_index > total_entries) { - current_start_index = total_entries; + if (current_start_index >= total_entries) { + current_start_index = (total_entries > MAX_BATCH) + ? (total_entries - MAX_BATCH) + : 0; } size_t count = 0;