/!\ This PR depends on https://github.com/crosspoint-reader/crosspoint-reader/pull/732 being merged first Also requires the https://github.com/open-x4-epaper/community-sdk/pull/18 PR ## Summary Lyra theme icons on the home menu, in the file browser and on empty book covers     ## Additional Context - Added a function to the open-x4-sdk renderer to draw transparent images - Added a scripts/convert_icon.py script to convert svg/png icons into a C array that can be directly imported into the project. Usage: ```bash python ./scripts/convert_icon.py 'path/to/icon.png' cover 32 32 ``` This will create a components/icons/cover.h file with a C array called CoverIcon, of size 32x32px. Lyra uses icons from https://lucide.dev/icons with a stroke width of 2px, that can be downloaded with any desired size on the site. > The file browser is noticeably slower with the addition of icons, and using an image buffer like on the home page doesn't help very much. Any suggestions to optimize this are welcome. --- ### 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? _**PARTIALLY**_ The icon conversion python script was generated by Copilot as I am not a python dev. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
232 lines
7.0 KiB
C++
232 lines
7.0 KiB
C++
#include "MyLibraryActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
|
|
#include <algorithm>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
#include "util/StringUtils.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 MyLibraryActivity::loadFiles() {
|
|
files.clear();
|
|
|
|
auto root = Storage.open(basepath.c_str());
|
|
if (!root || !root.isDirectory()) {
|
|
if (root) root.close();
|
|
return;
|
|
}
|
|
|
|
root.rewindDirectory();
|
|
|
|
char name[500];
|
|
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
|
|
file.getName(name, sizeof(name));
|
|
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) {
|
|
file.close();
|
|
continue;
|
|
}
|
|
|
|
if (file.isDirectory()) {
|
|
files.emplace_back(std::string(name) + "/");
|
|
} else {
|
|
auto filename = std::string(name);
|
|
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") ||
|
|
StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") ||
|
|
StringUtils::checkFileExtension(filename, ".md")) {
|
|
files.emplace_back(filename);
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
root.close();
|
|
sortFileList(files);
|
|
}
|
|
|
|
void MyLibraryActivity::onEnter() {
|
|
Activity::onEnter();
|
|
|
|
loadFiles();
|
|
selectorIndex = 0;
|
|
|
|
requestUpdate();
|
|
}
|
|
|
|
void MyLibraryActivity::onExit() {
|
|
Activity::onExit();
|
|
files.clear();
|
|
}
|
|
|
|
void MyLibraryActivity::loop() {
|
|
// Long press BACK (1s+) goes to root folder
|
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS &&
|
|
basepath != "/") {
|
|
basepath = "/";
|
|
loadFiles();
|
|
selectorIndex = 0;
|
|
return;
|
|
}
|
|
|
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
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]);
|
|
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() == '/') {
|
|
return filename.substr(0, filename.length() - 1);
|
|
}
|
|
const auto pos = filename.rfind('.');
|
|
return filename.substr(0, pos);
|
|
}
|
|
|
|
void MyLibraryActivity::render(Activity::RenderLock&&) {
|
|
renderer.clearScreen();
|
|
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
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 contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
|
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
|
if (files.empty()) {
|
|
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_BOOKS_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]); });
|
|
}
|
|
|
|
// Help text
|
|
const auto labels = mappedInput.mapLabels(basepath == "/" ? tr(STR_HOME) : tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP),
|
|
tr(STR_DIR_DOWN));
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
|
|
renderer.displayBuffer();
|
|
}
|
|
|
|
size_t MyLibraryActivity::findEntry(const std::string& name) const {
|
|
for (size_t i = 0; i < files.size(); i++)
|
|
if (files[i] == name) return i;
|
|
return 0;
|
|
} |