From 5e95d9a36fe9e8d562011fa64b37197c2c6bed54 Mon Sep 17 00:00:00 2001 From: Lev Roland-Kalb <114942703+Levrk@users.noreply.github.com> Date: Sat, 28 Feb 2026 11:58:10 -0500 Subject: [PATCH] feat: Long Click for File Deletion through File Browser (#909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Allow users to better manage their epub library by offloading unwanted or finished books and other files. Resolves #893 * **What changes are included?** Added Delete Book shortcut in the fil browser. Delete function implements the new ConfirmationActivity to show file name and solicit user interaction before either returning to the file browser on a press of the back button, or proceeding to delete. Delete function then deletes the file and returns user to the file browser menu at the current directory. Video of it working on my machine attached here: https://github.com/user-attachments/assets/329b0198-9e97-45ad-82aa-c39894351667 ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). Certainly potential risks associated with file deletion. Please let me know if there are any concerns that need to be better addressed. I think this is a very good feature to have to go along with the new screenshots so you don't get stuck with a bunch of extra files on your device. Also I did add this to the user guide. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Егор Мартынов Co-authored-by: Arthur Tazhitdinov Co-authored-by: Zach Nelson --- USER_GUIDE.md | 3 +- lib/I18n/translations/czech.yaml | 1 + lib/I18n/translations/english.yaml | 1 + lib/I18n/translations/french.yaml | 1 + lib/I18n/translations/german.yaml | 1 + lib/I18n/translations/portuguese.yaml | 1 + lib/I18n/translations/russian.yaml | 1 + lib/I18n/translations/spanish.yaml | 1 + lib/I18n/translations/swedish.yaml | 1 + src/activities/home/MyLibraryActivity.cpp | 70 +++++++++++++++--- src/activities/home/MyLibraryActivity.h | 5 ++ src/activities/util/ConfirmationActivity.cpp | 76 ++++++++++++++++++++ src/activities/util/ConfirmationActivity.h | 30 ++++++++ 13 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 src/activities/util/ConfirmationActivity.cpp create mode 100644 src/activities/util/ConfirmationActivity.h diff --git a/USER_GUIDE.md b/USER_GUIDE.md index ebb4c006..0f438fbf 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -84,7 +84,8 @@ See [Reading Mode](#4-reading-mode) below for more information. The Browse Files screen acts as a file and folder browser. * **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down. -* **Open Selection:** Press **Confirm** to open a folder or read a selected book. +* **Open Selection:** Press **Confirm** to open a folder or read a selected book. +* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported. ### 3.4 Recent Books Screen diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index f6294420..12fbda81 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Přejít na %" STR_GO_HOME_BUTTON: "Přejít Domů" STR_SYNC_PROGRESS: "Průběh synchronizace" STR_DELETE_CACHE: "Smazat mezipaměť knihy" +STR_DELETE: "Smazat" STR_CHAPTER_PREFIX: "Kapitola:" STR_PAGES_SEPARATOR: "stránek |" STR_BOOK_PREFIX: "Kniha:" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 8bc6105b..391f7c09 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -297,6 +297,7 @@ STR_GO_TO_PERCENT: "Go to %" STR_GO_HOME_BUTTON: "Go Home" STR_SYNC_PROGRESS: "Sync Progress" STR_DELETE_CACHE: "Delete Book Cache" +STR_DELETE: "Delete" STR_DISPLAY_QR: "Show page as QR" STR_CHAPTER_PREFIX: "Chapter: " STR_PAGES_SEPARATOR: " pages | " diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 316691c8..cad52604 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Aller à %" STR_GO_HOME_BUTTON: "Retour Accueil" STR_SYNC_PROGRESS: "Synchro progression" STR_DELETE_CACHE: "Supprimer cache livre" +STR_DELETE: "Supprimer" STR_CHAPTER_PREFIX: "Chapitre : " STR_PAGES_SEPARATOR: " pages | " STR_BOOK_PREFIX: "Livre : " diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 90db203b..ecd83ac7 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -280,6 +280,7 @@ STR_GO_TO_PERCENT: "Gehe zu %" STR_GO_HOME_BUTTON: "Zum Anfang" STR_SYNC_PROGRESS: "Fortschritt synchronisieren" STR_DELETE_CACHE: "Buch-Cache leeren" +STR_DELETE: "Löschen" STR_CHAPTER_PREFIX: "Kapitel:" STR_PAGES_SEPARATOR: " Seiten | " STR_BOOK_PREFIX: "Buch: " diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index fee77f3f..a305bfa7 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Ir para %" STR_GO_HOME_BUTTON: "Ir para o início" STR_SYNC_PROGRESS: "Sincronizar progresso" STR_DELETE_CACHE: "Excluir cache do livro" +STR_DELETE: "Excluir" STR_CHAPTER_PREFIX: "Capítulo:" STR_PAGES_SEPARATOR: "páginas |" STR_BOOK_PREFIX: "Livro:" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 2198e9b6..d748f9c4 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -296,6 +296,7 @@ STR_GO_TO_PERCENT: "Перейти к %" STR_GO_HOME_BUTTON: "На главную" STR_SYNC_PROGRESS: "Синхронизировать прогресс" STR_DELETE_CACHE: "Удалить кэш книги" +STR_DELETE: "Удалить" STR_CHAPTER_PREFIX: "Глава:" STR_DISPLAY_QR: "Показать страницу в виде QR-кода" STR_PAGES_SEPARATOR: "стр. |" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index fa2b238d..5b6e4c0e 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -277,6 +277,7 @@ STR_HW_LEFT_LABEL: "Izq. (Tercer botón)" STR_HW_RIGHT_LABEL: "Der. (Cuarto botón)" STR_GO_TO_PERCENT: "Ir a %" STR_GO_HOME_BUTTON: "Volver a inicio" +STR_DELETE: "Borrar" STR_SYNC_PROGRESS: "Sincronizar progreso de lectura" STR_DELETE_CACHE: "Borrar caché del libro" STR_CHAPTER_PREFIX: "Cap.:" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 1eca643e..fd60a663 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Gå till %" STR_GO_HOME_BUTTON: "Gå Hem" STR_SYNC_PROGRESS: "Synkroniseringsframsteg" STR_DELETE_CACHE: "Radera bokcache" +STR_DELETE: "Radera" STR_CHAPTER_PREFIX: "Kapitel:" STR_PAGES_SEPARATOR: " sidor | " STR_BOOK_PREFIX: "Bok:" diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 84428153..dcf88ac8 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -1,11 +1,13 @@ #include "MyLibraryActivity.h" +#include #include #include #include #include +#include "../util/ConfirmationActivity.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" @@ -116,6 +118,14 @@ void MyLibraryActivity::onExit() { files.clear(); } +void MyLibraryActivity::clearFileMetadata(const std::string& fullPath) { + // Only clear cache for .epub files + if (StringUtils::checkFileExtension(fullPath, ".epub")) { + Epub(fullPath, "/.crosspoint").clearCache(); + LOG_DBG("MyLibrary", "Cleared metadata cache for: %s", fullPath.c_str()); + } +} + void MyLibraryActivity::loop() { // Long press BACK (1s+) goes to root folder if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS && @@ -129,20 +139,58 @@ void MyLibraryActivity::loop() { const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (files.empty()) { - return; - } + if (files.empty()) return; - if (basepath.back() != '/') basepath += "/"; - if (files[selectorIndex].back() == '/') { - basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); - loadFiles(); - selectorIndex = 0; - requestUpdate(); - } else { - onSelectBook(basepath + files[selectorIndex]); + const std::string& entry = files[selectorIndex]; + bool isDirectory = (entry.back() == '/'); + + if (mappedInput.getHeldTime() >= GO_HOME_MS && !isDirectory) { + // --- LONG PRESS ACTION: DELETE FILE --- + std::string cleanBasePath = basepath; + if (cleanBasePath.back() != '/') cleanBasePath += "/"; + const std::string fullPath = cleanBasePath + entry; + + auto handler = [this, fullPath](const ActivityResult& res) { + if (!res.isCancelled) { + LOG_DBG("MyLibrary", "Attempting to delete: %s", fullPath.c_str()); + clearFileMetadata(fullPath); + if (Storage.remove(fullPath.c_str())) { + LOG_DBG("MyLibrary", "Deleted successfully"); + loadFiles(); + if (files.empty()) { + selectorIndex = 0; + } else if (selectorIndex >= files.size()) { + // Move selection to the new "last" item + selectorIndex = files.size() - 1; + } + + requestUpdate(true); + } else { + LOG_ERR("MyLibrary", "Failed to delete file: %s", fullPath.c_str()); + } + } else { + LOG_DBG("MyLibrary", "Delete cancelled by user"); + } + }; + + std::string heading = tr(STR_DELETE) + std::string("? "); + + startActivityForResult(std::make_unique(renderer, mappedInput, heading, entry), handler); return; + } else { + // --- SHORT PRESS ACTION: OPEN/NAVIGATE --- + if (basepath.back() != '/') basepath += "/"; + + if (isDirectory) { + basepath += entry.substr(0, entry.length() - 1); + loadFiles(); + selectorIndex = 0; + requestUpdate(); + } else { + onSelectBook(basepath + entry); + } } + return; } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index 6bc1faaa..d3fa7866 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -1,4 +1,5 @@ #pragma once + #include #include #include @@ -9,6 +10,10 @@ class MyLibraryActivity final : public Activity { private: + // Deletion + bool pendingSubActivityExit = false; + void clearFileMetadata(const std::string& fullPath); + ButtonNavigator buttonNavigator; size_t selectorIndex = 0; diff --git a/src/activities/util/ConfirmationActivity.cpp b/src/activities/util/ConfirmationActivity.cpp new file mode 100644 index 00000000..c11475c9 --- /dev/null +++ b/src/activities/util/ConfirmationActivity.cpp @@ -0,0 +1,76 @@ +#include "ConfirmationActivity.h" + +#include + +#include "../../components/UITheme.h" +#include "HalDisplay.h" + +ConfirmationActivity::ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::string& heading, const std::string& body) + : Activity("Confirmation", renderer, mappedInput), heading(heading), body(body) {} + +void ConfirmationActivity::onEnter() { + Activity::onEnter(); + + lineHeight = renderer.getLineHeight(fontId); + const int maxWidth = renderer.getScreenWidth() - (margin * 2); + + if (!heading.empty()) { + safeHeading = renderer.truncatedText(fontId, heading.c_str(), maxWidth, EpdFontFamily::BOLD); + } + if (!body.empty()) { + safeBody = renderer.truncatedText(fontId, body.c_str(), maxWidth, EpdFontFamily::REGULAR); + } + + int totalHeight = 0; + if (!safeHeading.empty()) totalHeight += lineHeight; + if (!safeBody.empty()) totalHeight += lineHeight; + if (!safeHeading.empty() && !safeBody.empty()) totalHeight += spacing; + + startY = (renderer.getScreenHeight() - totalHeight) / 2; + LOG_DBG("CONF", "startY: %d", startY); + LOG_DBG("CONF", "Heading: %s", safeHeading.c_str()); + + requestUpdate(true); +} + +void ConfirmationActivity::render(RenderLock&& lock) { + renderer.clearScreen(); + + int currentY = startY; + LOG_DBG("CONF", "currentY: %d", currentY); + // Draw Heading + if (!safeHeading.empty()) { + renderer.drawCenteredText(fontId, currentY, safeHeading.c_str(), true, EpdFontFamily::BOLD); + currentY += lineHeight + spacing; + } + + // Draw Body + if (!safeBody.empty()) { + renderer.drawCenteredText(fontId, currentY, safeBody.c_str(), true, EpdFontFamily::REGULAR); + } + + // Draw UI Elements + const auto labels = mappedInput.mapLabels("", "", I18N.get(StrId::STR_CANCEL), I18N.get(StrId::STR_CONFIRM)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(HalDisplay::RefreshMode::FAST_REFRESH); +} + +void ConfirmationActivity::loop() { + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { + ActivityResult res; + res.isCancelled = false; + setResult(std::move(res)); + finish(); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { + ActivityResult res; + res.isCancelled = true; + setResult(std::move(res)); + finish(); + return; + } +} \ No newline at end of file diff --git a/src/activities/util/ConfirmationActivity.h b/src/activities/util/ConfirmationActivity.h new file mode 100644 index 00000000..9f024d92 --- /dev/null +++ b/src/activities/util/ConfirmationActivity.h @@ -0,0 +1,30 @@ +#pragma once +#include +#include + +#include "../../fontIds.h" +#include "../Activity.h" + +class ConfirmationActivity : public Activity { + private: + // Input data + std::string heading; + std::string body; + + const int margin = 20; + const int spacing = 30; + const int fontId = UI_10_FONT_ID; + + std::string safeHeading; + std::string safeBody; + int startY = 0; + int lineHeight = 0; + + public: + ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& heading, + const std::string& body); + + void onEnter() override; + void loop() override; + void render(RenderLock&& lock) override; +}; \ No newline at end of file