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>
This commit is contained in:
Lev Roland-Kalb
2026-02-28 11:58:10 -05:00
committed by GitHub
parent 45a228a645
commit 5e95d9a36f
13 changed files with 180 additions and 12 deletions

View File

@@ -85,6 +85,7 @@ 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. * **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 ### 3.4 Recent Books Screen

View File

@@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Přejít na %"
STR_GO_HOME_BUTTON: "Přejít Domů" STR_GO_HOME_BUTTON: "Přejít Domů"
STR_SYNC_PROGRESS: "Průběh synchronizace" STR_SYNC_PROGRESS: "Průběh synchronizace"
STR_DELETE_CACHE: "Smazat mezipaměť knihy" STR_DELETE_CACHE: "Smazat mezipaměť knihy"
STR_DELETE: "Smazat"
STR_CHAPTER_PREFIX: "Kapitola:" STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "stránek |" STR_PAGES_SEPARATOR: "stránek |"
STR_BOOK_PREFIX: "Kniha:" STR_BOOK_PREFIX: "Kniha:"

View File

@@ -297,6 +297,7 @@ STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home" STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress" STR_SYNC_PROGRESS: "Sync Progress"
STR_DELETE_CACHE: "Delete Book Cache" STR_DELETE_CACHE: "Delete Book Cache"
STR_DELETE: "Delete"
STR_DISPLAY_QR: "Show page as QR" STR_DISPLAY_QR: "Show page as QR"
STR_CHAPTER_PREFIX: "Chapter: " STR_CHAPTER_PREFIX: "Chapter: "
STR_PAGES_SEPARATOR: " pages | " STR_PAGES_SEPARATOR: " pages | "

View File

@@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Aller à %"
STR_GO_HOME_BUTTON: "Retour Accueil" STR_GO_HOME_BUTTON: "Retour Accueil"
STR_SYNC_PROGRESS: "Synchro progression" STR_SYNC_PROGRESS: "Synchro progression"
STR_DELETE_CACHE: "Supprimer cache livre" STR_DELETE_CACHE: "Supprimer cache livre"
STR_DELETE: "Supprimer"
STR_CHAPTER_PREFIX: "Chapitre : " STR_CHAPTER_PREFIX: "Chapitre : "
STR_PAGES_SEPARATOR: " pages | " STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Livre : " STR_BOOK_PREFIX: "Livre : "

View File

@@ -280,6 +280,7 @@ STR_GO_TO_PERCENT: "Gehe zu %"
STR_GO_HOME_BUTTON: "Zum Anfang" STR_GO_HOME_BUTTON: "Zum Anfang"
STR_SYNC_PROGRESS: "Fortschritt synchronisieren" STR_SYNC_PROGRESS: "Fortschritt synchronisieren"
STR_DELETE_CACHE: "Buch-Cache leeren" STR_DELETE_CACHE: "Buch-Cache leeren"
STR_DELETE: "Löschen"
STR_CHAPTER_PREFIX: "Kapitel:" STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " Seiten | " STR_PAGES_SEPARATOR: " Seiten | "
STR_BOOK_PREFIX: "Buch: " STR_BOOK_PREFIX: "Buch: "

View File

@@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Ir para %"
STR_GO_HOME_BUTTON: "Ir para o início" STR_GO_HOME_BUTTON: "Ir para o início"
STR_SYNC_PROGRESS: "Sincronizar progresso" STR_SYNC_PROGRESS: "Sincronizar progresso"
STR_DELETE_CACHE: "Excluir cache do livro" STR_DELETE_CACHE: "Excluir cache do livro"
STR_DELETE: "Excluir"
STR_CHAPTER_PREFIX: "Capítulo:" STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: "páginas |" STR_PAGES_SEPARATOR: "páginas |"
STR_BOOK_PREFIX: "Livro:" STR_BOOK_PREFIX: "Livro:"

View File

