Integrate and extend pr 1372 by andreaturchet
This commit is contained in:
@@ -496,4 +496,8 @@ STR_MENU_READER_TWEAKS: "Reader Tweaks"
|
|||||||
STR_MENU_READER_SPACING: "Spacing"
|
STR_MENU_READER_SPACING: "Spacing"
|
||||||
STR_MENU_KOSYNC_SERVER: "Server Settings"
|
STR_MENU_KOSYNC_SERVER: "Server Settings"
|
||||||
STR_MENU_KOSYNC_AUTH: "Login / Register"
|
STR_MENU_KOSYNC_AUTH: "Login / Register"
|
||||||
STR_FORCE_REFRESH: "Refresh Screen"
|
STR_FORCE_REFRESH: "Refresh Screen"
|
||||||
|
STR_STARRED_PAGES: "Starred Pages"
|
||||||
|
STR_STAR_PAGE: "Star Page"
|
||||||
|
STR_NO_STARRED_PAGES: "No starred pages"
|
||||||
|
STR_PAGE_PREFIX: "p"
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Stores starred/bookmarked pages for a single book.
|
||||||
|
// Persisted as a binary file on SD card within the book's cache directory.
|
||||||
|
class BookmarkStore {
|
||||||
|
public:
|
||||||
|
struct Bookmark {
|
||||||
|
uint16_t spineIndex;
|
||||||
|
uint16_t pageNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load bookmarks from the cache directory (e.g. .crosspoint/epub_<hash>/).
|
||||||
|
void load(const std::string& cachePath) {
|
||||||
|
basePath = cachePath;
|
||||||
|
bookmarks.clear();
|
||||||
|
dirty = false;
|
||||||
|
|
||||||
|
FsFile f;
|
||||||
|
if (!Storage.openFileForRead("BKM", getFilePath(), f)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
if (f.read(reinterpret_cast<uint8_t*>(&version), sizeof(version)) != sizeof(version) || version != FILE_VERSION) {
|
||||||
|
f.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t count;
|
||||||
|
if (f.read(reinterpret_cast<uint8_t*>(&count), sizeof(count)) != sizeof(count) || count > MAX_BOOKMARKS) {
|
||||||
|
LOG_ERR("BKM", "Invalid bookmark count: %u", static_cast<unsigned>(count));
|
||||||
|
f.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarks.reserve(count);
|
||||||
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
|
Bookmark bm;
|
||||||
|
if (f.read(reinterpret_cast<uint8_t*>(&bm.spineIndex), sizeof(bm.spineIndex)) != sizeof(bm.spineIndex) ||
|
||||||
|
f.read(reinterpret_cast<uint8_t*>(&bm.pageNumber), sizeof(bm.pageNumber)) != sizeof(bm.pageNumber)) {
|
||||||
|
LOG_ERR("BKM", "Truncated bookmarks file at entry %d", i);
|
||||||
|
bookmarks.clear();
|
||||||
|
f.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bookmarks.push_back(bm);
|
||||||
|
}
|
||||||
|
|
||||||
|
f.close();
|
||||||
|
LOG_DBG("BKM", "Loaded %d bookmarks", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save bookmarks to SD card (only if changed).
|
||||||
|
void save() {
|
||||||
|
if (!dirty || basePath.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookmarks.size() > UINT16_MAX) {
|
||||||
|
LOG_ERR("BKM", "Too many bookmarks to save: %u", static_cast<unsigned>(bookmarks.size()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile f;
|
||||||
|
if (!Storage.openFileForWrite("BKM", getFilePath(), f)) {
|
||||||
|
LOG_ERR("BKM", "Failed to save bookmarks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto writePodChecked = [&f](const auto& value) {
|
||||||
|
return f.write(reinterpret_cast<const uint8_t*>(&value), sizeof(value)) == sizeof(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint16_t count = static_cast<uint16_t>(bookmarks.size());
|
||||||
|
bool ok = writePodChecked(FILE_VERSION) && writePodChecked(count);
|
||||||
|
|
||||||
|
for (const auto& bm : bookmarks) {
|
||||||
|
ok = ok && writePodChecked(bm.spineIndex) && writePodChecked(bm.pageNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = ok && f.close();
|
||||||
|
if (!ok) {
|
||||||
|
LOG_ERR("BKM", "Failed while writing bookmarks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dirty = false;
|
||||||
|
LOG_DBG("BKM", "Saved %d bookmarks", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle bookmark for the given page. Returns true if now starred, false if removed.
|
||||||
|
bool toggle(uint16_t spineIndex, uint16_t pageNumber) {
|
||||||
|
auto it = find(spineIndex, pageNumber);
|
||||||
|
if (it != bookmarks.end()) {
|
||||||
|
bookmarks.erase(it);
|
||||||
|
dirty = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bookmarks.push_back({spineIndex, pageNumber});
|
||||||
|
dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a page is starred.
|
||||||
|
[[nodiscard]] bool has(uint16_t spineIndex, uint16_t pageNumber) const {
|
||||||
|
return std::any_of(bookmarks.begin(), bookmarks.end(), [spineIndex, pageNumber](const Bookmark& bm) {
|
||||||
|
return bm.spineIndex == spineIndex && bm.pageNumber == pageNumber;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const std::vector<Bookmark>& getAll() const { return bookmarks; }
|
||||||
|
[[nodiscard]] bool isEmpty() const { return bookmarks.empty(); }
|
||||||
|
void markDirty() { dirty = true; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr uint8_t FILE_VERSION = 1;
|
||||||
|
static constexpr uint16_t MAX_BOOKMARKS = 1000;
|
||||||
|
|
||||||
|
std::vector<Bookmark> bookmarks;
|
||||||
|
std::string basePath;
|
||||||
|
bool dirty = false;
|
||||||
|
|
||||||
|
[[nodiscard]] std::string getFilePath() const { return basePath + "/bookmarks.bin"; }
|
||||||
|
|
||||||
|
std::vector<Bookmark>::iterator find(uint16_t spineIndex, uint16_t pageNumber) {
|
||||||
|
return std::find_if(bookmarks.begin(), bookmarks.end(), [spineIndex, pageNumber](const Bookmark& bm) {
|
||||||
|
return bm.spineIndex == spineIndex && bm.pageNumber == pageNumber;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -128,7 +128,15 @@ class CrossPointSettings {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Short power button press actions
|
// Short power button press actions
|
||||||
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, FORCE_REFRESH = 3, FOOTNOTES = 4, SHORT_PWRBTN_COUNT };
|
enum SHORT_PWRBTN {
|
||||||
|
IGNORE = 0,
|
||||||
|
SLEEP = 1,
|
||||||
|
PAGE_TURN = 2,
|
||||||
|
FORCE_REFRESH = 3,
|
||||||
|
FOOTNOTES = 4,
|
||||||
|
STAR_PAGE = 5,
|
||||||
|
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 };
|
||||||
|
|||||||
+4
-4
@@ -124,10 +124,10 @@ inline const std::vector<SettingInfo> list = {
|
|||||||
{StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS),
|
{StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS),
|
||||||
SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip",
|
SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip",
|
||||||
StrId::STR_CAT_CONTROLS),
|
StrId::STR_CAT_CONTROLS),
|
||||||
SettingInfo::Enum(
|
SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
|
||||||
StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
|
{StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH,
|
||||||
{StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES},
|
StrId::STR_FOOTNOTES, StrId::STR_STAR_PAGE},
|
||||||
"shortPwrBtn", StrId::STR_CAT_CONTROLS),
|
"shortPwrBtn", StrId::STR_CAT_CONTROLS),
|
||||||
|
|
||||||
// --- System ---
|
// --- System ---
|
||||||
SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles",
|
SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles",
|
||||||
|
|||||||
@@ -57,8 +57,13 @@ struct FootnoteResult {
|
|||||||
std::string href;
|
std::string href;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct StarredPageResult {
|
||||||
|
int spineIndex = 0;
|
||||||
|
int pageNumber = 0;
|
||||||
|
};
|
||||||
|
|
||||||
using ResultVariant = std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult,
|
using ResultVariant = std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult,
|
||||||
PageResult, SyncResult, NetworkModeResult, FootnoteResult>;
|
PageResult, SyncResult, NetworkModeResult, FootnoteResult, StarredPageResult>;
|
||||||
|
|
||||||
struct ActivityResult {
|
struct ActivityResult {
|
||||||
bool isCancelled = false;
|
bool isCancelled = false;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "QrDisplayActivity.h"
|
#include "QrDisplayActivity.h"
|
||||||
#include "ReaderUtils.h"
|
#include "ReaderUtils.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
#include "StarredPagesActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
#include "util/ScreenshotUtil.h"
|
#include "util/ScreenshotUtil.h"
|
||||||
@@ -118,6 +119,9 @@ void EpubReaderActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load bookmarks for this book
|
||||||
|
bookmarkStore.load(epub->getCachePath());
|
||||||
|
|
||||||
// Save current epub as last opened epub and add to recent books
|
// Save current epub as last opened epub and add to recent books
|
||||||
APP_STATE.openEpubPath = epub->getPath();
|
APP_STATE.openEpubPath = epub->getPath();
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
@@ -139,6 +143,9 @@ void EpubReaderActivity::onExit() {
|
|||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
logReaderMemSnapshot("onExit_before_release");
|
logReaderMemSnapshot("onExit_before_release");
|
||||||
|
|
||||||
|
// Save bookmarks before exit
|
||||||
|
bookmarkStore.save();
|
||||||
|
|
||||||
// Reset orientation back to portrait for the rest of the UI
|
// Reset orientation back to portrait for the rest of the UI
|
||||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||||
|
|
||||||
@@ -212,7 +219,7 @@ void EpubReaderActivity::loop() {
|
|||||||
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||||
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
|
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
|
||||||
bookImageRenderingOverride, SETTINGS.textDarkness),
|
bookImageRenderingOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty()),
|
||||||
[this](const ActivityResult& result) {
|
[this](const ActivityResult& result) {
|
||||||
// Always apply orientation/darkness change even if the menu was cancelled
|
// Always apply orientation/darkness change even if the menu was cancelled
|
||||||
const auto& menu = std::get<MenuResult>(result.data);
|
const auto& menu = std::get<MenuResult>(result.data);
|
||||||
@@ -265,6 +272,16 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Star page toggle via short power button press
|
||||||
|
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE &&
|
||||||
|
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
|
||||||
|
if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) {
|
||||||
|
bookmarkStore.toggle(static_cast<uint16_t>(currentSpineIndex), static_cast<uint16_t>(section->currentPage));
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||||
if (!prevTriggered && !nextTriggered) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
@@ -499,6 +516,22 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
requestUpdate();
|
requestUpdate();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EpubReaderMenuActivity::MenuAction::STARRED_PAGES: {
|
||||||
|
startActivityForResult(
|
||||||
|
std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore.getAll(), epub),
|
||||||
|
[this](const ActivityResult& result) {
|
||||||
|
if (!result.isCancelled) {
|
||||||
|
const auto& starred = std::get<StarredPageResult>(result.data);
|
||||||
|
if (currentSpineIndex != starred.spineIndex || !section || section->currentPage != starred.pageNumber) {
|
||||||
|
RenderLock lock(*this);
|
||||||
|
currentSpineIndex = starred.spineIndex;
|
||||||
|
nextPageNumber = starred.pageNumber;
|
||||||
|
section.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
@@ -514,6 +547,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
epub->clearCache();
|
epub->clearCache();
|
||||||
epub->setupCacheDir();
|
epub->setupCacheDir();
|
||||||
saveProgress(backupSpine, backupPage, backupPageCount);
|
saveProgress(backupSpine, backupPage, backupPageCount);
|
||||||
|
if (!bookmarkStore.isEmpty()) {
|
||||||
|
bookmarkStore.markDirty();
|
||||||
|
bookmarkStore.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onGoHome();
|
onGoHome();
|
||||||
@@ -1144,7 +1181,9 @@ void EpubReaderActivity::renderStatusBar() const {
|
|||||||
title = epub->getTitle();
|
title = epub->getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset);
|
const bool isStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
||||||
|
static_cast<uint16_t>(section->currentPage));
|
||||||
|
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, isStarred);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
|
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include "BookmarkStore.h"
|
||||||
#include "EpubReaderMenuActivity.h"
|
#include "EpubReaderMenuActivity.h"
|
||||||
#include "ReaderUtils.h"
|
#include "ReaderUtils.h"
|
||||||
#include "activities/Activity.h"
|
#include "activities/Activity.h"
|
||||||
@@ -52,6 +53,9 @@ class EpubReaderActivity final : public Activity {
|
|||||||
int8_t bookEmbeddedStyleOverride = -1;
|
int8_t bookEmbeddedStyleOverride = -1;
|
||||||
int8_t bookImageRenderingOverride = -1;
|
int8_t bookImageRenderingOverride = -1;
|
||||||
|
|
||||||
|
// Bookmarks (starred pages)
|
||||||
|
BookmarkStore bookmarkStore;
|
||||||
|
|
||||||
// Footnote support
|
// Footnote support
|
||||||
std::vector<FootnoteEntry> currentPageFootnotes;
|
std::vector<FootnoteEntry> currentPageFootnotes;
|
||||||
struct SavedPosition {
|
struct SavedPosition {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
|
|||||||
const int bookProgressPercent, const uint8_t currentOrientation,
|
const int bookProgressPercent, const uint8_t currentOrientation,
|
||||||
const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride,
|
const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride,
|
||||||
const int8_t initialImageRenderingOverride,
|
const int8_t initialImageRenderingOverride,
|
||||||
const uint8_t initialTextDarkness)
|
const uint8_t initialTextDarkness, const bool hasStarredPages)
|
||||||
: MenuListActivity("EpubReaderMenu", renderer, mappedInput),
|
: MenuListActivity("EpubReaderMenu", renderer, mappedInput),
|
||||||
pendingOrientation(currentOrientation),
|
pendingOrientation(currentOrientation),
|
||||||
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
|
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
|
||||||
@@ -23,16 +23,19 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
|
|||||||
currentPage(currentPage),
|
currentPage(currentPage),
|
||||||
totalPages(totalPages),
|
totalPages(totalPages),
|
||||||
bookProgressPercent(bookProgressPercent) {
|
bookProgressPercent(bookProgressPercent) {
|
||||||
buildMenuItems(hasFootnotes);
|
buildMenuItems(hasFootnotes, hasStarredPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPages) {
|
||||||
menuItems.reserve(18);
|
menuItems.reserve(19);
|
||||||
|
|
||||||
// --- Navigation ---
|
// --- Navigation ---
|
||||||
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION));
|
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION));
|
||||||
menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None));
|
menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None));
|
||||||
menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None));
|
menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None));
|
||||||
|
if (hasStarredPages) {
|
||||||
|
menuItems.push_back(SettingInfo::Action(StrId::STR_STARRED_PAGES, SettingAction::None));
|
||||||
|
}
|
||||||
if (hasFootnotes) {
|
if (hasFootnotes) {
|
||||||
menuItems.push_back(SettingInfo::Action(StrId::STR_FOOTNOTES, SettingAction::None));
|
menuItems.push_back(SettingInfo::Action(StrId::STR_FOOTNOTES, SettingAction::None));
|
||||||
}
|
}
|
||||||
@@ -98,6 +101,8 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId
|
|||||||
return MenuAction::SELECT_CHAPTER;
|
return MenuAction::SELECT_CHAPTER;
|
||||||
case StrId::STR_GO_TO_PERCENT:
|
case StrId::STR_GO_TO_PERCENT:
|
||||||
return MenuAction::GO_TO_PERCENT;
|
return MenuAction::GO_TO_PERCENT;
|
||||||
|
case StrId::STR_STARRED_PAGES:
|
||||||
|
return MenuAction::STARRED_PAGES;
|
||||||
case StrId::STR_FOOTNOTES:
|
case StrId::STR_FOOTNOTES:
|
||||||
return MenuAction::FOOTNOTES;
|
return MenuAction::FOOTNOTES;
|
||||||
case StrId::STR_AUTO_TURN_PAGES_PER_MIN:
|
case StrId::STR_AUTO_TURN_PAGES_PER_MIN:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class EpubReaderMenuActivity final : public MenuListActivity {
|
|||||||
GO_HOME,
|
GO_HOME,
|
||||||
PULL_REMOTE,
|
PULL_REMOTE,
|
||||||
PUSH_LOCAL,
|
PUSH_LOCAL,
|
||||||
|
SYNC,
|
||||||
|
STARRED_PAGES,
|
||||||
DELETE_CACHE
|
DELETE_CACHE
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,13 +35,14 @@ class EpubReaderMenuActivity final : public MenuListActivity {
|
|||||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||||
const uint8_t currentOrientation, const bool hasFootnotes,
|
const uint8_t currentOrientation, const bool hasFootnotes,
|
||||||
const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
|
const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
|
||||||
const uint8_t initialTextDarkness);
|
const uint8_t initialTextDarkness,
|
||||||
|
const bool hasStarredPages);
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void render(RenderLock&&) override;
|
void render(RenderLock&&) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildMenuItems(bool hasFootnotes);
|
void buildMenuItems(bool hasFootnotes, bool hasStarredPages);
|
||||||
void finishWithAction(MenuAction action);
|
void finishWithAction(MenuAction action);
|
||||||
|
|
||||||
// MenuListActivity overrides
|
// MenuListActivity overrides
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include "StarredPagesActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
int StarredPagesActivity::getPageItems() const {
|
||||||
|
constexpr int lineHeight = 30;
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
const auto orientation = renderer.getOrientation();
|
||||||
|
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
|
||||||
|
const int hintGutterHeight = isPortraitInverted ? 50 : 0;
|
||||||
|
const int startY = 60 + hintGutterHeight;
|
||||||
|
const int availableHeight = screenHeight - startY - lineHeight;
|
||||||
|
return std::max(1, availableHeight / lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string StarredPagesActivity::getItemLabel(int index) const {
|
||||||
|
const auto& bm = bookmarks[index];
|
||||||
|
char buf[64];
|
||||||
|
if (epub) {
|
||||||
|
// Try to get chapter title from TOC
|
||||||
|
const int tocIndex = epub->getTocIndexForSpineIndex(bm.spineIndex);
|
||||||
|
if (tocIndex != -1) {
|
||||||
|
const auto tocItem = epub->getTocItem(tocIndex);
|
||||||
|
snprintf(buf, sizeof(buf), "%d. ", index + 1);
|
||||||
|
return std::string(buf) + tocItem.title + " - " + tr(STR_PAGE_PREFIX) + std::to_string(bm.pageNumber + 1);
|
||||||
|
}
|
||||||
|
snprintf(buf, sizeof(buf), "%d. %s%d, %s%d", index + 1, tr(STR_SECTION_PREFIX), bm.spineIndex + 1,
|
||||||
|
tr(STR_PAGE_PREFIX), bm.pageNumber + 1);
|
||||||
|
} else {
|
||||||
|
// TXT file: just page number (spineIndex is always 0)
|
||||||
|
snprintf(buf, sizeof(buf), "%d. %s%d", index + 1, tr(STR_PAGE_PREFIX), bm.pageNumber + 1);
|
||||||
|
}
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StarredPagesActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StarredPagesActivity::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
|
void StarredPagesActivity::loop() {
|
||||||
|
const int totalItems = static_cast<int>(bookmarks.size());
|
||||||
|
const int pageItems = getPageItems();
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (!bookmarks.empty()) {
|
||||||
|
const auto& bm = bookmarks[selectorIndex];
|
||||||
|
setResult(StarredPageResult{bm.spineIndex, bm.pageNumber});
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
ActivityResult result;
|
||||||
|
result.isCancelled = true;
|
||||||
|
setResult(std::move(result));
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void StarredPagesActivity::render(RenderLock&&) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const int totalItems = static_cast<int>(bookmarks.size());
|
||||||
|
|
||||||
|
if (totalItems == 0) {
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_NO_STARRED_PAGES), true, EpdFontFamily::BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto orientation = renderer.getOrientation();
|
||||||
|
const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise;
|
||||||
|
const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise;
|
||||||
|
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
|
||||||
|
const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0;
|
||||||
|
const int contentX = isLandscapeCw ? hintGutterWidth : 0;
|
||||||
|
const int contentWidth = pageWidth - hintGutterWidth;
|
||||||
|
const int hintGutterHeight = isPortraitInverted ? 50 : 0;
|
||||||
|
const int contentY = hintGutterHeight;
|
||||||
|
const int pageItems = getPageItems();
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const int titleX =
|
||||||
|
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_STARRED_PAGES), EpdFontFamily::BOLD)) / 2;
|
||||||
|
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_STARRED_PAGES), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
|
|
||||||
|
// Highlight selection
|
||||||
|
renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30);
|
||||||
|
|
||||||
|
for (int i = 0; i < pageItems; i++) {
|
||||||
|
int itemIndex = pageStartIndex + i;
|
||||||
|
if (itemIndex >= totalItems) break;
|
||||||
|
const int displayY = 60 + contentY + i * 30;
|
||||||
|
const bool isSelected = (itemIndex == selectorIndex);
|
||||||
|
|
||||||
|
const std::string label = renderer.truncatedText(UI_10_FONT_ID, getItemLabel(itemIndex).c_str(), contentWidth - 40);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, label.c_str(), !isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Epub.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
#include "BookmarkStore.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
|
class StarredPagesActivity final : public Activity {
|
||||||
|
std::shared_ptr<Epub> epub; // nullptr for TXT files
|
||||||
|
const std::vector<BookmarkStore::Bookmark> bookmarks;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
int selectorIndex = 0;
|
||||||
|
|
||||||
|
int getPageItems() const;
|
||||||
|
std::string getItemLabel(int index) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit StarredPagesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::vector<BookmarkStore::Bookmark>& bookmarks,
|
||||||
|
std::shared_ptr<Epub> epub = nullptr)
|
||||||
|
: Activity("StarredPages", renderer, mappedInput), epub(std::move(epub)), bookmarks(bookmarks) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "ReaderUtils.h"
|
#include "ReaderUtils.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
#include "StarredPagesActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@@ -99,6 +100,9 @@ void TxtReaderActivity::onEnter() {
|
|||||||
|
|
||||||
txt->setupCacheDir();
|
txt->setupCacheDir();
|
||||||
|
|
||||||
|
// Load bookmarks for this file
|
||||||
|
bookmarkStore.load(txt->getCachePath());
|
||||||
|
|
||||||
// Save current txt as last opened file and add to recent books
|
// Save current txt as last opened file and add to recent books
|
||||||
auto filePath = txt->getPath();
|
auto filePath = txt->getPath();
|
||||||
auto fileName = filePath.substr(filePath.rfind('/') + 1);
|
auto fileName = filePath.substr(filePath.rfind('/') + 1);
|
||||||
@@ -113,6 +117,9 @@ void TxtReaderActivity::onEnter() {
|
|||||||
void TxtReaderActivity::onExit() {
|
void TxtReaderActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
|
// Save bookmarks before exit
|
||||||
|
bookmarkStore.save();
|
||||||
|
|
||||||
// Reset orientation back to portrait for the rest of the UI
|
// Reset orientation back to portrait for the rest of the UI
|
||||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||||
|
|
||||||
@@ -143,6 +150,29 @@ void TxtReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Star page toggle via short power button press
|
||||||
|
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE &&
|
||||||
|
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
|
||||||
|
bookmarkStore.toggle(0, static_cast<uint16_t>(currentPage));
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open starred pages list via Confirm button
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) {
|
||||||
|
startActivityForResult(std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore.getAll()),
|
||||||
|
[this](const ActivityResult& result) {
|
||||||
|
if (!result.isCancelled) {
|
||||||
|
const auto& starred = std::get<StarredPageResult>(result.data);
|
||||||
|
currentPage = starred.pageNumber;
|
||||||
|
if (currentPage >= totalPages) currentPage = totalPages - 1;
|
||||||
|
if (currentPage < 0) currentPage = 0;
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||||
if (!prevTriggered && !nextTriggered) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
@@ -373,7 +403,8 @@ void TxtReaderActivity::renderStatusBar() const {
|
|||||||
if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) {
|
if (SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE) {
|
||||||
title = txt->getTitle();
|
title = txt->getTitle();
|
||||||
}
|
}
|
||||||
GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title);
|
const bool isStarred = bookmarkStore.has(0, static_cast<uint16_t>(currentPage));
|
||||||
|
GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title, 0, 0, isStarred);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TxtReaderActivity::saveProgress() const {
|
void TxtReaderActivity::saveProgress() const {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BookmarkStore.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "ReaderUtils.h"
|
#include "ReaderUtils.h"
|
||||||
#include "activities/Activity.h"
|
#include "activities/Activity.h"
|
||||||
@@ -16,6 +17,9 @@ class TxtReaderActivity final : public Activity {
|
|||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
ReaderUtils::InputDrainGuard inputDrainGuard;
|
ReaderUtils::InputDrainGuard inputDrainGuard;
|
||||||
|
|
||||||
|
// Bookmarks (starred pages)
|
||||||
|
BookmarkStore bookmarkStore;
|
||||||
|
|
||||||
// Streaming text reader - stores file offsets for each page
|
// Streaming text reader - stores file offsets for each page
|
||||||
std::vector<size_t> pageOffsets; // File offset for start of each page
|
std::vector<size_t> pageOffsets; // File offset for start of each page
|
||||||
std::vector<std::string> currentPageLines;
|
std::vector<std::string> currentPageLines;
|
||||||
|
|||||||
@@ -748,8 +748,8 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
|
void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
|
||||||
const int pageCount, std::string title, const int paddingBottom,
|
const int pageCount, std::string title, const int paddingBottom, const int textYOffset,
|
||||||
const int textYOffset) const {
|
const bool isStarred) const {
|
||||||
auto metrics = UITheme::getInstance().getMetrics();
|
auto metrics = UITheme::getInstance().getMetrics();
|
||||||
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
||||||
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
||||||
@@ -826,9 +826,10 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
|
|||||||
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
|
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
|
||||||
|
|
||||||
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
||||||
|
const int starReserve = isStarred ? (renderer.getTextWidth(SMALL_FONT_ID, "*") + 6) : 0;
|
||||||
const int clockSize = clockTextWidth > 0 ? clockTextWidth + 8 : 0;
|
const int clockSize = clockTextWidth > 0 ? clockTextWidth + 8 : 0;
|
||||||
const int titleMarginLeft = batterySize + clockSize + 30;
|
const int titleMarginLeft = batterySize + clockSize + 30;
|
||||||
const int titleMarginRight = progressTextWidth + 30;
|
const int titleMarginRight = progressTextWidth + starReserve + 30;
|
||||||
|
|
||||||
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
|
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
|
||||||
// available space.
|
// available space.
|
||||||
@@ -852,6 +853,21 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
|
|||||||
(availableTitleSpace - titleWidth) / 2,
|
(availableTitleSpace - titleWidth) / 2,
|
||||||
textY, title.c_str());
|
textY, title.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw star indicator between title and progress text
|
||||||
|
if (isStarred) {
|
||||||
|
const int starWidth = renderer.getTextWidth(SMALL_FONT_ID, "*");
|
||||||
|
int starX;
|
||||||
|
if (progressTextWidth > 0) {
|
||||||
|
// Place star just left of the progress text with a small gap
|
||||||
|
starX = renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - progressTextWidth -
|
||||||
|
starWidth - 6;
|
||||||
|
} else {
|
||||||
|
// No progress text, place star at right edge
|
||||||
|
starX = renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - starWidth;
|
||||||
|
}
|
||||||
|
renderer.drawText(SMALL_FONT_ID, starX, textY + textYOffset, "*");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const {
|
void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class BaseTheme {
|
|||||||
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
|
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
|
||||||
virtual void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
|
virtual void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
|
||||||
const int pageCount, std::string title, const int paddingBottom = 0,
|
const int pageCount, std::string title, const int paddingBottom = 0,
|
||||||
const int textYOffset = 0) const;
|
const int textYOffset = 0, const bool isStarred = false) const;
|
||||||
virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
|
virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
|
||||||
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const;
|
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const;
|
||||||
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected,
|
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected,
|
||||||
|
|||||||
Reference in New Issue
Block a user