crosspoint-reader/src/activities/home/GridBrowserActivity.cpp

276 lines
8.7 KiB
C++
Raw Normal View History

2025-12-27 16:10:39 -08:00
#include "GridBrowserActivity.h"
#include <GfxRenderer.h>
#include <SD.h>
#include <InputManager.h>
2025-12-27 16:14:38 -08:00
#include <Epub.h>
2025-12-27 16:10:39 -08:00
#include "config.h"
#include "../../images/FolderIcon.h"
#include "../util/Window.h"
namespace {
constexpr int PAGE_ITEMS = 9;
constexpr int SKIP_PAGE_MS = 700;
constexpr int TILE_W = 135;
constexpr int TILE_H = 200;
constexpr int TILE_PADDING = 5;
constexpr int THUMB_W = 90;
constexpr int THUMB_H = 120;
constexpr int TILE_TEXT_H = 60;
2025-12-27 16:10:39 -08:00
constexpr int gridLeftOffset = 37;
2025-12-27 16:10:39 -08:00
constexpr int gridTopOffset = 125;
} // namespace
inline int min(const int a, const int b) { return a < b ? a : b; }
void GridBrowserActivity::sortFileList(std::vector<FileInfo>& strs) {
std::sort(begin(strs), end(strs), [](const FileInfo& f1, const FileInfo& f2) {
if (f1.type == F_DIRECTORY && f2.type != F_DIRECTORY) return true;
if (f1.type != F_DIRECTORY && f2.type == F_DIRECTORY) return false;
return lexicographical_compare(
begin(f1.name), end(f1.name), begin(f2.name), end(f2.name),
[](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); });
});
}
2025-12-27 16:14:38 -08:00
void GridBrowserActivity::displayTaskTrampoline(void* param) {
2025-12-27 16:10:39 -08:00
auto* self = static_cast<GridBrowserActivity*>(param);
self->displayTaskLoop();
}
2025-12-27 16:14:38 -08:00
// void GridBrowserActivity::loadThumbsTaskTrampoline(void* param) {
// auto* self = static_cast<GridBrowserActivity*>(param);
// self->displayTaskLoop();
// }
std::string GridBrowserActivity::loadEpubThumb(std::string path) {
File file;
Epub epubFile(path, "/.crosspoint");
if (!epubFile.load()) {
Serial.printf("[%lu] Failed to load epub: %s\n", millis(), path.c_str());
return "";
}
if (!epubFile.generateCoverBmp(true)) {
Serial.printf("[%lu] Failed to generate epub thumb\n", millis());
return "";
}
std::string thumbPath = epubFile.getThumbBmpPath();
Serial.printf("[%lu] epub has thumb at %s\n", millis(), thumbPath.c_str());
return thumbPath;
}
2025-12-27 16:10:39 -08:00
void GridBrowserActivity::loadFiles() {
files.clear();
selectorIndex = 0;
previousSelectorIndex = -1;
page = 0;
auto root = SD.open(basepath.c_str());
2025-12-27 16:14:38 -08:00
int count = 0;
2025-12-27 16:10:39 -08:00
for (File file = root.openNextFile(); file; file = root.openNextFile()) {
const std::string filename = std::string(file.name());
if (filename.empty() || filename[0] == '.') {
file.close();
continue;
}
if (file.isDirectory()) {
2025-12-27 16:14:38 -08:00
files.emplace_back(FileInfo{ filename, filename, F_DIRECTORY, "" });
2025-12-27 16:10:39 -08:00
} else {
FileType type = F_FILE;
size_t dot = filename.find_first_of('.');
std::string basename = filename;
if (dot != std::string::npos) {
std::string ext = filename.substr(dot);
basename = filename.substr(0, dot);
// lowercase ext for case-insensitive compare
2025-12-27 16:10:39 -08:00
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); });
2025-12-27 16:10:39 -08:00
if (ext == ".epub") {
type = F_EPUB;
2025-12-27 16:14:38 -08:00
// xTaskCreate(&GridBrowserActivity::taskTrampoline, "GridFileBrowserTask",
// 2048, // Stack size
// this, // Parameters
// 1, // Priority
// &displayTaskHandle // Task handle
// );
} else if (ext == ".bmp") {
2025-12-27 16:10:39 -08:00
type = F_BMP;
}
}
if (type != F_FILE) {
2025-12-27 16:14:38 -08:00
files.emplace_back(FileInfo{ filename, basename, type, "" });
2025-12-27 16:10:39 -08:00
}
}
file.close();
2025-12-27 16:14:38 -08:00
count ++;
2025-12-27 16:10:39 -08:00
}
root.close();
Serial.printf("Files loaded\n");
GridBrowserActivity::sortFileList(files);
Serial.printf("Files sorted\n");
}
void GridBrowserActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
loadFiles();
selectorIndex = 0;
page = 0;
// Trigger first render
renderRequired = true;
2025-12-27 16:14:38 -08:00
xTaskCreate(&GridBrowserActivity::displayTaskTrampoline, "GridFileBrowserTask",
2025-12-27 16:10:39 -08:00
8192, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void GridBrowserActivity::onExit() {
Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
files.clear();
}
void GridBrowserActivity::loop() {
const bool prevReleased = inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
const int selected = selectorIndex + page * PAGE_ITEMS;
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (files.empty()) {
return;
}
if (basepath.back() != '/') {
basepath += "/";
}
if (files[selected].type == F_DIRECTORY) {
// open subfolder
basepath += files[selected].name;
loadFiles();
renderRequired = true;
} else {
onSelect(basepath + files[selected].name);
}
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (basepath != "/") {
2025-12-27 16:10:39 -08:00
basepath.resize(basepath.rfind('/'));
2025-12-27 16:10:39 -08:00
if (basepath.empty()) basepath = "/";
loadFiles();
renderRequired = true;
} else {
// At root level, go back home
onGoHome();
}
} else if (prevReleased) {
previousSelectorIndex = selectorIndex;
if (selectorIndex == 0 || skipPage) {
if (page > 0) {
page--;
selectorIndex = 0;
previousSelectorIndex = -1;
renderRequired = true;
}
} else {
selectorIndex--;
updateRequired = true;
}
} else if (nextReleased) {
previousSelectorIndex = selectorIndex;
if (selectorIndex == min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS) - 1 || skipPage) {
if (page < files.size() / PAGE_ITEMS) {
page++;
selectorIndex = 0;
previousSelectorIndex = -1;
renderRequired = true;
}
} else {
selectorIndex++;
updateRequired = true;
}
}
}
void GridBrowserActivity::displayTaskLoop() {
while (true) {
if (renderRequired) {
renderRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render(true);
xSemaphoreGive(renderingMutex);
} else if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// update(true);
render(false);
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void GridBrowserActivity::render(bool clear) const {
if (clear) {
renderer.clearScreen();
auto folderName = basepath == "/" ? "SD card" : basepath.substr(basepath.rfind('/') + 1).c_str();
drawFullscreenWindowFrame(renderer, folderName);
}
2025-12-27 16:14:38 -08:00
if (!files.empty()) {
for (size_t i = 0; i < min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS); i++) {
const auto file = files[i + page * PAGE_ITEMS];
const int16_t tileX = gridLeftOffset + i % 3 * TILE_W;
const int16_t tileY = gridTopOffset + i / 3 * TILE_H;
2025-12-27 16:10:39 -08:00
2025-12-27 16:14:38 -08:00
if (file.type == F_DIRECTORY) {
constexpr int iconOffsetX = (TILE_W - FOLDERICON_WIDTH) / 2;
constexpr int iconOffsetY = (TILE_H - TILE_TEXT_H - FOLDERICON_HEIGHT) / 2;
renderer.drawIcon(FolderIcon, tileX + iconOffsetX, tileY + iconOffsetY, FOLDERICON_WIDTH, FOLDERICON_HEIGHT);
2025-12-27 16:10:39 -08:00
}
2025-12-27 16:14:38 -08:00
if (!file.thumbPath.empty()) {
Serial.printf("Rendering file thumb: %s\n", file.thumbPath.c_str());
File bmpFile = SD.open(file.thumbPath.c_str());
if (bmpFile) {
Bitmap bitmap(bmpFile);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
constexpr int thumbOffsetX = (TILE_W - THUMB_W) / 2;
constexpr int thumbOffsetY = (TILE_H - TILE_TEXT_H - THUMB_H) / 2;
renderer.drawBitmap(bitmap, tileX + thumbOffsetX, tileY + thumbOffsetY, THUMB_W, THUMB_H);
}
2025-12-27 16:10:39 -08:00
}
}
2025-12-27 16:14:38 -08:00
renderer.drawTextInBox(UI_FONT_ID, tileX + TILE_PADDING, tileY + TILE_H - TILE_TEXT_H, TILE_W - 2 * TILE_PADDING, TILE_TEXT_H, file.basename.c_str(), true);
2025-12-27 16:10:39 -08:00
}
2025-12-27 16:14:38 -08:00
update(false);
renderer.displayBuffer();
2025-12-27 16:10:39 -08:00
}
}
void GridBrowserActivity::drawSelectionRectangle(int tileIndex, bool black) const {
renderer.drawRoundedRect(gridLeftOffset + tileIndex % 3 * TILE_W, gridTopOffset + tileIndex / 3 * TILE_H, TILE_W, TILE_H, 2, 5, black);
}
void GridBrowserActivity::update(bool render) const {
// Redraw only changed tiles
// renderer.clearScreen();
if (previousSelectorIndex >= 0) {
drawSelectionRectangle(previousSelectorIndex, false);
}
drawSelectionRectangle(selectorIndex, true);
}