@@ -296,6 +296,7 @@ STR_GO_TO_PERCENT: "Перейти к %"
STR_GO_HOME_BUTTON: "На главную" STR_GO_HOME_BUTTON: "На главную"
STR_SYNC_PROGRESS: "Синхронизировать прогресс" STR_SYNC_PROGRESS: "Синхронизировать прогресс"
STR_DELETE_CACHE: "Удалить кэш книги" STR_DELETE_CACHE: "Удалить кэш книги"
STR_DELETE: "Удалить"
STR_CHAPTER_PREFIX: "Глава:" STR_CHAPTER_PREFIX: "Глава:"
STR_DISPLAY_QR: "Показать страницу в виде QR-кода" STR_DISPLAY_QR: "Показать страницу в виде QR-кода"
STR_PAGES_SEPARATOR: "стр. |" STR_PAGES_SEPARATOR: "стр. |"

View File

@@ -277,6 +277,7 @@ STR_HW_LEFT_LABEL: "Izq. (Tercer botón)"
STR_HW_RIGHT_LABEL: "Der. (Cuarto botón)" STR_HW_RIGHT_LABEL: "Der. (Cuarto botón)"
STR_GO_TO_PERCENT: "Ir a %" STR_GO_TO_PERCENT: "Ir a %"
STR_GO_HOME_BUTTON: "Volver a inicio" STR_GO_HOME_BUTTON: "Volver a inicio"
STR_DELETE: "Borrar"
STR_SYNC_PROGRESS: "Sincronizar progreso de lectura" STR_SYNC_PROGRESS: "Sincronizar progreso de lectura"
STR_DELETE_CACHE: "Borrar caché del libro" STR_DELETE_CACHE: "Borrar caché del libro"
STR_CHAPTER_PREFIX: "Cap.:" STR_CHAPTER_PREFIX: "Cap.:"

View File

@@ -279,6 +279,7 @@ STR_GO_TO_PERCENT: "Gå till %"
STR_GO_HOME_BUTTON: "Gå Hem" STR_GO_HOME_BUTTON: "Gå Hem"
STR_SYNC_PROGRESS: "Synkroniseringsframsteg" STR_SYNC_PROGRESS: "Synkroniseringsframsteg"
STR_DELETE_CACHE: "Radera bokcache" STR_DELETE_CACHE: "Radera bokcache"
STR_DELETE: "Radera"
STR_CHAPTER_PREFIX: "Kapitel:" STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " sidor | " STR_PAGES_SEPARATOR: " sidor | "
STR_BOOK_PREFIX: "Bok:" STR_BOOK_PREFIX: "Bok:"

View File

@@ -1,11 +1,13 @@
#include "MyLibraryActivity.h" #include "MyLibraryActivity.h"
#include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h> #include <I18n.h>
#include <algorithm> #include <algorithm>
#include "../util/ConfirmationActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
@@ -116,6 +118,14 @@ void MyLibraryActivity::onExit() {
files.clear(); 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() { void MyLibraryActivity::loop() {
// Long press BACK (1s+) goes to root folder // Long press BACK (1s+) goes to root folder
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS && 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); const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (files.empty()) { if (files.empty()) return;
return;
}
if (basepath.back() != '/') basepath += "/"; const std::string& entry = files[selectorIndex];
if (files[selectorIndex].back() == '/') { bool isDirectory = (entry.back() == '/');
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
loadFiles(); if (mappedInput.getHeldTime() >= GO_HOME_MS && !isDirectory) {
selectorIndex = 0; // --- LONG PRESS ACTION: DELETE FILE ---
requestUpdate(); std::string cleanBasePath = basepath;
} else { if (cleanBasePath.back() != '/') cleanBasePath += "/";
onSelectBook(basepath + files[selectorIndex]); 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<ConfirmationActivity>(renderer, mappedInput, heading, entry), handler);
return; 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)) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -9,6 +10,10 @@
class MyLibraryActivity final : public Activity { class MyLibraryActivity final : public Activity {
private: private:
// Deletion
bool pendingSubActivityExit = false;
void clearFileMetadata(const std::string& fullPath);
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
size_t selectorIndex = 0; size_t selectorIndex = 0;

View File

@@ -0,0 +1,76 @@
#include "ConfirmationActivity.h"
#include <I18n.h>
#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;
}
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <functional>
#include <string>
#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;
};