Compare commits

..
Author SHA1 Message Date
Uri Tauber fd00c185f2 feat: whole book page count 2026-07-15 17:58:45 +03:00
52 changed files with 431 additions and 858 deletions
+73
View File
@@ -0,0 +1,73 @@
#include "BookPages.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace {
// Coarse seed used only before any section has an exact or live count; replaced
// by the calibrated average as soon as one is available.
constexpr double DEFAULT_BYTES_PER_PAGE = 2000.0;
// A section's serialized pageCount is a uint16_t, so no estimate needs to exceed this.
constexpr int MAX_SECTION_PAGES = std::numeric_limits<uint16_t>::max();
int clampToInt(const uint64_t v) {
return v > static_cast<uint64_t>(std::numeric_limits<int>::max()) ? std::numeric_limits<int>::max()
: static_cast<int>(v);
}
} // namespace
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, const int sectionCount, const int spineIndex,
const int pageInSection, const int liveSectionPages) {
BookPagePosition pos;
if (!entries || sectionCount <= 0) {
return pos;
}
// Calibrate bytes-per-page from every section with an exact count (empty
// known-0 chapters excluded), plus the live estimate for spineIndex when it
// has no exact count yet.
uint64_t knownBytes = 0;
uint64_t knownPages = 0;
for (int i = 0; i < sectionCount; ++i) {
if (entries[i].pages > 0) {
knownBytes += entries[i].bytes;
knownPages += static_cast<uint32_t>(entries[i].pages);
}
}
const bool useLive = spineIndex >= 0 && spineIndex < sectionCount && entries[spineIndex].pages < 0 &&
liveSectionPages > 0 && entries[spineIndex].bytes > 0;
if (useLive) {
knownBytes += entries[spineIndex].bytes;
knownPages += static_cast<uint32_t>(liveSectionPages);
}
const double bytesPerPage = (knownBytes > 0 && knownPages > 0)
? static_cast<double>(knownBytes) / static_cast<double>(knownPages)
: DEFAULT_BYTES_PER_PAGE;
uint64_t total = 0;
uint64_t before = 0;
bool exact = true;
for (int i = 0; i < sectionCount; ++i) {
uint32_t pages;
if (entries[i].pages >= 0) {
pages = static_cast<uint32_t>(entries[i].pages); // exact (0 = genuinely empty chapter)
} else if (i == spineIndex && liveSectionPages > 0) {
pages = static_cast<uint32_t>(std::min(liveSectionPages, MAX_SECTION_PAGES));
exact = false;
} else {
const double raw = std::floor(static_cast<double>(entries[i].bytes) / bytesPerPage + 0.5);
pages = static_cast<uint32_t>(std::clamp(raw, 1.0, static_cast<double>(MAX_SECTION_PAGES)));
exact = false;
}
total += pages;
if (i < spineIndex) {
before += pages;
}
}
pos.totalPages = clampToInt(total);
pos.currentPage = clampToInt(before + static_cast<uint64_t>(std::max(0, pageInSection)) + 1);
pos.isEstimate = !exact;
return pos;
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
// Whole-book page accounting for book-global "page X of Y".
//
// The finalized section cache files (sections/*.bin) are the single source of
// truth for exact per-section counts; the caller harvests them into an array of
// BookPageEntry (nothing is persisted separately). Sections without an exact
// count are estimated from their byte size, calibrated against the sections
// whose counts are known — so the total gets more accurate as more of the book
// is paginated, and becomes exact once every section is.
struct BookPageEntry {
uint32_t bytes = 0; // uncompressed XHTML size, for estimating unknown sections
int32_t pages = -1; // exact count from a finalized section cache; -1 = unknown (0 = empty chapter)
};
struct BookPagePosition {
int currentPage = 0;
int totalPages = 0;
bool isEstimate = true; // true until every section has an exact count
};
// Book-global position: currentPage = pages before spineIndex + pageInSection + 1.
// liveSectionPages is the in-progress build's estimate for spineIndex (see
// Section::estimatedTotalPages); it is used for that section when it has no exact
// count yet and folded into the bytes-per-page calibration. Pure function: no I/O.
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, int sectionCount, int spineIndex,
int pageInSection, int liveSectionPages);
+38 -26
View File
@@ -39,8 +39,34 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
sizeof(uint32_t) + sizeof(uint32_t);
// Read the render-parameter block of the header (everything between the version
// byte and pageCount), in writeSectionFileHeader order. The file cursor must sit
// just past the version byte.
Section::RenderParams readHeaderRenderParams(HalFile& f) {
Section::RenderParams p;
serialization::readPod(f, p.fontId);
serialization::readPod(f, p.lineCompression);
serialization::readPod(f, p.extraParagraphSpacing);
serialization::readPod(f, p.paragraphAlignment);
serialization::readPod(f, p.viewportWidth);
serialization::readPod(f, p.viewportHeight);
serialization::readPod(f, p.hyphenationEnabled);
serialization::readPod(f, p.embeddedStyle);
serialization::readPod(f, p.imageRendering);
serialization::readPod(f, p.focusReadingEnabled);
return p;
}
} // namespace
bool Section::RenderParams::operator==(const RenderParams& o) const {
return fontId == o.fontId && lineCompression == o.lineCompression &&
extraParagraphSpacing == o.extraParagraphSpacing && paragraphAlignment == o.paragraphAlignment &&
viewportWidth == o.viewportWidth && viewportHeight == o.viewportHeight &&
hyphenationEnabled == o.hyphenationEnabled && embeddedStyle == o.embeddedStyle &&
imageRendering == o.imageRendering && focusReadingEnabled == o.focusReadingEnabled;
}
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
// constructed/destroyed where the parser's full definition is visible.
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
@@ -133,31 +159,11 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
}
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
int fileFontId;
uint16_t fileViewportWidth, fileViewportHeight;
float fileLineCompression;
bool fileExtraParagraphSpacing;
uint8_t fileParagraphAlignment;
bool fileHyphenationEnabled;
bool fileEmbeddedStyle;
uint8_t fileImageRendering;
bool fileFocusReadingEnabled;
serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing);
serialization::readPod(file, fileParagraphAlignment);
serialization::readPod(file, fileViewportWidth);
serialization::readPod(file, fileViewportHeight);
serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileImageRendering);
serialization::readPod(file, fileFocusReadingEnabled);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
const RenderParams fileParams = readHeaderRenderParams(file);
const RenderParams params = {fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
viewportWidth, viewportHeight, hyphenationEnabled, embeddedStyle,
imageRendering, focusReadingEnabled};
if (!(fileParams == params)) {
file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache();
@@ -754,7 +760,7 @@ std::string Section::getTextFromSectionFile() {
return fullText;
}
std::optional<uint16_t> Section::getCachedPageCount() const {
std::optional<uint16_t> Section::getCachedPageCount(const RenderParams* mustMatch) const {
HalFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return std::nullopt;
@@ -774,6 +780,12 @@ std::optional<uint16_t> Section::getCachedPageCount() const {
return std::nullopt;
}
// A file cached under other render settings paginates differently; its count is
// only stale-valid for rough mapping, so reject it when the caller needs a match.
if (mustMatch && !(readHeaderRenderParams(f) == *mustMatch)) {
return std::nullopt;
}
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
uint16_t count;
serialization::readPod(f, count);
+21 -1
View File
@@ -78,6 +78,23 @@ class Section {
std::unique_ptr<Page> loadPageDuringBuild(int page);
public:
// The render parameters that determine pagination, in section cache header order
// (see writeSectionFileHeader). A cached page count is only valid for one set.
struct RenderParams {
int fontId = 0;
float lineCompression = 0.0f;
bool extraParagraphSpacing = false;
uint8_t paragraphAlignment = 0;
uint16_t viewportWidth = 0;
uint16_t viewportHeight = 0;
bool hyphenationEnabled = false;
bool embeddedStyle = false;
uint8_t imageRendering = 0;
bool focusReadingEnabled = false;
bool operator==(const RenderParams& o) const;
};
uint16_t pageCount = 0;
int currentPage = 0;
@@ -144,7 +161,10 @@ class Section {
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
// Get the page count from the section cache file without fully loading it.
std::optional<uint16_t> getCachedPageCount() const;
// Finalized files only (a partial's count is just a build watermark). When
// `mustMatch` is given, the header's render parameters must equal it, so a
// count cached under different settings is never trusted.
std::optional<uint16_t> getCachedPageCount(const RenderParams* mustMatch = nullptr) const;
// Look up the page number for a synthetic paragraph index from XPath p[N].
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
+13 -19
View File
@@ -21,7 +21,6 @@
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
constexpr size_t PARSE_BUFFER_SIZE = 1024;
constexpr size_t MAX_BUFFERED_TEXT_WORDS = 300;
// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation
// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per
@@ -202,8 +201,6 @@ void ChapterHtmlSlimParser::flushPendingAnchor() {
// flush the contents of partWordBuffer to currentTextBlock
void ChapterHtmlSlimParser::flushPartWordBuffer() {
flushLongTextBlockIfNeeded();
// Determine font style from depth-based tracking and CSS effective style
const bool isBold = boldUntilDepth < depth || effectiveBold;
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
@@ -231,20 +228,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
listItemBulletOnly = false;
}
void ChapterHtmlSlimParser::flushLongTextBlockIfNeeded() {
if (!currentTextBlock || currentTextBlock->size() <= MAX_BUFFERED_TEXT_WORDS) {
return;
}
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth =
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
currentTextBlock->layoutAndExtractLines(
renderer, fontId, effectiveWidth,
[this](const std::shared_ptr<TextBlock>& textBlock) { this->addLineToPage(textBlock); }, false);
}
// start a new text block if needed
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
nextWordContinues = false; // New block = new paragraph, no continuation
@@ -1172,9 +1155,20 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
}
// If we have a large number of words buffered up, perform the layout and consume out all but the last line.
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
// memory.
// Spotted when reading Intermezzo, there are some really long text blocks in there.
self->flushLongTextBlockIfNeeded();
if (self->currentTextBlock->size() > 750) {
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
: self->viewportWidth;
self->currentTextBlock->layoutAndExtractLines(
self->renderer, self->fontId, effectiveWidth,
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
}
}
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
@@ -4,7 +4,6 @@
#include <expat.h>
#include <climits>
#include <deque>
#include <functional>
#include <memory>
#include <string>
@@ -85,7 +84,7 @@ class ChapterHtmlSlimParser {
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0;
std::deque<std::pair<std::string, uint16_t>> anchorData;
std::vector<std::pair<std::string, uint16_t>> anchorData;
std::string pendingAnchorId; // deferred until after previous text block is flushed
std::vector<std::string> tocAnchors; // the list of anchors that are TOC chapter boundaries
uint16_t xpathParagraphIndex = 0;
@@ -113,7 +112,6 @@ class ChapterHtmlSlimParser {
void startNewTextBlock(const BlockStyle& blockStyle);
void flushPendingAnchor();
void flushPartWordBuffer();
void flushLongTextBlockIfNeeded();
void makePages();
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
@@ -175,7 +173,7 @@ class ChapterHtmlSlimParser {
void abortParse(); // tear down without flushing (error / abandon)
void addLineToPage(std::shared_ptr<TextBlock> line);
const std::deque<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
-5
View File
@@ -298,11 +298,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколі"
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
STR_OPDS_DOWNLOAD_FOLDER: "Папка для спампоўкі"
STR_OPDS_FILENAME_FORMAT: "Фармат назвы файла"
STR_FMT_AUTHOR_TITLE: "Аўтар - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Аўтар"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
-393
View File
@@ -1,393 +0,0 @@
_language_name: "Bosanski"
_language_code: "BS"
_order: "29"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "POKRETANJE"
STR_SLEEPING: "REŽIM MIROVANJA"
STR_ENTERING_SLEEP: "Prelazak u režim mirovanja"
STR_BROWSE_FILES: "Pregled datoteka"
STR_FILE_TRANSFER: "Prijenos datoteka"
STR_SETTINGS_TITLE: "Postavke"
STR_CONTINUE_READING: "Nastavi čitanje"
STR_NO_OPEN_BOOK: "Nema otvorene knjige"
STR_START_READING: "Počni čitati ispod"
STR_NO_FILES_FOUND: "Nema pronađenih datoteka"
STR_SELECT_CHAPTER: "Odaberi poglavlje"
STR_NO_CHAPTERS: "Nema poglavlja"
STR_END_OF_BOOK: "Kraj knjige"
STR_EMPTY_CHAPTER: "Prazno poglavlje"
STR_INDEXING: "Indeksiranje"
STR_INDEX_FAILED: "Indeksiranje nije uspjelo - nevažeća knjiga"
STR_MEMORY_ERROR: "Greška memorije"
STR_PAGE_LOAD_ERROR: "Greška pri učitavanju stranice"
STR_EMPTY_FILE: "Prazna datoteka"
STR_OUT_OF_BOUNDS: "Izvan granica"
STR_LOADING: "Učitavanje..."
STR_LOADING_POPUP: "Učitavanje"
STR_WIFI_NETWORKS: "Wi-Fi mreže"
STR_NO_NETWORKS: "Nema pronađenih mreža"
STR_NETWORKS_FOUND: "%zu pronađenih mreža"
STR_SCANNING: "Skeniranje..."
STR_FINDING_SAVED_WIFI: "Traženje sačuvanog Wi-Fi-a..."
STR_CONNECTING: "Povezivanje..."
STR_CONNECTING_SAVED_WIFI: "Povezivanje na sačuvani Wi-Fi..."
STR_SHOW_NETWORKS: "Prikaži"
STR_CONNECTED: "Povezano!"
STR_CONNECTION_FAILED: "Povezivanje nije uspjelo"
STR_FORGET_NETWORK: "Zaboraviti mrežu?"
STR_SAVE_PASSWORD: "Sačuvati lozinku za sljedeći put?"
STR_PRESS_OK_SCAN: "Pritisni OK za ponovno skeniranje"
STR_JOIN_NETWORK: "Pridruži se mreži"
STR_CREATE_HOTSPOT: "Kreiraj hotspot"
STR_JOIN_DESC: "Poveži se na postojeću Wi-Fi mrežu"
STR_HOTSPOT_DESC: "Kreiraj Wi-Fi mrežu kojoj se drugi mogu pridružiti"
STR_STARTING_HOTSPOT: "Pokretanje hotspota..."
STR_HOTSPOT_MODE: "Hotspot način rada"
STR_CONNECT_WIFI_HINT: "Poveži svoj uređaj na ovu Wi-Fi mrežu"
STR_OPEN_URL_HINT: "Otvori ovaj URL u svom pregledniku"
STR_OR_HTTP_PREFIX: "ili http://"
STR_SCAN_QR_HINT: "ili skeniraj QR kod svojim telefonom:"
STR_CALIBRE_WIRELESS: "Calibre bežično"
STR_NETWORK_LEGEND: "* = Šifrovano | + = Sačuvano"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Provjera Wi-Fi-a..."
STR_ENTER_WIFI_PASSWORD: "Unesi Wi-Fi lozinku"
STR_ADD_HIDDEN_NETWORK: "Dodaj skrivenu mrežu..."
STR_ENTER_WIFI_SSID: "Unesi naziv mreže (SSID)"
STR_TO_PREFIX: "na "
STR_CALIBRE_RECEIVING: "Primanje: "
STR_CALIBRE_RECEIVED: "Primljeno: "
STR_CALIBRE_INSTRUCTION_1: "1) Instaliraj CrossPoint Reader dodatak"
STR_CALIBRE_INSTRUCTION_2: "2) Budi na istoj Wi-Fi mreži"
STR_CALIBRE_INSTRUCTION_3: "3) U Calibreu: \"Pošalji na uređaj\""
STR_CALIBRE_INSTRUCTION_4: "\"Drži ovaj ekran otvorenim tokom slanja\""
STR_CAT_DISPLAY: "Ekran"
STR_CAT_READER: "Čitač"
STR_CAT_CONTROLS: "Kontrole"
STR_CAT_SYSTEM: "Sistem"
STR_SLEEP_SCREEN: "Ekran mirovanja"
STR_QUICK_RESUME_TIMEOUT: "Brzi nastavak nakon isteka vremena"
STR_SLEEP_COVER_MODE: "Način prikaza korica na ekranu mirovanja"
STR_HIDE_BATTERY: "Sakrij % baterije"
STR_EXTRA_SPACING: "Dodatni razmak između paragrafa"
STR_TEXT_AA: "Zaglađivanje teksta"
STR_IMAGES: "Slike"
STR_IMAGES_DISPLAY: "Prikaz"
STR_IMAGES_PLACEHOLDER: "Rezervisano mjesto"
STR_IMAGES_SUPPRESS: "Sakrij"
STR_EOB_HOME: "Početna"
STR_EOB_CONTINUE_WITH: "Nastavi sa"
STR_SHORT_PWR_BTN: "Kratak klik dugmeta napajanja"
STR_ORIENTATION: "Orijentacija čitanja"
STR_SIDE_BTN_LAYOUT: "Raspored bočnih dugmadi (čitač)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orijentiši prednja dugmad"
STR_LONG_PRESS_BEHAVIOR: "Ponašanje dugog pritiska dugmeta"
STR_LONG_PRESS_BEHAVIOR_OFF: "ISKLJUČENO"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskakanje poglavlja"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Promjena orijentacije"
STR_LONG_PRESS_MENU: "Meni dugog pritiska"
STR_FONT_PREVIEW_TEXT: "Brza smeđa lisica preskače lijenog psa"
STR_FONT_FAMILY: "Porodica fonta čitača"
STR_FONT_SIZE: "Veličina fonta čitača"
STR_LINE_SPACING: "Razmak između redova čitača"
STR_SCREEN_MARGIN: "Margina ekrana čitača"
STR_PARA_ALIGNMENT: "Poravnanje paragrafa čitača"
STR_HYPHENATION: "Rastavljanje riječi"
STR_TIME_TO_SLEEP: "Vrijeme do mirovanja"
STR_SHOW_HIDDEN_FILES: "Prikaži skrivene datoteke"
STR_REMOVE_READ_FROM_RECENTS: "Ukloni pročitane knjige sa liste nedavnih"
STR_MOVE_FINISHED_TO_READ: "Premjesti završene knjige u fasciklu Read"
STR_REFRESH_FREQ: "Učestalost osvježavanja"
STR_KOREADER_SYNC: "KOReader sinhronizacija"
STR_CHECK_UPDATES: "Provjeri ažuriranja"
STR_LANGUAGE: "Jezik"
STR_CLEAR_READING_CACHE: "Očisti keš čitanja"
STR_USERNAME: "Korisničko ime"
STR_PASSWORD: "Lozinka"
STR_SYNC_SERVER_URL: "URL servera za sinhronizaciju"
STR_DOCUMENT_MATCHING: "Podudaranje dokumenata"
STR_SEND_METADATA: "Pošalji metapodatke dokumenta"
STR_AUTHENTICATE: "Autentifikuj"
STR_KOREADER_USERNAME: "KOReader korisničko ime"
STR_KOREADER_PASSWORD: "KOReader lozinka"
STR_FILENAME: "Naziv datoteke"
STR_BINARY: "Binarno"
STR_SET_CREDENTIALS_FIRST: "Prvo postavi vjerodajnice"
STR_WIFI_CONN_FAILED: "Wi-Fi povezivanje nije uspjelo"
STR_AUTHENTICATING: "Autentifikacija..."
STR_AUTH_SUCCESS: "Uspješno autentifikovano!"
STR_KOREADER_AUTH: "KOReader autentifikacija"
STR_SYNC_READY: "KOReader sinhronizacija je spremna za upotrebu"
STR_AUTH_FAILED: "Autentifikacija nije uspjela"
STR_DONE: "Gotovo"
STR_CLEAR_CACHE_WARNING_1: "Ovo će očistiti sve keširane podatke knjiga."
STR_CLEAR_CACHE_WARNING_2: "Sav napredak čitanja će biti izgubljen!"
STR_CLEAR_CACHE_WARNING_3: "Knjige će morati biti ponovo indeksirane"
STR_CLEAR_CACHE_WARNING_4: "kada se ponovo otvore."
STR_CLEARING_CACHE: "Čišćenje keša..."
STR_CACHE_CLEARED: "Keš očišćen"
STR_ITEMS_REMOVED: "stavki uklonjeno"
STR_FAILED_LOWER: "nije uspjelo"
STR_CLEAR_CACHE_FAILED: "Čišćenje keša nije uspjelo"
STR_CHECK_SERIAL_OUTPUT: "Provjeri serijski izlaz za detalje"
STR_DARK: "Tamno"
STR_LIGHT: "Svijetlo"
STR_CUSTOM: "Prilagođeno"
STR_COVER: "Korice"
STR_NONE_OPT: "Ništa"
STR_FIT: "Prilagodi"
STR_CROP: "Isjeci"
STR_NEVER: "Nikad"
STR_IN_READER: "U čitaču"
STR_ALWAYS: "Uvijek"
STR_IGNORE: "Ignoriši"
STR_SLEEP: "Mirovanje"
STR_PAGE_TURN: "Okretanje stranice"
STR_FORCE_REFRESH: "Osvježi ekran"
STR_PORTRAIT: "Uspravno"
STR_LANDSCAPE_CW: "Položeno (u smjeru kazaljke na satu)"
STR_INVERTED: "Obrnuto"
STR_ORIENTATION_INVERTED: "Uspravno 180°"
STR_LANDSCAPE_CCW: "Položeno (suprotno smjeru kazaljke na satu)"
STR_PREV_NEXT: "Prethodno/Sljedeće"
STR_NEXT_PREV: "Sljedeće/Prethodno"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Obilježivač"
STR_DISABLED: "Onemogućeno"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malo"
STR_MEDIUM: "Srednje"
STR_LARGE: "Veliko"
STR_X_LARGE: "Vrlo veliko"
STR_TIGHT: "Usko"
STR_NORMAL: "Normalno"
STR_WIDE: "Široko"
STR_JUSTIFY: "Poravnaj obostrano"
STR_ALIGN_LEFT: "Lijevo"
STR_CENTER: "Centrirano"
STR_ALIGN_RIGHT: "Desno"
STR_PAGES_1: "1 stranica"
STR_PAGES_5: "5 stranica"
STR_PAGES_10: "10 stranica"
STR_PAGES_15: "15 stranica"
STR_PAGES_30: "30 stranica"
STR_UPDATE: "Ažuriraj"
STR_CHECKING_UPDATE: "Provjera ažuriranja..."
STR_NEW_UPDATE: "Dostupno je novo ažuriranje!"
STR_CURRENT_VERSION: "Trenutna verzija: "
STR_NEW_VERSION: "Nova verzija: "
STR_UPDATING: "Ažuriranje..."
STR_NO_UPDATE: "Nema dostupnih ažuriranja"
STR_UPDATE_FAILED: "Ažuriranje nije uspjelo"
STR_UPDATE_COMPLETE: "Ažuriranje završeno"
STR_POWER_ON_HINT: "Pritisni i drži dugme napajanja da ponovo uključiš"
STR_RESTARTING_HINT: "Ponovno pokretanje... Ako se uređaj ne pokrene ponovo, drži dugme napajanja nekoliko sekundi."
STR_NO_ENTRIES: "Nema pronađenih unosa"
STR_DOWNLOADING: "Preuzimanje..."
STR_DOWNLOAD_FAILED: "Preuzimanje nije uspjelo"
STR_ERROR_MSG: "Greška:"
STR_UNNAMED: "Neimenovano"
STR_HOLD_OPEN_TO_DELETE: "Drži Otvori za brisanje"
STR_NO_SERVER_URL: "Nije podešen URL servera"
STR_FETCH_FEED_FAILED: "Dohvatanje feeda nije uspjelo"
STR_PARSE_FEED_FAILED: "Obrada feeda nije uspjela"
STR_NEXT_PAGE: "Sljedeća stranica »"
STR_PREV_PAGE: "« Prethodna stranica"
STR_NETWORK_PREFIX: "Mreža: "
STR_IP_ADDRESS_PREFIX: "IP adresa: "
STR_ERROR_GENERAL_FAILURE: "Greška: Opšta greška"
STR_ERROR_NETWORK_NOT_FOUND: "Greška: Mreža nije pronađena"
STR_ERROR_CONNECTION_TIMEOUT: "Greška: Isteklo vrijeme veze"
STR_SD_CARD: "SD kartica"
STR_BACK: "« Nazad"
STR_EXIT: "« Izlaz"
STR_HOME: "« Početna"
STR_SELECT: "Odaberi"
STR_SELECTED: "Odabrano"
STR_TOGGLE: "Promijeni"
STR_TOGGLE_BOOKMARK: "Promijeni obilježivač"
STR_CONFIRM: "Potvrdi"
STR_CANCEL: "Otkaži"
STR_CONNECT: "Poveži"
STR_OPEN: "Otvori"
STR_DOWNLOAD: "Preuzmi"
STR_RETRY: "Pokušaj ponovo"
STR_YES: "Da"
STR_NO: "Ne"
STR_SHOW: "Prikaži"
STR_HIDE: "Sakrij"
STR_STATE_ON: "UKLJUČENO"
STR_STATE_OFF: "ISKLJUČENO"
STR_NOT_SET: "Nije postavljeno"
STR_DIR_LEFT: "Lijevo"
STR_DIR_RIGHT: "Desno"
STR_DIR_UP: "Gore"
STR_DIR_DOWN: "Dolje"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter korica ekrana mirovanja"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Prilagodi statusnu traku"
STR_CHAPTER_PAGE_COUNT: "Broj stranica poglavlja"
STR_BOOK_PROGRESS_PERCENTAGE: "Procenat napretka knjige"
STR_PROGRESS_BAR: "Traka napretka"
STR_PROGRESS_BAR_THICKNESS: "Debljina trake napretka"
STR_PROGRESS_BAR_THIN: "Tanka"
STR_PROGRESS_BAR_MEDIUM: "Srednja"
STR_PROGRESS_BAR_THICK: "Debela"
STR_BOOK: "Knjiga"
STR_CHAPTER: "Poglavlje"
STR_EXAMPLE_CHAPTER: "Poglavlje 21"
STR_EXAMPLE_BOOK: "Naslov knjige"
STR_PREVIEW: "Pregled"
STR_TITLE: "Naslov"
STR_BATTERY: "Baterija"
STR_XTC_STATUS_BAR: "XTC statusna traka"
STR_BOTTOM: "Dolje"
STR_TOP: "Gore"
STR_CLOCK: "Sat"
STR_CLOCK_UTC_OFFSET: "UTC odstupanje sata"
STR_CLOCK_FORMAT: "Format sata"
STR_CLOCK_FORMAT_24H: "24-časovni"
STR_CLOCK_FORMAT_12H: "12-časovni"
STR_CURRENT_TIME: "Trenutno vrijeme:"
STR_NEXT_FIELD: "Sljedeće"
STR_CLOCK_SYNC: "Sinhronizuj sat"
STR_CLOCK_SYNC_NOW: "Sinhronizuj sat sada"
STR_CLOCK_SYNCING: "Sinhronizacija sa NTP-a..."
STR_CLOCK_SYNC_OK: "Sat sinhronizovan"
STR_CLOCK_SYNC_FAIL: "Sinhronizacija nije uspjela"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nije povezan"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Prvo se poveži na Wi-Fi, pa pokušaj ponovo."
STR_CLOCK_SYNCED: "Sat sinhronizovan"
STR_UI_THEME: "Tema korisničkog interfejsa"
STR_THEME_CLASSIC: "Klasična"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra proširena"
STR_SUNLIGHT_FADING_FIX: "Ispravka blijeđenja na suncu"
STR_REMAP_FRONT_BUTTONS: "Premapiraj prednja dugmad"
STR_BOOKMARKS: "Obilježivači"
STR_BOOKMARK_ADDED: "Obilježivač dodan."
STR_BOOKMARK_REMOVED: "Obilježivač uklonjen."
STR_OPDS_BROWSER: "OPDS pregledač"
STR_SEARCH: "Pretraga"
STR_COVER_CUSTOM: "Korice + Prilagođeno"
STR_QUICK_RESUME: "Brzi nastavak"
STR_MENU_RECENT_BOOKS: "Nedavne knjige"
STR_REMOVE_FROM_RECENTS: "Ukloniti iz nedavnih knjiga?"
STR_NO_RECENT_BOOKS: "Nema nedavnih knjiga"
STR_CALIBRE_DESC: "Koristi Calibre bežični prijenos uređaja"
STR_FORGET_AND_REMOVE: "Zaboraviti mrežu i ukloniti sačuvanu lozinku?"
STR_FORGET_BUTTON: "Zaboravi"
STR_CALIBRE_STARTING: "Pokretanje Calibrea..."
STR_CALIBRE_SETUP: "Podešavanje"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Očisti"
STR_DEFAULT_VALUE: "Zadano"
STR_REMAP_PROMPT: "Pritisni prednje dugme za svaku ulogu"
STR_UNASSIGNED: "Nedodijeljeno"
STR_ALREADY_ASSIGNED: "Već dodijeljeno"
STR_REMAP_RESET_HINT: "Bočno dugme Gore: Vrati na zadani raspored"
STR_REMAP_CANCEL_HINT: "Bočno dugme Dolje: Otkaži premapiranje"
STR_HW_BACK_LABEL: "Nazad (1. dugme)"
STR_HW_CONFIRM_LABEL: "Potvrdi (2. dugme)"
STR_HW_LEFT_LABEL: "Lijevo (3. dugme)"
STR_HW_RIGHT_LABEL: "Desno (4. dugme)"
STR_GO_TO_PERCENT: "Idi na %"
STR_GO_HOME_BUTTON: "Idi na početnu"
STR_SYNC_PROGRESS: "Sinhronizuj napredak"
STR_DELETE_CACHE: "Obriši keš knjige"
STR_DELETE: "Obriši"
STR_CONFIRM_DELETE_BOOKMARK: "Obrisati ovaj obilježivač?"
STR_DISPLAY_QR: "Prikaži stranicu kao QR"
STR_CHAPTER_PREFIX: "Poglavlje: "
STR_PAGES_SEPARATOR: " stranica | "
STR_BOOK_PREFIX: "Knjiga: "
STR_CALIBRE_URL_HINT: "Za Calibre, dodaj /opds na svoj URL"
STR_SYNCING_TIME: "Sinhronizacija vremena..."
STR_CALC_HASH: "Izračunavanje heša dokumenta..."
STR_HASH_FAILED: "Izračunavanje heša dokumenta nije uspjelo"
STR_FETCH_PROGRESS: "Dohvatanje udaljenog napretka..."
STR_UPLOAD_PROGRESS: "Slanje napretka..."
STR_NO_CREDENTIALS_MSG: "Nisu podešene vjerodajnice"
STR_KOREADER_SETUP_HINT: "Podesi KOReader nalog u Postavkama"
STR_PROGRESS_FOUND: "Napredak pronađen!"
STR_REMOTE_LABEL: "Udaljeno:"
STR_LOCAL_LABEL: "Lokalno:"
STR_PAGE_OVERALL_FORMAT: "Stranica %d, %.2f%% ukupno"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stranica %d/%d, %.2f%% ukupno"
STR_DEVICE_FROM_FORMAT: " Sa: %s"
STR_APPLY_REMOTE: "Primijeni udaljeni napredak"
STR_UPLOAD_LOCAL: "Pošalji lokalni napredak"
STR_NO_REMOTE_MSG: "Nije pronađen udaljeni napredak"
STR_UPLOAD_PROMPT: "Poslati trenutnu poziciju?"
STR_UPLOAD_SUCCESS: "Napredak poslan!"
STR_SYNC_FAILED_MSG: "Sinhronizacija nije uspjela"
STR_SAVE_PROGRESS_FAILED: "Napredak nije mogao biti sačuvan"
STR_SECTION_PREFIX: "Sekcija "
STR_UPLOAD: "Pošalji"
STR_BOOK_S_STYLE: "Stil knjige"
STR_EMBEDDED_STYLE: "Ugrađeni stil"
STR_FOCUS_READING: "Fokusirano čitanje"
STR_OPDS_SERVER_URL: "URL OPDS servera"
STR_PWR_BTN_FOOTNOTE_BACK: "Brzi povratak iz fusnota"
STR_SET_SLEEP_COVER: "Postavi korice"
STR_FOOTNOTES: "Fusnote"
STR_NO_FOOTNOTES: "Nema fusnota na ovoj stranici"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Snimi ekran"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikad"
STR_STEP_HINT_FRONT: "Prednja dugmad:"
STR_STEP_HINT_SIDE: "Bočna dugmad:"
STR_ADD_SERVER: "Dodaj server"
STR_SERVER_NAME: "Naziv servera"
STR_NO_SERVERS: "Nema podešenih OPDS servera"
STR_DELETE_SERVER: "Obriši server"
STR_OPDS_SERVERS: "OPDS serveri"
STR_AUTO_TURN_ENABLED: "Automatsko okretanje omogućeno: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatsko okretanje (stranica po minuti)"
STR_MANAGE_FONTS: "Upravljaj fontovima"
STR_FONT_BROWSER: "Pregledač fontova"
STR_LOADING_FONT_LIST: "Učitavanje liste fontova..."
STR_NO_FONTS_AVAILABLE: "Nema dostupnih fontova"
STR_FONT_INSTALLED: "Font instaliran!"
STR_FONT_INSTALL_FAILED: "Instalacija fonta nije uspjela"
STR_INSTALLED: "Instalirano"
STR_DOWNLOAD_ALL: "Preuzmi sve"
STR_UPDATE_ALL: "Ažuriraj sve"
STR_UPDATE_AVAILABLE: "Ažuriranje"
STR_CRASH_TITLE: "Sistemski pad"
STR_CRASH_DESCRIPTION: "Detaljan izvještaj je sačuvan u crash_report.txt. Molimo priloži ovu datoteku u svoj izvještaj o grešci."
STR_CRASH_REASON: "Razlog pada:"
STR_CRASH_NO_REASON: "(Razlog nije zabilježen)"
STR_TILT_PAGE_TURN: "Okretanje stranice naginjanjem"
STR_KB_HINT_MOVE_CURSOR: "Pritisni LIJEVO ili DESNO za pomjeranje kursora"
STR_KB_HINT_RETURN_CURSOR: "Pritisni LIJEVO za povratak na poziciju kursora"
STR_KB_HINT_HIDE_PASSWORD: "Drži DESNO pa pritisni [***] za skrivanje lozinke"
STR_KB_HINT_SHOW_PASSWORD: "Drži DESNO pa pritisni [abc] za prikaz lozinke"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pritisni [***] za skrivanje lozinke"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pritisni [abc] za prikaz lozinke"
STR_KB_HINT_EDIT_ENTRY: "Drži GORE za uređivanje unosa"
STR_KB_TIPS: "Savjeti:"
STR_KB_HINT_RETURN_KEYBOARD: "Pritisni DOLJE za povratak na tastaturu"
STR_KB_HINT_EXIT_URL_MODE: "Pritisni ABC za izlazak iz URL načina"
STR_KB_HINT_CLEAR_TEXT: "Drži DEL za brisanje cijelog teksta"
STR_KB_HINT_SECONDARY_CHAR: "Drži SELECT za sekundarni znak"
STR_KB_HINT_UPPER_SECONDARY: "Drži SELECT za VELIKA SLOVA ili sekundarni znak"
STR_KB_HINT_LOWER_SECONDARY: "Drži SELECT za mala slova ili sekundarni znak"
STR_KB_HINT_URL_SNIPPETS: "Pritisni URL za isječke"
STR_SD_FIRMWARE_UPDATE: "Ažuriranje firmvera sa SD kartice"
STR_SELECT_FIRMWARE_FILE: "Odaberi datoteku firmvera (.bin)"
STR_NO_BIN_FILES: "Nema pronađenih .bin datoteka"
STR_VALIDATING_FIRMWARE: "Provjera firmvera..."
STR_INVALID_FIRMWARE: "Nevažeća datoteka firmvera"
STR_FIRMWARE_TOO_LARGE: "Firmver je prevelik za particiju"
STR_FIRMWARE_TOO_SMALL: "Datoteka firmvera je premala"
STR_FIRMWARE_UPDATE_PROMPT: "Ažurirati firmver?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Datoteka se ne može otvoriti"
STR_FIRMWARE_WRITE_FAILED: "Upisivanje firmvera nije uspjelo"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne isključuj napajanje!"
STR_RECOVERY_MODE: "Način oporavka"
STR_RECOVERY_MODE_HINT: "Stavi firmware.bin u korijenski direktorij SD kartice i odaberi ga"
-5
View File
@@ -347,11 +347,6 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Suprimeix el servidor"
STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
-5
View File
@@ -270,11 +270,6 @@ STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl"
STR_FOCUS_READING: "Soustředěné čtení"
STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Složka pro stahování"
STR_OPDS_FILENAME_FORMAT: "Formát názvu souboru"
STR_FMT_AUTHOR_TITLE: "Autor - Název"
STR_FMT_TITLE_AUTHOR: "Název - Autor"
STR_FMT_TITLE: "Název"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
-5
View File
@@ -300,11 +300,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig"
STR_STEP_HINT_FRONT: "Frontknapper:"
STR_STEP_HINT_SIDE: "Sideknapper:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmappe"
STR_OPDS_FILENAME_FORMAT: "Filnavnsformat"
STR_FMT_AUTHOR_TITLE: "Forfatter - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Forfatter"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
-5
View File
@@ -300,11 +300,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nooit"
STR_STEP_HINT_FRONT: "Voorknoppen:"
STR_STEP_HINT_SIDE: "Zijknoppen:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmap"
STR_OPDS_FILENAME_FORMAT: "Bestandsnaamformaat"
STR_FMT_AUTHOR_TITLE: "Auteur - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Auteur"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
+1 -7
View File
@@ -229,7 +229,7 @@ STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar"
STR_CHAPTER_PAGE_COUNT: "Chapter Page Count"
STR_CHAPTER_PAGE_COUNT: "Page Count"
STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
STR_PROGRESS_BAR: "Progress Bar"
STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness"
@@ -347,12 +347,6 @@ STR_SERVER_NAME: "Server Name"
STR_NO_SERVERS: "No OPDS servers configured"
STR_DELETE_SERVER: "Delete Server"
STR_OPDS_SERVERS: "OPDS Servers"
STR_OPDS_DOWNLOAD_FOLDER: "Download folder"
STR_OPDS_FILENAME_FORMAT: "Filename format"
STR_FMT_AUTHOR_TITLE: "Author - Title"
STR_FMT_TITLE_AUTHOR: "Title - Author"
STR_FMT_TITLE: "Title"
STR_OPDS_SD_ROOT: "SD root"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_MANAGE_FONTS: "Manage Fonts"
-5
View File
@@ -268,11 +268,6 @@ STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_FOCUS_READING: "Keskittynyt lukeminen"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_OPDS_DOWNLOAD_FOLDER: "Latauskansio"
STR_OPDS_FILENAME_FORMAT: "Tiedostonimen muoto"
STR_FMT_AUTHOR_TITLE: "Tekijä - Nimi"
STR_FMT_TITLE_AUTHOR: "Nimi - Tekijä"
STR_FMT_TITLE: "Nimi"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Ei koskaan"
-5
View File
@@ -301,11 +301,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Jamais"
STR_STEP_HINT_FRONT: "Boutons avant :"
STR_STEP_HINT_SIDE: "Boutons latéraux :"
STR_OPDS_DOWNLOAD_FOLDER: "Dossier de téléchargement"
STR_OPDS_FILENAME_FORMAT: "Format du nom de fichier"
STR_FMT_AUTHOR_TITLE: "Auteur - Titre"
STR_FMT_TITLE_AUTHOR: "Titre - Auteur"
STR_FMT_TITLE: "Titre"
STR_SCREENSHOT_BUTTON: "Capture d'écran"
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
-5
View File
@@ -329,11 +329,6 @@ STR_SERVER_NAME: "Servername"
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
STR_DELETE_SERVER: "Server entfernen"
STR_OPDS_SERVERS: "OPDS-Server"
STR_OPDS_DOWNLOAD_FOLDER: "Download-Ordner"
STR_OPDS_FILENAME_FORMAT: "Dateinamenformat"
STR_FMT_AUTHOR_TITLE: "Autor - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Autor"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Auto-Umblättern aktiv: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)"
STR_IMAGES: "Bilder"
-5
View File
@@ -306,11 +306,6 @@ STR_NO_SERVERS: "לא הוגדרו שרתי OPDS"
STR_DELETE_SERVER: "מחק שרת"
STR_DELETE_CONFIRM: "למחוק שרת זה?"
STR_OPDS_SERVERS: "שרתי OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "תיקיית הורדות"
STR_OPDS_FILENAME_FORMAT: "תבנית שם קובץ"
STR_FMT_AUTHOR_TITLE: "מחבר - כותרת"
STR_FMT_TITLE_AUTHOR: "כותרת - מחבר"
STR_FMT_TITLE: "כותרת"
STR_AUTO_TURN_ENABLED: "דפדוף אוטומטי פועל: "
STR_AUTO_TURN_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)"
STR_MANAGE_FONTS: "ניהול גופנים"
-5
View File
@@ -299,11 +299,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
STR_SLEEP_NEVER: "Soha"
STR_STEP_HINT_FRONT: "Elülső gombok:"
STR_STEP_HINT_SIDE: "Oldalsó gombok:"
STR_OPDS_DOWNLOAD_FOLDER: "Letöltési mappa"
STR_OPDS_FILENAME_FORMAT: "Fájlnév formátuma"
STR_FMT_AUTHOR_TITLE: "Szerző - Cím"
STR_FMT_TITLE_AUTHOR: "Cím - Szerző"
STR_FMT_TITLE: "Cím"
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
-5
View File
@@ -315,11 +315,6 @@ STR_SERVER_NAME: "Nome server"
STR_NO_SERVERS: "Nessun server OPDS configurato"
STR_DELETE_SERVER: "Elimina server"
STR_OPDS_SERVERS: "Server OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Cartella download"
STR_OPDS_FILENAME_FORMAT: "Formato nome file"
STR_FMT_AUTHOR_TITLE: "Autore - Titolo"
STR_FMT_TITLE_AUTHOR: "Titolo - Autore"
STR_FMT_TITLE: "Titolo"
STR_AUTO_TURN_ENABLED: "Volta pagina automatico: "
STR_AUTO_TURN_PAGES_PER_MIN: "Volta pagina automatico (pag/min)"
STR_MANAGE_FONTS: "Gestisci font"
-5
View File
@@ -296,11 +296,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Ешқашан"
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
STR_STEP_HINT_SIDE: "Бүйір түймелері:"
STR_OPDS_DOWNLOAD_FOLDER: "Жүктеп алу қалтасы"
STR_OPDS_FILENAME_FORMAT: "Файл атауының пішімі"
STR_FMT_AUTHOR_TITLE: "Автор - Атауы"
STR_FMT_TITLE_AUTHOR: "Атауы - Автор"
STR_FMT_TITLE: "Атауы"
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
-5
View File
@@ -297,11 +297,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Niekada"
STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
STR_OPDS_DOWNLOAD_FOLDER: "Atsisiuntimų aplankas"
STR_OPDS_FILENAME_FORMAT: "Failo pavadinimo formatas"
STR_FMT_AUTHOR_TITLE: "Autorius - Pavadinimas"
STR_FMT_TITLE_AUTHOR: "Pavadinimas - Autorius"
STR_FMT_TITLE: "Pavadinimas"
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
-5
View File
@@ -316,11 +316,6 @@ STR_SERVER_NAME: "Nazwa serwera"
STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS"
STR_DELETE_SERVER: "Usuń serwer"
STR_OPDS_SERVERS: "Serwery OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Folder pobierania"
STR_OPDS_FILENAME_FORMAT: "Format nazwy pliku"
STR_FMT_AUTHOR_TITLE: "Autor - Tytuł"
STR_FMT_TITLE_AUTHOR: "Tytuł - Autor"
STR_FMT_TITLE: "Tytuł"
STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)"
STR_DOWNLOAD_FONTS: "Pobierz czcionki"
-5
View File
@@ -339,11 +339,6 @@ STR_SERVER_NAME: "Nome do Servidor"
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
STR_DELETE_SERVER: "Excluir Servidor"
STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Pasta de downloads"
STR_OPDS_FILENAME_FORMAT: "Formato do nome do arquivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
STR_MANAGE_FONTS: "Gerenciar fontes"
-5
View File
@@ -300,11 +300,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Niciodată"
STR_STEP_HINT_FRONT: "Butoane frontale:"
STR_STEP_HINT_SIDE: "Butoane laterale:"
STR_OPDS_DOWNLOAD_FOLDER: "Dosar descărcări"
STR_OPDS_FILENAME_FORMAT: "Format nume fișier"
STR_FMT_AUTHOR_TITLE: "Autor - Titlu"
STR_FMT_TITLE_AUTHOR: "Titlu - Autor"
STR_FMT_TITLE: "Titlu"
STR_SCREENSHOT_BUTTON: "Captură ecran"
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
-5
View File
@@ -339,11 +339,6 @@ STR_SERVER_NAME: "Имя сервера"
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
STR_DELETE_SERVER: "Удалить сервер"
STR_OPDS_SERVERS: "Серверы OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка загрузок"
STR_OPDS_FILENAME_FORMAT: "Формат имени файла"
STR_FMT_AUTHOR_TITLE: "Автор - Название"
STR_FMT_TITLE_AUTHOR: "Название - Автор"
STR_FMT_TITLE: "Название"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)"
STR_MANAGE_FONTS: "Управление шрифтами"
-5
View File
@@ -335,11 +335,6 @@ STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery"
STR_OPDS_DOWNLOAD_FOLDER: "Priečinok sťahovania"
STR_OPDS_FILENAME_FORMAT: "Formát názvu súboru"
STR_FMT_AUTHOR_TITLE: "Autor - Názov"
STR_FMT_TITLE_AUTHOR: "Názov - Autor"
STR_FMT_TITLE: "Názov"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem"
-5
View File
@@ -297,11 +297,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikoli"
STR_STEP_HINT_FRONT: "Sprednji gumbi:"
STR_STEP_HINT_SIDE: "Stranski gumbi:"
STR_OPDS_DOWNLOAD_FOLDER: "Mapa za prenose"
STR_OPDS_FILENAME_FORMAT: "Oblika imena datoteke"
STR_FMT_AUTHOR_TITLE: "Avtor - Naslov"
STR_FMT_TITLE_AUTHOR: "Naslov - Avtor"
STR_FMT_TITLE: "Naslov"
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
-5
View File
@@ -345,11 +345,6 @@ STR_SERVER_NAME: "Nombre de servidor"
STR_NO_SERVERS: "No se configuraron servidores OPDS"
STR_DELETE_SERVER: "Borrar servidor"
STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de descargas"
STR_OPDS_FILENAME_FORMAT: "Formato del nombre de archivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Avance activado: "
STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)"
STR_MANAGE_FONTS: "Gestionar tipografías"
-5
View File
@@ -342,11 +342,6 @@ STR_SERVER_NAME: "Servernamn"
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
STR_DELETE_SERVER: "Ta bort server"
STR_OPDS_SERVERS: "OPDS-servrar"
STR_OPDS_DOWNLOAD_FOLDER: "Hämtningsmapp"
STR_OPDS_FILENAME_FORMAT: "Filnamnsformat"
STR_FMT_AUTHOR_TITLE: "Författare - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Författare"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_MANAGE_FONTS: "Hantera teckensnitt"
-5
View File
@@ -291,11 +291,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
STR_SLEEP_NEVER: "Asla"
STR_STEP_HINT_FRONT: "Ön tuşlar:"
STR_STEP_HINT_SIDE: "Yan tuşlar:"
STR_OPDS_DOWNLOAD_FOLDER: "İndirme klasörü"
STR_OPDS_FILENAME_FORMAT: "Dosya adı biçimi"
STR_FMT_AUTHOR_TITLE: "Yazar - Başlık"
STR_FMT_TITLE_AUTHOR: "Başlık - Yazar"
STR_FMT_TITLE: "Başlık"
STR_NO_FILES_FOUND: "Dosya bulunamadı"
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme"
-5
View File
@@ -336,11 +336,6 @@ STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
STR_DELETE_SERVER: "Видалити сервер"
STR_OPDS_SERVERS: "Сервери OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка завантажень"
STR_OPDS_FILENAME_FORMAT: "Формат імені файлу"
STR_FMT_AUTHOR_TITLE: "Автор - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Автор"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
STR_MANAGE_FONTS: "Керування шрифтами"
-5
View File
@@ -348,11 +348,6 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Elimina el servidor"
STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
-5
View File
@@ -335,11 +335,6 @@ STR_SERVER_NAME: "Tên máy chủ"
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
STR_DELETE_SERVER: "Xóa máy chủ"
STR_OPDS_SERVERS: "Máy chủ OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Thư mục tải xuống"
STR_OPDS_FILENAME_FORMAT: "Định dạng tên tệp"
STR_FMT_AUTHOR_TITLE: "Tác giả - Tựa đề"
STR_FMT_TITLE_AUTHOR: "Tựa đề - Tác giả"
STR_FMT_TITLE: "Tựa đề"
STR_AUTO_TURN_ENABLED: "Tự lật trang: "
STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)"
STR_MANAGE_FONTS: "Quản lý phông chữ"
+19 -8
View File
@@ -225,17 +225,29 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
void HalGPIO::startDeepSleep() {
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (inputMgr.isPressed(BTN_POWER)) {
delay(50);
inputMgr.update();
}
// Arm the wakeup trigger *after* the button is released
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep
esp_deep_sleep_start();
}
void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
if (shortPressAllowed) {
// Fast path - no duration check needed
return true;
return;
}
// TODO: Intermittent edge case remains: a single tap followed by another single tap
// can still power on the device. Tighten wake debounce/state handling here.
// Calibrate: subtract boot time already elapsed, assuming button held since boot.
const unsigned long calibration = millis();
const unsigned long calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1;
// Calibrate: subtract boot time already elapsed, assuming button held since boot
const uint16_t calibration = millis();
const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1;
const auto start = millis();
inputMgr.update();
@@ -250,12 +262,11 @@ bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre
inputMgr.update();
} while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration);
if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) {
return false;
startDeepSleep();
}
} else {
return false;
startDeepSleep();
}
return true;
}
bool HalGPIO::isUsbConnected() const {
+5 -2
View File
@@ -72,10 +72,13 @@ class HalGPIO {
unsigned long getHeldTime() const;
unsigned long getPowerButtonHeldTime() const;
// Setup wake up GPIO and enter deep sleep
void startDeepSleep();
// Verify power button was held long enough after wakeup.
// Returns true if verification succeeded, false if device should return to sleep.
// If verification fails, enters deep sleep and does not return.
// Should only be called when wakeup reason is PowerButton.
bool verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed);
void verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed);
// Check if USB is connected
bool isUsbConnected() const;
+10 -9
View File
@@ -58,6 +58,14 @@ class CrossPointSettings {
HIDE_PROGRESS = 2,
STATUS_BAR_PROGRESS_BAR_COUNT
};
// Values 0/1 keep the meaning of the old show/hide toggle (persisted under the
// legacy "statusBarChapterPageCount" key), so existing settings files load as-is.
enum STATUS_BAR_PAGE_COUNT {
HIDE_PAGE_COUNT = 0,
CHAPTER_PAGE_COUNT = 1,
BOOK_PAGE_COUNT = 2,
STATUS_BAR_PAGE_COUNT_COUNT
};
enum STATUS_BAR_PROGRESS_BAR_THICKNESS {
PROGRESS_BAR_THIN = 0,
PROGRESS_BAR_NORMAL = 1,
@@ -189,7 +197,8 @@ class CrossPointSettings {
uint8_t sleepScreenCoverFilter = NO_FILTER;
// Status bar settings (statusBar retained for migration only)
uint8_t statusBar = FULL;
uint8_t statusBarChapterPageCount = 1;
// STATUS_BAR_PAGE_COUNT; persisted under the legacy "statusBarChapterPageCount" key.
uint8_t statusBarPageCount = CHAPTER_PAGE_COUNT;
uint8_t statusBarBookProgressPercentage = 1;
uint8_t statusBarProgressBar = HIDE_PROGRESS;
uint8_t statusBarProgressBarThickness = PROGRESS_BAR_NORMAL;
@@ -238,14 +247,6 @@ class CrossPointSettings {
// Reader screen margin settings
uint8_t screenMargin = 5;
// OPDS download destination folder ("" = SD root). Global; edited from the
// OPDS server list. Persisted via a category-less SettingInfo::String in
// SettingsList.h, so it stays out of the on-device Settings screen.
char opdsDownloadFolder[64] = "";
// On-disk filename format for OPDS downloads (0=Author-Title default, 1=Title-Author,
// 2=Title). See OpdsFilenameFormat. Persisted via a category-less SettingInfo::Enum,
// edited from the OPDS server list; hidden from the on-device Settings screen.
uint8_t opdsFilenameFormat = 0;
// Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior
+6 -6
View File
@@ -21,35 +21,35 @@
void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
switch (static_cast<CrossPointSettings::STATUS_BAR_MODE>(settings.statusBar)) {
case CrossPointSettings::NONE:
settings.statusBarChapterPageCount = 0;
settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
settings.statusBarBattery = 0;
break;
case CrossPointSettings::NO_PROGRESS:
settings.statusBarChapterPageCount = 0;
settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
settings.statusBarBattery = 1;
break;
case CrossPointSettings::BOOK_PROGRESS_BAR:
settings.statusBarChapterPageCount = 1;
settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
settings.statusBarBattery = 1;
break;
case CrossPointSettings::ONLY_BOOK_PROGRESS_BAR:
settings.statusBarChapterPageCount = 1;
settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 0;
settings.statusBarProgressBar = CrossPointSettings::BOOK_PROGRESS;
settings.statusBarTitle = CrossPointSettings::HIDE_TITLE;
settings.statusBarBattery = 0;
break;
case CrossPointSettings::CHAPTER_PROGRESS_BAR:
settings.statusBarChapterPageCount = 0;
settings.statusBarPageCount = CrossPointSettings::HIDE_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 1;
settings.statusBarProgressBar = CrossPointSettings::CHAPTER_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
@@ -57,7 +57,7 @@ void applyLegacyStatusBarSettings(CrossPointSettings& settings) {
break;
case CrossPointSettings::FULL:
default:
settings.statusBarChapterPageCount = 1;
settings.statusBarPageCount = CrossPointSettings::CHAPTER_PAGE_COUNT;
settings.statusBarBookProgressPercentage = 1;
settings.statusBarProgressBar = CrossPointSettings::HIDE_PROGRESS;
settings.statusBarTitle = CrossPointSettings::CHAPTER_TITLE;
+4 -12
View File
@@ -194,16 +194,6 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_MOVE_FINISHED_TO_READ, &CrossPointSettings::moveFinishedToReadFolder,
"moveFinishedToReadFolder", StrId::STR_CAT_SYSTEM),
// OPDS download folder: persisted + web-exposed, but category-less so it
// is hidden from the on-device Settings screen (edited via OPDS UI).
SettingInfo::String(StrId::STR_OPDS_DOWNLOAD_FOLDER, &SETTINGS.opdsDownloadFolder[0],
sizeof(SETTINGS.opdsDownloadFolder), "opdsDownloadFolder"),
// OPDS download filename format: persisted + web-exposed, category-less so it
// is hidden from the on-device Settings screen (cycled from the OPDS UI).
SettingInfo::Enum(StrId::STR_OPDS_FILENAME_FORMAT, &CrossPointSettings::opdsFilenameFormat,
{StrId::STR_FMT_AUTHOR_TITLE, StrId::STR_FMT_TITLE_AUTHOR, StrId::STR_FMT_TITLE},
"opdsFilenameFormat"),
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
SettingInfo::DynamicString(
StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); },
@@ -243,8 +233,10 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
},
"koSendMetadata", StrId::STR_KOREADER_SYNC),
// --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) ---
SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount,
"statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR),
// Key kept from the old show/hide toggle so existing settings files load unchanged.
SettingInfo::Enum(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarPageCount,
{StrId::STR_HIDE, StrId::STR_CHAPTER, StrId::STR_BOOK}, "statusBarChapterPageCount",
StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage,
"statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar,
@@ -2,13 +2,11 @@
#include <Arduino.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Logging.h>
#include <OpdsStream.h>
#include <WiFi.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "SilentRestart.h"
#include "activities/network/WifiSelectionActivity.h"
@@ -17,7 +15,6 @@
#include "fontIds.h"
#include "network/HttpDownloader.h"
#include "util/BookCacheUtils.h"
#include "util/OpdsFilename.h"
#include "util/StringUtils.h"
#include "util/UrlUtils.h"
@@ -282,26 +279,8 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
// Build full download URL relative to the current feed, not the root server URL
const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath);
std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href);
// opdsDownloadFolder is already a null-terminated char[64]; use it directly —
// no std::string copy. exists()/mkdir() take const char*.
const char* folder = SETTINGS.opdsDownloadFolder; // "" => SD root
bool haveFolder = folder[0] != '\0';
if (haveFolder && !Storage.exists(folder) && !Storage.mkdir(folder)) {
// exists()-guard first: mkdir's return-on-existing is unconfirmed, and every
// existing caller checks exists() before mkdir. On real failure, fall back
// to SD root so the download is never lost.
LOG_ERR("OPDS", "mkdir failed for %s, using SD root", folder);
haveFolder = false;
}
// downloadToFile() needs a std::string, and titles are unbounded (a fixed
// char[] would truncate). Cold path (a multi-second download follows), so one
// reserve'd, in-place-appended owning string is the right call.
std::string filename;
filename.reserve(96);
if (haveFolder) filename += folder;
filename += '/';
filename += opdsBookFilename(book.author, book.title, static_cast<OpdsFilenameFormat>(SETTINGS.opdsFilenameFormat));
std::string filename =
"/" + StringUtils::sanitizeFilename((book.author.empty() ? "" : book.author + " - ") + book.title) + ".epub";
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
int lastRenderedPercent = -1;
+106 -3
View File
@@ -44,6 +44,22 @@ constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12};
constexpr size_t initialBookmarkCacheCapacity = 16;
constexpr float bookmarkProgressEpsilon = 0.0001f;
// The render parameters the reader paginates with, as passed to loadSectionFile /
// createSectionFile / startBuild below. Used to validate other sections' cached
// page counts and to detect when a settings change invalidates the harvest.
Section::RenderParams readerRenderParams(const uint16_t viewportWidth, const uint16_t viewportHeight) {
return {SETTINGS.getReaderFontId(),
SETTINGS.getReaderLineCompression(),
static_cast<bool>(SETTINGS.extraParagraphSpacing),
SETTINGS.paragraphAlignment,
viewportWidth,
viewportHeight,
static_cast<bool>(SETTINGS.hyphenationEnabled),
static_cast<bool>(SETTINGS.embeddedStyle),
SETTINGS.imageRendering,
static_cast<bool>(SETTINGS.focusReadingEnabled)};
}
int clampPercent(int percent) {
if (percent < 0) {
return 0;
@@ -319,6 +335,25 @@ void EpubReaderActivity::loop() {
}
}
// Whole-book page counter: harvest one other section's cached page count per tick
// (a header-only peek), so the total converges to exact without visiting every
// chapter. Idle-priority — skipped whenever a render is pending or a build is
// running. No re-render is requested; the counter refreshes on the next page turn.
if (bookPages && bookPagesSweepIndex < epub->getSpineItemsCount() && !RenderLock::peek() &&
!(section && section->isBuilding())) {
RenderLock lock;
// Re-check under the lock: render() may have just reset/reallocated the table.
if (bookPages && bookPagesSweepIndex < epub->getSpineItemsCount()) {
const int index = bookPagesSweepIndex++;
if (bookPages[index].pages < 0) {
const Section peekSection(epub, index, renderer);
if (const auto count = peekSection.getCachedPageCount(&bookPagesParams)) {
bookPages[index].pages = *count;
}
}
}
}
// End-of-Book screen reached (currentSpineIndex == spine count) means the book is
// finished. Two independent finished-book features key off this same condition.
const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount();
@@ -980,6 +1015,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
buildViewportWidth = viewportWidth;
buildViewportHeight = viewportHeight;
ensureBookPages(viewportWidth, viewportHeight);
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
@@ -1222,6 +1259,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
applyDeferredReposition();
recordCurrentSectionPages();
renderer.clearScreen();
if (section->pageCount == 0) {
@@ -1331,6 +1370,59 @@ bool EpubReaderActivity::applyDeferredReposition() {
return changed;
}
void EpubReaderActivity::ensureBookPages(const uint16_t viewportWidth, const uint16_t viewportHeight) {
if (SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::BOOK_PAGE_COUNT) {
bookPages.reset();
return;
}
const Section::RenderParams params = readerRenderParams(viewportWidth, viewportHeight);
if (bookPages && params == bookPagesParams) {
return;
}
// First render, or a render-params change (font/orientation/...): the harvested
// counts are for the old pagination, so start over. The section caches themselves
// are the persistence; there is nothing else to invalidate.
bookPagesParams = params;
bookPagesSweepIndex = 0;
bookPages.reset();
const int sectionCount = epub->getSpineItemsCount();
if (sectionCount <= 0) {
return;
}
// 8 bytes per spine item, held for the reading session; freed with the activity
// or when the feature is switched off. On OOM stays null (chapter-local counts).
bookPages = makeUniqueNoThrow<BookPageEntry[]>(sectionCount);
if (!bookPages) {
LOG_ERR("ERS", "OOM: book page table (%d sections)", sectionCount);
return;
}
size_t prev = 0;
for (int i = 0; i < sectionCount; ++i) {
const size_t cum = epub->getCumulativeSpineItemSize(i);
bookPages[i].bytes = static_cast<uint32_t>(cum >= prev ? cum - prev : 0);
prev = cum;
}
}
void EpubReaderActivity::recordCurrentSectionPages() {
// Only a finalized section's pageCount is the chapter total; a building or
// partial section's is just its current watermark.
if (!bookPages || !section || section->isBuilding() || section->isPartial()) {
return;
}
if (currentSpineIndex >= 0 && currentSpineIndex < epub->getSpineItemsCount()) {
bookPages[currentSpineIndex].pages = section->pageCount;
}
}
std::optional<BookPagePosition> EpubReaderActivity::bookPagePosition() const {
if (!bookPages || !section) {
return std::nullopt;
}
return computeBookPagePosition(bookPages.get(), epub->getSpineItemsCount(), currentSpineIndex, section->currentPage,
section->estimatedTotalPages());
}
bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount);
}
@@ -1512,10 +1604,21 @@ void EpubReaderActivity::renderStatusBar() const {
// Calculate progress in book. Use the estimated total while a giant spine is still building so
// "page X of Y" and the progress bar don't read off the small build watermark.
const int currentPage = section->currentPage + 1;
const float pageCount = section->estimatedTotalPages();
const int pageCount = section->estimatedTotalPages();
const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
// Page counter values: whole-book numbers when Page Count is set to Book (and the
// table is alive), otherwise chapter-local. The progress bar stays on bookProgress.
int counterPage = currentPage;
int counterTotal = pageCount;
bool counterIsEstimate = section->isBuilding();
if (const auto book = bookPagePosition()) {
counterPage = book->currentPage;
counterTotal = book->totalPages;
counterIsEstimate = book->isEstimate;
}
std::string title;
int textYOffset = 0;
@@ -1543,8 +1646,8 @@ void EpubReaderActivity::renderStatusBar() const {
title = epub->getTitle();
}
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked,
section->isBuilding());
GUI.drawStatusBar(renderer, bookProgress, counterPage, counterTotal, title, 0, textYOffset, true,
currentPageBookmarked, counterIsEstimate);
}
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
@@ -1,5 +1,6 @@
#pragma once
#include <Epub.h>
#include <Epub/BookPages.h>
#include <Epub/FootnoteEntry.h>
#include <Epub/Section.h>
@@ -23,6 +24,17 @@ class EpubReaderActivity final : public Activity {
int pagesUntilFullRefresh = 0;
int cachedSpineIndex = 0;
int cachedChapterTotalPageCount = 0;
// Whole-book page accounting ("page X of Y" across the whole book), active only
// when the status bar Page Count is set to Book. Exact counts are harvested from
// finalized section caches — nothing extra is persisted (see BookPages.h). Null
// while inactive or on OOM; every book-page path degrades to chapter-local counts.
// Shared between the render task and loop(): only touch under the RenderLock.
std::unique_ptr<BookPageEntry[]> bookPages;
// Render params the harvested counts are valid for; a change resets the harvest.
Section::RenderParams bookPagesParams;
// Next spine index for loop()'s background sweep that peeks other sections'
// cached counts (one per tick); >= spine count once the sweep is done.
int bookPagesSweepIndex = 0;
unsigned long lastPageTurnTime = 0UL;
unsigned long pageTurnDuration = 0UL;
// Signals that the next render should reposition within the newly loaded section
@@ -113,6 +125,14 @@ class EpubReaderActivity final : public Activity {
// (used after a settings change re-paginates a chapter). Returns true if currentPage moved.
// No-op while the section is still building or when the pagination is unchanged (plain resume).
bool applyDeferredReposition();
// (Re)allocate and reset bookPages when the feature turns on or the render params
// change. Called from render() (under the RenderLock) where the viewport is known.
void ensureBookPages(uint16_t viewportWidth, uint16_t viewportHeight);
// Store the current section's exact page count once its pagination is final
// (no-op while building or partial). Caller must hold the RenderLock.
void recordCurrentSectionPages();
// Book-global position for the current page, or nullopt while inactive.
std::optional<BookPagePosition> bookPagePosition() const;
bool saveProgress(int spineIndex, int currentPage, int pageCount);
// Jump to a percentage of the book (0-100), mapping it to spine and page.
void jumpToPercent(int percent);
+3 -15
View File
@@ -208,27 +208,15 @@ void KOReaderSyncActivity::performUpload() {
// Optionally include document metadata (KOReader PR #15306)
if (KOREADER_STORE.getSendMetadata()) {
// The Epub is released before the sync network calls and is only reloaded on the
// remote-progress path (performSync). When uploading from NO_REMOTE_PROGRESS the
// Epub is still null, so reload it here and guard the title/author reads to avoid
// dereferencing a null Epub. Filename is derived from the path and is always safe.
ensureEpubLoaded();
KOReaderMetadata meta;
// Extract filename from path
const auto lastSlash = epubPath.rfind('/');
meta.filename = (lastSlash != std::string::npos) ? epubPath.substr(lastSlash + 1) : epubPath;
if (epub) {
meta.title = epub->getTitle();
meta.authors = epub->getAuthor();
} else {
LOG_ERR("KOSync", "Epub unavailable for metadata; sending filename only");
}
meta.title = epub->getTitle();
meta.authors = epub->getAuthor();
progress.metadata = std::move(meta);
}
// Release the Epub before the network call so the TLS handshake has enough free heap
// (consistent with the release-before-sync pattern in performSync); nothing below needs it.
epub.reset();
const auto result = KOReaderSyncClient::updateProgress(progress);
// Drop the radio while user reads the result; full teardown happens at silent reboot.
@@ -3,51 +3,19 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <cstring>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "OpdsServerStore.h"
#include "OpdsSettingsActivity.h"
#include "activities/ActivityManager.h"
#include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/OpdsFilename.h"
namespace {
// Normalizes a user-typed folder: trims spaces, "" => SD root, otherwise a
// single leading '/' and no trailing '/'. Cold path (runs once per edit).
std::string normalizeFolder(std::string v) {
while (!v.empty() && (v.front() == ' ' || v.front() == '\t')) v.erase(v.begin());
while (!v.empty() && (v.back() == ' ' || v.back() == '\t')) v.pop_back();
if (v.empty()) return "";
if (v.front() != '/') v.insert(v.begin(), '/');
while (v.size() > 1 && v.back() == '/') v.pop_back();
if (v == "/") return ""; // a bare slash is SD root, same as empty
return v;
}
// Label shown for the current OPDS filename format in the list subtitle.
StrId opdsFormatLabel(uint8_t format) {
switch (format) {
case static_cast<uint8_t>(OpdsFilenameFormat::TitleAuthor):
return StrId::STR_FMT_TITLE_AUTHOR;
case static_cast<uint8_t>(OpdsFilenameFormat::TitleOnly):
return StrId::STR_FMT_TITLE;
default:
return StrId::STR_FMT_AUTHOR_TITLE;
}
}
} // namespace
int OpdsServerListActivity::getItemCount() const {
int count = static_cast<int>(OPDS_STORE.getCount());
// Settings mode appends three virtual items: "Add Server", "Download folder"
// and "Filename format".
// In settings mode, append a virtual "Add Server" item; in picker mode, only show real servers
if (!pickerMode) {
count += 3;
count++;
}
return count;
}
@@ -106,34 +74,6 @@ void OpdsServerListActivity::handleSelection() {
return;
}
// Index layout: [servers 0..serverCount-1], [Add Server], [Download folder], [Filename format].
if (selectedIndex == serverCount + 1) {
auto folderHandler = [this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
const std::string norm = normalizeFolder(kb.text);
strncpy(SETTINGS.opdsDownloadFolder, norm.c_str(), sizeof(SETTINGS.opdsDownloadFolder) - 1);
SETTINGS.opdsDownloadFolder[sizeof(SETTINGS.opdsDownloadFolder) - 1] = '\0';
SETTINGS.saveToFile();
requestUpdate();
}
};
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_OPDS_DOWNLOAD_FOLDER),
std::string(SETTINGS.opdsDownloadFolder), 63, InputType::Text),
folderHandler);
return;
}
// "Filename format": tap cycles through the available formats.
if (selectedIndex == serverCount + 2) {
SETTINGS.opdsFilenameFormat =
static_cast<uint8_t>((SETTINGS.opdsFilenameFormat + 1) % static_cast<uint8_t>(OpdsFilenameFormat::Count));
SETTINGS.saveToFile();
requestUpdate();
return;
}
// Settings mode: open editor for selected server, or create a new one
auto resultHandler = [this](const ActivityResult&) {
// Reload server list when returning from editor
@@ -171,30 +111,17 @@ void OpdsServerListActivity::render(RenderLock&&) {
// Secondary label: server URL (shown as subtitle when name is set).
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex,
[&servers, serverCount](int index) -> std::string {
[&servers, serverCount](int index) {
if (index < serverCount) {
const auto& server = servers[index];
return server.name.empty() ? server.url : server.name;
}
if (index == serverCount) {
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER));
}
if (index == serverCount + 1) {
return std::string(I18n::getInstance().get(StrId::STR_OPDS_DOWNLOAD_FOLDER));
}
return std::string(I18n::getInstance().get(StrId::STR_OPDS_FILENAME_FORMAT));
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER));
},
[&servers, serverCount](int index) -> std::string {
[&servers, serverCount](int index) {
if (index < serverCount && !servers[index].name.empty()) {
return servers[index].url;
}
if (index == serverCount + 1) {
const char* f = SETTINGS.opdsDownloadFolder;
return f[0] ? std::string(f) : std::string(I18n::getInstance().get(StrId::STR_OPDS_SD_ROOT));
}
if (index == serverCount + 2) {
return std::string(I18n::getInstance().get(opdsFormatLabel(SETTINGS.opdsFilenameFormat)));
}
return std::string("");
});
}
@@ -64,6 +64,10 @@ std::string formatUtcOffset(uint8_t biasedQ) {
snprintf(buf, sizeof(buf), "UTC%c%d:%02d", neg ? '-' : '+', hours, mins);
return buf;
}
// Order follows STATUS_BAR_PAGE_COUNT (hide=0, chapter=1, book=2).
constexpr int PAGE_COUNT_ITEMS = 3;
const StrId pageCountNames[PAGE_COUNT_ITEMS] = {StrId::STR_HIDE, StrId::STR_CHAPTER, StrId::STR_BOOK};
constexpr int PROGRESS_BAR_ITEMS = 3;
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
@@ -90,7 +94,11 @@ void StatusBarSettingsActivity::onEnter() {
selectedIndex = 0;
visibleItemCount = halClock.isAvailable() ? FULL_MENU_ITEMS : BASE_MENU_ITEMS;
// Clamp statusBarProgressBar and statusBarTitle in case of corrupt/migrated data
// Clamp enum-valued settings in case of corrupt/migrated data
if (SETTINGS.statusBarPageCount >= PAGE_COUNT_ITEMS) {
SETTINGS.statusBarPageCount = CrossPointSettings::STATUS_BAR_PAGE_COUNT::CHAPTER_PAGE_COUNT;
}
if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) {
SETTINGS.statusBarProgressBar = CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
}
@@ -163,8 +171,12 @@ void StatusBarSettingsActivity::loop() {
void StatusBarSettingsActivity::handleSelection() {
switch (selectedIndex) {
case ITEM_CHAPTER_PAGE_COUNT:
SETTINGS.statusBarChapterPageCount = (SETTINGS.statusBarChapterPageCount + 1) % 2;
break;
optionPopup.show(StrId::STR_CHAPTER_PAGE_COUNT, pageCountNames, PAGE_COUNT_ITEMS, SETTINGS.statusBarPageCount,
[this](int idx) {
SETTINGS.statusBarPageCount = idx;
SETTINGS.saveToFile();
});
return;
case ITEM_BOOK_PROGRESS_PERCENTAGE:
SETTINGS.statusBarBookProgressPercentage = (SETTINGS.statusBarBookProgressPercentage + 1) % 2;
break;
@@ -236,7 +248,7 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
[](int index) -> std::string {
switch (index) {
case ITEM_CHAPTER_PAGE_COUNT:
return SETTINGS.statusBarChapterPageCount ? tr(STR_SHOW) : tr(STR_HIDE);
return I18N.get(pageCountNames[SETTINGS.statusBarPageCount]);
case ITEM_BOOK_PROGRESS_PERCENTAGE:
return SETTINGS.statusBarBookProgressPercentage ? tr(STR_SHOW) : tr(STR_HIDE);
case ITEM_PROGRESS_BAR:
+2 -1
View File
@@ -132,7 +132,8 @@ int UITheme::getStatusBarHeight() {
// Add status bar margin
const bool showStatusBar =
SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT ||
SETTINGS.statusBarBookProgressPercentage ||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
SETTINGS.statusBarClock != CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE;
const bool showProgressBar =
+10 -6
View File
@@ -27,7 +27,8 @@ constexpr int bookmarkStatusIconGap = 4;
constexpr int bookmarkStatusIconTopCrop = 2;
bool statusBarTextLaneVisible() {
return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
return SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT ||
SETTINGS.statusBarBookProgressPercentage ||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
(SETTINGS.statusBarClock && halClock.isAvailable());
}
@@ -765,20 +766,23 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
int leftClusterWidth = 0;
int rightClusterWidth = 0;
if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) {
const bool showPageCount = SETTINGS.statusBarPageCount != CrossPointSettings::STATUS_BAR_PAGE_COUNT::HIDE_PAGE_COUNT;
if (SETTINGS.statusBarBookProgressPercentage || showPageCount) {
// Right aligned text for progress counter
char progressStr[32];
// Prefix the page count with "~" while a still-building spine only yields an estimated total.
// Mark the total with "~" while it is only an estimate (a still-building spine's
// watermark, or a whole-book total with not-yet-paginated chapters). The current
// page is always exact, so the marker sits on the total.
const char* estimatePrefix = pageCountEstimated ? "~" : "";
if (SETTINGS.statusBarBookProgressPercentage && SETTINGS.statusBarChapterPageCount) {
snprintf(progressStr, sizeof(progressStr), "%s%d/%d %.0f%%", estimatePrefix, currentPage, pageCount,
if (SETTINGS.statusBarBookProgressPercentage && showPageCount) {
snprintf(progressStr, sizeof(progressStr), "%d/%s%d %.0f%%", currentPage, estimatePrefix, pageCount,
bookProgress);
} else if (SETTINGS.statusBarBookProgressPercentage) {
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
} else {
snprintf(progressStr, sizeof(progressStr), "%s%d/%d", estimatePrefix, currentPage, pageCount);
snprintf(progressStr, sizeof(progressStr), "%d/%s%d", currentPage, estimatePrefix, pageCount);
}
int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
+45 -4
View File
@@ -159,6 +159,49 @@ void silentRestartToReader() {
ESP.restart();
}
// Verify power button press duration on wake-up from deep sleep
// Pre-condition: isWakeupByPowerButton() == true
void verifyPowerButtonDuration() {
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) {
// Fast path for short press
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
return;
}
// Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
const auto start = millis();
bool abort = false;
// Subtract the current time, because inputManager only starts counting the HeldTime from the first update()
// This way, we remove the time we already took to reach here from the duration,
// assuming the button was held until now from millis()==0 (i.e. device start time).
const uint16_t calibration = start;
const uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
gpio.update();
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) {
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
gpio.update();
}
t2 = millis();
if (gpio.isPressed(HalGPIO::BTN_POWER)) {
do {
delay(10);
gpio.update();
} while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() < calibratedPressDuration);
abort = gpio.getPowerButtonHeldTime() < calibratedPressDuration;
} else {
abort = true;
}
if (abort) {
// Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
powerManager.startDeepSleep(gpio);
}
}
void waitForPowerRelease() {
gpio.update();
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
@@ -314,10 +357,8 @@ void setup() {
switch (wakeupReason) {
case HalGPIO::WakeupReason::PowerButton:
LOG_DBG("MAIN", "Verifying power button press duration");
if (!gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP)) {
powerManager.startDeepSleep(gpio);
}
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP);
break;
case HalGPIO::WakeupReason::AfterUSBPower:
// If USB power caused a cold boot, go back to sleep
-23
View File
@@ -1,23 +0,0 @@
#include "OpdsFilename.h"
#include "StringUtils.h"
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format) {
std::string base;
switch (format) {
case OpdsFilenameFormat::TitleAuthor:
base = author.empty() ? title : title + " - " + author;
break;
case OpdsFilenameFormat::TitleOnly:
base = title;
break;
case OpdsFilenameFormat::AuthorTitle:
default:
base = author.empty() ? title : author + " - " + title;
break;
}
// sanitizeFilename caps at 100 bytes and never returns empty (falls back to
// "book"); ".epub" is appended after so the extension is never truncated —
// identical treatment to the previous inline construction.
return StringUtils::sanitizeFilename(base) + ".epub";
}
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include <cstdint>
#include <string>
// On-disk filename format for books downloaded from an OPDS server. Stored as a
// uint8_t in CrossPointSettings; cast to this enum at the call sites. `Count` is
// the number of selectable formats (used to cycle the setting in the UI).
enum class OpdsFilenameFormat : uint8_t {
AuthorTitle = 0, // "Author - Title.epub" (default; matches legacy behaviour)
TitleAuthor = 1, // "Title - Author.epub"
TitleOnly = 2, // "Title.epub"
Count = 3,
};
// Composes and sanitizes the on-disk filename (including the ".epub" extension)
// for a downloaded OPDS book, according to `format`. When the author is empty,
// every format collapses to just the sanitized title. Pure: no I/O, no globals.
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format);
-1
View File
@@ -43,6 +43,5 @@ add_subdirectory(release_json_parser)
add_subdirectory(differential_rounding)
add_subdirectory(hyphenation_eval)
add_subdirectory(utf8_compose)
add_subdirectory(opds_filename)
add_subdirectory(minibidi_arabic)
add_subdirectory(combining_marks)
-18
View File
@@ -1,18 +0,0 @@
add_executable(OpdsFilenameTest
OpdsFilenameTest.cpp
${REPO_ROOT}/src/util/OpdsFilename.cpp
${REPO_ROOT}/src/util/StringUtils.cpp
${REPO_ROOT}/lib/Utf8/Utf8.cpp
)
target_include_directories(OpdsFilenameTest PRIVATE
${REPO_ROOT}/src/util
${REPO_ROOT}/lib/Utf8
)
target_link_libraries(OpdsFilenameTest PRIVATE
crosspoint_test_common
GTest::gtest_main
)
gtest_discover_tests(OpdsFilenameTest)
-52
View File
@@ -1,52 +0,0 @@
#include <gtest/gtest.h>
#include <string>
#include "OpdsFilename.h"
namespace {
TEST(OpdsFilename, AuthorTitleIsDefaultOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::AuthorTitle), "J. Doe - My Book.epub");
}
TEST(OpdsFilename, TitleAuthorSwapsOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book - J. Doe.epub");
}
TEST(OpdsFilename, TitleOnlyIgnoresAuthor) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, EmptyAuthorCollapsesToTitleForEveryFormat) {
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::AuthorTitle), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, IllegalCharactersAreSanitized) {
// '/' ':' '*' '?' etc. are replaced with '_' by sanitizeFilename.
EXPECT_EQ(opdsBookFilename("A/B", "C:D*E?", OpdsFilenameFormat::AuthorTitle), "A_B - C_D_E_.epub");
}
TEST(OpdsFilename, EmptyAuthorAndTitleFallsBackToBook) {
// sanitizeFilename returns "book" when nothing usable remains.
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::AuthorTitle), "book.epub");
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::TitleOnly), "book.epub");
}
TEST(OpdsFilename, LongNameIsTruncatedToByteBudgetBeforeExtension) {
// sanitizeFilename caps the base at 100 bytes; ".epub" is appended after.
const std::string longTitle(200, 'a');
const std::string result = opdsBookFilename("", longTitle, OpdsFilenameFormat::TitleOnly);
EXPECT_EQ(result, std::string(100, 'a') + ".epub");
EXPECT_EQ(result.size(), 105u);
}
TEST(OpdsFilename, UnknownFormatValueFallsBackToAuthorTitle) {
// Defensive: a persisted value outside the enum still yields a valid name.
const auto bogus = static_cast<OpdsFilenameFormat>(99);
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", bogus), "J. Doe - My Book.epub");
}
} // namespace