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 ebb4c0066..0f438fbf5 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 f62944205..12fbda81f 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 8bc6105bc..391f7c09c 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 316691c88..cad52604d 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 90db203b7..ecd83ac75 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 fee77f3f6..a305bfa7a 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 2198e9b6f..d748f9c45 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 fa2b238d6..5b6e4c0e9 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 1eca643e5..fd60a6638 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 844281536..dcf88ac88 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 6bc1faaa0..d3fa78668 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 000000000..c11475c95 --- /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 000000000..9f024d929 --- /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