A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Long Click for File Deletion through File Browser (#909)

## 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: Егор Мартынов <martynovegorOF@yandex.ru>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
Co-authored-by: Zach Nelson <zach@zdnelson.com>

+180 -12
+2 -1
USER_GUIDE.md
··· 84 84 The Browse Files screen acts as a file and folder browser. 85 85 86 86 * **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. 87 - * **Open Selection:** Press **Confirm** to open a folder or read a selected book. 87 + * **Open Selection:** Press **Confirm** to open a folder or read a selected book. 88 + * **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. 88 89 89 90 ### 3.4 Recent Books Screen 90 91
+1
lib/I18n/translations/czech.yaml
··· 279 279 STR_GO_HOME_BUTTON: "Přejít Domů" 280 280 STR_SYNC_PROGRESS: "Průběh synchronizace" 281 281 STR_DELETE_CACHE: "Smazat mezipaměť knihy" 282 + STR_DELETE: "Smazat" 282 283 STR_CHAPTER_PREFIX: "Kapitola:" 283 284 STR_PAGES_SEPARATOR: "stránek |" 284 285 STR_BOOK_PREFIX: "Kniha:"
+1
lib/I18n/translations/english.yaml
··· 297 297 STR_GO_HOME_BUTTON: "Go Home" 298 298 STR_SYNC_PROGRESS: "Sync Progress" 299 299 STR_DELETE_CACHE: "Delete Book Cache" 300 + STR_DELETE: "Delete" 300 301 STR_DISPLAY_QR: "Show page as QR" 301 302 STR_CHAPTER_PREFIX: "Chapter: " 302 303 STR_PAGES_SEPARATOR: " pages | "
+1
lib/I18n/translations/french.yaml
··· 279 279 STR_GO_HOME_BUTTON: "Retour Accueil" 280 280 STR_SYNC_PROGRESS: "Synchro progression" 281 281 STR_DELETE_CACHE: "Supprimer cache livre" 282 + STR_DELETE: "Supprimer" 282 283 STR_CHAPTER_PREFIX: "Chapitre : " 283 284 STR_PAGES_SEPARATOR: " pages | " 284 285 STR_BOOK_PREFIX: "Livre : "
+1
lib/I18n/translations/german.yaml
··· 280 280 STR_GO_HOME_BUTTON: "Zum Anfang" 281 281 STR_SYNC_PROGRESS: "Fortschritt synchronisieren" 282 282 STR_DELETE_CACHE: "Buch-Cache leeren" 283 + STR_DELETE: "Löschen" 283 284 STR_CHAPTER_PREFIX: "Kapitel:" 284 285 STR_PAGES_SEPARATOR: " Seiten | " 285 286 STR_BOOK_PREFIX: "Buch: "
+1
lib/I18n/translations/portuguese.yaml
··· 279 279 STR_GO_HOME_BUTTON: "Ir para o início" 280 280 STR_SYNC_PROGRESS: "Sincronizar progresso" 281 281 STR_DELETE_CACHE: "Excluir cache do livro" 282 + STR_DELETE: "Excluir" 282 283 STR_CHAPTER_PREFIX: "Capítulo:" 283 284 STR_PAGES_SEPARATOR: "páginas |" 284 285 STR_BOOK_PREFIX: "Livro:"
+1
lib/I18n/translations/russian.yaml
··· 296 296 STR_GO_HOME_BUTTON: "На главную" 297 297 STR_SYNC_PROGRESS: "Синхронизировать прогресс" 298 298 STR_DELETE_CACHE: "Удалить кэш книги" 299 + STR_DELETE: "Удалить" 299 300 STR_CHAPTER_PREFIX: "Глава:" 300 301 STR_DISPLAY_QR: "Показать страницу в виде QR-кода" 301 302 STR_PAGES_SEPARATOR: "стр. |"
+1
lib/I18n/translations/spanish.yaml
··· 277 277 STR_HW_RIGHT_LABEL: "Der. (Cuarto botón)" 278 278 STR_GO_TO_PERCENT: "Ir a %" 279 279 STR_GO_HOME_BUTTON: "Volver a inicio" 280 + STR_DELETE: "Borrar" 280 281 STR_SYNC_PROGRESS: "Sincronizar progreso de lectura" 281 282 STR_DELETE_CACHE: "Borrar caché del libro" 282 283 STR_CHAPTER_PREFIX: "Cap.:"
+1
lib/I18n/translations/swedish.yaml
··· 279 279 STR_GO_HOME_BUTTON: "Gå Hem" 280 280 STR_SYNC_PROGRESS: "Synkroniseringsframsteg" 281 281 STR_DELETE_CACHE: "Radera bokcache" 282 + STR_DELETE: "Radera" 282 283 STR_CHAPTER_PREFIX: "Kapitel:" 283 284 STR_PAGES_SEPARATOR: " sidor | " 284 285 STR_BOOK_PREFIX: "Bok:"
+59 -11
src/activities/home/MyLibraryActivity.cpp
··· 1 1 #include "MyLibraryActivity.h" 2 2 3 + #include <Epub.h> 3 4 #include <GfxRenderer.h> 4 5 #include <HalStorage.h> 5 6 #include <I18n.h> 6 7 7 8 #include <algorithm> 8 9 10 + #include "../util/ConfirmationActivity.h" 9 11 #include "MappedInputManager.h" 10 12 #include "components/UITheme.h" 11 13 #include "fontIds.h" ··· 116 118 files.clear(); 117 119 } 118 120 121 + void MyLibraryActivity::clearFileMetadata(const std::string& fullPath) { 122 + // Only clear cache for .epub files 123 + if (StringUtils::checkFileExtension(fullPath, ".epub")) { 124 + Epub(fullPath, "/.crosspoint").clearCache(); 125 + LOG_DBG("MyLibrary", "Cleared metadata cache for: %s", fullPath.c_str()); 126 + } 127 + } 128 + 119 129 void MyLibraryActivity::loop() { 120 130 // Long press BACK (1s+) goes to root folder 121 131 if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS && ··· 129 139 const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); 130 140 131 141 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 132 - if (files.empty()) { 133 - return; 134 - } 142 + if (files.empty()) return; 135 143 136 - if (basepath.back() != '/') basepath += "/"; 137 - if (files[selectorIndex].back() == '/') { 138 - basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); 139 - loadFiles(); 140 - selectorIndex = 0; 141 - requestUpdate(); 142 - } else { 143 - onSelectBook(basepath + files[selectorIndex]); 144 + const std::string& entry = files[selectorIndex]; 145 + bool isDirectory = (entry.back() == '/'); 146 + 147 + if (mappedInput.getHeldTime() >= GO_HOME_MS && !isDirectory) { 148 + // --- LONG PRESS ACTION: DELETE FILE --- 149 + std::string cleanBasePath = basepath; 150 + if (cleanBasePath.back() != '/') cleanBasePath += "/"; 151 + const std::string fullPath = cleanBasePath + entry; 152 + 153 + auto handler = [this, fullPath](const ActivityResult& res) { 154 + if (!res.isCancelled) { 155 + LOG_DBG("MyLibrary", "Attempting to delete: %s", fullPath.c_str()); 156 + clearFileMetadata(fullPath); 157 + if (Storage.remove(fullPath.c_str())) { 158 + LOG_DBG("MyLibrary", "Deleted successfully"); 159 + loadFiles(); 160 + if (files.empty()) { 161 + selectorIndex = 0; 162 + } else if (selectorIndex >= files.size()) { 163 + // Move selection to the new "last" item 164 + selectorIndex = files.size() - 1; 165 + } 166 + 167 + requestUpdate(true); 168 + } else { 169 + LOG_ERR("MyLibrary", "Failed to delete file: %s", fullPath.c_str()); 170 + } 171 + } else { 172 + LOG_DBG("MyLibrary", "Delete cancelled by user"); 173 + } 174 + }; 175 + 176 + std::string heading = tr(STR_DELETE) + std::string("? "); 177 + 178 + startActivityForResult(std::make_unique<ConfirmationActivity>(renderer, mappedInput, heading, entry), handler); 144 179 return; 180 + } else { 181 + // --- SHORT PRESS ACTION: OPEN/NAVIGATE --- 182 + if (basepath.back() != '/') basepath += "/"; 183 + 184 + if (isDirectory) { 185 + basepath += entry.substr(0, entry.length() - 1); 186 + loadFiles(); 187 + selectorIndex = 0; 188 + requestUpdate(); 189 + } else { 190 + onSelectBook(basepath + entry); 191 + } 145 192 } 193 + return; 146 194 } 147 195 148 196 if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
+5
src/activities/home/MyLibraryActivity.h
··· 1 1 #pragma once 2 + 2 3 #include <functional> 3 4 #include <string> 4 5 #include <vector> ··· 9 10 10 11 class MyLibraryActivity final : public Activity { 11 12 private: 13 + // Deletion 14 + bool pendingSubActivityExit = false; 15 + void clearFileMetadata(const std::string& fullPath); 16 + 12 17 ButtonNavigator buttonNavigator; 13 18 14 19 size_t selectorIndex = 0;
+76
src/activities/util/ConfirmationActivity.cpp
··· 1 + #include "ConfirmationActivity.h" 2 + 3 + #include <I18n.h> 4 + 5 + #include "../../components/UITheme.h" 6 + #include "HalDisplay.h" 7 + 8 + ConfirmationActivity::ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 9 + const std::string& heading, const std::string& body) 10 + : Activity("Confirmation", renderer, mappedInput), heading(heading), body(body) {} 11 + 12 + void ConfirmationActivity::onEnter() { 13 + Activity::onEnter(); 14 + 15 + lineHeight = renderer.getLineHeight(fontId); 16 + const int maxWidth = renderer.getScreenWidth() - (margin * 2); 17 + 18 + if (!heading.empty()) { 19 + safeHeading = renderer.truncatedText(fontId, heading.c_str(), maxWidth, EpdFontFamily::BOLD); 20 + } 21 + if (!body.empty()) { 22 + safeBody = renderer.truncatedText(fontId, body.c_str(), maxWidth, EpdFontFamily::REGULAR); 23 + } 24 + 25 + int totalHeight = 0; 26 + if (!safeHeading.empty()) totalHeight += lineHeight; 27 + if (!safeBody.empty()) totalHeight += lineHeight; 28 + if (!safeHeading.empty() && !safeBody.empty()) totalHeight += spacing; 29 + 30 + startY = (renderer.getScreenHeight() - totalHeight) / 2; 31 + LOG_DBG("CONF", "startY: %d", startY); 32 + LOG_DBG("CONF", "Heading: %s", safeHeading.c_str()); 33 + 34 + requestUpdate(true); 35 + } 36 + 37 + void ConfirmationActivity::render(RenderLock&& lock) { 38 + renderer.clearScreen(); 39 + 40 + int currentY = startY; 41 + LOG_DBG("CONF", "currentY: %d", currentY); 42 + // Draw Heading 43 + if (!safeHeading.empty()) { 44 + renderer.drawCenteredText(fontId, currentY, safeHeading.c_str(), true, EpdFontFamily::BOLD); 45 + currentY += lineHeight + spacing; 46 + } 47 + 48 + // Draw Body 49 + if (!safeBody.empty()) { 50 + renderer.drawCenteredText(fontId, currentY, safeBody.c_str(), true, EpdFontFamily::REGULAR); 51 + } 52 + 53 + // Draw UI Elements 54 + const auto labels = mappedInput.mapLabels("", "", I18N.get(StrId::STR_CANCEL), I18N.get(StrId::STR_CONFIRM)); 55 + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 56 + 57 + renderer.displayBuffer(HalDisplay::RefreshMode::FAST_REFRESH); 58 + } 59 + 60 + void ConfirmationActivity::loop() { 61 + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { 62 + ActivityResult res; 63 + res.isCancelled = false; 64 + setResult(std::move(res)); 65 + finish(); 66 + return; 67 + } 68 + 69 + if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { 70 + ActivityResult res; 71 + res.isCancelled = true; 72 + setResult(std::move(res)); 73 + finish(); 74 + return; 75 + } 76 + }
+30
src/activities/util/ConfirmationActivity.h
··· 1 + #pragma once 2 + #include <functional> 3 + #include <string> 4 + 5 + #include "../../fontIds.h" 6 + #include "../Activity.h" 7 + 8 + class ConfirmationActivity : public Activity { 9 + private: 10 + // Input data 11 + std::string heading; 12 + std::string body; 13 + 14 + const int margin = 20; 15 + const int spacing = 30; 16 + const int fontId = UI_10_FONT_ID; 17 + 18 + std::string safeHeading; 19 + std::string safeBody; 20 + int startY = 0; 21 + int lineHeight = 0; 22 + 23 + public: 24 + ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& heading, 25 + const std::string& body); 26 + 27 + void onEnter() override; 28 + void loop() override; 29 + void render(RenderLock&& lock) override; 30 + };