feat: Library improvements - bookmarks, search, and tab navigation
Adds bookmark functionality with persistent storage, quick menu for in-reader actions, Search tab with character picker, and unified tab bar navigation across all library tabs. Includes: - BookmarkStore and BookmarkListActivity for bookmark management - QuickMenuActivity for in-reader quick actions - Reader bookmark integration with visual indicators - Enhanced tab bar with scrolling, overflow indicators, and cursor - Search tab with character picker and result navigation - Consistent tab bar navigation (Up from top enters tab bar mode)
This commit is contained in:
commit
5dab3ad5a3
@ -1,12 +1,11 @@
|
|||||||
## Feature Requests:
|
## Feature Requests:
|
||||||
|
|
||||||
1) Ability to clear all books and clear individual books from Recents.
|
1) search for books/library
|
||||||
2) Bookmarks
|
2) Bookmarks
|
||||||
2a) crosspoint logo on firmware flashing screen
|
3) quick menu
|
||||||
3) ability to add/remove books from lists on device.
|
4) crosspoint logo on firmware flashing screen
|
||||||
4) quick menu
|
5) ability to add/remove books from lists on device.
|
||||||
5) hide "system folders" from files view
|
6) hide "system folders" from files view
|
||||||
- dictionaries/
|
- dictionaries/
|
||||||
6) sorting options for files view
|
7) sorting options for files view
|
||||||
7) search for books/library
|
|
||||||
8) Time spent reading tracking
|
8) Time spent reading tracking
|
||||||
301
src/BookmarkStore.cpp
Normal file
301
src/BookmarkStore.cpp
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
#include "BookmarkStore.h"
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
|
// Include the BookmarkedBook struct definition
|
||||||
|
#include "activities/home/MyLibraryActivity.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint8_t BOOKMARKS_FILE_VERSION = 1;
|
||||||
|
constexpr char BOOKMARKS_FILENAME[] = "bookmarks.bin";
|
||||||
|
constexpr int MAX_BOOKMARKS_PER_BOOK = 100;
|
||||||
|
|
||||||
|
// Get cache directory path for a book (same logic as BookManager)
|
||||||
|
std::string getCacheDir(const std::string& bookPath) {
|
||||||
|
const size_t hash = std::hash<std::string>{}(bookPath);
|
||||||
|
|
||||||
|
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
|
||||||
|
return "/.crosspoint/epub_" + std::to_string(hash);
|
||||||
|
} else if (StringUtils::checkFileExtension(bookPath, ".txt") ||
|
||||||
|
StringUtils::checkFileExtension(bookPath, ".TXT") ||
|
||||||
|
StringUtils::checkFileExtension(bookPath, ".md")) {
|
||||||
|
return "/.crosspoint/txt_" + std::to_string(hash);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string BookmarkStore::getBookmarksFilePath(const std::string& bookPath) {
|
||||||
|
const std::string cacheDir = getCacheDir(bookPath);
|
||||||
|
if (cacheDir.empty()) return "";
|
||||||
|
return cacheDir + "/" + BOOKMARKS_FILENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Bookmark> BookmarkStore::getBookmarks(const std::string& bookPath) {
|
||||||
|
std::vector<Bookmark> bookmarks;
|
||||||
|
loadBookmarks(bookPath, bookmarks);
|
||||||
|
return bookmarks;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& bookmark) {
|
||||||
|
std::vector<Bookmark> bookmarks;
|
||||||
|
loadBookmarks(bookPath, bookmarks);
|
||||||
|
|
||||||
|
// Check if bookmark already exists at this location
|
||||||
|
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
||||||
|
return b.spineIndex == bookmark.spineIndex && b.contentOffset == bookmark.contentOffset;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it != bookmarks.end()) {
|
||||||
|
Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n",
|
||||||
|
millis(), bookmark.spineIndex, bookmark.contentOffset);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new bookmark
|
||||||
|
bookmarks.push_back(bookmark);
|
||||||
|
|
||||||
|
// Trim to max size (remove oldest)
|
||||||
|
if (bookmarks.size() > MAX_BOOKMARKS_PER_BOOK) {
|
||||||
|
// Sort by timestamp and remove oldest
|
||||||
|
std::sort(bookmarks.begin(), bookmarks.end(), [](const Bookmark& a, const Bookmark& b) {
|
||||||
|
return a.timestamp > b.timestamp; // Newest first
|
||||||
|
});
|
||||||
|
bookmarks.resize(MAX_BOOKMARKS_PER_BOOK);
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveBookmarks(bookPath, bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookmarkStore::removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
|
||||||
|
std::vector<Bookmark> bookmarks;
|
||||||
|
loadBookmarks(bookPath, bookmarks);
|
||||||
|
|
||||||
|
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
||||||
|
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it == bookmarks.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarks.erase(it);
|
||||||
|
Serial.printf("[%lu] [BMS] Removed bookmark at spine %u, offset %u\n", millis(), spineIndex, contentOffset);
|
||||||
|
|
||||||
|
return saveBookmarks(bookPath, bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookmarkStore::isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
|
||||||
|
std::vector<Bookmark> bookmarks;
|
||||||
|
loadBookmarks(bookPath, bookmarks);
|
||||||
|
|
||||||
|
return std::any_of(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
|
||||||
|
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int BookmarkStore::getBookmarkCount(const std::string& bookPath) {
|
||||||
|
const std::string filePath = getBookmarksFilePath(bookPath);
|
||||||
|
if (filePath.empty()) return 0;
|
||||||
|
|
||||||
|
FsFile inputFile;
|
||||||
|
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(inputFile, version);
|
||||||
|
if (version != BOOKMARKS_FILE_VERSION) {
|
||||||
|
inputFile.close();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t count;
|
||||||
|
serialization::readPod(inputFile, count);
|
||||||
|
inputFile.close();
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
|
||||||
|
std::vector<BookmarkedBook> result;
|
||||||
|
|
||||||
|
// Scan /.crosspoint/ directory for cache folders with bookmarks
|
||||||
|
auto crosspoint = SdMan.open("/.crosspoint");
|
||||||
|
if (!crosspoint || !crosspoint.isDirectory()) {
|
||||||
|
if (crosspoint) crosspoint.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
crosspoint.rewindDirectory();
|
||||||
|
char name[256];
|
||||||
|
|
||||||
|
for (auto entry = crosspoint.openNextFile(); entry; entry = crosspoint.openNextFile()) {
|
||||||
|
entry.getName(name, sizeof(name));
|
||||||
|
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
entry.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this directory has a bookmarks file
|
||||||
|
std::string dirPath = "/.crosspoint/";
|
||||||
|
dirPath += name;
|
||||||
|
std::string bookmarksPath = dirPath + "/" + BOOKMARKS_FILENAME;
|
||||||
|
|
||||||
|
if (SdMan.exists(bookmarksPath.c_str())) {
|
||||||
|
// Read the bookmarks file to get count and book info
|
||||||
|
FsFile bookmarksFile;
|
||||||
|
if (SdMan.openFileForRead("BMS", bookmarksPath, bookmarksFile)) {
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(bookmarksFile, version);
|
||||||
|
|
||||||
|
if (version == BOOKMARKS_FILE_VERSION) {
|
||||||
|
uint8_t count;
|
||||||
|
serialization::readPod(bookmarksFile, count);
|
||||||
|
|
||||||
|
// Read book metadata (stored at end of file)
|
||||||
|
std::string bookPath, bookTitle, bookAuthor;
|
||||||
|
|
||||||
|
// Skip bookmark entries to get to metadata
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
std::string tempName;
|
||||||
|
uint16_t tempSpine;
|
||||||
|
uint32_t tempOffset, tempTimestamp;
|
||||||
|
uint16_t tempPage;
|
||||||
|
serialization::readString(bookmarksFile, tempName);
|
||||||
|
serialization::readPod(bookmarksFile, tempSpine);
|
||||||
|
serialization::readPod(bookmarksFile, tempOffset);
|
||||||
|
serialization::readPod(bookmarksFile, tempPage);
|
||||||
|
serialization::readPod(bookmarksFile, tempTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read book metadata
|
||||||
|
serialization::readString(bookmarksFile, bookPath);
|
||||||
|
serialization::readString(bookmarksFile, bookTitle);
|
||||||
|
serialization::readString(bookmarksFile, bookAuthor);
|
||||||
|
|
||||||
|
if (!bookPath.empty() && count > 0) {
|
||||||
|
BookmarkedBook book;
|
||||||
|
book.path = bookPath;
|
||||||
|
book.title = bookTitle;
|
||||||
|
book.author = bookAuthor;
|
||||||
|
book.bookmarkCount = count;
|
||||||
|
result.push_back(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bookmarksFile.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.close();
|
||||||
|
}
|
||||||
|
crosspoint.close();
|
||||||
|
|
||||||
|
// Sort by title
|
||||||
|
std::sort(result.begin(), result.end(), [](const BookmarkedBook& a, const BookmarkedBook& b) {
|
||||||
|
return a.title < b.title;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkStore::clearBookmarks(const std::string& bookPath) {
|
||||||
|
const std::string filePath = getBookmarksFilePath(bookPath);
|
||||||
|
if (filePath.empty()) return;
|
||||||
|
|
||||||
|
SdMan.remove(filePath.c_str());
|
||||||
|
Serial.printf("[%lu] [BMS] Cleared all bookmarks for %s\n", millis(), bookPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector<Bookmark>& bookmarks) {
|
||||||
|
const std::string cacheDir = getCacheDir(bookPath);
|
||||||
|
if (cacheDir.empty()) return false;
|
||||||
|
|
||||||
|
// Make sure the directory exists
|
||||||
|
SdMan.mkdir(cacheDir.c_str());
|
||||||
|
|
||||||
|
const std::string filePath = cacheDir + "/" + BOOKMARKS_FILENAME;
|
||||||
|
|
||||||
|
FsFile outputFile;
|
||||||
|
if (!SdMan.openFileForWrite("BMS", filePath, outputFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::writePod(outputFile, BOOKMARKS_FILE_VERSION);
|
||||||
|
const uint8_t count = static_cast<uint8_t>(std::min(bookmarks.size(), static_cast<size_t>(255)));
|
||||||
|
serialization::writePod(outputFile, count);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
const auto& bookmark = bookmarks[i];
|
||||||
|
serialization::writeString(outputFile, bookmark.name);
|
||||||
|
serialization::writePod(outputFile, bookmark.spineIndex);
|
||||||
|
serialization::writePod(outputFile, bookmark.contentOffset);
|
||||||
|
serialization::writePod(outputFile, bookmark.pageNumber);
|
||||||
|
serialization::writePod(outputFile, bookmark.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store book metadata at end (for getBooksWithBookmarks to read)
|
||||||
|
// Extract title from path if we don't have it
|
||||||
|
std::string title = bookPath;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::writeString(outputFile, bookPath);
|
||||||
|
serialization::writeString(outputFile, title);
|
||||||
|
serialization::writeString(outputFile, ""); // Author (not always available)
|
||||||
|
|
||||||
|
outputFile.close();
|
||||||
|
Serial.printf("[%lu] [BMS] Bookmarks saved for %s (%d entries)\n", millis(), bookPath.c_str(), count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector<Bookmark>& bookmarks) {
|
||||||
|
bookmarks.clear();
|
||||||
|
|
||||||
|
const std::string filePath = getBookmarksFilePath(bookPath);
|
||||||
|
if (filePath.empty()) return false;
|
||||||
|
|
||||||
|
FsFile inputFile;
|
||||||
|
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(inputFile, version);
|
||||||
|
if (version != BOOKMARKS_FILE_VERSION) {
|
||||||
|
Serial.printf("[%lu] [BMS] Unknown bookmarks file version: %u\n", millis(), version);
|
||||||
|
inputFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t count;
|
||||||
|
serialization::readPod(inputFile, count);
|
||||||
|
bookmarks.reserve(count);
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
Bookmark bookmark;
|
||||||
|
serialization::readString(inputFile, bookmark.name);
|
||||||
|
serialization::readPod(inputFile, bookmark.spineIndex);
|
||||||
|
serialization::readPod(inputFile, bookmark.contentOffset);
|
||||||
|
serialization::readPod(inputFile, bookmark.pageNumber);
|
||||||
|
serialization::readPod(inputFile, bookmark.timestamp);
|
||||||
|
bookmarks.push_back(bookmark);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFile.close();
|
||||||
|
Serial.printf("[%lu] [BMS] Bookmarks loaded for %s (%d entries)\n", millis(), bookPath.c_str(), count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
63
src/BookmarkStore.h
Normal file
63
src/BookmarkStore.h
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Forward declaration for BookmarkedBook (used by MyLibraryActivity)
|
||||||
|
struct BookmarkedBook;
|
||||||
|
|
||||||
|
// A single bookmark within a book
|
||||||
|
struct Bookmark {
|
||||||
|
std::string name; // Display name (e.g., "Chapter 1 - Page 42")
|
||||||
|
uint16_t spineIndex = 0; // For EPUB: which spine item
|
||||||
|
uint32_t contentOffset = 0; // Content offset for stable positioning
|
||||||
|
uint16_t pageNumber = 0; // Page number at time of bookmark (for display)
|
||||||
|
uint32_t timestamp = 0; // Unix timestamp when created
|
||||||
|
|
||||||
|
bool operator==(const Bookmark& other) const {
|
||||||
|
return spineIndex == other.spineIndex && contentOffset == other.contentOffset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BookmarkStore manages bookmarks for books.
|
||||||
|
* Bookmarks are stored per-book in the book's cache directory:
|
||||||
|
* /.crosspoint/{epub_|txt_}<hash>/bookmarks.bin
|
||||||
|
*
|
||||||
|
* This is a static utility class, not a singleton, since bookmarks
|
||||||
|
* are loaded/saved on demand for specific books.
|
||||||
|
*/
|
||||||
|
class BookmarkStore {
|
||||||
|
public:
|
||||||
|
// Get all bookmarks for a book
|
||||||
|
static std::vector<Bookmark> getBookmarks(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Add a bookmark to a book
|
||||||
|
// Returns true if added, false if bookmark already exists at that location
|
||||||
|
static bool addBookmark(const std::string& bookPath, const Bookmark& bookmark);
|
||||||
|
|
||||||
|
// Remove a bookmark from a book by content offset
|
||||||
|
// Returns true if removed, false if not found
|
||||||
|
static bool removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
|
||||||
|
|
||||||
|
// Check if a specific page is bookmarked
|
||||||
|
static bool isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
|
||||||
|
|
||||||
|
// Get count of bookmarks for a book (without loading all data)
|
||||||
|
static int getBookmarkCount(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Get all books that have bookmarks (for Bookmarks tab)
|
||||||
|
static std::vector<BookmarkedBook> getBooksWithBookmarks();
|
||||||
|
|
||||||
|
// Delete all bookmarks for a book
|
||||||
|
static void clearBookmarks(const std::string& bookPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Get the bookmarks file path for a book
|
||||||
|
static std::string getBookmarksFilePath(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Save bookmarks to file
|
||||||
|
static bool saveBookmarks(const std::string& bookPath, const std::vector<Bookmark>& bookmarks);
|
||||||
|
|
||||||
|
// Load bookmarks from file
|
||||||
|
static bool loadBookmarks(const std::string& bookPath, std::vector<Bookmark>& bookmarks);
|
||||||
|
};
|
||||||
@ -86,7 +86,7 @@ class CrossPointSettings {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Short power button press actions
|
// Short power button press actions
|
||||||
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, DICTIONARY = 3, SHORT_PWRBTN_COUNT };
|
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, DICTIONARY = 3, QUICK_MENU = 4, SHORT_PWRBTN_COUNT };
|
||||||
|
|
||||||
// Hide battery percentage
|
// Hide battery percentage
|
||||||
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
|
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
|
||||||
|
|||||||
@ -90,33 +90,155 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si
|
|||||||
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
|
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs) {
|
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs, int selectedIndex, bool showCursor) {
|
||||||
constexpr int tabPadding = 20; // Horizontal padding between tabs
|
constexpr int tabPadding = 20; // Horizontal padding between tabs
|
||||||
constexpr int leftMargin = 20; // Left margin for first tab
|
constexpr int leftMargin = 20; // Left margin for first tab
|
||||||
|
constexpr int rightMargin = 20; // Right margin
|
||||||
constexpr int underlineHeight = 2; // Height of selection underline
|
constexpr int underlineHeight = 2; // Height of selection underline
|
||||||
constexpr int underlineGap = 4; // Gap between text and underline
|
constexpr int underlineGap = 4; // Gap between text and underline
|
||||||
|
constexpr int cursorPadding = 4; // Space between bullet cursor and tab text
|
||||||
|
constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators
|
||||||
|
|
||||||
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||||
const int tabBarHeight = lineHeight + underlineGap + underlineHeight;
|
const int tabBarHeight = lineHeight + underlineGap + underlineHeight;
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
||||||
|
const int bezelRight = renderer.getBezelOffsetRight();
|
||||||
|
const int availableWidth = screenWidth - bezelLeft - bezelRight - leftMargin - rightMargin;
|
||||||
|
|
||||||
int currentX = leftMargin;
|
// Find selected index if not provided
|
||||||
|
if (selectedIndex < 0) {
|
||||||
|
for (size_t i = 0; i < tabs.size(); i++) {
|
||||||
|
if (tabs[i].selected) {
|
||||||
|
selectedIndex = static_cast<int>(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total width of all tabs and individual tab widths
|
||||||
|
std::vector<int> tabWidths;
|
||||||
|
int totalWidth = 0;
|
||||||
for (const auto& tab : tabs) {
|
for (const auto& tab : tabs) {
|
||||||
const int textWidth =
|
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||||
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
tabWidths.push_back(textWidth);
|
||||||
|
totalWidth += textWidth;
|
||||||
|
}
|
||||||
|
totalWidth += static_cast<int>(tabs.size() - 1) * tabPadding; // Add padding between tabs
|
||||||
|
|
||||||
|
// Calculate scroll offset to keep selected tab visible
|
||||||
|
int scrollOffset = 0;
|
||||||
|
if (totalWidth > availableWidth && selectedIndex >= 0) {
|
||||||
|
// Calculate position of selected tab
|
||||||
|
int selectedStart = 0;
|
||||||
|
for (int i = 0; i < selectedIndex; i++) {
|
||||||
|
selectedStart += tabWidths[i] + tabPadding;
|
||||||
|
}
|
||||||
|
int selectedEnd = selectedStart + tabWidths[selectedIndex];
|
||||||
|
|
||||||
|
// If selected tab would be cut off on the right, scroll left
|
||||||
|
if (selectedEnd > availableWidth) {
|
||||||
|
scrollOffset = selectedEnd - availableWidth + tabPadding;
|
||||||
|
}
|
||||||
|
// If selected tab would be cut off on the left (after scrolling), adjust
|
||||||
|
if (selectedStart - scrollOffset < 0) {
|
||||||
|
scrollOffset = selectedStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentX = leftMargin + bezelLeft - scrollOffset;
|
||||||
|
|
||||||
|
// Bullet cursor settings
|
||||||
|
constexpr int bulletRadius = 3;
|
||||||
|
const int bulletCenterY = y + lineHeight / 2;
|
||||||
|
|
||||||
|
// Calculate visible area boundaries (leave room for overflow indicators)
|
||||||
|
const bool hasLeftOverflow = scrollOffset > 0;
|
||||||
|
const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth;
|
||||||
|
const int visibleLeft = bezelLeft + (hasLeftOverflow ? overflowIndicatorWidth : 0);
|
||||||
|
const int visibleRight = screenWidth - bezelRight - (hasRightOverflow ? overflowIndicatorWidth : 0);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < tabs.size(); i++) {
|
||||||
|
const auto& tab = tabs[i];
|
||||||
|
const int textWidth = tabWidths[i];
|
||||||
|
|
||||||
|
// Only draw if at least partially visible (accounting for overflow indicator space)
|
||||||
|
if (currentX + textWidth > visibleLeft && currentX < visibleRight) {
|
||||||
|
// Draw bullet cursor before selected tab when showCursor is true
|
||||||
|
if (showCursor && tab.selected) {
|
||||||
|
// Draw filled circle using distance-squared check
|
||||||
|
const int bulletCenterX = currentX - cursorPadding - bulletRadius;
|
||||||
|
const int radiusSq = bulletRadius * bulletRadius;
|
||||||
|
for (int dy = -bulletRadius; dy <= bulletRadius; ++dy) {
|
||||||
|
for (int dx = -bulletRadius; dx <= bulletRadius; ++dx) {
|
||||||
|
if (dx * dx + dy * dy <= radiusSq) {
|
||||||
|
renderer.drawPixel(bulletCenterX + dx, bulletCenterY + dy, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw tab label
|
// Draw tab label
|
||||||
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
|
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
|
||||||
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||||
|
|
||||||
|
// Draw bullet cursor after selected tab when showCursor is true
|
||||||
|
if (showCursor && tab.selected) {
|
||||||
|
// Draw filled circle using distance-squared check
|
||||||
|
const int bulletCenterX = currentX + textWidth + cursorPadding + bulletRadius;
|
||||||
|
const int radiusSq = bulletRadius * bulletRadius;
|
||||||
|
for (int dy = -bulletRadius; dy <= bulletRadius; ++dy) {
|
||||||
|
for (int dx = -bulletRadius; dx <= bulletRadius; ++dx) {
|
||||||
|
if (dx * dx + dy * dy <= radiusSq) {
|
||||||
|
renderer.drawPixel(bulletCenterX + dx, bulletCenterY + dy, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw underline for selected tab
|
// Draw underline for selected tab
|
||||||
if (tab.selected) {
|
if (tab.selected) {
|
||||||
renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight);
|
renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentX += textWidth + tabPadding;
|
currentX += textWidth + tabPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw overflow indicators if content extends beyond visible area
|
||||||
|
if (totalWidth > availableWidth) {
|
||||||
|
constexpr int triangleHeight = 12; // Height of the triangle (vertical)
|
||||||
|
constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated
|
||||||
|
const int triangleCenterY = y + lineHeight / 2;
|
||||||
|
|
||||||
|
// Left overflow indicator (more content to the left) - thin triangle pointing left
|
||||||
|
if (scrollOffset > 0) {
|
||||||
|
// Clear background behind indicator to hide any overlapping text
|
||||||
|
renderer.fillRect(bezelLeft, y - 2, overflowIndicatorWidth, lineHeight + 4, false);
|
||||||
|
// Draw left-pointing triangle: point on left, base on right
|
||||||
|
const int tipX = bezelLeft + 2;
|
||||||
|
for (int i = 0; i < triangleWidth; ++i) {
|
||||||
|
// Scale height based on position (0 at tip, full height at base)
|
||||||
|
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
|
||||||
|
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight,
|
||||||
|
tipX + i, triangleCenterY + lineHalfHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Right overflow indicator (more content to the right) - thin triangle pointing right
|
||||||
|
if (scrollOffset < totalWidth - availableWidth) {
|
||||||
|
// Clear background behind indicator to hide any overlapping text
|
||||||
|
renderer.fillRect(screenWidth - bezelRight - overflowIndicatorWidth, y - 2, overflowIndicatorWidth, lineHeight + 4, false);
|
||||||
|
// Draw right-pointing triangle: base on left, point on right
|
||||||
|
const int baseX = screenWidth - bezelRight - 2 - triangleWidth;
|
||||||
|
for (int i = 0; i < triangleWidth; ++i) {
|
||||||
|
// Scale height based on position (full height at base, 0 at tip)
|
||||||
|
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
|
||||||
|
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight,
|
||||||
|
baseX + i, triangleCenterY + lineHalfHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tabBarHeight;
|
return tabBarHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,9 @@ class ScreenComponents {
|
|||||||
|
|
||||||
// Draw a horizontal tab bar with underline indicator for selected tab
|
// Draw a horizontal tab bar with underline indicator for selected tab
|
||||||
// Returns the height of the tab bar (for positioning content below)
|
// Returns the height of the tab bar (for positioning content below)
|
||||||
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs);
|
// When selectedIndex is provided, tabs scroll so the selected tab is visible
|
||||||
|
// When showCursor is true, bullet indicators are drawn around the selected tab
|
||||||
|
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs, int selectedIndex = -1, bool showCursor = false);
|
||||||
|
|
||||||
// Draw a scroll/page indicator on the right side of the screen
|
// Draw a scroll/page indicator on the right side of the screen
|
||||||
// Shows up/down arrows and current page fraction (e.g., "1/3")
|
// Shows up/down arrows and current page fraction (e.g., "1/3")
|
||||||
|
|||||||
262
src/activities/home/BookmarkListActivity.cpp
Normal file
262
src/activities/home/BookmarkListActivity.cpp
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
#include "BookmarkListActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "BookmarkStore.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "ScreenComponents.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int BASE_TAB_BAR_Y = 15;
|
||||||
|
constexpr int BASE_CONTENT_START_Y = 60;
|
||||||
|
constexpr int LINE_HEIGHT = 50; // Taller for bookmark name + location
|
||||||
|
constexpr int BASE_LEFT_MARGIN = 20;
|
||||||
|
constexpr int BASE_RIGHT_MARGIN = 40;
|
||||||
|
constexpr unsigned long ACTION_MENU_MS = 700; // Long press to delete
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int BookmarkListActivity::getPageItems() const {
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
const int bottomBarHeight = 60;
|
||||||
|
const int bezelTop = renderer.getBezelOffsetTop();
|
||||||
|
const int bezelBottom = renderer.getBezelOffsetBottom();
|
||||||
|
const int availableHeight = screenHeight - (BASE_CONTENT_START_Y + bezelTop) - bottomBarHeight - bezelBottom;
|
||||||
|
int items = availableHeight / LINE_HEIGHT;
|
||||||
|
if (items < 1) items = 1;
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BookmarkListActivity::getTotalPages() const {
|
||||||
|
const int itemCount = static_cast<int>(bookmarks.size());
|
||||||
|
const int pageItems = getPageItems();
|
||||||
|
if (itemCount == 0) return 1;
|
||||||
|
return (itemCount + pageItems - 1) / pageItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BookmarkListActivity::getCurrentPage() const {
|
||||||
|
const int pageItems = getPageItems();
|
||||||
|
return selectorIndex / pageItems + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::loadBookmarks() {
|
||||||
|
bookmarks = BookmarkStore::getBookmarks(bookPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<BookmarkListActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
loadBookmarks();
|
||||||
|
selectorIndex = 0;
|
||||||
|
updateRequired = true;
|
||||||
|
|
||||||
|
xTaskCreate(&BookmarkListActivity::taskTrampoline, "BookmarkListTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
|
||||||
|
bookmarks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::loop() {
|
||||||
|
// Handle confirmation state
|
||||||
|
if (uiState == UIState::Confirming) {
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
uiState = UIState::Normal;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
// Delete the bookmark
|
||||||
|
if (!bookmarks.empty() && selectorIndex < static_cast<int>(bookmarks.size())) {
|
||||||
|
const auto& bm = bookmarks[selectorIndex];
|
||||||
|
BookmarkStore::removeBookmark(bookPath, bm.spineIndex, bm.contentOffset);
|
||||||
|
loadBookmarks();
|
||||||
|
|
||||||
|
// Adjust selector if needed
|
||||||
|
if (selectorIndex >= static_cast<int>(bookmarks.size()) && !bookmarks.empty()) {
|
||||||
|
selectorIndex = static_cast<int>(bookmarks.size()) - 1;
|
||||||
|
} else if (bookmarks.empty()) {
|
||||||
|
selectorIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiState = UIState::Normal;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal state handling
|
||||||
|
const int itemCount = static_cast<int>(bookmarks.size());
|
||||||
|
const int pageItems = getPageItems();
|
||||||
|
|
||||||
|
// Long press Confirm to delete bookmark
|
||||||
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
||||||
|
mappedInput.getHeldTime() >= ACTION_MENU_MS && !bookmarks.empty() &&
|
||||||
|
selectorIndex < itemCount) {
|
||||||
|
uiState = UIState::Confirming;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short press Confirm - navigate to bookmark
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (mappedInput.getHeldTime() >= ACTION_MENU_MS) {
|
||||||
|
return; // Was a long press
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookmarks.empty() && selectorIndex < itemCount) {
|
||||||
|
const auto& bm = bookmarks[selectorIndex];
|
||||||
|
onSelectBookmark(bm.spineIndex, bm.contentOffset);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
onGoBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
||||||
|
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||||
|
|
||||||
|
if (upReleased && itemCount > 0) {
|
||||||
|
selectorIndex = (selectorIndex + itemCount - 1) % itemCount;
|
||||||
|
updateRequired = true;
|
||||||
|
} else if (downReleased && itemCount > 0) {
|
||||||
|
selectorIndex = (selectorIndex + 1) % itemCount;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
render();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::render() const {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
if (uiState == UIState::Confirming) {
|
||||||
|
renderConfirmation();
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const int pageItems = getPageItems();
|
||||||
|
const int itemCount = static_cast<int>(bookmarks.size());
|
||||||
|
|
||||||
|
// Calculate bezel-adjusted margins
|
||||||
|
const int bezelTop = renderer.getBezelOffsetTop();
|
||||||
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
||||||
|
const int bezelRight = renderer.getBezelOffsetRight();
|
||||||
|
const int bezelBottom = renderer.getBezelOffsetBottom();
|
||||||
|
const int CONTENT_START_Y = BASE_CONTENT_START_Y + bezelTop;
|
||||||
|
const int LEFT_MARGIN = BASE_LEFT_MARGIN + bezelLeft;
|
||||||
|
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
|
||||||
|
|
||||||
|
// Draw title
|
||||||
|
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, bookTitle.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
||||||
|
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, BASE_TAB_BAR_Y + bezelTop, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
if (itemCount == 0) {
|
||||||
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No bookmarks");
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
|
|
||||||
|
// Draw selection highlight
|
||||||
|
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
|
||||||
|
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
|
||||||
|
|
||||||
|
// Draw items
|
||||||
|
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
||||||
|
const auto& bm = bookmarks[i];
|
||||||
|
const int y = CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT;
|
||||||
|
const bool isSelected = (i == selectorIndex);
|
||||||
|
|
||||||
|
// Line 1: Bookmark name
|
||||||
|
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, bm.name.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 2, truncatedName.c_str(), !isSelected);
|
||||||
|
|
||||||
|
// Line 2: Location info
|
||||||
|
std::string locText = "Page " + std::to_string(bm.pageNumber + 1);
|
||||||
|
renderer.drawText(SMALL_FONT_ID, LEFT_MARGIN, y + 26, locText.c_str(), !isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw scroll indicator
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
const int contentHeight = screenHeight - CONTENT_START_Y - 60 - bezelBottom;
|
||||||
|
ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight);
|
||||||
|
|
||||||
|
// Draw side button hints
|
||||||
|
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
|
||||||
|
|
||||||
|
// Draw bottom button hints
|
||||||
|
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "Go to", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookmarkListActivity::renderConfirmation() const {
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Title
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 20, "Delete Bookmark?", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Show bookmark name
|
||||||
|
if (!bookmarks.empty() && selectorIndex < static_cast<int>(bookmarks.size())) {
|
||||||
|
const auto& bm = bookmarks[selectorIndex];
|
||||||
|
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, bm.name.c_str(), pageWidth - 40);
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, truncatedName.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning text
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 20, "This cannot be undone.");
|
||||||
|
|
||||||
|
// Draw bottom button hints
|
||||||
|
const auto labels = mappedInput.mapLabels("\xc2\xab Cancel", "Delete", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
}
|
||||||
65
src/activities/home/BookmarkListActivity.h
Normal file
65
src/activities/home/BookmarkListActivity.h
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
#include "BookmarkStore.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BookmarkListActivity displays all bookmarks for a specific book.
|
||||||
|
* - Short press: Navigate to bookmark location
|
||||||
|
* - Long press Confirm: Delete bookmark (with confirmation)
|
||||||
|
* - Back: Return to previous screen
|
||||||
|
*/
|
||||||
|
class BookmarkListActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
enum class UIState { Normal, Confirming };
|
||||||
|
|
||||||
|
private:
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
|
||||||
|
std::string bookPath;
|
||||||
|
std::string bookTitle;
|
||||||
|
std::vector<Bookmark> bookmarks;
|
||||||
|
int selectorIndex = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
UIState uiState = UIState::Normal;
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
const std::function<void()> onGoBack;
|
||||||
|
const std::function<void(uint16_t spineIndex, uint32_t contentOffset)> onSelectBookmark;
|
||||||
|
|
||||||
|
// Number of items that fit on a page
|
||||||
|
int getPageItems() const;
|
||||||
|
int getTotalPages() const;
|
||||||
|
int getCurrentPage() const;
|
||||||
|
|
||||||
|
// Data loading
|
||||||
|
void loadBookmarks();
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render() const;
|
||||||
|
void renderConfirmation() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BookmarkListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::string& bookPath, const std::string& bookTitle,
|
||||||
|
const std::function<void()>& onGoBack,
|
||||||
|
const std::function<void(uint16_t spineIndex, uint32_t contentOffset)>& onSelectBookmark)
|
||||||
|
: Activity("BookmarkList", renderer, mappedInput),
|
||||||
|
bookPath(bookPath),
|
||||||
|
bookTitle(bookTitle),
|
||||||
|
onGoBack(onGoBack),
|
||||||
|
onSelectBookmark(onSelectBookmark) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@ -19,9 +19,25 @@ struct ThumbExistsCache {
|
|||||||
bool exists = false; // Whether thumbnail exists
|
bool exists = false; // Whether thumbnail exists
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Search result for the Search tab
|
||||||
|
struct SearchResult {
|
||||||
|
std::string path;
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
int matchScore = 0; // Higher = better match
|
||||||
|
};
|
||||||
|
|
||||||
|
// Book with bookmarks info for the Bookmarks tab
|
||||||
|
struct BookmarkedBook {
|
||||||
|
std::string path;
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
int bookmarkCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class MyLibraryActivity final : public Activity {
|
class MyLibraryActivity final : public Activity {
|
||||||
public:
|
public:
|
||||||
enum class Tab { Recent, Lists, Files };
|
enum class Tab { Recent, Lists, Bookmarks, Search, Files };
|
||||||
enum class UIState { Normal, ActionMenu, Confirming, ListActionMenu, ListConfirmingDelete, ClearAllRecentsConfirming };
|
enum class UIState { Normal, ActionMenu, Confirming, ListActionMenu, ListConfirmingDelete, ClearAllRecentsConfirming };
|
||||||
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
|
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
|
||||||
|
|
||||||
@ -32,6 +48,7 @@ class MyLibraryActivity final : public Activity {
|
|||||||
Tab currentTab = Tab::Recent;
|
Tab currentTab = Tab::Recent;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
bool inTabBar = false; // true = focus on tab bar for switching tabs (all tabs)
|
||||||
|
|
||||||
// Action menu state
|
// Action menu state
|
||||||
UIState uiState = UIState::Normal;
|
UIState uiState = UIState::Normal;
|
||||||
@ -61,6 +78,17 @@ class MyLibraryActivity final : public Activity {
|
|||||||
int listMenuSelection = 0; // 0 = Pin/Unpin, 1 = Delete
|
int listMenuSelection = 0; // 0 = Pin/Unpin, 1 = Delete
|
||||||
std::string listActionTargetName;
|
std::string listActionTargetName;
|
||||||
|
|
||||||
|
// Bookmarks tab state
|
||||||
|
std::vector<BookmarkedBook> bookmarkedBooks;
|
||||||
|
|
||||||
|
// Search tab state
|
||||||
|
std::string searchQuery;
|
||||||
|
std::vector<SearchResult> searchResults;
|
||||||
|
std::vector<SearchResult> allBooks; // Cached index of all books
|
||||||
|
std::vector<char> searchCharacters; // Dynamic character set from library
|
||||||
|
int searchCharIndex = 0; // Current position in character picker
|
||||||
|
bool searchInResults = false; // true = navigating results, false = in character picker
|
||||||
|
|
||||||
// Files tab state (from FileSelectionActivity)
|
// Files tab state (from FileSelectionActivity)
|
||||||
std::string basepath = "/";
|
std::string basepath = "/";
|
||||||
std::vector<std::string> files;
|
std::vector<std::string> files;
|
||||||
@ -69,6 +97,7 @@ class MyLibraryActivity final : public Activity {
|
|||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
const std::function<void(const std::string& path, Tab fromTab)> onSelectBook;
|
const std::function<void(const std::string& path, Tab fromTab)> onSelectBook;
|
||||||
const std::function<void(const std::string& listName)> onSelectList;
|
const std::function<void(const std::string& listName)> onSelectList;
|
||||||
|
const std::function<void(const std::string& path, const std::string& title)> onSelectBookmarkedBook;
|
||||||
|
|
||||||
// Number of items that fit on a page
|
// Number of items that fit on a page
|
||||||
int getPageItems() const;
|
int getPageItems() const;
|
||||||
@ -79,6 +108,9 @@ class MyLibraryActivity final : public Activity {
|
|||||||
// Data loading
|
// Data loading
|
||||||
void loadRecentBooks();
|
void loadRecentBooks();
|
||||||
void loadLists();
|
void loadLists();
|
||||||
|
void loadBookmarkedBooks();
|
||||||
|
void loadAllBooks();
|
||||||
|
void updateSearchResults();
|
||||||
void loadFiles();
|
void loadFiles();
|
||||||
size_t findEntry(const std::string& name) const;
|
size_t findEntry(const std::string& name) const;
|
||||||
|
|
||||||
@ -88,10 +120,16 @@ class MyLibraryActivity final : public Activity {
|
|||||||
void render() const;
|
void render() const;
|
||||||
void renderRecentTab() const;
|
void renderRecentTab() const;
|
||||||
void renderListsTab() const;
|
void renderListsTab() const;
|
||||||
|
void renderBookmarksTab() const;
|
||||||
|
void renderSearchTab() const;
|
||||||
void renderFilesTab() const;
|
void renderFilesTab() const;
|
||||||
void renderActionMenu() const;
|
void renderActionMenu() const;
|
||||||
void renderConfirmation() const;
|
void renderConfirmation() const;
|
||||||
|
|
||||||
|
// Search character picker helpers
|
||||||
|
void buildSearchCharacters();
|
||||||
|
void renderCharacterPicker(int y) const;
|
||||||
|
|
||||||
// Action handling
|
// Action handling
|
||||||
void openActionMenu();
|
void openActionMenu();
|
||||||
void executeAction();
|
void executeAction();
|
||||||
@ -114,13 +152,15 @@ class MyLibraryActivity final : public Activity {
|
|||||||
const std::function<void()>& onGoHome,
|
const std::function<void()>& onGoHome,
|
||||||
const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook,
|
const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook,
|
||||||
const std::function<void(const std::string& listName)>& onSelectList,
|
const std::function<void(const std::string& listName)>& onSelectList,
|
||||||
|
const std::function<void(const std::string& path, const std::string& title)>& onSelectBookmarkedBook = nullptr,
|
||||||
Tab initialTab = Tab::Recent, std::string initialPath = "/")
|
Tab initialTab = Tab::Recent, std::string initialPath = "/")
|
||||||
: Activity("MyLibrary", renderer, mappedInput),
|
: Activity("MyLibrary", renderer, mappedInput),
|
||||||
currentTab(initialTab),
|
currentTab(initialTab),
|
||||||
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
||||||
onGoHome(onGoHome),
|
onGoHome(onGoHome),
|
||||||
onSelectBook(onSelectBook),
|
onSelectBook(onSelectBook),
|
||||||
onSelectList(onSelectList) {}
|
onSelectList(onSelectList),
|
||||||
|
onSelectBookmarkedBook(onSelectBookmarkedBook) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "BookManager.h"
|
#include "BookManager.h"
|
||||||
|
#include "BookmarkStore.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderChapterSelectionActivity.h"
|
||||||
@ -17,6 +18,7 @@
|
|||||||
#include "activities/dictionary/DictionaryMenuActivity.h"
|
#include "activities/dictionary/DictionaryMenuActivity.h"
|
||||||
#include "activities/dictionary/DictionarySearchActivity.h"
|
#include "activities/dictionary/DictionarySearchActivity.h"
|
||||||
#include "activities/dictionary/EpubWordSelectionActivity.h"
|
#include "activities/dictionary/EpubWordSelectionActivity.h"
|
||||||
|
#include "activities/util/QuickMenuActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -366,6 +368,149 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quick Menu power button press
|
||||||
|
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::QUICK_MENU &&
|
||||||
|
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
|
||||||
|
// Check if current page is bookmarked
|
||||||
|
bool isBookmarked = false;
|
||||||
|
if (section) {
|
||||||
|
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
||||||
|
isBookmarked = BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new QuickMenuActivity(
|
||||||
|
renderer, mappedInput,
|
||||||
|
[this](QuickMenuAction action) {
|
||||||
|
// Cache values before exitActivity
|
||||||
|
EpubReaderActivity* self = this;
|
||||||
|
GfxRenderer& cachedRenderer = renderer;
|
||||||
|
MappedInputManager& cachedMappedInput = mappedInput;
|
||||||
|
Section* cachedSection = section.get();
|
||||||
|
SemaphoreHandle_t cachedMutex = renderingMutex;
|
||||||
|
|
||||||
|
exitActivity();
|
||||||
|
|
||||||
|
if (action == QuickMenuAction::DICTIONARY) {
|
||||||
|
// Open dictionary menu
|
||||||
|
self->enterNewActivity(new DictionaryMenuActivity(
|
||||||
|
cachedRenderer, cachedMappedInput,
|
||||||
|
[self](DictionaryMode mode) {
|
||||||
|
GfxRenderer& r = self->renderer;
|
||||||
|
MappedInputManager& m = self->mappedInput;
|
||||||
|
Section* s = self->section.get();
|
||||||
|
SemaphoreHandle_t mtx = self->renderingMutex;
|
||||||
|
|
||||||
|
self->exitActivity();
|
||||||
|
|
||||||
|
if (mode == DictionaryMode::ENTER_WORD) {
|
||||||
|
self->enterNewActivity(new DictionarySearchActivity(r, m,
|
||||||
|
[self]() {
|
||||||
|
self->exitActivity();
|
||||||
|
self->updateRequired = true;
|
||||||
|
}, ""));
|
||||||
|
} else if (s) {
|
||||||
|
xSemaphoreTake(mtx, portMAX_DELAY);
|
||||||
|
auto page = s->loadPageFromSectionFile();
|
||||||
|
if (page) {
|
||||||
|
int mt, mr, mb, ml;
|
||||||
|
r.getOrientedViewableTRBL(&mt, &mr, &mb, &ml);
|
||||||
|
mt += SETTINGS.screenMargin;
|
||||||
|
ml += SETTINGS.screenMargin;
|
||||||
|
const int fontId = SETTINGS.getReaderFontId();
|
||||||
|
|
||||||
|
self->enterNewActivity(new EpubWordSelectionActivity(
|
||||||
|
r, m, std::move(page), fontId, ml, mt,
|
||||||
|
[self](const std::string& word) {
|
||||||
|
self->exitActivity();
|
||||||
|
self->enterNewActivity(new DictionarySearchActivity(
|
||||||
|
self->renderer, self->mappedInput,
|
||||||
|
[self]() {
|
||||||
|
self->exitActivity();
|
||||||
|
self->updateRequired = true;
|
||||||
|
}, word));
|
||||||
|
},
|
||||||
|
[self]() {
|
||||||
|
self->exitActivity();
|
||||||
|
self->updateRequired = true;
|
||||||
|
}));
|
||||||
|
xSemaphoreGive(mtx);
|
||||||
|
} else {
|
||||||
|
xSemaphoreGive(mtx);
|
||||||
|
self->updateRequired = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self->updateRequired = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[self]() {
|
||||||
|
self->exitActivity();
|
||||||
|
self->updateRequired = true;
|
||||||
|
},
|
||||||
|
self->section != nullptr));
|
||||||
|
} else if (action == QuickMenuAction::ADD_BOOKMARK) {
|
||||||
|
// Toggle bookmark on current page
|
||||||
|
if (self->section) {
|
||||||
|
const uint32_t contentOffset = self->section->getContentOffsetForPage(self->section->currentPage);
|
||||||
|
const std::string& bookPath = self->epub->getPath();
|
||||||
|
|
||||||
|
if (BookmarkStore::isPageBookmarked(bookPath, self->currentSpineIndex, contentOffset)) {
|
||||||
|
// Remove bookmark
|
||||||
|
BookmarkStore::removeBookmark(bookPath, self->currentSpineIndex, contentOffset);
|
||||||
|
} else {
|
||||||
|
// Add bookmark with auto-generated name
|
||||||
|
Bookmark bm;
|
||||||
|
bm.spineIndex = self->currentSpineIndex;
|
||||||
|
bm.contentOffset = contentOffset;
|
||||||
|
bm.pageNumber = self->section->currentPage;
|
||||||
|
bm.timestamp = millis() / 1000; // Approximate timestamp
|
||||||
|
|
||||||
|
// Generate name: "Chapter - Page X" or fallback
|
||||||
|
std::string chapterTitle;
|
||||||
|
const int tocIndex = self->epub->getTocIndexForSpineIndex(self->currentSpineIndex);
|
||||||
|
if (tocIndex >= 0) {
|
||||||
|
chapterTitle = self->epub->getTocItem(tocIndex).title;
|
||||||
|
}
|
||||||
|
if (!chapterTitle.empty()) {
|
||||||
|
bm.name = chapterTitle + " - Page " + std::to_string(self->section->currentPage + 1);
|
||||||
|
} else {
|
||||||
|
bm.name = "Page " + std::to_string(self->section->currentPage + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookmarkStore::addBookmark(bookPath, bm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self->updateRequired = true;
|
||||||
|
} else if (action == QuickMenuAction::CLEAR_CACHE) {
|
||||||
|
// Navigate to Clear Cache activity
|
||||||
|
if (self->onGoToClearCache) {
|
||||||
|
xSemaphoreGive(cachedMutex);
|
||||||
|
self->onGoToClearCache();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->updateRequired = true;
|
||||||
|
} else if (action == QuickMenuAction::GO_TO_SETTINGS) {
|
||||||
|
// Navigate to Settings activity
|
||||||
|
if (self->onGoToSettings) {
|
||||||
|
xSemaphoreGive(cachedMutex);
|
||||||
|
self->onGoToSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->updateRequired = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[this]() {
|
||||||
|
EpubReaderActivity* self = this;
|
||||||
|
exitActivity();
|
||||||
|
self->updateRequired = true;
|
||||||
|
},
|
||||||
|
isBookmarked));
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||||
@ -632,6 +777,24 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
const int orientedMarginRight, const int orientedMarginBottom,
|
const int orientedMarginRight, const int orientedMarginBottom,
|
||||||
const int orientedMarginLeft) {
|
const int orientedMarginLeft) {
|
||||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||||
|
|
||||||
|
// Draw bookmark indicator (folded corner) if this page is bookmarked
|
||||||
|
if (section) {
|
||||||
|
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
|
||||||
|
if (BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset)) {
|
||||||
|
// Draw folded corner in top-right
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
constexpr int cornerSize = 20;
|
||||||
|
const int cornerX = screenWidth - orientedMarginRight - cornerSize;
|
||||||
|
const int cornerY = orientedMarginTop;
|
||||||
|
|
||||||
|
// Draw triangle (folded corner effect)
|
||||||
|
const int xPoints[3] = {cornerX, cornerX + cornerSize, cornerX + cornerSize};
|
||||||
|
const int yPoints[3] = {cornerY, cornerY, cornerY + cornerSize};
|
||||||
|
renderer.fillPolygon(xPoints, yPoints, 3, true); // Black triangle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
const std::function<void()> onGoToClearCache;
|
||||||
|
const std::function<void()> onGoToSettings;
|
||||||
|
|
||||||
// End-of-book prompt state
|
// End-of-book prompt state
|
||||||
bool showingEndOfBookPrompt = false;
|
bool showingEndOfBookPrompt = false;
|
||||||
@ -38,11 +40,15 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
|
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
|
||||||
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome)
|
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome,
|
||||||
|
const std::function<void()>& onGoToClearCache = nullptr,
|
||||||
|
const std::function<void()>& onGoToSettings = nullptr)
|
||||||
: ActivityWithSubactivity("EpubReader", renderer, mappedInput),
|
: ActivityWithSubactivity("EpubReader", renderer, mappedInput),
|
||||||
epub(std::move(epub)),
|
epub(std::move(epub)),
|
||||||
onGoBack(onGoBack),
|
onGoBack(onGoBack),
|
||||||
onGoHome(onGoHome) {}
|
onGoHome(onGoHome),
|
||||||
|
onGoToClearCache(onGoToClearCache),
|
||||||
|
onGoToSettings(onGoToSettings) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@ -62,7 +62,11 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
|||||||
currentBookPath = epubPath;
|
currentBookPath = epubPath;
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new EpubReaderActivity(
|
enterNewActivity(new EpubReaderActivity(
|
||||||
renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); }));
|
renderer, mappedInput, std::move(epub),
|
||||||
|
[this, epubPath] { goToLibrary(epubPath); },
|
||||||
|
[this] { onGoBack(); },
|
||||||
|
onGoToClearCache,
|
||||||
|
onGoToSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
||||||
|
|||||||
@ -13,6 +13,8 @@ class ReaderActivity final : public ActivityWithSubactivity {
|
|||||||
MyLibraryActivity::Tab libraryTab; // Track which tab to return to
|
MyLibraryActivity::Tab libraryTab; // Track which tab to return to
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary;
|
const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary;
|
||||||
|
const std::function<void()> onGoToClearCache;
|
||||||
|
const std::function<void()> onGoToSettings;
|
||||||
static std::unique_ptr<Epub> loadEpub(const std::string& path);
|
static std::unique_ptr<Epub> loadEpub(const std::string& path);
|
||||||
static std::unique_ptr<Txt> loadTxt(const std::string& path);
|
static std::unique_ptr<Txt> loadTxt(const std::string& path);
|
||||||
static bool isTxtFile(const std::string& path);
|
static bool isTxtFile(const std::string& path);
|
||||||
@ -25,11 +27,15 @@ class ReaderActivity final : public ActivityWithSubactivity {
|
|||||||
public:
|
public:
|
||||||
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
|
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
|
||||||
MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack,
|
MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack,
|
||||||
const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary)
|
const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary,
|
||||||
|
const std::function<void()>& onGoToClearCache = nullptr,
|
||||||
|
const std::function<void()>& onGoToSettings = nullptr)
|
||||||
: ActivityWithSubactivity("Reader", renderer, mappedInput),
|
: ActivityWithSubactivity("Reader", renderer, mappedInput),
|
||||||
initialBookPath(std::move(initialBookPath)),
|
initialBookPath(std::move(initialBookPath)),
|
||||||
libraryTab(libraryTab),
|
libraryTab(libraryTab),
|
||||||
onGoBack(onGoBack),
|
onGoBack(onGoBack),
|
||||||
onGoToLibrary(onGoToLibrary) {}
|
onGoToLibrary(onGoToLibrary),
|
||||||
|
onGoToClearCache(onGoToClearCache),
|
||||||
|
onGoToSettings(onGoToSettings) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,6 +20,8 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
|||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
const std::function<void()> onGoToClearCache;
|
||||||
|
const std::function<void()> onGoToSettings;
|
||||||
|
|
||||||
// End-of-book prompt state
|
// End-of-book prompt state
|
||||||
bool showingEndOfBookPrompt = false;
|
bool showingEndOfBookPrompt = false;
|
||||||
@ -56,11 +58,15 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt,
|
explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt,
|
||||||
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome)
|
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome,
|
||||||
|
const std::function<void()>& onGoToClearCache = nullptr,
|
||||||
|
const std::function<void()>& onGoToSettings = nullptr)
|
||||||
: ActivityWithSubactivity("TxtReader", renderer, mappedInput),
|
: ActivityWithSubactivity("TxtReader", renderer, mappedInput),
|
||||||
txt(std::move(txt)),
|
txt(std::move(txt)),
|
||||||
onGoBack(onGoBack),
|
onGoBack(onGoBack),
|
||||||
onGoHome(onGoHome) {}
|
onGoHome(onGoHome),
|
||||||
|
onGoToClearCache(onGoToClearCache),
|
||||||
|
onGoToSettings(onGoToSettings) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@ -84,7 +84,7 @@ const SettingInfo controlsSettings[controlsSettingsCount] = {
|
|||||||
{"Prev, Next", "Next, Prev"}),
|
{"Prev, Next", "Next, Prev"}),
|
||||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
||||||
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn,
|
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn,
|
||||||
{"Ignore", "Sleep", "Page Turn", "Dictionary"})};
|
{"Ignore", "Sleep", "Page Turn", "Dictionary", "Quick Menu"})};
|
||||||
|
|
||||||
constexpr int systemSettingsCount = 4;
|
constexpr int systemSettingsCount = 4;
|
||||||
const SettingInfo systemSettings[systemSettingsCount] = {
|
const SettingInfo systemSettings[systemSettingsCount] = {
|
||||||
|
|||||||
173
src/activities/util/QuickMenuActivity.cpp
Normal file
173
src/activities/util/QuickMenuActivity.cpp
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
#include "QuickMenuActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int MENU_ITEM_COUNT = 4;
|
||||||
|
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Settings"};
|
||||||
|
const char* MENU_DESCRIPTIONS_ADD[MENU_ITEM_COUNT] = {
|
||||||
|
"Look up a word",
|
||||||
|
"Add bookmark to this page",
|
||||||
|
"Free up storage space",
|
||||||
|
"Open settings menu"
|
||||||
|
};
|
||||||
|
const char* MENU_DESCRIPTIONS_REMOVE[MENU_ITEM_COUNT] = {
|
||||||
|
"Look up a word",
|
||||||
|
"Remove bookmark from this page",
|
||||||
|
"Free up storage space",
|
||||||
|
"Open settings menu"
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void QuickMenuActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<QuickMenuActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickMenuActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
// Reset selection
|
||||||
|
selectedIndex = 0;
|
||||||
|
|
||||||
|
// Trigger first update
|
||||||
|
updateRequired = true;
|
||||||
|
|
||||||
|
xTaskCreate(&QuickMenuActivity::taskTrampoline, "QuickMenuTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickMenuActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
|
||||||
|
// Wait until not rendering to delete task
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickMenuActivity::loop() {
|
||||||
|
// Handle back button - cancel
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle confirm button - select current option
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
QuickMenuAction action;
|
||||||
|
switch (selectedIndex) {
|
||||||
|
case 0:
|
||||||
|
action = QuickMenuAction::DICTIONARY;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
action = QuickMenuAction::ADD_BOOKMARK;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
action = QuickMenuAction::CLEAR_CACHE;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
default:
|
||||||
|
action = QuickMenuAction::GO_TO_SETTINGS;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
onActionSelected(action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
||||||
|
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
||||||
|
|
||||||
|
if (prevPressed) {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
|
||||||
|
updateRequired = true;
|
||||||
|
} else if (nextPressed) {
|
||||||
|
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickMenuActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
render();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickMenuActivity::render() const {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Get bezel offsets
|
||||||
|
const int bezelTop = renderer.getBezelOffsetTop();
|
||||||
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
||||||
|
const int bezelRight = renderer.getBezelOffsetRight();
|
||||||
|
const int bezelBottom = renderer.getBezelOffsetBottom();
|
||||||
|
|
||||||
|
// Calculate usable content area
|
||||||
|
const int marginLeft = 20 + bezelLeft;
|
||||||
|
const int marginRight = 20 + bezelRight;
|
||||||
|
const int marginTop = 15 + bezelTop;
|
||||||
|
const int contentWidth = pageWidth - marginLeft - marginRight;
|
||||||
|
const int contentHeight = pageHeight - marginTop - 60 - bezelBottom; // 60 for button hints
|
||||||
|
|
||||||
|
// Draw header
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Quick Menu", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Select descriptions based on bookmark state
|
||||||
|
const char* const* descriptions = isPageBookmarked ? MENU_DESCRIPTIONS_REMOVE : MENU_DESCRIPTIONS_ADD;
|
||||||
|
|
||||||
|
// Draw menu items centered in content area
|
||||||
|
constexpr int itemHeight = 50; // Height for each menu item (including description)
|
||||||
|
const int startY = marginTop + (contentHeight - (MENU_ITEM_COUNT * itemHeight)) / 2;
|
||||||
|
|
||||||
|
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
|
||||||
|
const int itemY = startY + i * itemHeight;
|
||||||
|
const bool isSelected = (i == selectedIndex);
|
||||||
|
|
||||||
|
// Draw selection highlight (black fill) for selected item
|
||||||
|
if (isSelected) {
|
||||||
|
renderer.fillRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw menu item text
|
||||||
|
const char* itemText = MENU_ITEMS[i];
|
||||||
|
// For bookmark item, show different text based on state
|
||||||
|
if (i == 1) {
|
||||||
|
itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark";
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected);
|
||||||
|
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[i], !isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw help text at bottom
|
||||||
|
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "Select", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
46
src/activities/util/QuickMenuActivity.h
Normal file
46
src/activities/util/QuickMenuActivity.h
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
|
||||||
|
// Enum for quick menu selection
|
||||||
|
enum class QuickMenuAction { DICTIONARY, ADD_BOOKMARK, CLEAR_CACHE, GO_TO_SETTINGS };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QuickMenuActivity presents a quick access menu triggered by short power button press.
|
||||||
|
* Options:
|
||||||
|
* - "Dictionary" - Look up a word
|
||||||
|
* - "Add/Remove Bookmark" - Toggle bookmark on current page
|
||||||
|
*
|
||||||
|
* The onActionSelected callback is called with the user's choice.
|
||||||
|
* The onCancel callback is called if the user presses back.
|
||||||
|
*/
|
||||||
|
class QuickMenuActivity final : public Activity {
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
int selectedIndex = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
const std::function<void(QuickMenuAction)> onActionSelected;
|
||||||
|
const std::function<void()> onCancel;
|
||||||
|
const bool isPageBookmarked; // True if current page already has a bookmark
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit QuickMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::function<void(QuickMenuAction)>& onActionSelected,
|
||||||
|
const std::function<void()>& onCancel, bool isPageBookmarked = false)
|
||||||
|
: Activity("QuickMenu", renderer, mappedInput),
|
||||||
|
onActionSelected(onActionSelected),
|
||||||
|
onCancel(onCancel),
|
||||||
|
isPageBookmarked(isPageBookmarked) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
36
src/main.cpp
36
src/main.cpp
@ -21,11 +21,13 @@
|
|||||||
#include "activities/boot_sleep/BootActivity.h"
|
#include "activities/boot_sleep/BootActivity.h"
|
||||||
#include "activities/boot_sleep/SleepActivity.h"
|
#include "activities/boot_sleep/SleepActivity.h"
|
||||||
#include "activities/browser/OpdsBookBrowserActivity.h"
|
#include "activities/browser/OpdsBookBrowserActivity.h"
|
||||||
|
#include "activities/home/BookmarkListActivity.h"
|
||||||
#include "activities/home/HomeActivity.h"
|
#include "activities/home/HomeActivity.h"
|
||||||
#include "activities/home/ListViewActivity.h"
|
#include "activities/home/ListViewActivity.h"
|
||||||
#include "activities/home/MyLibraryActivity.h"
|
#include "activities/home/MyLibraryActivity.h"
|
||||||
#include "activities/network/CrossPointWebServerActivity.h"
|
#include "activities/network/CrossPointWebServerActivity.h"
|
||||||
#include "activities/reader/ReaderActivity.h"
|
#include "activities/reader/ReaderActivity.h"
|
||||||
|
#include "activities/settings/ClearCacheActivity.h"
|
||||||
#include "activities/settings/SettingsActivity.h"
|
#include "activities/settings/SettingsActivity.h"
|
||||||
#include "activities/util/FullScreenMessageActivity.h"
|
#include "activities/util/FullScreenMessageActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@ -336,10 +338,13 @@ void enterDeepSleep() {
|
|||||||
void onGoHome();
|
void onGoHome();
|
||||||
void onGoToMyLibrary();
|
void onGoToMyLibrary();
|
||||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab);
|
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab);
|
||||||
|
void onGoToClearCache();
|
||||||
|
void onGoToSettings();
|
||||||
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
|
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(
|
enterNewActivity(
|
||||||
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab));
|
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab,
|
||||||
|
onGoToClearCache, onGoToSettings));
|
||||||
}
|
}
|
||||||
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
|
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
|
||||||
|
|
||||||
@ -348,7 +353,7 @@ void onGoToReaderFromList(const std::string& bookPath) {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
// When opening from a list, treat it like opening from Recent (will return to list view via back)
|
// When opening from a list, treat it like opening from Recent (will return to list view via back)
|
||||||
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, MyLibraryActivity::Tab::Recent, onGoHome,
|
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, MyLibraryActivity::Tab::Recent, onGoHome,
|
||||||
onGoToMyLibraryWithTab));
|
onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
// View a specific list
|
// View a specific list
|
||||||
@ -358,6 +363,22 @@ void onGoToListView(const std::string& listName) {
|
|||||||
new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
|
new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// View bookmarks for a specific book
|
||||||
|
void onGoToBookmarkList(const std::string& bookPath, const std::string& bookTitle) {
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new BookmarkListActivity(
|
||||||
|
renderer, mappedInputManager, bookPath, bookTitle,
|
||||||
|
onGoToMyLibrary, // On back, return to library
|
||||||
|
[bookPath](uint16_t spineIndex, uint32_t contentOffset) {
|
||||||
|
// Navigate to bookmark location in the book
|
||||||
|
// For now, just open the book (TODO: pass bookmark location to reader)
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath,
|
||||||
|
MyLibraryActivity::Tab::Bookmarks, onGoHome, onGoToMyLibraryWithTab,
|
||||||
|
onGoToClearCache, onGoToSettings));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Go to pinned list (if exists) or Lists tab
|
// Go to pinned list (if exists) or Lists tab
|
||||||
void onGoToListsOrPinned() {
|
void onGoToListsOrPinned() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
@ -368,7 +389,7 @@ void onGoToListsOrPinned() {
|
|||||||
} else {
|
} else {
|
||||||
// Go to Lists tab in My Library
|
// Go to Lists tab in My Library
|
||||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView,
|
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView,
|
||||||
MyLibraryActivity::Tab::Lists));
|
onGoToBookmarkList, MyLibraryActivity::Tab::Lists));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,14 +403,19 @@ void onGoToSettings() {
|
|||||||
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onGoToClearCache() {
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new ClearCacheActivity(renderer, mappedInputManager, onGoHome));
|
||||||
|
}
|
||||||
|
|
||||||
void onGoToMyLibrary() {
|
void onGoToMyLibrary() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView));
|
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, tab, path));
|
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList, tab, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToBrowser() {
|
void onGoToBrowser() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user