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:
@@ -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.
|
||||
* **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
|
||||
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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 | "
|
||||
|
||||
@@ -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 : "
|
||||
|
||||
@@ -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: "
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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: "стр. |"
|
||||
|
||||
@@ -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.:"
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#include "MyLibraryActivity.h"
|
||||
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#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,21 +139,59 @@ void MyLibraryActivity::loop() {
|
||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (files.empty()) return;
|
||||
|
||||
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()) {
|
||||
return;
|
||||
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;
|
||||
} else {
|
||||
// --- SHORT PRESS ACTION: OPEN/NAVIGATE ---
|
||||
if (basepath.back() != '/') basepath += "/";
|
||||
if (files[selectorIndex].back() == '/') {
|
||||
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
||||
|
||||
if (isDirectory) {
|
||||
basepath += entry.substr(0, entry.length() - 1);
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
requestUpdate();
|
||||
} else {
|
||||
onSelectBook(basepath + files[selectorIndex]);
|
||||
return;
|
||||
onSelectBook(basepath + entry);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
// Short press: go up one directory, or go home if at root
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -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;
|
||||
|
||||
76
src/activities/util/ConfirmationActivity.cpp
Normal file
76
src/activities/util/ConfirmationActivity.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
30
src/activities/util/ConfirmationActivity.h
Normal file
30
src/activities/util/ConfirmationActivity.h
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user