Merge branch 'master' of https://github.com/jpirnay/crosspoint-reader into feat-vietnam

This commit is contained in:
jpirnay
2026-04-27 11:38:31 +02:00
55 changed files with 1127 additions and 492 deletions
+2 -2
View File
@@ -847,8 +847,8 @@ rm -rf /path/to/sd/.crosspoint/epub_<hash>/sections/
**Source**: `lib/Epub/Epub/Section.cpp`, `lib/Epub/Epub/BookMetadataCache.cpp`
**Current Versions** (as of docs/file-formats.md):
- `book.bin`: **Version 5** (metadata structure)
- `section.bin`: **Version 20** (layout structure, includes paragraph LUT)
- `book.bin`: **Version 7** (header A includes cached `tocReliable` byte)
- `section.bin`: **Version 23** (layout structure, includes paragraph LUT)
**Version Increment Rules**:
1. **ALWAYS increment version** BEFORE changing binary structure
+8
View File
@@ -22,6 +22,14 @@ Spine items before the first TOC entry (cover pages) and after the last (appendi
- `getTocItem(i)` returns the TOC entry (title, spineIndex, anchor) for TOC index `i` -- also a file seek per call, not cached in memory. Code that queries TOC metadata in a loop should cache the results locally first.
- `getSpineIndexForTocIndex(i)` does the reverse lookup (TOC index to spine index).
### Cached TOC reliability flag
`hasReliableToc()` answers whether the TOC has enough spine coverage (>=25% of spines referenced) to drive chapter UX, with short-circuits for `tocCount <= 0` and the "large book with one TOC entry" pathology.
The result is computed once during `buildBookBin` (folded into the existing `spineIndex->tocIndex` scan, so no extra disk pass) and persisted as a single byte in book.bin's header A. `Epub::hasReliableToc()` reads `BookMetadataCache::isTocReliable()` and caches the bool in `tocReliabilityState`.
This matters because the check used to recompute the answer on demand by calling `getTocEntry(i)` for every TOC entry, which does two SD-card seeks per call. On a 2858-entry web-novel TOC that was ~5700 seeks (~7 seconds) added to first-page latency. `BOOK_CACHE_VERSION` was bumped to 7 for this layout change; older caches are rebuilt on next open.
## Section cache file format
The section cache (`.bin`) stores pre-rendered page data for a spine item. The file layout:
+8 -3
View File
@@ -2,7 +2,7 @@
## `book.bin`
### Version 3
### Version 7
ImHex Pattern:
@@ -12,7 +12,7 @@ import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 3
#define EXPECTED_VERSION 7
#define MAX_STRING_LENGTH 65535
// === String Structure ===
@@ -34,8 +34,12 @@ fn format_string(String s) {
struct Metadata {
String title [[comment("Book title")]];
String author [[comment("Book author")]];
String language [[comment("BCP47 language tag")]];
String coverItemHref [[comment("Path to cover image")]];
String textReferenceHref [[comment("Path to guided first text reference")]];
String series [[comment("Series name")]];
String seriesIndex [[comment("Series index/position")]];
String description [[comment("Book description / blurb")]];
} [[comment("Book metadata information")]];
// === Spine Entry Structure ===
@@ -70,7 +74,8 @@ struct BookBin {
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
u8 tocReliable [[comment("1 if TOC has >=25% spine coverage, 0 otherwise"), color("F4A261")]];
// Metadata section
Metadata metadata [[comment("Book metadata")]];
+4 -29
View File
@@ -868,35 +868,10 @@ bool Epub::hasReliableToc() const {
return false;
}
const int spineCount = bookMetadataCache->getSpineCount();
const int tocCount = bookMetadataCache->getTocCount();
if (spineCount <= 0 || tocCount <= 0) {
tocReliabilityState = 0;
return false;
}
// If a larger book only exposes one TOC entry, treat TOC as unusable for chapter UX.
if (spineCount >= 8 && tocCount <= 1) {
tocReliabilityState = 0;
return false;
}
std::vector<bool> spineReferenced(static_cast<size_t>(spineCount), false);
int distinctSpinesReferenced = 0;
for (int i = 0; i < tocCount; i++) {
const auto toc = bookMetadataCache->getTocEntry(i);
if (toc.spineIndex >= 0 && toc.spineIndex < spineCount) {
const size_t idx = static_cast<size_t>(toc.spineIndex);
if (!spineReferenced[idx]) {
spineReferenced[idx] = true;
distinctSpinesReferenced++;
}
}
}
// Require at least 25% spine coverage from TOC references.
const bool reliable = (distinctSpinesReferenced * 4 >= spineCount);
// Reliability is computed once at indexing time and persisted in book.bin's header.
// This avoids the O(tocCount) seek-heavy scan that previously fired on first page load
// for every book — a large web-novel TOC (~3000 entries) added several seconds of latency.
const bool reliable = bookMetadataCache->isTocReliable();
tocReliabilityState = reliable ? 1 : 0;
return reliable;
}
+28 -5
View File
@@ -9,7 +9,7 @@
#include "FsHelpers.h"
namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 6;
constexpr uint8_t BOOK_CACHE_VERSION = 7;
constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
@@ -113,8 +113,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
return false;
}
constexpr uint32_t headerASize =
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
constexpr uint32_t headerASize = sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) +
sizeof(tocCount) + sizeof(uint8_t) /* tocReliable */;
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
metadata.series.size() + metadata.seriesIndex.size() + metadata.description.size() +
@@ -122,11 +122,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
const uint32_t lutOffset = headerASize + metadataSize;
// Header A
// Header A. tocReliable is patched at the end once the TOC scan below has computed it.
const uint32_t tocReliableHeaderPos =
sizeof(BOOK_CACHE_VERSION) + sizeof(uint32_t) /* lutOffset */ + sizeof(spineCount) + sizeof(tocCount);
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
serialization::writePod(bookFile, lutOffset);
serialization::writePod(bookFile, spineCount);
serialization::writePod(bookFile, tocCount);
serialization::writePod(bookFile, static_cast<uint8_t>(0)); // placeholder for tocReliable
// Metadata
serialization::writeString(bookFile, metadata.title);
serialization::writeString(bookFile, metadata.author);
@@ -156,18 +159,31 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
// LUTs complete
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m)).
// Also count distinct spines referenced by the TOC so tocReliable can be persisted in the
// header below — without this, every first-page load on a large book pays an O(tocCount)
// seek-heavy scan in Epub::hasReliableToc().
std::deque<int16_t> spineToTocIndex(spineCount, -1);
int distinctSpinesReferenced = 0;
tocFile.seek(0);
for (int j = 0; j < tocCount; j++) {
auto tocEntry = readTocEntry(tocFile);
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
distinctSpinesReferenced++;
}
}
}
// Mirrors the heuristic in Epub::hasReliableToc(): require >=25% distinct spine coverage,
// with short-circuits for edge cases (no entries, or large book with a single TOC entry).
if (spineCount > 0 && tocCount > 0 && !(spineCount >= 8 && tocCount <= 1)) {
tocReliable = (distinctSpinesReferenced * 4 >= spineCount);
} else {
tocReliable = false;
}
ZipFile zip(epubPath);
// Pre-open zip file to speed up size calculations
if (!zip.open()) {
@@ -269,6 +285,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
writeTocEntry(bookFile, tocEntry);
}
// Patch tocReliable placeholder in header A
bookFile.seek(tocReliableHeaderPos);
serialization::writePod(bookFile, static_cast<uint8_t>(tocReliable ? 1 : 0));
bookFile.close();
spineFile.close();
tocFile.close();
@@ -384,6 +404,9 @@ bool BookMetadataCache::load() {
serialization::readPod(bookFile, lutOffset);
serialization::readPod(bookFile, spineCount);
serialization::readPod(bookFile, tocCount);
uint8_t tocReliableByte;
serialization::readPod(bookFile, tocReliableByte);
tocReliable = (tocReliableByte != 0);
serialization::readString(bookFile, coreMetadata.title);
serialization::readString(bookFile, coreMetadata.author);
+9 -1
View File
@@ -50,6 +50,7 @@ class BookMetadataCache {
size_t lutOffset;
uint16_t spineCount;
uint16_t tocCount;
bool tocReliable;
bool loaded;
bool buildMode;
@@ -88,7 +89,13 @@ class BookMetadataCache {
BookMetadata coreMetadata;
explicit BookMetadataCache(std::string cachePath)
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
: cachePath(std::move(cachePath)),
lutOffset(0),
spineCount(0),
tocCount(0),
tocReliable(false),
loaded(false),
buildMode(false) {}
~BookMetadataCache() = default;
// Building phase (stream to disk immediately)
@@ -111,5 +118,6 @@ class BookMetadataCache {
TocEntry getTocEntry(int index);
int getSpineCount() const { return spineCount; }
int getTocCount() const { return tocCount; }
bool isTocReliable() const { return tocReliable; }
bool isLoaded() const { return loaded; }
};
-7
View File
@@ -66,9 +66,7 @@ STR_SLEEP_COVER_MODE: "Рэжым вокладкі сну"
STR_HIDE_BATTERY: "Схаваць % батарэі"
STR_EXTRA_SPACING: "Дадат. інтэрвал абзаца"
STR_TEXT_AA: "Згладжванне тэксту"
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
STR_ORIENTATION: "Арыентацыя чытання"
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
STR_FONT_FAMILY: "Шрыфт чытання"
STR_FONT_SIZE: "Памер шрыфту інтэрфейсу"
@@ -119,15 +117,10 @@ STR_CROP: "Абрэзаць"
STR_NEVER: "Ніколі"
STR_IN_READER: "У рэжыме чытання"
STR_ALWAYS: "Заўсёды"
STR_IGNORE: "Ігнараваць"
STR_SLEEP: "Сон"
STR_PAGE_TURN: "Перагортванне"
STR_PORTRAIT: "Партрэт"
STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Інверсія"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Наперад"
STR_NEXT_PREV: "Наперад/Назад"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -70,9 +70,7 @@ STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra"
STR_IMAGES_SUPPRESS: "Suprimir"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura"
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
STR_LONG_PRESS_SKIP: "Pressió llarga omet el capítol"
STR_FONT_FAMILY: "Tipus de lletra"
STR_FONT_SIZE: "Mida de la lletra (UI)"
@@ -124,15 +122,10 @@ STR_CROP: "Retallar"
STR_NEVER: "Mai"
STR_IN_READER: "Al lector"
STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignora"
STR_SLEEP: "Dormir"
STR_PAGE_TURN: "Canvi de pàgina"
STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit"
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -66,9 +66,7 @@ STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
STR_HIDE_BATTERY: "Skrýt baterii %"
STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
STR_TEXT_AA: "Vyhlazování textu"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu"
STR_FONT_FAMILY: "Rodina písem čtečky"
STR_FONT_SIZE: "Velikost písma rozhraní"
@@ -119,15 +117,10 @@ STR_CROP: "Oříznout"
STR_NEVER: "Nikdy"
STR_IN_READER: "Ve čtečce"
STR_ALWAYS: "Vždy"
STR_IGNORE: "Ignorovat"
STR_SLEEP: "Spánek"
STR_PAGE_TURN: "Otáčení stránek"
STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
STR_INVERTED: "Invertovaný"
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
STR_PREV_NEXT: "Předchozí/Další"
STR_NEXT_PREV: "Další/Předchozí"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -70,9 +70,7 @@ STR_IMAGES: "Billeder"
STR_IMAGES_DISPLAY: "Vis"
STR_IMAGES_PLACEHOLDER: "Pladsholder"
STR_IMAGES_SUPPRESS: "Skjul"
STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap"
STR_ORIENTATION: "Læseretning"
STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)"
STR_LONG_PRESS_SKIP: "Langt tryk spring kapitel over"
STR_FONT_FAMILY: "Læser skrifttype"
STR_FONT_SIZE: "Læser skriftstørrelse"
@@ -124,15 +122,10 @@ STR_CROP: "Beskær"
STR_NEVER: "Aldrig"
STR_IN_READER: "I læseren"
STR_ALWAYS: "Altid"
STR_IGNORE: "Ignorer"
STR_SLEEP: "Hvile"
STR_PAGE_TURN: "Sideskift"
STR_PORTRAIT: "Portræt"
STR_LANDSCAPE_CW: "Liggende med uret"
STR_INVERTED: "Inverteret"
STR_LANDSCAPE_CCW: "Liggende mod uret"
STR_PREV_NEXT: "Forrige/Næste"
STR_NEXT_PREV: "Næste/Forrige"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Gediffuseerde Bayer"
STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Maak fallback voor ongeldige inhoudsopgave"
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
STR_ORIENTATION: "Leesstand"
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
STR_LONG_PRESS_SKIP: "Hoofdstuk overslaan (lang indrukken)"
STR_FONT_FAMILY: "Lettertype lezer"
STR_FONT_SIZE: "Lettergrootte lezer"
@@ -199,15 +197,10 @@ STR_CROP: "Bijsnijden"
STR_NEVER: "Nooit"
STR_IN_READER: "In lezer"
STR_ALWAYS: "Altijd"
STR_IGNORE: "Negeren"
STR_SLEEP: "Slaap"
STR_PAGE_TURN: "Pagina omslaan"
STR_PORTRAIT: "Staand"
STR_LANDSCAPE_CW: "Liggend (rechtsom)"
STR_INVERTED: "Omgekeerd"
STR_LANDSCAPE_CCW: "Liggend (linksom)"
STR_PREV_NEXT: "Vorige/Volgende"
STR_NEXT_PREV: "Volgende/Vorige"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
+39 -7
View File
@@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffused Bayer"
STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Create fallback for invalid TOC"
STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
STR_LONG_PRESS_SKIP: "Long-press Chapter Skip"
STR_FONT_FAMILY: "Reader Font Family"
STR_FONT_SIZE: "Reader Font Size"
@@ -199,15 +197,10 @@ STR_CROP: "Crop"
STR_NEVER: "Never"
STR_IN_READER: "In Reader"
STR_ALWAYS: "Always"
STR_IGNORE: "Ignore"
STR_SLEEP: "Sleep"
STR_PAGE_TURN: "Page Turn"
STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Landscape CW"
STR_INVERTED: "Inverted"
STR_LANDSCAPE_CCW: "Landscape CCW"
STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
@@ -547,3 +540,42 @@ STR_CAPTIVE_PORTAL_DETECTED: "Login Required"
STR_CAPTIVE_PORTAL_HINT_1: "Network requires browser login. On another device,"
STR_CAPTIVE_PORTAL_HINT_2: "visit the URL below to authorize, then press OK."
STR_CAPTIVE_PORTAL_DONE: "I'm authorized"
STR_MENU_BTN_ACTIONS: "Button Actions"
STR_MENU_BTN_PHYSICAL: "Physical Buttons"
STR_BTN_SHORT_PRESS: "Short Press"
STR_BTN_DOUBLE_PRESS: "Double Press"
STR_BTN_LONG_PRESS: "Long Press"
STR_BTN_BACK: "Back Button"
STR_BTN_CONFIRM: "Confirm Button"
STR_BTN_LEFT: "Left Button"
STR_BTN_RIGHT: "Right Button"
STR_BTN_PAGE_BACK: "Page Back Button"
STR_BTN_PAGE_FORWARD: "Page Forward Button"
STR_BTN_POWER: "Power Button"
STR_BTN_ACT_DEFAULT: "Default"
STR_BTN_DEF_IGNORE: "Default (ignore)"
STR_BTN_DEF_EXIT_READER: "Default (exit reader)"
STR_BTN_DEF_GO_HOME: "Default (go home)"
STR_BTN_DEF_READER_MENU: "Default (reader menu)"
STR_BTN_DEF_KOREADER_SYNC: "Default (KOReader sync)"
STR_BTN_DEF_PREV_PAGE: "Default (previous page)"
STR_BTN_DEF_NEXT_PAGE: "Default (next page)"
STR_BTN_DEF_CHAPTER_BACK: "Default (chapter back)"
STR_BTN_DEF_CHAPTER_FORWARD: "Default (chapter forward)"
STR_BTN_DEF_SLEEP: "Default (sleep)"
STR_BTN_ACT_PAGE_FORWARD: "Next Page"
STR_BTN_ACT_PAGE_BACK: "Previous Page"
STR_BTN_ACT_PAGE_FORWARD_10: "Skip 10 Pages Forward"
STR_BTN_ACT_PAGE_BACK_10: "Skip 10 Pages Back"
STR_BTN_ACT_GO_HOME: "Go Home"
STR_BTN_ACT_SLEEP: "Sleep"
STR_BTN_ACT_FORCE_REFRESH: "Refresh Screen"
STR_BTN_ACT_OPEN_TOC: "Open Table of Contents"
STR_BTN_ACT_OPEN_BOOKMARKS: "Open Bookmarks"
STR_BTN_ACT_STAR_PAGE: "Star Page"
STR_BTN_ACT_FOOTNOTES: "Footnotes"
STR_BTN_ACT_NEXT_SECTION: "Next Section / Chapter"
STR_BTN_ACT_PREV_SECTION: "Previous Section / Chapter"
STR_BTN_ACT_EXIT_READER: "Exit Reader"
STR_BTN_ACT_READER_MENU: "Reader Menu"
STR_BTN_ACT_KOREADER_SYNC: "KOReader Sync"
-7
View File
@@ -66,9 +66,7 @@ STR_SLEEP_COVER_MODE: "Lepotilanäytön kansitila"
STR_HIDE_BATTERY: "Piilota akun %"
STR_EXTRA_SPACING: "Kappaleiden lisäväli"
STR_TEXT_AA: "Tekstin reunanpehmennys"
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
STR_ORIENTATION: "Lukusuunta"
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
STR_LONG_PRESS_SKIP: "Pitkä painallus: lukuhyppy"
STR_FONT_FAMILY: "Lukijan fonttiperhe"
STR_FONT_SIZE: "Käyttöliittymän fonttikoko"
@@ -119,15 +117,10 @@ STR_CROP: "Rajaa"
STR_NEVER: "Ei koskaan"
STR_IN_READER: "Lukijassa"
STR_ALWAYS: "Aina"
STR_IGNORE: "Ohita"
STR_SLEEP: "Lepotila"
STR_PAGE_TURN: "Sivunkääntö"
STR_PORTRAIT: "Pysty"
STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
STR_INVERTED: "Käännetty"
STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
STR_PREV_NEXT: "Edell/Seur"
STR_NEXT_PREV: "Seur/Edell"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -91,9 +91,7 @@ STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Affichage"
STR_IMAGES_PLACEHOLDER: "Espace réservé"
STR_IMAGES_SUPPRESS: "Masquer"
STR_SHORT_PWR_BTN: "Appui court alim."
STR_ORIENTATION: "Orientation de lecture"
STR_SIDE_BTN_LAYOUT: "Boutons latéraux"
STR_LONG_PRESS_SKIP: "Appui long saut de chapitre"
STR_FONT_FAMILY: "Police de caractères du lecteur"
STR_FONT_SIZE: "Taille texte interface"
@@ -147,15 +145,10 @@ STR_CROP: "Rogné"
STR_NEVER: "Jamais"
STR_IN_READER: "Dans le lecteur"
STR_ALWAYS: "Toujours"
STR_IGNORE: "Ignorer"
STR_SLEEP: "Mise en veille"
STR_PAGE_TURN: "Page suivante"
STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Paysage"
STR_INVERTED: "Inversé"
STR_LANDSCAPE_CCW: "Paysage inversé"
STR_PREV_NEXT: "Préc/Suiv"
STR_NEXT_PREV: "Suiv/Préc"
STR_ABC: "abc"
STR_FORCE_REFRESH: "Actualiser l'écran"
STR_NEXT: "Suiv"
-7
View File
@@ -79,9 +79,7 @@ STR_IMAGE_DITHERING: "Bild-Dithering"
STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffundiertes Bayer"
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
STR_ORIENTATION: "Leseausrichtung"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
STR_LONG_PRESS_SKIP: "Langes Drücken springt Kap."
STR_FONT_FAMILY: "Lese-Schriftfamilie"
STR_FONT_SIZE: "Schriftgröße"
@@ -138,15 +136,10 @@ STR_CROP: "Zuschnitt"
STR_NEVER: "Nie"
STR_IN_READER: "Beim Lesen"
STR_ALWAYS: "Immer"
STR_IGNORE: "Ignorieren"
STR_SLEEP: "Standby"
STR_PAGE_TURN: "Umblättern"
STR_PORTRAIT: "Hochformat"
STR_LANDSCAPE_CW: "Querformat rechts"
STR_INVERTED: "Invertiert"
STR_LANDSCAPE_CCW: "Querformat links"
STR_PREV_NEXT: "Zurück/Weiter"
STR_NEXT_PREV: "Weiter/Zurück"
STR_ABC: "abc"
STR_FORCE_REFRESH: "Bildschirm aktualisieren"
STR_NEXT: "Weiter"
-7
View File
@@ -70,9 +70,7 @@ STR_IMAGES: "Képek"
STR_IMAGES_DISPLAY: "Megjelenítés"
STR_IMAGES_PLACEHOLDER: "Helyőrző"
STR_IMAGES_SUPPRESS: "Elnyomás"
STR_SHORT_PWR_BTN: "Rövid bekapcsológomb nyomás"
STR_ORIENTATION: "Olvasási irány"
STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
STR_FONT_FAMILY: "Olvasó betűkészlet"
STR_FONT_SIZE: "Olvasó betűméret"
@@ -124,15 +122,10 @@ STR_CROP: "Körbevágás"
STR_NEVER: "Soha"
STR_IN_READER: "Olvasóban"
STR_ALWAYS: "Mindig"
STR_IGNORE: "Mellőzés"
STR_SLEEP: "Alvás"
STR_PAGE_TURN: "Lapozás"
STR_PORTRAIT: "Álló"
STR_LANDSCAPE_CW: "Fekvő jobbra"
STR_INVERTED: "Fordított"
STR_LANDSCAPE_CCW: "Fekvő balra"
STR_PREV_NEXT: "Előző/Következő"
STR_NEXT_PREV: "Következő/Előző"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -91,9 +91,7 @@ STR_IMAGES: "Immagini"
STR_IMAGES_DISPLAY: "Visualizza"
STR_IMAGES_PLACEHOLDER: "Segnaposto"
STR_IMAGES_SUPPRESS: "Nascondi"
STR_SHORT_PWR_BTN: "Pressione breve tasto accensione"
STR_ORIENTATION: "Orientamento lettura"
STR_SIDE_BTN_LAYOUT: "Pulsanti laterali (lettore)"
STR_LONG_PRESS_SKIP: "Pressione lunga: salta capitolo"
STR_FONT_FAMILY: "Font lettore"
STR_FONT_SIZE: "Dimensione font lettore"
@@ -147,15 +145,10 @@ STR_CROP: "Ritaglia"
STR_NEVER: "Mai"
STR_IN_READER: "Nel lettore"
STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignora"
STR_SLEEP: "Sospendi"
STR_PAGE_TURN: "Cambio pagina"
STR_PORTRAIT: "Verticale"
STR_LANDSCAPE_CW: "Orizzontale ↻"
STR_INVERTED: "Invertito"
STR_LANDSCAPE_CCW: "Orizzontale ↺"
STR_PREV_NEXT: "Prec/Succ"
STR_NEXT_PREV: "Succ/Prec"
STR_ABC: "abc"
STR_FORCE_REFRESH: "Aggiorna schermo"
STR_NEXT: "Succ"
-7
View File
@@ -65,9 +65,7 @@ STR_SLEEP_COVER_MODE: "Ұйқы экраны мұқаба режимі"
STR_HIDE_BATTERY: "Батарея % жасыру"
STR_EXTRA_SPACING: "Қосымша абзац аралығы"
STR_TEXT_AA: "Мәтін сырғытпасы"
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
STR_ORIENTATION: "Оқу бағдары"
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
STR_FONT_FAMILY: "Оқырман қаріп тобы"
STR_FONT_SIZE: "Интерфейс қаріп өлшемі"
@@ -118,15 +116,10 @@ STR_CROP: "Кесу"
STR_NEVER: "Ешқашан"
STR_IN_READER: "Оқырманда"
STR_ALWAYS: "Әрқашан"
STR_IGNORE: "Елемеу"
STR_SLEEP: "Ұйқы"
STR_PAGE_TURN: "Бет аудару"
STR_PORTRAIT: "Тік бағдар"
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
STR_INVERTED: "Төңкерілген"
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
STR_PREV_NEXT: "Алдыңғы/Келесі"
STR_NEXT_PREV: "Келесі/Алдыңғы"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -70,9 +70,7 @@ STR_IMAGES: "Paveikslėliai"
STR_IMAGES_DISPLAY: "Rodyti"
STR_IMAGES_PLACEHOLDER: "Vietaženklis"
STR_IMAGES_SUPPRESS: "Slėpti"
STR_SHORT_PWR_BTN: "Trumpas įjungimo pasp."
STR_ORIENTATION: "Orientacija"
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
STR_FONT_FAMILY: "Šriftas"
STR_FONT_SIZE: "Šrifto dydis"
@@ -124,15 +122,10 @@ STR_CROP: "Kirpti"
STR_NEVER: "Niekada"
STR_IN_READER: "Skaitytuve"
STR_ALWAYS: "Visada"
STR_IGNORE: "Nepaisyti"
STR_SLEEP: "Miegas"
STR_PAGE_TURN: "Versti psl."
STR_PORTRAIT: "Stačias"
STR_LANDSCAPE_CW: "Gulsčias (P)"
STR_INVERTED: "Apverstas"
STR_LANDSCAPE_CCW: "Gulsčias (A)"
STR_PREV_NEXT: "Atgal/Pirmyn"
STR_NEXT_PREV: "Pirmyn/Atgal"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "O. Dyslexic"
-7
View File
@@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Dyfundowany Bayer"
STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Utwórz fallback dla nieprawidłowego spisu treści"
STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania"
STR_ORIENTATION: "Układ czytania"
STR_SIDE_BTN_LAYOUT: "Układ przycisków bocznych"
STR_LONG_PRESS_SKIP: "Przytrzymaj aby przeskoczyć rozdział"
STR_FONT_FAMILY: "Czcionka"
STR_FONT_SIZE: "Rozmiar czcionki"
@@ -199,15 +197,10 @@ STR_CROP: "Przytnij"
STR_NEVER: "Nigdy"
STR_IN_READER: "W czytniku"
STR_ALWAYS: "Zawsze"
STR_IGNORE: "Ignoruj"
STR_SLEEP: "Uśpienie"
STR_PAGE_TURN: "Nast. str."
STR_PORTRAIT: "Pionowo"
STR_LANDSCAPE_CW: "Poziomo P"
STR_INVERTED: "Odwrócony"
STR_LANDSCAPE_CCW: "Poziomo L"
STR_PREV_NEXT: "Poprz./Nast."
STR_NEXT_PREV: "Nast./Poprz."
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -95,9 +95,7 @@ STR_IMAGE_DITHERING: "Dithering de imagem"
STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Bayer difuso"
STR_SHORT_PWR_BTN: "Clique curto no botão de ligar"
STR_ORIENTATION: "Orientação de leitura"
STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais"
STR_LONG_PRESS_SKIP: "Pular capítulo com pressão longa"
STR_FONT_FAMILY: "Fonte do leitor"
STR_FONT_SIZE: "Tam. da fonte da UI"
@@ -151,15 +149,10 @@ STR_CROP: "Recortar"
STR_NEVER: "Nunca"
STR_IN_READER: "No leitor"
STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Repouso"
STR_PAGE_TURN: "Virar página"
STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Paisagem AH"
STR_PREV_NEXT: "Ant/Próx"
STR_NEXT_PREV: "Próx/Ant"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -87,9 +87,7 @@ STR_TEXT_AA: "Suavização do texto"
STR_TEXT_DARKNESS: "Escuridão do texto"
STR_EXTRA_DARK: "Extra escuro"
STR_MAX_DARK: "Máximo"
STR_SHORT_PWR_BTN: "Pressão curta do botão de energia"
STR_ORIENTATION: "Orientação de leitura"
STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais"
STR_LONG_PRESS_SKIP: "Saltar capítulo com pressão longa"
STR_FONT_FAMILY: "Tipo de letra do leitor"
STR_FONT_SIZE: "Tamanho da letra do leitor"
@@ -146,15 +144,10 @@ STR_CROP: "Recortar"
STR_NEVER: "Nunca"
STR_IN_READER: "No leitor"
STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Repouso"
STR_PAGE_TURN: "Virar página"
STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Paisagem AH"
STR_PREV_NEXT: "Ant./Próx."
STR_NEXT_PREV: "Próx./Ant."
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -70,9 +70,7 @@ STR_IMAGES: "Imagini"
STR_IMAGES_DISPLAY: "Afişare"
STR_IMAGES_PLACEHOLDER: "Substituent"
STR_IMAGES_SUPPRESS: "Suprimare"
STR_SHORT_PWR_BTN: "Apăsare scurtă întrerupător"
STR_ORIENTATION: "Orientare lectură"
STR_SIDE_BTN_LAYOUT: "Aspect butoane laterale (lectură)"
STR_LONG_PRESS_SKIP: "Sărire capitol la apăsare lungă"
STR_FONT_FAMILY: "Familie font lectură"
STR_FONT_SIZE: "Dimensiune font"
@@ -124,15 +122,10 @@ STR_CROP: "Decupat"
STR_NEVER: "Niciodată"
STR_IN_READER: "În lectură"
STR_ALWAYS: "Întotdeauna"
STR_IGNORE: "Ignoră"
STR_SLEEP: "Repaus"
STR_PAGE_TURN: "Răsfoire pagină"
STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Orizontal dreapta"
STR_INVERTED: "Invers"
STR_LANDSCAPE_CCW: "Orizontal stânga"
STR_PREV_NEXT: "Înainte/Înapoi"
STR_NEXT_PREV: "Înapoi/Înainte"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -88,9 +88,7 @@ STR_IMAGES: "Изображения"
STR_IMAGES_DISPLAY: "Показать"
STR_IMAGES_PLACEHOLDER: "Заглушки"
STR_IMAGES_SUPPRESS: "Скрыть"
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
STR_ORIENTATION: "Ориентация чтения"
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
STR_LONG_PRESS_SKIP: "Долгое нажатие - смена главы"
STR_FONT_FAMILY: "Шрифт чтения"
STR_FONT_SIZE: "Размер шрифта интерфейса"
@@ -144,15 +142,10 @@ STR_CROP: "Обрезать"
STR_NEVER: "Никогда"
STR_IN_READER: "В режиме чтения"
STR_ALWAYS: "Всегда"
STR_IGNORE: "Игнорировать"
STR_SLEEP: "Сон"
STR_PAGE_TURN: "Перелистывание"
STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Инверсия"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Вперёд"
STR_NEXT_PREV: "Вперёд/Назад"
STR_ABC: "abc"
STR_FORCE_REFRESH: "Обновить экран"
STR_NEXT: "Далее"
-7
View File
@@ -84,9 +84,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Difuzni Bayer"
STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Ustvari rezervo za neveljavno kazalo"
STR_SHORT_PWR_BTN: "Kratek pritisk na gumb za vklop"
STR_ORIENTATION: "Orientacija branja"
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
STR_FONT_FAMILY: "Pisava bralnika"
STR_FONT_SIZE: "Velikost pisave"
@@ -181,15 +179,10 @@ STR_CROP: "Obreži"
STR_NEVER: "Nikoli"
STR_IN_READER: "V bralniku"
STR_ALWAYS: "Vedno"
STR_IGNORE: "Prezri"
STR_SLEEP: "Spanje"
STR_PAGE_TURN: "Obračanje strani"
STR_PORTRAIT: "Pokončno"
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)"
STR_INVERTED: "Obrnjeno"
STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
STR_PREV_NEXT: "Nazaj/Naprej"
STR_NEXT_PREV: "Naprej/Nazaj"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -85,9 +85,7 @@ STR_IMAGES: "Imágenes"
STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Reemplazar"
STR_IMAGES_SUPPRESS: "Ocultar"
STR_SHORT_PWR_BTN: "Toque corto botón encendido"
STR_ORIENTATION: "Orientación"
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
STR_LONG_PRESS_SKIP: "Saltar capítulo (pulsación larga)"
STR_FONT_FAMILY: "Tipografía"
STR_FONT_SIZE: "Tamaño"
@@ -141,15 +139,10 @@ STR_CROP: "Recortar"
STR_NEVER: "Nunca"
STR_IN_READER: "En el lector"
STR_ALWAYS: "Siempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Suspender"
STR_PAGE_TURN: "Pasar página"
STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horizontal (horario)"
STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
STR_PREV_NEXT: "Ant./Sig."
STR_NEXT_PREV: "Sig./Ant."
STR_ABC: "abc"
STR_FORCE_REFRESH: "Actualizar pantalla"
STR_NEXT: "Sig"
-7
View File
@@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffus Bayer"
STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Skapa fallback för ogiltig innehållsförteckning"
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
STR_ORIENTATION: "Läsrikting"
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
STR_LONG_PRESS_SKIP: "Lång-tryck Kapitelskippning"
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
@@ -199,15 +197,10 @@ STR_CROP: "Beskär"
STR_NEVER: "Aldrig"
STR_IN_READER: "I Eboksläsare"
STR_ALWAYS: "Alltid"
STR_IGNORE: "Ignorera"
STR_SLEEP: "Vila"
STR_PAGE_TURN: "Sidvändning"
STR_PORTRAIT: "Porträtt"
STR_LANDSCAPE_CW: "Landskap medurs"
STR_INVERTED: "Inverterad"
STR_LANDSCAPE_CCW: "Landskap moturs"
STR_PREV_NEXT: "Förra/Nästa"
STR_NEXT_PREV: "Nästa/Förra"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Öppen Dyslexic"
-7
View File
@@ -65,9 +65,7 @@ STR_SLEEP_COVER_MODE: "Uyku Ekranı Kapak Modu"
STR_HIDE_BATTERY: "Pil Yüzdesini Gizle"
STR_EXTRA_SPACING: "Ekstra Paragraf Boşluğu"
STR_TEXT_AA: "Metin Yumuşatma (AA)"
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
STR_LONG_PRESS_SKIP: "Uzun Basışla Bölüm Atla"
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
@@ -120,15 +118,10 @@ STR_CROP: "Kırp"
STR_NEVER: "Asla"
STR_IN_READER: "Okuyucuda"
STR_ALWAYS: "Her Zaman"
STR_IGNORE: "Yoksay"
STR_SLEEP: "Uyku"
STR_PAGE_TURN: "Sayfa Çevirme"
STR_PORTRAIT: "Dikey"
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
STR_INVERTED: "Ters"
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
STR_PREV_NEXT: "Önceki/Sonraki"
STR_NEXT_PREV: "Sonraki/Önceki"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
-7
View File
@@ -95,9 +95,7 @@ STR_IMAGE_DITHERING: "Дітеринг зображення"
STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Дифузний Bayer"
STR_SHORT_PWR_BTN: "Коротке натискання кнопки живлення"
STR_ORIENTATION: "Орієнтація читання"
STR_SIDE_BTN_LAYOUT: "Розташування бічних кнопок (читач)"
STR_LONG_PRESS_SKIP: "Пропуск розділу при довгому натисканні"
STR_FONT_FAMILY: "Сімейство шрифтів"
STR_FONT_SIZE: "Розмір шрифту інтерфейсу"
@@ -151,15 +149,10 @@ STR_CROP: "Обрізати"
STR_NEVER: "Ніколи"
STR_IN_READER: "В читачі"
STR_ALWAYS: "Завжди"
STR_IGNORE: "Ігнорувати"
STR_SLEEP: "Сон"
STR_PAGE_TURN: "Перегортання сторінки"
STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Альбомний за годинниковою"
STR_INVERTED: "Перевернутий"
STR_LANDSCAPE_CCW: "Альбомний проти годинникової"
STR_PREV_NEXT: "Попер/Наст"
STR_NEXT_PREV: "Наст/Попер"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
+117
View File
@@ -0,0 +1,117 @@
#include "ButtonEventManager.h"
#include "CrossPointSettings.h"
// Required for constexpr array out-of-class definition (C++14).
constexpr ButtonEventManager::Button ButtonEventManager::ALL_BUTTONS[ButtonEventManager::NUM_BUTTONS];
bool ButtonEventManager::hasDoubleAction(const Button button) {
using BA = CrossPointSettings::BUTTON_ACTION;
switch (button) {
case Button::Back:
return SETTINGS.btnDoubleBack != BA::BTN_DEFAULT;
case Button::Confirm:
return SETTINGS.btnDoubleConfirm != BA::BTN_DEFAULT;
case Button::Left:
return SETTINGS.btnDoubleLeft != BA::BTN_DEFAULT;
case Button::Right:
return SETTINGS.btnDoubleRight != BA::BTN_DEFAULT;
case Button::PageBack:
return SETTINGS.btnDoublePageBack != BA::BTN_DEFAULT;
case Button::PageForward:
return SETTINGS.btnDoublePageForward != BA::BTN_DEFAULT;
case Button::Power:
return SETTINGS.btnDoublePower != BA::BTN_DEFAULT;
}
return false;
}
void ButtonEventManager::pushEvent(const Button button, const PressType type) {
const int next = (eventTail + 1) % EVENT_BUF;
if (next == eventHead) return; // buffer full, drop oldest not possible — just drop newest
eventBuf[eventTail] = {button, type};
eventTail = next;
}
bool ButtonEventManager::consumeEvent(ButtonEvent& out) {
if (eventHead == eventTail) return false;
out = eventBuf[eventHead];
eventHead = (eventHead + 1) % EVENT_BUF;
return true;
}
void ButtonEventManager::drain() {
for (auto& b : buttons) {
b.state = State::Idle;
b.pressDownTime = 0;
b.releaseTime = 0;
}
eventHead = eventTail = 0;
}
void ButtonEventManager::processButton(const int idx, const Button btn) {
PerButton& s = buttons[idx];
const unsigned long now = millis();
const bool pressed = input.wasPressed(btn);
const bool released = input.wasReleased(btn);
const bool held = input.isPressed(btn);
switch (s.state) {
case State::Idle:
if (pressed) {
s.state = State::Pressed;
s.pressDownTime = now;
}
break;
case State::Pressed:
if (released) {
const unsigned long heldMs = now - s.pressDownTime;
if (heldMs >= LONG_PRESS_MS) {
pushEvent(btn, PressType::Long);
s.state = State::Idle;
} else if (hasDoubleAction(btn)) {
// Delay short-press decision until double-click window expires
s.releaseTime = now;
s.state = State::ReleasedOnce;
} else {
// No double action configured — fire immediately
pushEvent(btn, PressType::Short);
s.state = State::Idle;
}
} else if (!held) {
// Button disappeared without wasReleased edge (e.g. after drain) — reset
s.state = State::Idle;
}
break;
case State::ReleasedOnce:
if (pressed) {
// Second press within window — start tracking it
s.state = State::DoublePressed;
s.pressDownTime = now;
} else if (now - s.releaseTime >= DOUBLE_WINDOW_MS) {
// Window expired without a second press — it was a short press
pushEvent(btn, PressType::Short);
s.state = State::Idle;
}
break;
case State::DoublePressed:
if (released) {
pushEvent(btn, PressType::Double);
s.state = State::Idle;
} else if (!held) {
// Disappeared without edge — treat as double anyway
pushEvent(btn, PressType::Double);
s.state = State::Idle;
}
break;
}
}
void ButtonEventManager::update() {
for (int i = 0; i < NUM_BUTTONS; i++) {
processButton(i, ALL_BUTTONS[i]);
}
}
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include <Arduino.h>
#include "MappedInputManager.h"
// Forward declaration for the global accessor used by Activity.h.
// Defined in main.cpp alongside the ButtonEventManager instance.
class ButtonEventManager;
ButtonEventManager& globalButtonEvents();
// Classifies raw button edges into Short, Double, and Long press events.
//
// Per-button state machines run each loop() tick. The key latency rule:
// - If no double-click action is configured for a button, Short fires immediately
// on release (zero extra wait).
// - If a double-click action IS configured, Short is delayed by DOUBLE_WINDOW_MS
// to allow disambiguation.
// - Long fires on release once hold time >= LONG_PRESS_MS (no extra wait).
// - Double fires on the second release within DOUBLE_WINDOW_MS.
//
// Activities query consumeEvent() each loop tick to receive pending events.
// drain() resets all state machines — call it on activity transitions.
class ButtonEventManager {
public:
using Button = MappedInputManager::Button;
enum class PressType { Short, Double, Long };
struct ButtonEvent {
Button button;
PressType type;
};
// Timing constants (milliseconds)
static constexpr unsigned long LONG_PRESS_MS = 600;
static constexpr unsigned long DOUBLE_WINDOW_MS = 300;
explicit ButtonEventManager(MappedInputManager& input) : input(input) {}
// Call once per main loop tick, after MappedInputManager::update().
void update();
// Returns the next pending event, or false if none. Call repeatedly until
// false to drain all events for this tick.
bool consumeEvent(ButtonEvent& out);
// Reset all per-button FSMs. Call on activity transitions to prevent bleed-through.
void drain();
// Returns true if a double-click action is configured for this button.
// ButtonEventManager queries CrossPointSettings internally.
static bool hasDoubleAction(Button button);
private:
static constexpr int NUM_BUTTONS = 7;
static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = {
Button::Back, Button::Confirm, Button::Left, Button::Right, Button::PageBack, Button::PageForward, Button::Power,
};
enum class State { Idle, Pressed, ReleasedOnce, DoublePressed };
struct PerButton {
State state = State::Idle;
unsigned long pressDownTime = 0; // when the current (or first) press started
unsigned long releaseTime = 0; // when the first release happened (for double-click window)
};
PerButton buttons[NUM_BUTTONS];
// Pending events ring buffer (small — at most one event per button per tick)
static constexpr int EVENT_BUF = 16;
ButtonEvent eventBuf[EVENT_BUF] = {};
int eventHead = 0;
int eventTail = 0;
MappedInputManager& input;
void pushEvent(Button button, PressType type);
void processButton(int idx, Button btn);
};
+12 -3
View File
@@ -140,7 +140,10 @@ bool CrossPointSettings::loadFromBinaryFile() {
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, extraParagraphSpacing);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, shortPwrBtn, SHORT_PWRBTN_COUNT);
{
uint8_t ignored;
serialization::readPod(inputFile, ignored);
} // legacy shortPwrBtn field
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT); // legacy
if (++settingsRead >= fileSettingsCount) break;
@@ -148,7 +151,10 @@ bool CrossPointSettings::loadFromBinaryFile() {
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT);
{
uint8_t ignored;
serialization::readPod(inputFile, ignored);
} // legacy sideButtonLayout field
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT);
if (++settingsRead >= fileSettingsCount) break;
@@ -177,7 +183,10 @@ bool CrossPointSettings::loadFromBinaryFile() {
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT);
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, longPressChapterSkip);
{
uint8_t ignored;
serialization::readPod(inputFile, ignored);
} // was longPressChapterSkip
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hyphenationEnabled);
if (++settingsRead >= fileSettingsCount) break;
+51 -24
View File
@@ -88,11 +88,6 @@ class CrossPointSettings {
FRONT_BUTTON_HARDWARE_COUNT
};
// Side button layout options
// Default: Previous, Next
// Swapped: Next, Previous
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT };
// Font family options
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };
// Font size options
@@ -127,17 +122,6 @@ class CrossPointSettings {
REFRESH_FREQUENCY_COUNT
};
// Short power button press actions
enum SHORT_PWRBTN {
IGNORE = 0,
SLEEP = 1,
PAGE_TURN = 2,
FORCE_REFRESH = 3,
FOOTNOTES = 4,
STAR_PAGE = 5,
SHORT_PWRBTN_COUNT
};
// Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
@@ -214,14 +198,11 @@ class CrossPointSettings {
// Text darkness (0 = normal, 1 = dark, 2 = extra dark). Default 1 preserves
// historical AA rendering (both grayscale shades drawn in the MSB pass).
uint8_t textDarkness = DARKNESS_DARK;
// Short power button click behaviour
uint8_t shortPwrBtn = IGNORE;
// EPUB reading orientation settings
// 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise
uint8_t orientation = PORTRAIT;
// Button layouts (front layout retained for migration only)
uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT;
uint8_t sideButtonLayout = PREV_NEXT;
// Front button remap (logical -> hardware)
// Used by MappedInputManager to translate logical buttons into physical front buttons.
uint8_t frontButtonBack = FRONT_HW_BACK;
@@ -249,8 +230,6 @@ class CrossPointSettings {
char opdsPassword[64] = "";
// Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press chapter skip on side buttons
uint8_t longPressChapterSkip = 1;
// UI Theme
uint8_t uiTheme = LYRA;
// Sunlight fading compensation
@@ -279,14 +258,62 @@ class CrossPointSettings {
// Show the Weather home screen menu item (1 = enabled, 0 = hidden)
uint8_t useWeather = 1;
// Configurable actions for short / double / long press on each logical button.
// BTN_DEFAULT means "use the button's normal built-in behaviour".
enum BUTTON_ACTION {
BTN_DEFAULT = 0,
BTN_PAGE_FORWARD,
BTN_PAGE_BACK,
BTN_PAGE_FORWARD_10,
BTN_PAGE_BACK_10,
BTN_GO_HOME,
BTN_SLEEP,
BTN_FORCE_REFRESH,
BTN_OPEN_TOC,
BTN_OPEN_BOOKMARKS,
BTN_STAR_PAGE,
BTN_FOOTNOTES,
BTN_NEXT_SECTION,
BTN_PREV_SECTION,
BTN_EXIT_READER,
BTN_READER_MENU,
BTN_KOREADER_SYNC,
BUTTON_ACTION_COUNT
};
// Short-press actions (default: built-in)
uint8_t btnShortBack = BTN_DEFAULT;
uint8_t btnShortConfirm = BTN_DEFAULT;
uint8_t btnShortLeft = BTN_DEFAULT;
uint8_t btnShortRight = BTN_DEFAULT;
uint8_t btnShortPageBack = BTN_DEFAULT;
uint8_t btnShortPageForward = BTN_DEFAULT;
uint8_t btnShortPower = BTN_DEFAULT;
// Double-press actions (default: BTN_DEFAULT = disabled, no disambiguation wait)
uint8_t btnDoubleBack = BTN_DEFAULT;
uint8_t btnDoubleConfirm = BTN_DEFAULT;
uint8_t btnDoubleLeft = BTN_DEFAULT;
uint8_t btnDoubleRight = BTN_DEFAULT;
uint8_t btnDoublePageBack = BTN_DEFAULT;
uint8_t btnDoublePageForward = BTN_DEFAULT;
uint8_t btnDoublePower = BTN_DEFAULT;
// Long-press actions (default: built-in)
uint8_t btnLongBack = BTN_DEFAULT;
uint8_t btnLongConfirm = BTN_DEFAULT;
uint8_t btnLongLeft = BTN_DEFAULT;
uint8_t btnLongRight = BTN_DEFAULT;
uint8_t btnLongPageBack = BTN_DEFAULT;
uint8_t btnLongPageForward = BTN_DEFAULT;
uint8_t btnLongPower = BTN_DEFAULT;
~CrossPointSettings() = default;
// Get singleton instance
static CrossPointSettings& getInstance() { return instance; }
uint16_t getPowerButtonDuration() const {
return (shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 10 : 400;
}
static constexpr uint16_t getPowerButtonDuration() { return 400; }
int getReaderFontId() const;
// If count_only is true, returns the number of settings items that would be written.
+2 -30
View File
@@ -2,55 +2,27 @@
#include "CrossPointSettings.h"
namespace {
using ButtonIndex = uint8_t;
struct SideLayoutMap {
ButtonIndex pageBack;
ButtonIndex pageForward;
};
// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT.
constexpr SideLayoutMap kSideLayouts[] = {
{HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
{HalGPIO::BTN_DOWN, HalGPIO::BTN_UP},
};
} // namespace
bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
const auto& side = kSideLayouts[sideLayout];
switch (button) {
case Button::Back:
// Logical Back maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonBack);
case Button::Confirm:
// Logical Confirm maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonConfirm);
case Button::Left:
// Logical Left maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonLeft);
case Button::Right:
// Logical Right maps to user-configured front button.
return (gpio.*fn)(SETTINGS.frontButtonRight);
case Button::Up:
// Side buttons remain fixed for Up/Down.
return (gpio.*fn)(HalGPIO::BTN_UP);
case Button::Down:
// Side buttons remain fixed for Up/Down.
return (gpio.*fn)(HalGPIO::BTN_DOWN);
case Button::Power:
// Power button bypasses remapping.
return (gpio.*fn)(HalGPIO::BTN_POWER);
case Button::PageBack:
// Reader page navigation uses side buttons and can be swapped via settings.
return (gpio.*fn)(side.pageBack);
return (gpio.*fn)(HalGPIO::BTN_UP);
case Button::PageForward:
// Reader page navigation uses side buttons and can be swapped via settings.
return (gpio.*fn)(side.pageForward);
return (gpio.*fn)(HalGPIO::BTN_DOWN);
}
return false;
}
+87 -9
View File
@@ -130,15 +130,93 @@ inline const std::vector<SettingInfo> list = {
"syntheticTocFallback", StrId::STR_CAT_READER)
.withSubcategory(StrId::STR_MENU_READER_TWEAKS),
// --- Controls ---
SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout,
{StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip",
StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
{StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH,
StrId::STR_FOOTNOTES, StrId::STR_STAR_PAGE},
"shortPwrBtn", StrId::STR_CAT_CONTROLS),
// --- Controls ---
// --- Button Actions (short / double / long press per logical button) ---
// All entries share the same ordered action-label list; the submenu groups them behind
// a single placeholder row in the device UI.
// Shared action options (everything except the first "default" entry).
#define BTN_ACT_OPTIONS \
StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \
StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \
StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, StrId::STR_BTN_ACT_OPEN_BOOKMARKS, \
StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, StrId::STR_BTN_ACT_NEXT_SECTION, \
StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, StrId::STR_BTN_ACT_READER_MENU, \
StrId::STR_BTN_ACT_KOREADER_SYNC
// Back button: short=exit reader, double=ignore, long=go home
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack,
{StrId::STR_BTN_DEF_EXIT_READER, BTN_ACT_OPTIONS}, "btnShortBack", StrId::STR_CAT_CONTROLS)
.withSubcategory(StrId::STR_MENU_BTN_ACTIONS)
.withSubmenu(StrId::STR_BTN_BACK),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleBack,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleBack", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_BACK),
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongBack,
{StrId::STR_BTN_DEF_GO_HOME, BTN_ACT_OPTIONS}, "btnLongBack", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_BACK),
// Confirm button: short=reader menu, double=ignore, long=KOReader sync
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortConfirm,
{StrId::STR_BTN_DEF_READER_MENU, BTN_ACT_OPTIONS}, "btnShortConfirm", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_CONFIRM),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleConfirm,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleConfirm", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_CONFIRM),
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongConfirm,
{StrId::STR_BTN_DEF_KOREADER_SYNC, BTN_ACT_OPTIONS}, "btnLongConfirm", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_CONFIRM),
// Left button: short=previous page, double=ignore, long=chapter back
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortLeft,
{StrId::STR_BTN_DEF_PREV_PAGE, BTN_ACT_OPTIONS}, "btnShortLeft", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_LEFT),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleLeft,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleLeft", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_LEFT),
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongLeft,
{StrId::STR_BTN_DEF_CHAPTER_BACK, BTN_ACT_OPTIONS}, "btnLongLeft", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_LEFT),
// Right button: short=next page, double=ignore, long=chapter forward
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortRight,
{StrId::STR_BTN_DEF_NEXT_PAGE, BTN_ACT_OPTIONS}, "btnShortRight", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_RIGHT),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleRight,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleRight", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_RIGHT),
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongRight,
{StrId::STR_BTN_DEF_CHAPTER_FORWARD, BTN_ACT_OPTIONS}, "btnLongRight", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_RIGHT),
// Page Back button: short=previous page, double=ignore, long=chapter back
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPageBack,
{StrId::STR_BTN_DEF_PREV_PAGE, BTN_ACT_OPTIONS}, "btnShortPageBack", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_PAGE_BACK),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePageBack,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoublePageBack", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_PAGE_BACK),
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPageBack,
{StrId::STR_BTN_DEF_CHAPTER_BACK, BTN_ACT_OPTIONS}, "btnLongPageBack", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_PAGE_BACK),
// Page Forward button: short=next page, double=ignore, long=chapter forward
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPageForward,
{StrId::STR_BTN_DEF_NEXT_PAGE, BTN_ACT_OPTIONS}, "btnShortPageForward", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_PAGE_FORWARD),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePageForward,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoublePageForward", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_PAGE_FORWARD),
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPageForward,
{StrId::STR_BTN_DEF_CHAPTER_FORWARD, BTN_ACT_OPTIONS}, "btnLongPageForward",
StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_PAGE_FORWARD),
// Power button: short=ignore, double=ignore, long=sleep (via hold timer, not event system)
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPower,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnShortPower", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_POWER),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePower,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoublePower", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_POWER),
SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPower, {StrId::STR_BTN_DEF_SLEEP},
"btnLongPower", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_POWER),
#undef BTN_ACT_OPTIONS
// --- System ---
SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles",
+8 -1
View File
@@ -8,6 +8,7 @@
#include "ActivityManager.h" // for using the ActivityManager singleton
#include "ActivityResult.h"
#include "ButtonEventManager.h"
#include "GfxRenderer.h"
#include "MappedInputManager.h"
#include "RenderLock.h"
@@ -19,13 +20,14 @@ class Activity {
std::string name;
GfxRenderer& renderer;
MappedInputManager& mappedInput;
ButtonEventManager& buttonEvents;
ActivityResultHandler resultHandler;
ActivityResult result;
public:
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput), buttonEvents(globalButtonEvents()) {}
virtual ~Activity() = default;
const std::string& getName() const { return name; }
virtual void onEnter();
@@ -45,6 +47,11 @@ class Activity {
virtual bool preventAutoSleep() { return false; }
virtual bool isReaderActivity() const { return false; }
// Called by ActivityManager when a globally-configured button action targets the
// current activity. Override in reader activities to handle reader-specific actions.
// Non-reader activities can ignore this (default is no-op).
virtual void onButtonAction(CrossPointSettings::BUTTON_ACTION) {}
// Start a new activity without destroying the current one
// Note: requestUpdate() will be invoked automatically once resultHandler finishes
void startActivityForResult(std::unique_ptr<Activity>&& activity, ActivityResultHandler resultHandler);
+8
View File
@@ -154,6 +154,7 @@ void ActivityManager::loop() {
// Arm input drain so the button that triggered the pop doesn't bleed into the
// restored activity (or into a new activity the handler just pushed).
drainInput = true;
buttonEvents.drain();
// Request an update to ensure the popped activity gets re-rendered
if (pendingAction == PendingAction::None) {
@@ -204,6 +205,7 @@ void ActivityManager::loop() {
// Arm input drain so the button that triggered the transition doesn't bleed
// into the new activity.
drainInput = true;
buttonEvents.drain();
// onEnter may request another pending action, we will handle it in the next loop iteration
continue;
@@ -387,6 +389,12 @@ bool ActivityManager::isReaderActivity() const { return currentActivity && curre
bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); }
void ActivityManager::dispatchButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
if (currentActivity && currentActivity->isReaderActivity()) {
currentActivity->onButtonAction(action);
}
}
void ActivityManager::requestUpdate(bool immediate) {
if (immediate) {
if (renderTaskHandle) {
+12 -1
View File
@@ -9,6 +9,8 @@
#include <string>
#include <vector>
#include "ButtonEventManager.h"
#include "CrossPointSettings.h"
#include "GfxRenderer.h"
#include "MappedInputManager.h"
@@ -52,6 +54,7 @@ class ActivityManager {
protected:
GfxRenderer& renderer;
MappedInputManager& mappedInput;
ButtonEventManager& buttonEvents;
std::vector<std::unique_ptr<Activity>> stackActivities;
std::unique_ptr<Activity> currentActivity;
@@ -96,7 +99,10 @@ class ActivityManager {
public:
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
: renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
: renderer(renderer),
mappedInput(mappedInput),
buttonEvents(globalButtonEvents()),
renderingMutex(xSemaphoreCreateMutex()) {
assert(renderingMutex != nullptr && "Failed to create rendering mutex");
stackActivities.reserve(10);
}
@@ -160,6 +166,11 @@ class ActivityManager {
bool isReaderActivity() const;
bool skipLoopDelay() const;
// Dispatch a globally-configured button action to the current activity.
// Reader-specific actions (page navigation, TOC, bookmarks, footnotes) are forwarded
// only when the current activity is a reader; others are no-ops in other contexts.
void dispatchButtonAction(CrossPointSettings::BUTTON_ACTION action);
// If immediate is true, the update will be triggered immediately.
// Otherwise, it will be deferred until the end of the current loop iteration.
void requestUpdate(bool immediate = false);
+3 -3
View File
@@ -51,9 +51,9 @@ class HomeActivity final : public Activity {
void dispatchMenuAction(MenuAction action);
void rebuildMenuEntries();
bool storeCoverBuffer(); // Store frame buffer for cover image
bool restoreCoverBuffer(); // Restore frame buffer from stored cover
void freeCoverBuffer(); // Free the stored cover buffer
bool storeCoverBuffer();
bool restoreCoverBuffer();
void freeCoverBuffer();
void loadRecentBooks(int maxBooks);
void loadRecentCovers(int coverHeight);
+186 -51
View File
@@ -51,22 +51,37 @@ void logReaderMemSnapshot(const char* stage) {
inline void logReaderMemSnapshot(const char*) {}
#endif
// Computes the [0..100] EPUB progress percent. Returns 0 when pageCount is unknown (sync/bookmark
// pre-render writes), in which case the next saveProgress() will overwrite progress.bin with the
// real value before the user can leave the reader.
uint8_t epubProgressPercentByte(const Epub& epub, const int spineIndex, const int currentPage, const int pageCount) {
if (pageCount <= 0) {
return 0;
}
const float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
return ReaderUtils::fractionProgressPercentByte(epub.calculateProgress(spineIndex, chapterProgress));
}
// Writes the canonical EPUB progress.bin layout: spine(2) + page(2) + pageCount(2) + percent(1).
// Used by the per-page saveProgress() and by transient writers (sync restore, bookmark jump) so
// the on-disk format stays consistent regardless of caller.
bool writeReaderProgressCache(const std::string& cachePath, const int spineIndex, const int currentPage,
const int pageCount) {
const int pageCount, const uint8_t percent) {
FsFile f;
if (!Storage.openFileForWrite("ERS", cachePath + "/progress.bin", f)) {
LOG_ERR("ERS", "Failed to open progress cache for sync restore: %s", cachePath.c_str());
LOG_ERR("ERS", "Failed to open progress cache: %s", cachePath.c_str());
return false;
}
uint8_t data[6];
uint8_t data[7];
data[0] = spineIndex & 0xFF;
data[1] = (spineIndex >> 8) & 0xFF;
data[2] = currentPage & 0xFF;
data[3] = (currentPage >> 8) & 0xFF;
data[4] = pageCount & 0xFF;
data[5] = (pageCount >> 8) & 0xFF;
f.write(data, 6);
data[6] = percent;
f.write(data, 7);
f.close();
return true;
}
@@ -295,37 +310,6 @@ void EpubReaderActivity::loop() {
return;
}
const bool screenshotChordReleased = gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN);
// Handle short power button press for footnotes
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FOOTNOTES &&
mappedInput.wasReleased(MappedInputManager::Button::Power) && !screenshotChordReleased) {
if (currentPageFootnotes.size() == 1) {
navigateToHref(currentPageFootnotes[0].href, true);
} else if (currentPageFootnotes.size() > 1) {
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& footnoteResult = std::get<FootnoteResult>(result.data);
navigateToHref(footnoteResult.href, true);
}
requestUpdate();
});
}
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);
if (!prevTriggered && !nextTriggered) {
return;
@@ -343,7 +327,7 @@ void EpubReaderActivity::loop() {
return;
}
const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs;
const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs;
// Chapter skip navigates by TOC entries, not spine boundaries.
// Spine items without their own TOC entry inherit the previous spine's tocIndex
@@ -931,7 +915,9 @@ void EpubReaderActivity::applyPendingSyncSession() {
// Store 0 to disable rescaling; the paragraph lookup handles precise positioning.
const int restorePageCount = (restoreSpineIndex == sync.spineIndex) ? sync.totalPagesInSpine : 0;
if (writeReaderProgressCache(epub->getCachePath(), restoreSpineIndex, restorePage, restorePageCount)) {
// Transient write — the next render's saveProgress() supplies the real percent before the user
// can return to the home screen, so a placeholder 0 here is harmless.
if (writeReaderProgressCache(epub->getCachePath(), restoreSpineIndex, restorePage, restorePageCount, 0)) {
cachedSpineIndex = restoreSpineIndex;
cachedChapterTotalPageCount = restorePageCount;
LOG_DBG("ERS", "Prepared progress.bin for sync restore: spine=%d page=%d/%d", restoreSpineIndex, restorePage,
@@ -955,7 +941,8 @@ void EpubReaderActivity::applyPendingBookmarkJump() {
return;
}
LOG_DBG("ERS", "Applying pending bookmark jump: spine=%u page=%u", jump.spineIndex, jump.pageNumber);
if (writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0)) {
// Transient write before initializeReader; saveProgress() overwrites with the real percent.
if (writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0, 0)) {
cachedSpineIndex = jump.spineIndex;
cachedChapterTotalPageCount = 0;
} else {
@@ -1414,21 +1401,12 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
}
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
FsFile f;
if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
uint8_t data[6];
data[0] = spineIndex & 0xFF;
data[1] = (spineIndex >> 8) & 0xFF;
data[2] = currentPage & 0xFF;
data[3] = (currentPage >> 8) & 0xFF;
data[4] = pageCount & 0xFF;
data[5] = (pageCount >> 8) & 0xFF;
f.write(data, 6);
f.close();
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage);
} else {
const uint8_t percent = epubProgressPercentByte(*epub, spineIndex, currentPage, pageCount);
if (!writeReaderProgressCache(epub->getCachePath(), spineIndex, currentPage, pageCount, percent)) {
LOG_ERR("ERS", "Could not save progress!");
return;
}
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d (%d%%)", spineIndex, currentPage, percent);
}
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom,
@@ -1795,3 +1773,160 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
// No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay
return true;
}
void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
using BA = CrossPointSettings::BUTTON_ACTION;
switch (action) {
case BA::BTN_PAGE_FORWARD:
pageTurn(true);
break;
case BA::BTN_PAGE_BACK:
pageTurn(false);
break;
case BA::BTN_PAGE_FORWARD_10:
for (int i = 0; i < 10; i++) {
if (!stepPageState(true)) break;
}
requestUpdate();
break;
case BA::BTN_PAGE_BACK_10:
for (int i = 0; i < 10; i++) {
if (!stepPageState(false)) break;
}
requestUpdate();
break;
case BA::BTN_STAR_PAGE:
if (section) {
bookmarkStore.toggle(static_cast<uint16_t>(currentSpineIndex), static_cast<uint16_t>(section->currentPage));
requestUpdate();
}
break;
case BA::BTN_FOOTNOTES:
if (!currentPageFootnotes.empty()) {
if (currentPageFootnotes.size() == 1) {
navigateToHref(currentPageFootnotes[0].href, true);
} else {
startActivityForResult(
std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& footnoteResult = std::get<FootnoteResult>(result.data);
navigateToHref(footnoteResult.href, true);
}
});
}
}
break;
case BA::BTN_OPEN_TOC:
if (epub) {
const int spineIdx = currentSpineIndex;
const int tocIdx = section ? section->getTocIndexForPage(section->currentPage)
: epub->getTocIndexForSpineIndex(currentSpineIndex);
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(std::make_unique<EpubReaderChapterSelectionActivity>(renderer, mappedInput, epub,
epub->getPath(), spineIdx, tocIdx),
[this](const ActivityResult& result) {
if (result.isCancelled) return;
RenderLock lock(*this);
const auto& chapter = std::get<ChapterResult>(result.data);
auto resolvedPage =
(chapter.tocIndex && chapter.spineIndex == currentSpineIndex && section)
? section->getPageForTocIndex(*chapter.tocIndex)
: std::nullopt;
if (resolvedPage) {
section->currentPage = *resolvedPage;
} else {
pendingTocIndex = chapter.tocIndex;
currentSpineIndex = chapter.spineIndex;
nextPageNumber = 0;
section.reset();
}
});
}
break;
case BA::BTN_NEXT_SECTION:
case BA::BTN_PREV_SECTION: {
const bool forward = (action == BA::BTN_NEXT_SECTION);
{
RenderLock lock(*this);
if (section && section->pageCount > 0) {
const int curTocIndex = section->getTocIndexForPage(section->currentPage);
const int nextTocIndex = forward ? curTocIndex + 1 : curTocIndex - 1;
if (curTocIndex < 0) {
nextPageNumber = 0;
currentSpineIndex = forward ? currentSpineIndex + 1 : currentSpineIndex - 1;
section.reset();
} else if (nextTocIndex >= 0 && nextTocIndex < epub->getTocItemsCount()) {
const int newSpineIndex = epub->getSpineIndexForTocIndex(nextTocIndex);
if (newSpineIndex == currentSpineIndex) {
if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) {
section->currentPage = *resolvedPage;
}
} else {
pendingTocIndex = nextTocIndex;
nextPageNumber = 0;
currentSpineIndex = newSpineIndex;
section.reset();
}
} else if (forward) {
nextPageNumber = 0;
currentSpineIndex = epub->getSpineItemsCount();
section.reset();
} else {
nextPageNumber = 0;
currentSpineIndex = epub->getTocItem(curTocIndex).spineIndex - 1;
section.reset();
}
} else {
nextPageNumber = 0;
currentSpineIndex = forward ? currentSpineIndex + 1 : currentSpineIndex - 1;
section.reset();
}
}
requestUpdate();
break;
}
case BA::BTN_EXIT_READER:
ReaderUtils::enforceExitFullRefresh(renderer);
finish();
break;
case BA::BTN_READER_MENU:
if (epub) {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress =
static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
static_cast<uint16_t>(section->currentPage));
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness,
!bookmarkStore.isEmpty(), isCurrentPageStarred),
[this](const ActivityResult& result) {
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
menu.fontSizeOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
}
break;
case BA::BTN_KOREADER_SYNC:
launchKOReaderSync(SyncLaunchMode::COMPARE);
break;
default:
break;
}
}
@@ -188,6 +188,7 @@ class EpubReaderActivity final : public Activity {
void loop() override;
void render(RenderLock&& lock) override;
bool isReaderActivity() const override { return true; }
void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override;
// Renders the last saved page to the frame buffer without flushing to display.
// Used by SleepActivity to prepare the background for the overlay sleep mode.
+90 -15
View File
@@ -253,7 +253,7 @@ void MdReaderActivity::loop() {
return;
}
const bool headingSkip = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > HEADING_SKIP_MS;
const bool headingSkip = mappedInput.getHeldTime() > HEADING_SKIP_MS;
if (headingSkip && !headings.empty()) {
jumpToHeading(nextTriggered);
return;
@@ -747,29 +747,38 @@ void MdReaderActivity::renderStatusBar() const {
void MdReaderActivity::saveProgress() const {
FsFile f;
if (Storage.openFileForWrite("MDR", txt->getCachePath() + "/progress.bin", f)) {
uint32_t page = static_cast<uint32_t>(currentPage < 0 ? 0 : currentPage);
uint8_t data[4];
data[0] = page & 0xFF;
data[1] = (page >> 8) & 0xFF;
data[2] = (page >> 16) & 0xFF;
data[3] = (page >> 24) & 0xFF;
f.write(data, 4);
// 7-byte format matching TxtReaderActivity: page(2 bytes LE) + file offset(4 bytes LE) + overallPercent(1 byte)
const size_t offset =
(currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
uint8_t data[7];
data[0] = currentPage & 0xFF;
data[1] = (currentPage >> 8) & 0xFF;
data[2] = offset & 0xFF;
data[3] = (offset >> 8) & 0xFF;
data[4] = (offset >> 16) & 0xFF;
data[5] = (offset >> 24) & 0xFF;
data[6] = ReaderUtils::pageProgressPercentByte(currentPage, totalPages);
f.write(data, 7);
f.close();
}
}
void MdReaderActivity::loadProgress() {
FsFile f;
if (Storage.openFileForRead("MDR", txt->getCachePath() + "/progress.bin", f)) {
uint8_t data[4];
if (f.read(data, 4) == 4) {
uint32_t loadedPage = static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
uint8_t data[7];
const int dataSize = f.read(data, 7);
f.close();
if (dataSize >= 4) {
// Page sits in bytes 0-1 in both the old 4-byte uint32 format and the new 7-byte format
// (page counts stay well under 65536, so the upper bytes were always zero).
int loadedPage = data[0] + (data[1] << 8);
if (totalPages == 0) {
currentPage = 0;
} else if (loadedPage >= static_cast<uint32_t>(totalPages)) {
} else if (loadedPage >= totalPages) {
currentPage = totalPages - 1;
} else {
currentPage = static_cast<int>(loadedPage);
currentPage = loadedPage;
}
LOG_DBG("MDR", "Loaded progress: page %d/%d", currentPage, totalPages);
}
@@ -892,4 +901,70 @@ void MdReaderActivity::savePageIndexCache() const {
}
LOG_DBG("MDR", "Saved page index cache: %d pages", totalPages);
}
}
void MdReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
using BA = CrossPointSettings::BUTTON_ACTION;
auto clampPage = [this]() {
if (totalPages == 0) {
currentPage = 0;
return;
}
if (currentPage < 0) currentPage = 0;
if (currentPage >= totalPages) currentPage = totalPages - 1;
};
switch (action) {
case BA::BTN_PAGE_FORWARD:
if (currentPage < totalPages - 1) {
currentPage++;
currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]);
requestUpdate();
}
break;
case BA::BTN_PAGE_BACK:
if (currentPage > 0) {
currentPage--;
currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]);
requestUpdate();
}
break;
case BA::BTN_PAGE_FORWARD_10:
currentPage += 10;
clampPage();
currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]);
requestUpdate();
break;
case BA::BTN_PAGE_BACK_10:
currentPage -= 10;
clampPage();
currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]);
requestUpdate();
break;
case BA::BTN_NEXT_SECTION:
jumpToHeading(true);
break;
case BA::BTN_PREV_SECTION:
jumpToHeading(false);
break;
case BA::BTN_OPEN_TOC:
if (!headings.empty()) {
currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]);
startActivityForResult(
std::make_unique<MdReaderTocSelectionActivity>(renderer, mappedInput, headings, currentHeadingIndex),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
currentPage = std::get<PageResult>(result.data).page;
currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]);
requestUpdate();
}
});
}
break;
case BA::BTN_EXIT_READER:
ReaderUtils::enforceExitFullRefresh(renderer);
finish();
break;
default:
break;
}
}
+1
View File
@@ -89,4 +89,5 @@ class MdReaderActivity final : public Activity {
void loop() override;
void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; }
void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override;
};
+37 -13
View File
@@ -4,12 +4,37 @@
#include <GfxRenderer.h>
#include <Logging.h>
#include <cstdint>
#include "MappedInputManager.h"
namespace ReaderUtils {
constexpr unsigned long GO_HOME_MS = 1000;
// Round-half-up integer division clamped to [0, 100], used as the percent byte appended to
// progress.bin so the home screen can render a per-book badge without re-loading the document.
// All reader types must funnel through this so the displayed value matches across formats.
inline uint8_t pageProgressPercentByte(int currentPage, int totalPages) {
if (totalPages <= 0 || currentPage < 0) {
return 0;
}
const long numerator = static_cast<long>(currentPage + 1) * 200L + totalPages;
const long percent = numerator / (2L * totalPages);
if (percent < 0) return 0;
if (percent > 100) return 100;
return static_cast<uint8_t>(percent);
}
// Round-half-up clamp for a pre-computed [0,1] progress fraction (used by EPUB, where progress
// is byte-weighted across spine items rather than a simple page ratio).
inline uint8_t fractionProgressPercentByte(float fraction) {
const int percent = static_cast<int>(fraction * 100.0f + 0.5f);
if (percent < 0) return 0;
if (percent > 100) return 100;
return static_cast<uint8_t>(percent);
}
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
switch (orientation) {
case CrossPointSettings::ORIENTATION::PORTRAIT:
@@ -31,8 +56,8 @@ inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
// Suppresses input processing on activity entry until the user has released all buttons and a
// clean frame (no pending press/release events) has been observed. Without this, the power-button
// hold used to wake the device leaks into detectPageTurn() and triggers a page turn or, with
// longPressChapterSkip enabled, a chapter skip (the wake-hold easily exceeds skipChapterMs).
// hold used to wake the device leaks into detectPageTurn() and triggers a page turn or chapter
// skip (the wake-hold easily exceeds skipChapterMs).
// Each reader holds an instance, calls arm() in onEnter(), and calls shouldDrain() at the top
// of loop() — returning early when it returns true.
struct InputDrainGuard {
@@ -62,17 +87,16 @@ struct PageTurnResult {
};
inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
const bool usePress = !SETTINGS.longPressChapterSkip;
const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) ||
input.wasPressed(MappedInputManager::Button::Left))
: (input.wasReleased(MappedInputManager::Button::PageBack) ||
input.wasReleased(MappedInputManager::Button::Left));
const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
input.wasReleased(MappedInputManager::Button::Power);
const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn ||
input.wasPressed(MappedInputManager::Button::Right))
: (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn ||
input.wasReleased(MappedInputManager::Button::Right));
// Only treat wasReleased as a page turn when the button's short-press action is default.
// Non-default short-press actions are dispatched by the global dispatcher in main.cpp;
// counting wasReleased as well would double-fire the action.
using BA = CrossPointSettings::BUTTON_ACTION;
const bool prev =
(SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageBack)) ||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Left));
const bool next =
(SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageForward)) ||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Right));
return {prev, next};
}
+63 -13
View File
@@ -155,16 +155,6 @@ void TxtReaderActivity::loop() {
return;
}
// Star page toggle via short power button press
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE &&
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
if (currentPage >= 0) {
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()) {
ReaderUtils::enforceExitFullRefresh(renderer);
@@ -418,17 +408,18 @@ void TxtReaderActivity::renderStatusBar() const {
void TxtReaderActivity::saveProgress() const {
FsFile f;
if (Storage.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
// 6-byte format: page(2 bytes LE) + file offset(4 bytes LE)
// 7-byte format: page(2 bytes LE) + file offset(4 bytes LE) + overallPercent(1 byte)
// The offset lets drawCurrentPageToBuffer render without requiring index.bin.
const size_t offset = (currentPage < static_cast<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
uint8_t data[6];
uint8_t data[7];
data[0] = currentPage & 0xFF;
data[1] = (currentPage >> 8) & 0xFF;
data[2] = offset & 0xFF;
data[3] = (offset >> 8) & 0xFF;
data[4] = (offset >> 16) & 0xFF;
data[5] = (offset >> 24) & 0xFF;
f.write(data, 6);
data[6] = ReaderUtils::pageProgressPercentByte(currentPage, totalPages);
f.write(data, 7);
f.close();
}
}
@@ -782,3 +773,62 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx
}
return true;
}
void TxtReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
using BA = CrossPointSettings::BUTTON_ACTION;
auto clampPage = [this]() {
if (currentPage < 0) currentPage = 0;
if (currentPage >= totalPages) currentPage = totalPages - 1;
};
switch (action) {
case BA::BTN_PAGE_FORWARD:
if (currentPage < totalPages - 1) {
currentPage++;
requestUpdate();
}
break;
case BA::BTN_PAGE_BACK:
if (currentPage > 0) {
currentPage--;
requestUpdate();
}
break;
case BA::BTN_PAGE_FORWARD_10:
currentPage += 10;
clampPage();
requestUpdate();
break;
case BA::BTN_PAGE_BACK_10:
currentPage -= 10;
clampPage();
requestUpdate();
break;
case BA::BTN_STAR_PAGE:
bookmarkStore.toggle(0, static_cast<uint16_t>(currentPage));
requestUpdate();
break;
case BA::BTN_OPEN_BOOKMARKS:
if (!bookmarkStore.isEmpty()) {
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& starred = std::get<StarredPageResult>(result.data);
currentPage = starred.pageNumber;
requestUpdate();
}
});
}
break;
case BA::BTN_NEXT_SECTION:
case BA::BTN_PREV_SECTION:
// TXT files have no headings/chapters; treat as unsupported (no-op).
break;
case BA::BTN_EXIT_READER:
ReaderUtils::enforceExitFullRefresh(renderer);
finish();
break;
default:
break;
}
}
@@ -58,6 +58,7 @@ class TxtReaderActivity final : public Activity {
void loop() override;
void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; }
void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override;
// Renders the last saved page to the frame buffer without flushing to display.
// Used by SleepActivity to prepare the background for the overlay sleep mode.
+68 -17
View File
@@ -12,13 +12,14 @@
#include <HalStorage.h>
#include <I18n.h>
#include <algorithm>
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "MappedInputManager.h"
#include "ReaderUtils.h"
#include "RecentBooksStore.h"
#include "XtcReaderChapterSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
@@ -92,19 +93,10 @@ void XtcReaderActivity::loop() {
return;
}
// When long-press chapter skip is disabled, turn pages on press instead of release.
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
mappedInput.wasPressed(MappedInputManager::Button::Left))
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
mappedInput.wasReleased(MappedInputManager::Button::Left));
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
mappedInput.wasReleased(MappedInputManager::Button::Power);
const bool nextTriggered = usePressForPageTurn
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
mappedInput.wasPressed(MappedInputManager::Button::Right))
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
mappedInput.wasReleased(MappedInputManager::Button::Right));
const bool prevTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool nextTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
mappedInput.wasReleased(MappedInputManager::Button::Right);
if (!prevTriggered && !nextTriggered) {
return;
@@ -121,7 +113,7 @@ void XtcReaderActivity::loop() {
return;
}
const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs;
const bool skipPages = mappedInput.getHeldTime() > skipPageMs;
const int skipAmount = skipPages ? 10 : 1;
if (prevTriggered) {
@@ -341,12 +333,14 @@ void XtcReaderActivity::renderPage() {
void XtcReaderActivity::saveProgress() const {
FsFile f;
if (Storage.openFileForWrite("XTR", xtc->getCachePath() + "/progress.bin", f)) {
uint8_t data[4];
uint8_t data[5];
data[0] = currentPage & 0xFF;
data[1] = (currentPage >> 8) & 0xFF;
data[2] = (currentPage >> 16) & 0xFF;
data[3] = (currentPage >> 24) & 0xFF;
f.write(data, 4);
data[4] =
ReaderUtils::pageProgressPercentByte(static_cast<int>(currentPage), static_cast<int>(xtc->getPageCount()));
f.write(data, 5);
f.close();
}
}
@@ -442,3 +436,60 @@ bool XtcReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx
free(pageBuffer);
return true;
}
void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
using BA = CrossPointSettings::BUTTON_ACTION;
if (!xtc) return;
const uint32_t pageCount = xtc->getPageCount();
switch (action) {
case BA::BTN_PAGE_FORWARD:
if (currentPage + 1 < pageCount) {
currentPage++;
requestUpdate();
}
break;
case BA::BTN_PAGE_BACK:
if (currentPage > 0) {
currentPage--;
requestUpdate();
}
break;
case BA::BTN_PAGE_FORWARD_10:
currentPage = (currentPage + 10 < pageCount) ? currentPage + 10 : pageCount - 1;
requestUpdate();
break;
case BA::BTN_PAGE_BACK_10:
currentPage = (currentPage >= 10) ? currentPage - 10 : 0;
requestUpdate();
break;
case BA::BTN_NEXT_SECTION:
if (xtc->hasChapters()) {
const auto& chapters = xtc->getChapters();
const auto it = std::find_if(chapters.begin(), chapters.end(),
[this](const auto& ch) { return ch.startPage > currentPage; });
if (it != chapters.end()) {
currentPage = it->startPage;
requestUpdate();
}
}
break;
case BA::BTN_PREV_SECTION:
if (xtc->hasChapters()) {
const auto& chapters = xtc->getChapters();
const auto prevChapter = std::find_if(chapters.rbegin(), chapters.rend(),
[this](const auto& ch) { return ch.startPage < currentPage; });
if (prevChapter != chapters.rend()) {
currentPage = prevChapter->startPage;
requestUpdate();
}
}
break;
case BA::BTN_EXIT_READER:
ReaderUtils::enforceExitFullRefresh(renderer);
finish();
break;
default:
break;
}
}
@@ -31,6 +31,7 @@ class XtcReaderActivity final : public Activity {
void loop() override;
void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; }
void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override;
// Renders the last saved page to the frame buffer without flushing to display.
// Used by SleepActivity to prepare the background for the overlay sleep mode.
+3 -1
View File
@@ -264,7 +264,9 @@ inline void SettingInfo::prepareSubmenus(std::vector<SettingInfo>& items,
auto it = std::find_if(preparedSubmenus.begin(), preparedSubmenus.end(),
[&item](const SubmenuData& d) { return d.id == item.submenu; });
if (it == preparedSubmenus.end()) {
preparedItems.push_back(SettingInfo::SubmenuEntry(item.submenu));
auto placeholder = SettingInfo::SubmenuEntry(item.submenu);
placeholder.subcategory = item.subcategory; // inherit so addTo inserts the separator
preparedItems.push_back(std::move(placeholder));
preparedSubmenus.push_back({item.submenu, {}});
it = preparedSubmenus.end() - 1;
}
+12 -3
View File
@@ -2,6 +2,8 @@
#include <GfxRenderer.h>
#include <HalClock.h>
#include <HalDisplay.h>
#include <HalGPIO.h>
#include <Logging.h>
#include "CrossPointSettings.h"
@@ -21,6 +23,7 @@ bool SettingsActivity::isListItemSelectable(int settingIdx) const {
void SettingsActivity::onEnter() {
Activity::onEnter();
needsHalfRefresh = true;
// Build per-category vectors from the shared settings list.
// addTo tracks the last subcategory per vector and automatically inserts a separator
@@ -78,6 +81,7 @@ void SettingsActivity::onEnter() {
// Device-only ACTION items — subcategory drives separator insertion automatically.
controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
controlsSettings.insert(controlsSettings.begin(), SettingInfo::Separator(StrId::STR_MENU_BTN_PHYSICAL));
addToMoved(readerSettings, lastReaderSub,
SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
@@ -215,11 +219,15 @@ void SettingsActivity::toggleCurrentSetting() {
if (setting.type == SettingType::ACTION) {
auto resultHandler = [this](const ActivityResult& result) {
SETTINGS.saveToFile();
needsHalfRefresh = true;
const auto* menuResult = std::get_if<MenuResult>(&result.data);
if (menuResult && menuResult->action != -1) {
auto activity = createActivityForAction(static_cast<SettingAction>(menuResult->action), renderer, mappedInput);
if (activity) {
startActivityForResult(std::move(activity), [this](const ActivityResult&) { SETTINGS.saveToFile(); });
startActivityForResult(std::move(activity), [this](const ActivityResult&) {
SETTINGS.saveToFile();
needsHalfRefresh = true;
});
}
}
};
@@ -277,6 +285,7 @@ void SettingsActivity::render(RenderLock&&) {
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Always use standard refresh for settings screen
renderer.displayBuffer();
const bool halfRefresh = gpio.deviceIsX3() && needsHalfRefresh;
needsHalfRefresh = false;
renderer.displayBuffer(halfRefresh ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH);
}
@@ -25,6 +25,7 @@ class SettingsActivity final : public Activity {
static const StrId categoryNames[categoryCount];
std::vector<SettingInfo::SubmenuData> submenuData;
bool needsHalfRefresh = false;
void enterCategory(int categoryIndex);
void toggleCurrentSetting();
@@ -1,6 +1,8 @@
#include "SettingsSubmenuActivity.h"
#include <GfxRenderer.h>
#include <HalDisplay.h>
#include <HalGPIO.h>
#include <I18n.h>
#include "CrossPointSettings.h"
@@ -11,6 +13,7 @@
void SettingsSubmenuActivity::onEnter() {
Activity::onEnter();
needsHalfRefresh = true;
initMenuList();
requestUpdate();
}
@@ -63,5 +66,7 @@ void SettingsSubmenuActivity::render(RenderLock&&) {
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();
const bool halfRefresh = gpio.deviceIsX3() && needsHalfRefresh;
needsHalfRefresh = false;
renderer.displayBuffer(halfRefresh ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH);
}
@@ -12,6 +12,7 @@
class SettingsSubmenuActivity final : public MenuListActivity {
StrId titleId;
std::function<std::string(const SettingInfo&)> itemValueStringOverride;
bool needsHalfRefresh = false;
// MenuListActivity overrides
void onEnter() override;
+25 -98
View File
@@ -8,7 +8,6 @@
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Serialization.h>
#include <Txt.h>
#include <Xtc.h>
@@ -58,30 +57,6 @@ int clampProgressPercent(const int progressPercent) {
return progressPercent;
}
int readTxtTotalPages(const std::string& cachePath) {
FsFile indexFile;
if (!Storage.openFileForRead("LYR", cachePath + "/index.bin", indexFile)) {
return 0;
}
uint32_t magic = 0;
uint8_t version = 0;
serialization::readPod(indexFile, magic);
serialization::readPod(indexFile, version);
static constexpr uint32_t INDEX_CACHE_MAGIC = 0x54585449; // "TXTI"
static constexpr uint8_t INDEX_CACHE_VERSION = 2;
if (magic != INDEX_CACHE_MAGIC || version != INDEX_CACHE_VERSION) {
indexFile.close();
return 0;
}
indexFile.seek(32);
uint32_t totalPages = 0;
serialization::readPod(indexFile, totalPages);
indexFile.close();
return static_cast<int>(totalPages);
}
void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight,
uint16_t percentage) {
BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
@@ -152,92 +127,44 @@ const uint8_t* iconForName(UIIcon icon, int size) {
}
} // namespace
// Reads the overall progress percent stored as the last byte of progress.bin.
// The cache path is derived from the book path alone (no epub/xtc/txt loading needed).
// Returns -1 if the file is absent or the percent byte is not yet written.
int LyraTheme::getRecentBookProgressPercent(const RecentBook& book) {
if (book.path.empty()) {
return -1;
}
std::string cachePath;
int percentByteOffset = 0; // byte index of the percent field in progress.bin
if (FsHelpers::hasEpubExtension(book.path)) {
Epub epub(book.path, "/.crosspoint");
if (!epub.load(true, true)) {
return -1;
}
FsFile progressFile;
if (!Storage.openFileForRead("LYR", epub.getCachePath() + "/progress.bin", progressFile)) {
return -1;
}
uint8_t data[6];
const int dataSize = progressFile.read(data, 6);
progressFile.close();
if (dataSize != 4 && dataSize != 6) {
return -1;
}
const int currentSpineIndex = data[0] + (data[1] << 8);
const int currentPage = data[2] + (data[3] << 8);
const int pageCount = (dataSize == 6) ? (data[4] + (data[5] << 8)) : 0;
if (pageCount <= 0) {
return -1;
}
const float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
return clampProgressPercent(
static_cast<int>(std::lround(epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f)));
cachePath = Epub(book.path, "/.crosspoint").getCachePath();
percentByteOffset = 6; // epub: [spineIdx(2), page(2), chapterPageCount(2), percent(1)]
} else if (FsHelpers::hasXtcExtension(book.path)) {
cachePath = Xtc(book.path, "/.crosspoint").getCachePath();
percentByteOffset = 4; // xtc: [page(4), percent(1)]
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
cachePath = Txt(book.path, "/.crosspoint").getCachePath();
percentByteOffset = 6; // [page(2), offset(4), percent(1)]
} else {
return -1;
}
if (FsHelpers::hasXtcExtension(book.path)) {
Xtc xtc(book.path, "/.crosspoint");
if (!xtc.load()) {
return -1;
}
FsFile progressFile;
if (!Storage.openFileForRead("LYR", xtc.getCachePath() + "/progress.bin", progressFile)) {
return -1;
}
uint8_t data[4];
if (progressFile.read(data, 4) != 4) {
progressFile.close();
return -1;
}
progressFile.close();
const uint32_t currentPage = static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
return clampProgressPercent(static_cast<int>(xtc.calculateProgress(currentPage)));
FsFile progressFile;
if (!Storage.openFileForRead("LYR", cachePath + "/progress.bin", progressFile)) {
return -1;
}
if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
Txt txt(book.path, "/.crosspoint");
if (!txt.load()) {
return -1;
}
uint8_t data[7];
const int dataSize = progressFile.read(data, 7);
progressFile.close();
FsFile progressFile;
if (!Storage.openFileForRead("LYR", txt.getCachePath() + "/progress.bin", progressFile)) {
return -1;
}
uint8_t data[4];
if (progressFile.read(data, 4) != 4) {
progressFile.close();
return -1;
}
progressFile.close();
const int currentPage = data[0] + (data[1] << 8);
const int totalPages = readTxtTotalPages(txt.getCachePath());
if (totalPages <= 0) {
return -1;
}
return clampProgressPercent(static_cast<int>(std::lround((currentPage + 1) * 100.0f / totalPages)));
if (dataSize < percentByteOffset + 1) {
return -1; // old format (or pre-render placeholder) without the percent byte
}
return -1;
return clampProgressPercent(static_cast<int>(data[percentByteOffset]));
}
void LyraTheme::drawProgressBadge(const GfxRenderer& renderer, Rect anchorRect, int progressPercent) {
+151 -8
View File
@@ -17,6 +17,7 @@
#include <cstring>
#include "ButtonEventManager.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "GlobalBookmarkIndex.h"
@@ -33,6 +34,8 @@
#include "util/ScreenshotUtil.h"
MappedInputManager mappedInputManager(gpio);
ButtonEventManager buttonEventManager(mappedInputManager);
ButtonEventManager& globalButtonEvents() { return buttonEventManager; }
GfxRenderer renderer(display);
ActivityManager activityManager(renderer, mappedInputManager);
FontDecompressor fontDecompressor;
@@ -284,6 +287,7 @@ void loop() {
static unsigned long lastMemPrint = 0;
gpio.update();
buttonEventManager.update();
HalClock::updatePeriodic();
renderer.setFadingFix(SETTINGS.fadingFix);
@@ -346,6 +350,8 @@ void loop() {
// Track power button hold for sleep. We require a fresh press edge (wasPressed)
// before starting to measure hold time, so that a hold carried over from boot
// (wake-up press) is never misinterpreted as a "go to sleep" press.
// The power button long-press is not user-remappable, so this path always owns it.
// Sleep mapped to other buttons is handled by the dispatcher's BTN_SLEEP case below.
static unsigned long powerHoldStart = 0;
if (gpio.wasPressed(HalGPIO::BTN_POWER)) {
powerHoldStart = millis();
@@ -366,14 +372,6 @@ void loop() {
}
}
// Refresh screen when power button is short-pressed with FORCE_REFRESH setting.
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FORCE_REFRESH &&
mappedInputManager.wasReleased(MappedInputManager::Button::Power)) {
LOG_DBG("MAIN", "Manual screen refresh triggered");
RenderLock lock;
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
if (!gpio.isPressed(HalGPIO::BTN_POWER)) {
powerHoldStart = 0;
}
@@ -384,6 +382,151 @@ void loop() {
activityManager.requestUpdate();
}
// Dispatch globally-configured button actions before handing control to the activity.
// Only non-Default actions are intercepted here; Default falls through to the activity.
{
using BA = CrossPointSettings::BUTTON_ACTION;
using B = MappedInputManager::Button;
ButtonEventManager::ButtonEvent ev;
while (buttonEventManager.consumeEvent(ev)) {
auto actionFor = [&](B btn) -> uint8_t {
switch (btn) {
case B::Back:
switch (ev.type) {
case ButtonEventManager::PressType::Short:
return SETTINGS.btnShortBack;
case ButtonEventManager::PressType::Double:
return SETTINGS.btnDoubleBack;
case ButtonEventManager::PressType::Long:
return SETTINGS.btnLongBack;
}
break;
case B::Confirm:
switch (ev.type) {
case ButtonEventManager::PressType::Short:
return SETTINGS.btnShortConfirm;
case ButtonEventManager::PressType::Double:
return SETTINGS.btnDoubleConfirm;
case ButtonEventManager::PressType::Long:
return SETTINGS.btnLongConfirm;
}
break;
case B::Left:
switch (ev.type) {
case ButtonEventManager::PressType::Short:
return SETTINGS.btnShortLeft;
case ButtonEventManager::PressType::Double:
return SETTINGS.btnDoubleLeft;
case ButtonEventManager::PressType::Long:
return SETTINGS.btnLongLeft;
}
break;
case B::Right:
switch (ev.type) {
case ButtonEventManager::PressType::Short:
return SETTINGS.btnShortRight;
case ButtonEventManager::PressType::Double:
return SETTINGS.btnDoubleRight;
case ButtonEventManager::PressType::Long:
return SETTINGS.btnLongRight;
}
break;
case B::PageBack:
switch (ev.type) {
case ButtonEventManager::PressType::Short:
return SETTINGS.btnShortPageBack;
case ButtonEventManager::PressType::Double:
return SETTINGS.btnDoublePageBack;
case ButtonEventManager::PressType::Long:
return SETTINGS.btnLongPageBack;
}
break;
case B::PageForward:
switch (ev.type) {
case ButtonEventManager::PressType::Short:
return SETTINGS.btnShortPageForward;
case ButtonEventManager::PressType::Double:
return SETTINGS.btnDoublePageForward;
case ButtonEventManager::PressType::Long:
return SETTINGS.btnLongPageForward;
}
break;
case B::Power:
switch (ev.type) {
case ButtonEventManager::PressType::Short:
return SETTINGS.btnShortPower;
case ButtonEventManager::PressType::Double:
return SETTINGS.btnDoublePower;
case ButtonEventManager::PressType::Long:
return SETTINGS.btnLongPower;
}
break;
default:
break; // Up/Down have no FSMs — ButtonEventManager never emits these
}
return BA::BTN_DEFAULT;
};
const uint8_t action = actionFor(ev.button);
if (action == BA::BTN_DEFAULT) continue;
switch (static_cast<BA>(action)) {
case BA::BTN_PAGE_FORWARD:
activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD);
break;
case BA::BTN_PAGE_BACK:
activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK);
break;
case BA::BTN_PAGE_FORWARD_10:
activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD_10);
break;
case BA::BTN_PAGE_BACK_10:
activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK_10);
break;
case BA::BTN_GO_HOME:
activityManager.goHome();
break;
case BA::BTN_SLEEP:
activityManager.goToSleep();
break;
case BA::BTN_FORCE_REFRESH: {
RenderLock lock;
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
break;
}
case BA::BTN_OPEN_TOC:
activityManager.dispatchButtonAction(BA::BTN_OPEN_TOC);
break;
case BA::BTN_OPEN_BOOKMARKS:
activityManager.goToGlobalBookmarks();
break;
case BA::BTN_STAR_PAGE:
activityManager.dispatchButtonAction(BA::BTN_STAR_PAGE);
break;
case BA::BTN_FOOTNOTES:
activityManager.dispatchButtonAction(BA::BTN_FOOTNOTES);
break;
case BA::BTN_NEXT_SECTION:
activityManager.dispatchButtonAction(BA::BTN_NEXT_SECTION);
break;
case BA::BTN_PREV_SECTION:
activityManager.dispatchButtonAction(BA::BTN_PREV_SECTION);
break;
case BA::BTN_EXIT_READER:
activityManager.dispatchButtonAction(BA::BTN_EXIT_READER);
break;
case BA::BTN_READER_MENU:
activityManager.dispatchButtonAction(BA::BTN_READER_MENU);
break;
case BA::BTN_KOREADER_SYNC:
activityManager.dispatchButtonAction(BA::BTN_KOREADER_SYNC);
break;
default:
break;
}
}
}
const unsigned long activityStartTime = millis();
activityManager.loop();
const unsigned long activityDuration = millis() - activityStartTime;