Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
12 kB · 347 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348#include "FileBrowserActivity.h"
#include <Epub.h>#include <FsHelpers.h>#include <GfxRenderer.h>#include <HalStorage.h>#include <I18n.h>
#include <algorithm>
#include "../util/ConfirmationActivity.h"#include "CrossPointSettings.h"#include "MappedInputManager.h"#include "components/UITheme.h"#include "fontIds.h"
namespace {constexpr unsigned long GO_HOME_MS = 1000;} // namespace
void sortFileList(std::vector<std::string>& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { // Directories first bool isDir1 = str1.back() == '/'; bool isDir2 = str2.back() == '/'; if (isDir1 != isDir2) return isDir1;
// Start naive natural sort const char* s1 = str1.c_str(); const char* s2 = str2.c_str();
// Iterate while both strings have characters while (*s1 && *s2) { // Check if both are at the start of a number if (isdigit(*s1) && isdigit(*s2)) { // Skip leading zeros and track them const char* start1 = s1; const char* start2 = s2; while (*s1 == '0') s1++; while (*s2 == '0') s2++;
// Count digits to compare lengths first int len1 = 0, len2 = 0; while (isdigit(s1[len1])) len1++; while (isdigit(s2[len2])) len2++;
// Different length so return smaller integer value if (len1 != len2) return len1 < len2;
// Same length so compare digit by digit for (int i = 0; i < len1; i++) { if (s1[i] != s2[i]) return s1[i] < s2[i]; }
// Numbers equal so advance pointers s1 += len1; s2 += len2; } else { // Regular case-insensitive character comparison char c1 = tolower(*s1); char c2 = tolower(*s2); if (c1 != c2) return c1 < c2; s1++; s2++; } }
// One string is prefix of other return *s1 == '\0' && *s2 != '\0'; });}
void FileBrowserActivity::loadFiles() { files.clear();
auto root = Storage.open(basepath.c_str()); if (!root || !root.isDirectory()) { return; }
root.rewindDirectory();
char name[500]; for (auto file = root.openNextFile(); file; file = root.openNextFile()) { file.getName(name, sizeof(name)); if ((!SETTINGS.showHiddenFiles && name[0] == '.') || strcmp(name, "System Volume Information") == 0) { continue; }
if (file.isDirectory()) { files.emplace_back(std::string(name) + "/"); } else { std::string_view filename{name}; if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) || FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) || FsHelpers::hasBmpExtension(filename)) { files.emplace_back(filename); } } } sortFileList(files);}
void FileBrowserActivity::onEnter() { Activity::onEnter();
selectorIndex = 0;
auto root = Storage.open(basepath.c_str()); if (!root) { basepath = "/"; loadFiles(); } else if (!root.isDirectory()) { lockLongPressBack = mappedInput.isPressed(MappedInputManager::Button::Back);
const std::string oldPath = basepath; basepath = FsHelpers::extractFolderPath(basepath); loadFiles();
const auto pos = oldPath.find_last_of('/'); const std::string fileName = oldPath.substr(pos + 1); selectorIndex = findEntry(fileName); } else { loadFiles(); }
requestUpdate();}
void FileBrowserActivity::onExit() { Activity::onExit(); files.clear();}
void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) { // Only clear cache for .epub files if (FsHelpers::hasEpubExtension(fullPath)) { Epub(fullPath, "/.crosspoint").clearCache(); LOG_DBG("FileBrowser", "Cleared metadata cache for: %s", fullPath.c_str()); }}
void FileBrowserActivity::loop() { // Long press BACK (1s+) goes to root folder // but Long press BACK (1s+) from ReaderActivity sends us here with the MappedInput already set. // So ignore it the first time. if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS && basepath != "/" && !lockLongPressBack) { basepath = "/"; loadFiles(); selectorIndex = 0; requestUpdate(); return; }
if (lockLongPressBack && mappedInput.wasReleased(MappedInputManager::Button::Back)) { lockLongPressBack = false; return; }
const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing; const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved);
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("FileBrowser", "Attempting to delete: %s", fullPath.c_str()); clearFileMetadata(fullPath); if (Storage.remove(fullPath.c_str())) { LOG_DBG("FileBrowser", "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("FileBrowser", "Failed to delete file: %s", fullPath.c_str()); } } else { LOG_DBG("FileBrowser", "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 (isDirectory) { basepath += entry.substr(0, entry.length() - 1); loadFiles(); selectorIndex = 0; requestUpdate(); } else { onSelectBook(basepath + entry); } } return; }
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { // Short press: go up one directory, or go home if at root if (mappedInput.getHeldTime() < GO_HOME_MS) { if (basepath != "/") { const std::string oldPath = basepath;
basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); if (basepath.empty()) basepath = "/"; loadFiles();
const auto pos = oldPath.find_last_of('/'); const std::string dirName = oldPath.substr(pos + 1) + "/"; selectorIndex = findEntry(dirName);
requestUpdate(); } else { onGoHome(); } } }
int listSize = static_cast<int>(files.size()); buttonNavigator.onNextRelease([this, listSize] { selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); requestUpdate(); });
buttonNavigator.onPreviousRelease([this, listSize] { selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize); requestUpdate(); });
buttonNavigator.onNextContinuous([this, listSize, pageItems] { selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); requestUpdate(); });
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); requestUpdate(); });}
std::string getFileName(std::string filename) { if (filename.back() == '/') { filename.pop_back(); if (!UITheme::getInstance().getTheme().showsFileIcons()) { return "[" + filename + "]"; } return filename; } const auto pos = filename.rfind('.'); return filename.substr(0, pos);}
std::string getFileExtension(std::string filename) { if (filename.back() == '/') { return ""; } const auto pos = filename.rfind('.'); return filename.substr(pos);}
void FileBrowserActivity::render(RenderLock&&) { renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); const auto& metrics = UITheme::getInstance().getMetrics();
std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str());
const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID); const int pathReserved = pathLineHeight + metrics.verticalSpacing; const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing - pathReserved; if (files.empty()) { renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_FILES_FOUND)); } else { GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex, [this](int index) { return getFileName(files[index]); }, nullptr, [this](int index) { return UITheme::getFileIcon(files[index]); }, [this](int index) { return getFileExtension(files[index]); }, false); }
// Full path display { const int pathY = pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - pathLineHeight; const int separatorY = pathY - metrics.verticalSpacing / 2; renderer.drawLine(0, separatorY, pageWidth - 1, separatorY, 3, true); const int pathMaxWidth = pageWidth - metrics.contentSidePadding * 2; // Left-truncate so the deepest directory is always visible const char* pathStr = basepath.c_str(); const char* pathDisplay = pathStr; char leftTruncBuf[256]; if (renderer.getTextWidth(SMALL_FONT_ID, pathStr) > pathMaxWidth) { const char ellipsis[] = "\xe2\x80\xa6"; // UTF-8 ellipsis (…) const int ellipsisWidth = renderer.getTextWidth(SMALL_FONT_ID, ellipsis); const int available = pathMaxWidth - ellipsisWidth; // Walk forward from the start until the suffix fits, skipping UTF-8 continuation bytes const char* p = pathStr; while (*p) { if (renderer.getTextWidth(SMALL_FONT_ID, p) <= available) break; ++p; while (*p && (static_cast<unsigned char>(*p) & 0xC0) == 0x80) ++p; } snprintf(leftTruncBuf, sizeof(leftTruncBuf), "%s%s", ellipsis, p); pathDisplay = leftTruncBuf; } renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, pathY, pathDisplay); }
// Help text const auto labels = mappedInput.mapLabels(basepath == "/" ? tr(STR_HOME) : tr(STR_BACK), files.empty() ? "" : tr(STR_OPEN), files.empty() ? "" : tr(STR_DIR_UP), files.empty() ? "" : tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();}
size_t FileBrowserActivity::findEntry(const std::string& name) const { for (size_t i = 0; i < files.size(); i++) if (files[i] == name) return i; return 0;}