|
|
|
|
@@ -17,6 +17,7 @@ namespace {
|
|
|
|
|
constexpr int TAB_BAR_Y = 15;
|
|
|
|
|
constexpr int CONTENT_START_Y = 60;
|
|
|
|
|
constexpr int LINE_HEIGHT = 30;
|
|
|
|
|
constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items
|
|
|
|
|
constexpr int LEFT_MARGIN = 20;
|
|
|
|
|
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
|
|
|
|
|
|
|
|
|
|
@@ -40,7 +41,8 @@ int MyLibraryActivity::getPageItems() const {
|
|
|
|
|
const int screenHeight = renderer.getScreenHeight();
|
|
|
|
|
const int bottomBarHeight = 60; // Space for button hints
|
|
|
|
|
const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight;
|
|
|
|
|
int items = availableHeight / LINE_HEIGHT;
|
|
|
|
|
const int lineHeight = (currentTab == Tab::Recent) ? RECENTS_LINE_HEIGHT : LINE_HEIGHT;
|
|
|
|
|
int items = availableHeight / lineHeight;
|
|
|
|
|
if (items < 1) {
|
|
|
|
|
items = 1;
|
|
|
|
|
}
|
|
|
|
|
@@ -49,7 +51,7 @@ int MyLibraryActivity::getPageItems() const {
|
|
|
|
|
|
|
|
|
|
int MyLibraryActivity::getCurrentItemCount() const {
|
|
|
|
|
if (currentTab == Tab::Recent) {
|
|
|
|
|
return static_cast<int>(bookTitles.size());
|
|
|
|
|
return static_cast<int>(recentBooks.size());
|
|
|
|
|
}
|
|
|
|
|
return static_cast<int>(files.size());
|
|
|
|
|
}
|
|
|
|
|
@@ -67,34 +69,16 @@ int MyLibraryActivity::getCurrentPage() const {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MyLibraryActivity::loadRecentBooks() {
|
|
|
|
|
constexpr size_t MAX_RECENT_BOOKS = 20;
|
|
|
|
|
|
|
|
|
|
bookTitles.clear();
|
|
|
|
|
bookPaths.clear();
|
|
|
|
|
recentBooks.clear();
|
|
|
|
|
const auto& books = RECENT_BOOKS.getBooks();
|
|
|
|
|
bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS));
|
|
|
|
|
bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS));
|
|
|
|
|
|
|
|
|
|
for (const auto& path : books) {
|
|
|
|
|
// Limit to maximum number of recent books
|
|
|
|
|
if (bookTitles.size() >= MAX_RECENT_BOOKS) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
recentBooks.reserve(books.size());
|
|
|
|
|
|
|
|
|
|
for (const auto& book : books) {
|
|
|
|
|
// Skip if file no longer exists
|
|
|
|
|
if (!SdMan.exists(path.c_str())) {
|
|
|
|
|
if (!SdMan.exists(book.path.c_str())) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract filename from path for display
|
|
|
|
|
std::string title = path;
|
|
|
|
|
const size_t lastSlash = title.find_last_of('/');
|
|
|
|
|
if (lastSlash != std::string::npos) {
|
|
|
|
|
title = title.substr(lastSlash + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookTitles.push_back(title);
|
|
|
|
|
bookPaths.push_back(path);
|
|
|
|
|
recentBooks.push_back(book);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -177,14 +161,13 @@ void MyLibraryActivity::onExit() {
|
|
|
|
|
vSemaphoreDelete(renderingMutex);
|
|
|
|
|
renderingMutex = nullptr;
|
|
|
|
|
|
|
|
|
|
bookTitles.clear();
|
|
|
|
|
bookPaths.clear();
|
|
|
|
|
recentBooks.clear();
|
|
|
|
|
files.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool MyLibraryActivity::isSelectedItemAFile() const {
|
|
|
|
|
if (currentTab == Tab::Recent) {
|
|
|
|
|
return !bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size());
|
|
|
|
|
return !recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size());
|
|
|
|
|
} else {
|
|
|
|
|
// Files tab - check if it's a file (not a directory)
|
|
|
|
|
if (files.empty() || selectorIndex >= static_cast<int>(files.size())) {
|
|
|
|
|
@@ -200,8 +183,18 @@ void MyLibraryActivity::openActionMenu() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentTab == Tab::Recent) {
|
|
|
|
|
actionTargetPath = bookPaths[selectorIndex];
|
|
|
|
|
actionTargetName = bookTitles[selectorIndex];
|
|
|
|
|
const auto& book = recentBooks[selectorIndex];
|
|
|
|
|
actionTargetPath = book.path;
|
|
|
|
|
// Use title if available, otherwise extract from path
|
|
|
|
|
if (!book.title.empty()) {
|
|
|
|
|
actionTargetName = book.title;
|
|
|
|
|
} else {
|
|
|
|
|
actionTargetName = book.path;
|
|
|
|
|
const size_t lastSlash = actionTargetName.find_last_of('/');
|
|
|
|
|
if (lastSlash != std::string::npos) {
|
|
|
|
|
actionTargetName = actionTargetName.substr(lastSlash + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (basepath.back() != '/') {
|
|
|
|
|
actionTargetPath = basepath + "/" + files[selectorIndex];
|
|
|
|
|
@@ -335,8 +328,8 @@ void MyLibraryActivity::loop() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentTab == Tab::Recent) {
|
|
|
|
|
if (!bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size())) {
|
|
|
|
|
onSelectBook(bookPaths[selectorIndex], currentTab);
|
|
|
|
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
|
|
|
|
onSelectBook(recentBooks[selectorIndex].path, currentTab);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Files tab
|
|
|
|
|
@@ -475,7 +468,7 @@ void MyLibraryActivity::render() const {
|
|
|
|
|
void MyLibraryActivity::renderRecentTab() const {
|
|
|
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
|
|
|
const int pageItems = getPageItems();
|
|
|
|
|
const int bookCount = static_cast<int>(bookTitles.size());
|
|
|
|
|
const int bookCount = static_cast<int>(recentBooks.size());
|
|
|
|
|
|
|
|
|
|
if (bookCount == 0) {
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books");
|
|
|
|
|
@@ -485,14 +478,37 @@ void MyLibraryActivity::renderRecentTab() const {
|
|
|
|
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
|
|
|
|
|
|
|
|
|
// Draw selection highlight
|
|
|
|
|
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN,
|
|
|
|
|
LINE_HEIGHT);
|
|
|
|
|
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2,
|
|
|
|
|
pageWidth - RIGHT_MARGIN, RECENTS_LINE_HEIGHT);
|
|
|
|
|
|
|
|
|
|
// Draw items
|
|
|
|
|
for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) {
|
|
|
|
|
auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(),
|
|
|
|
|
i != selectorIndex);
|
|
|
|
|
const auto& book = recentBooks[i];
|
|
|
|
|
const int y = CONTENT_START_Y + (i % pageItems) * RECENTS_LINE_HEIGHT;
|
|
|
|
|
|
|
|
|
|
// Line 1: Title
|
|
|
|
|
std::string title = book.title;
|
|
|
|
|
if (title.empty()) {
|
|
|
|
|
// Fallback for older entries or files without metadata
|
|
|
|
|
title = book.path;
|
|
|
|
|
const size_t lastSlash = title.find_last_of('/');
|
|
|
|
|
if (lastSlash != std::string::npos) {
|
|
|
|
|
title = title.substr(lastSlash + 1);
|
|
|
|
|
}
|
|
|
|
|
const size_t dot = title.find_last_of('.');
|
|
|
|
|
if (dot != std::string::npos) {
|
|
|
|
|
title.resize(dot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
|
|
|
|
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, y + 2, truncatedTitle.c_str(), i != selectorIndex);
|
|
|
|
|
|
|
|
|
|
// Line 2: Author
|
|
|
|
|
if (!book.author.empty()) {
|
|
|
|
|
auto truncatedAuthor =
|
|
|
|
|
renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
|
|
|
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), i != selectorIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|