diff --git a/CHANGELOG.md b/CHANGELOG.md index 333d214b413..3b770254f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - The web status API (`/api/status`) reports a `temperatures` block: the chip temperature on ESP32-S3 devices, the battery fuel gauge's temperature on boards that have one (such as the X3), and on the X4 Pro the e-ink panel controller's own sensor, sampled after a screen refresh at most once a minute. Sensors a board lacks are left out. - Turn reading stats tracking on or off for the whole device or individual EPUB and XTC books, while keeping saved history and Time Left estimates. - Assign separate short-press and long-press actions to the Left/Up and Right/Down side buttons; existing side-button layouts migrate to matching individual actions. +- Assign a side-button shortcut to flip the reading screen 180°, alongside clockwise and counterclockwise turns. - Assign Library to power, long-press, button-chord, Home-button, or Quick Actions shortcuts to open the book list directly. - Customize the top and bottom reader status bars separately, including item positions and progress bars, in EPUB, TXT, and XTC books. Each bar can be previewed where it appears while reading. - View a selected book's reading stats from its Library or File Browser action menu. @@ -117,6 +118,7 @@ - Korean text keeps natural syllable spacing when justified and wraps by word. - Footnote choices can be selected directly on the reading page, with a list fallback for links without a visible target. - Changing global font or page layout settings from the pull-down panel on touch devices now updates the open book when it inherits those settings. +- KOReader authentication now rejects unexpectedly large server responses to avoid crashes. ## [v1.6.0] - 2026-09-21 diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index e754fc3370f..0f4288236c4 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -1319,7 +1319,10 @@ bool Epub::generateThumbBmp(int width, int height, const GfxRenderer* renderer, } bool Epub::generateThumbBmpFromSource(int height, const GfxRenderer* renderer, const int readerFontId) { - int width = 0; + return generateThumbBmpFromSource(0, height, renderer, readerFontId); +} + +bool Epub::generateThumbBmpFromSource(int width, int height, const GfxRenderer* renderer, const int readerFontId) { normalizeThumbDimensions(width, height); const std::string thumbPath = getThumbBmpPathForDimensions(cachePath, width, height); if (cachedBmpMatchesDimensions(thumbPath, width, height)) return true; diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 9602ccb3bdb..aae8d308e25 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -171,6 +171,7 @@ class Epub { bool generateThumbBmp(int width, int height, const GfxRenderer* renderer = nullptr, int readerFontId = 0) const; // Generate a Home thumbnail from the EPUB cover without building reader pages or indexes. bool generateThumbBmpFromSource(int height, const GfxRenderer* renderer = nullptr, int readerFontId = 0); + bool generateThumbBmpFromSource(int width, int height, const GfxRenderer* renderer = nullptr, int readerFontId = 0); // Writes a thumbnail that can either crop-to-fill or contain unusual cover // ratios, depending on the source image dimensions. bool generateAdaptiveThumbBmp(int width, int height, const GfxRenderer* renderer = nullptr, diff --git a/lib/I18n/translations/arabic.yaml b/lib/I18n/translations/arabic.yaml index b6fd664e84d..6c4f8913daf 100644 --- a/lib/I18n/translations/arabic.yaml +++ b/lib/I18n/translations/arabic.yaml @@ -120,6 +120,9 @@ STR_EOB_HOME: "الرئيسية" STR_EOB_CONTINUE_WITH: "المتابعة إلى" STR_SHORT_PWR_BTN: "ضغطة قصيرة" STR_ORIENTATION: "اتجاه القراءة" +STR_ROTATE_CCW: "تدوير الصفحة CCW" +STR_ROTATE_CW: "تدوير الصفحة CW" +STR_ROTATE_FLIP: "تدوير الصفحة 180°" STR_SIDE_BTN_LAYOUT: "تخطيط الأزرار الجانبية (أثناء القراءة)" STR_ORIENTATION_AWARE: "علم التوجه" STR_NAV_BUTTONS: "أزرار التنقل" diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 117349f8f44..37570ed771f 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -105,6 +105,9 @@ STR_STATS_EVENING_READER: "Вячэрні чытач" STR_STATS_NIGHT_READER: "Начны чытач" STR_SHORT_PWR_BTN: "Кароткае націсканне" STR_ORIENTATION: "Арыентацыя чытання" +STR_ROTATE_CCW: "Павярнуць старонку CCW" +STR_ROTATE_CW: "Павярнуць старонку CW" +STR_ROTATE_FLIP: "Павярнуць старонку 180°" STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі" STR_ORIENTATION_AWARE: "З улікам арыентацыі" STR_NAV_BUTTONS: "Кнопкі навігацыі" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 7299a0fcf3b..fe87800ba0b 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Text de mostra" STR_IMAGES_SUPPRESS: "Suprimir" STR_SHORT_PWR_BTN: "Pressió curta" STR_ORIENTATION: "Orientació de lectura" +STR_ROTATE_CCW: "Gira la pàgina CCW" +STR_ROTATE_CW: "Gira la pàgina CW" +STR_ROTATE_FLIP: "Gira la pàgina 180°" STR_SIDE_BTN_LAYOUT: "Disposició botons laterals" STR_ORIENTATION_AWARE: "Segons orientació" STR_NAV_BUTTONS: "Botons de navegació" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index a8b0dab2832..4a7adaae15e 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -106,6 +106,9 @@ STR_STATS_EVENING_READER: "Večerní čtenář" STR_STATS_NIGHT_READER: "Noční čtenář" STR_SHORT_PWR_BTN: "Krátký stisk" STR_ORIENTATION: "Orientace čtení" +STR_ROTATE_CCW: "Otočit stránku CCW" +STR_ROTATE_CW: "Otočit stránku CW" +STR_ROTATE_FLIP: "Otočit stránku 180°" STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)" STR_ORIENTATION_AWARE: "Podle orientace" STR_NAV_BUTTONS: "Navigační tlačítka" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index d62a143cd8f..cfefc87d494 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Pladsholder" STR_IMAGES_SUPPRESS: "Skjul" STR_SHORT_PWR_BTN: "Kort tryk" STR_ORIENTATION: "Læseretning" +STR_ROTATE_CCW: "Rotér siden CCW" +STR_ROTATE_CW: "Rotér siden CW" +STR_ROTATE_FLIP: "Rotér siden 180°" STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)" STR_ORIENTATION_AWARE: "Orienteringsbevidst" STR_NAV_BUTTONS: "Navigationsknapper" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 7b2e91db069..653d0045d4c 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_SUPPRESS: "Verbergen" STR_SHORT_PWR_BTN: "Kort indrukken" STR_ORIENTATION: "Leesstand" +STR_ROTATE_CCW: "Pagina draaien CCW" +STR_ROTATE_CW: "Pagina draaien CW" +STR_ROTATE_FLIP: "Pagina draaien 180°" STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)" STR_ORIENTATION_AWARE: "Oriëntatiebewust" STR_NAV_BUTTONS: "Navigatieknoppen" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index a77a2d8a645..fb2367a58ff 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -184,6 +184,9 @@ STR_EOB_HOME: "Home" STR_EOB_CONTINUE_WITH: "Continue with" STR_SHORT_PWR_BTN: "Short-press" STR_ORIENTATION: "Orientation" +STR_ROTATE_CCW: "Rotate Page CCW" +STR_ROTATE_CW: "Rotate Page CW" +STR_ROTATE_FLIP: "Rotate Page 180°" STR_SIDE_BTN_LAYOUT: "Layout" STR_ORIENTATION_AWARE: "Orientation Aware" STR_NAV_BUTTONS: "Nav Buttons" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index cd80f6b6d55..f526c3530ce 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -106,6 +106,9 @@ STR_STATS_EVENING_READER: "Iltalukija" STR_STATS_NIGHT_READER: "Yölukija" STR_SHORT_PWR_BTN: "Lyhyt painallus" STR_ORIENTATION: "Lukusuunta" +STR_ROTATE_CCW: "Kierrä sivua CCW" +STR_ROTATE_CW: "Kierrä sivua CW" +STR_ROTATE_FLIP: "Kierrä sivua 180°" STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)" STR_ORIENTATION_AWARE: "Suunnan huomioiva" STR_NAV_BUTTONS: "Navigointipainikkeet" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index bec5e124667..ba8dbaf4e14 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Espace réservé" STR_IMAGES_SUPPRESS: "Masquer" STR_SHORT_PWR_BTN: "Appui court" STR_ORIENTATION: "Orientation de lecture" +STR_ROTATE_CCW: "Faire pivoter la page CCW" +STR_ROTATE_CW: "Faire pivoter la page CW" +STR_ROTATE_FLIP: "Faire pivoter la page 180°" STR_SIDE_BTN_LAYOUT: "Boutons latéraux" STR_ORIENTATION_AWARE: "Selon l'orientation" STR_NAV_BUTTONS: "Boutons navigation" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 67a02a39ed6..9ae40f41263 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -115,6 +115,9 @@ STR_IMAGES_PLACEHOLDER: "Platzhalter" STR_IMAGES_SUPPRESS: "Unterdrücken" STR_SHORT_PWR_BTN: "Kurzer Druck" STR_ORIENTATION: "Leseausrichtung" +STR_ROTATE_CCW: "Seite drehen CCW" +STR_ROTATE_CW: "Seite drehen CW" +STR_ROTATE_FLIP: "Seite drehen 180°" STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)" STR_ORIENTATION_AWARE: "Ausrichtungsabhängig" STR_NAV_BUTTONS: "Navigationstasten" diff --git a/lib/I18n/translations/hebrew.yaml b/lib/I18n/translations/hebrew.yaml index 68578bc0c2a..1ab8664b613 100644 --- a/lib/I18n/translations/hebrew.yaml +++ b/lib/I18n/translations/hebrew.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "סמן" STR_IMAGES_SUPPRESS: "הסתר תמונות" STR_SHORT_PWR_BTN: "לחיצה קצרה" STR_ORIENTATION: "כיוון קריאה (מסך)" +STR_ROTATE_CCW: "סיבוב הדף CCW" +STR_ROTATE_CW: "סיבוב הדף CW" +STR_ROTATE_FLIP: "סיבוב הדף 180°" STR_SIDE_BTN_LAYOUT: "פריסת כפתורי צד (בקריאה)" STR_TOUCH_READER_CONTROLS: "שליטה במגע (קורא)" STR_PINCH_FONT_RESIZE: "גודל גופן בצביטה" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 7b8ee7dd0f6..9de0bf8dfb4 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -112,6 +112,9 @@ STR_IMAGES_PLACEHOLDER: "Helyőrző" STR_IMAGES_SUPPRESS: "Elnyomás" STR_SHORT_PWR_BTN: "Rövid nyomás" STR_ORIENTATION: "Olvasási irány" +STR_ROTATE_CCW: "Oldal forgatása CCW" +STR_ROTATE_CW: "Oldal forgatása CW" +STR_ROTATE_FLIP: "Oldal forgatása 180°" STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)" STR_ORIENTATION_AWARE: "Tájolásfüggő" STR_NAV_BUTTONS: "Navigációs gombok" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 4ef5365fef9..2ba61f8677a 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Segnaposto" STR_IMAGES_SUPPRESS: "Nascondi" STR_SHORT_PWR_BTN: "Pressione breve" STR_ORIENTATION: "Orientamento lettura" +STR_ROTATE_CCW: "Ruota pagina CCW" +STR_ROTATE_CW: "Ruota pagina CW" +STR_ROTATE_FLIP: "Ruota pagina 180°" STR_SIDE_BTN_LAYOUT: "Pulsanti laterali (lettore)" STR_ORIENTATION_AWARE: "In base all'orientamento" STR_NAV_BUTTONS: "Pulsanti navigazione" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 7e9629eaed8..7cf65a511d5 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -105,6 +105,9 @@ STR_STATS_EVENING_READER: "Кешкі оқырман" STR_STATS_NIGHT_READER: "Түнгі оқырман" STR_SHORT_PWR_BTN: "Қысқа басу" STR_ORIENTATION: "Оқу бағдары" +STR_ROTATE_CCW: "Бетті бұру CCW" +STR_ROTATE_CW: "Бетті бұру CW" +STR_ROTATE_FLIP: "Бетті бұру 180°" STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)" STR_ORIENTATION_AWARE: "Бағдарға қарай" STR_NAV_BUTTONS: "Навигация түймелері" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index c6abdb77cbc..84894557895 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Vietaženklis" STR_IMAGES_SUPPRESS: "Slėpti" STR_SHORT_PWR_BTN: "Trumpas paspaudimas" STR_ORIENTATION: "Orientacija" +STR_ROTATE_CCW: "Pasukti puslapį CCW" +STR_ROTATE_CW: "Pasukti puslapį CW" +STR_ROTATE_FLIP: "Pasukti puslapį 180°" STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai" STR_ORIENTATION_AWARE: "Pagal orientaciją" STR_NAV_BUTTONS: "Navigacijos mygtukai" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 5763f149e14..a89ba015b3c 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Ramki" STR_IMAGES_SUPPRESS: "Pomijaj" STR_SHORT_PWR_BTN: "Krótkie naciśnięcie" STR_ORIENTATION: "Układ czytania" +STR_ROTATE_CCW: "Obróć stronę CCW" +STR_ROTATE_CW: "Obróć stronę CW" +STR_ROTATE_FLIP: "Obróć stronę 180°" STR_SIDE_BTN_LAYOUT: "Układ przycisków bocznych" STR_ORIENTATION_AWARE: "Zależnie od orientacji" STR_NAV_BUTTONS: "Przyciski nawigacji" diff --git a/lib/I18n/translations/portuguese-BR.yaml b/lib/I18n/translations/portuguese-BR.yaml index 8fd3bbf02cc..876ee63ffc7 100644 --- a/lib/I18n/translations/portuguese-BR.yaml +++ b/lib/I18n/translations/portuguese-BR.yaml @@ -114,6 +114,9 @@ STR_EOB_HOME: "Início" STR_EOB_CONTINUE_WITH: "Continuar com" STR_SHORT_PWR_BTN: "Toque curto" STR_ORIENTATION: "Orientação" +STR_ROTATE_CCW: "Girar página CCW" +STR_ROTATE_CW: "Girar página CW" +STR_ROTATE_FLIP: "Girar página 180°" STR_SIDE_BTN_LAYOUT: "Layout" STR_ORIENTATION_AWARE: "Sensível à orientação" STR_NAV_BUTTONS: "Botões de navegação" diff --git a/lib/I18n/translations/portuguese-PT.yaml b/lib/I18n/translations/portuguese-PT.yaml index 9bb895a2a7d..edbb666864a 100644 --- a/lib/I18n/translations/portuguese-PT.yaml +++ b/lib/I18n/translations/portuguese-PT.yaml @@ -165,6 +165,9 @@ STR_EOB_HOME: "Início" STR_EOB_CONTINUE_WITH: "Continuar com" STR_SHORT_PWR_BTN: "Toque breve" STR_ORIENTATION: "Orientação" +STR_ROTATE_CCW: "Rodar página CCW" +STR_ROTATE_CW: "Rodar página CW" +STR_ROTATE_FLIP: "Rodar página 180°" STR_SIDE_BTN_LAYOUT: "Esquema" STR_ORIENTATION_AWARE: "Sensível à orientação" STR_NAV_BUTTONS: "Botões de navegação" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index eb517f6ec28..88a32ff4833 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Substituent" STR_IMAGES_SUPPRESS: "Suprimare" STR_SHORT_PWR_BTN: "Apăsare scurtă" STR_ORIENTATION: "Orientare lectură" +STR_ROTATE_CCW: "Rotește pagina CCW" +STR_ROTATE_CW: "Rotește pagina CW" +STR_ROTATE_FLIP: "Rotește pagina 180°" STR_SIDE_BTN_LAYOUT: "Aspect butoane laterale (lectură)" STR_ORIENTATION_AWARE: "În funcție de orientare" STR_NAV_BUTTONS: "Butoane navigare" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index e6ac54eb909..5a79ee9cab2 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Заглушки" STR_IMAGES_SUPPRESS: "Скрыть" STR_SHORT_PWR_BTN: "Короткое нажатие" STR_ORIENTATION: "Ориентация чтения" +STR_ROTATE_CCW: "Повернуть страницу CCW" +STR_ROTATE_CW: "Повернуть страницу CW" +STR_ROTATE_FLIP: "Повернуть страницу 180°" STR_SIDE_BTN_LAYOUT: "Боковые кнопки" STR_ORIENTATION_AWARE: "С учетом ориентации" STR_NAV_BUTTONS: "Кнопки навигации" diff --git a/lib/I18n/translations/slovak.yaml b/lib/I18n/translations/slovak.yaml index d9236868638..bc8453a1992 100644 --- a/lib/I18n/translations/slovak.yaml +++ b/lib/I18n/translations/slovak.yaml @@ -111,6 +111,9 @@ STR_IMAGES_PLACEHOLDER: "Rezervované miesto" STR_IMAGES_SUPPRESS: "Potlačiť" STR_SHORT_PWR_BTN: "Krátke stlačenie" STR_ORIENTATION: "Orientácia čítania" +STR_ROTATE_CCW: "Otočiť stránku CCW" +STR_ROTATE_CW: "Otočiť stránku CW" +STR_ROTATE_FLIP: "Otočiť stránku 180°" STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)" STR_ORIENTATION_AWARE: "Podľa orientácie" STR_NAV_BUTTONS: "Navigačné tlačidlá" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index f11ff5569bb..c77d23c0e2c 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Oznaka mesta" STR_IMAGES_SUPPRESS: "Zatdi" STR_SHORT_PWR_BTN: "Kratek pritisk" STR_ORIENTATION: "Orientacija branja" +STR_ROTATE_CCW: "Zavrti stran CCW" +STR_ROTATE_CW: "Zavrti stran CW" +STR_ROTATE_FLIP: "Zavrti stran 180°" STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov" STR_ORIENTATION_AWARE: "Glede na usmerjenost" STR_NAV_BUTTONS: "Navigacijski gumbi" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index c29c5bda3be..5300b253c72 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -114,6 +114,9 @@ STR_EOB_HOME: "Inicio" STR_EOB_CONTINUE_WITH: "Continuar con" STR_SHORT_PWR_BTN: "Pulsación corta" STR_ORIENTATION: "Orientación" +STR_ROTATE_CCW: "Girar página CCW" +STR_ROTATE_CW: "Girar página CW" +STR_ROTATE_FLIP: "Girar página 180°" STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)" STR_ORIENTATION_AWARE: "Según orientación" STR_NAV_BUTTONS: "Botones navegación" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 28e00a1dcd0..ec666948f1f 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Platshållare" STR_IMAGES_SUPPRESS: "Dölj" STR_SHORT_PWR_BTN: "Kort tryck" STR_ORIENTATION: "Läsrikting" +STR_ROTATE_CCW: "Rotera sidan CCW" +STR_ROTATE_CW: "Rotera sidan CW" +STR_ROTATE_FLIP: "Rotera sidan 180°" STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)" STR_ORIENTATION_AWARE: "Orienteringsanpassad" STR_NAV_BUTTONS: "Navigeringsknappar" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 7f885ce2b60..dd57ae42984 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -105,6 +105,9 @@ STR_STATS_EVENING_READER: "Akşam okuyucusu" STR_STATS_NIGHT_READER: "Gece okuyucusu" STR_SHORT_PWR_BTN: "Kısa Basma" STR_ORIENTATION: "Okuma Yönü" +STR_ROTATE_CCW: "Sayfayı döndür CCW" +STR_ROTATE_CW: "Sayfayı döndür CW" +STR_ROTATE_FLIP: "Sayfayı döndür 180°" STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)" STR_ORIENTATION_AWARE: "Yöne duyarlı" STR_NAV_BUTTONS: "Gezinme tuşları" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 2aedb369f5f..640c539fb94 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Заглушка" STR_IMAGES_SUPPRESS: "Приховати" STR_SHORT_PWR_BTN: "Коротке натискання" STR_ORIENTATION: "Орієнтація читання" +STR_ROTATE_CCW: "Повернути сторінку CCW" +STR_ROTATE_CW: "Повернути сторінку CW" +STR_ROTATE_FLIP: "Повернути сторінку 180°" STR_SIDE_BTN_LAYOUT: "Схема бічних кнопок" STR_ORIENTATION_AWARE: "З урахуванням орієнтації" STR_NAV_BUTTONS: "Кнопки навігації" diff --git a/lib/I18n/translations/valencian.yaml b/lib/I18n/translations/valencian.yaml index 6d2dbeac24d..b0d1a48d400 100644 --- a/lib/I18n/translations/valencian.yaml +++ b/lib/I18n/translations/valencian.yaml @@ -110,6 +110,9 @@ STR_IMAGES_PLACEHOLDER: "Text de mostra" STR_IMAGES_SUPPRESS: "Eliminar" STR_SHORT_PWR_BTN: "Clic curt" STR_ORIENTATION: "Orientació de lectura" +STR_ROTATE_CCW: "Gira la pàgina CCW" +STR_ROTATE_CW: "Gira la pàgina CW" +STR_ROTATE_FLIP: "Gira la pàgina 180°" STR_SIDE_BTN_LAYOUT: "Disposició botons laterals" STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector" STR_PINCH_FONT_RESIZE: "Mida lletra amb pessic" diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index ac63b295bd8..0e46424a52d 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -111,6 +111,9 @@ STR_IMAGES_PLACEHOLDER: "Giữ chỗ" STR_IMAGES_SUPPRESS: "Ẩn" STR_SHORT_PWR_BTN: "Nhấn ngắn" STR_ORIENTATION: "Hướng đọc" +STR_ROTATE_CCW: "Xoay trang CCW" +STR_ROTATE_CW: "Xoay trang CW" +STR_ROTATE_FLIP: "Xoay trang 180°" STR_SIDE_BTN_LAYOUT: "Bố cục" STR_ORIENTATION_AWARE: "Theo hướng xoay" STR_NAV_BUTTONS: "Nút điều hướng" diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index ada3c3b8fb5..931bd083a84 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -22,6 +22,7 @@ #include #include "KOReaderCredentialStore.h" +#include "Memory.h" #if !defined(SIMULATOR) && defined(FREEINK_WOLFSSL_DEBUG) // With FREEINK_WOLFSSL_DEBUG, wolfSSL is built with DEBUG_WOLFSSL, whose Arduino @@ -123,6 +124,9 @@ KOReaderSyncClient::Error validateAuthResponse(const char* body) { // floors for total free heap and the largest contiguous block. constexpr uint32_t MIN_FREE_HEAP_FOR_TLS = 35000; constexpr uint32_t MIN_MAX_ALLOC_HEAP_FOR_TLS = 20000; +// Authentication returns a small JSON object. Cap unexpected HTML/error pages +// before they can exhaust the C3 heap while the TLS connection is still open. +constexpr size_t MAX_AUTH_RESPONSE_BYTES = 4096; #ifdef SIMULATOR void addAuthHeaders(HTTPClient& http) { @@ -192,6 +196,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { LOG_DBG("KOSync", "Auth response: %d", httpCode); if (httpCode == 200) { + if (http.getSize() > static_cast(MAX_AUTH_RESPONSE_BYTES)) { + LOG_ERR("KOSync", "Auth response exceeded %u bytes (HTTP %d)", static_cast(MAX_AUTH_RESPONSE_BYTES), + httpCode); + http.end(); + return INVALID_AUTH_RESPONSE; + } String responseBody = http.getString(); http.end(); return validateAuthResponse(responseBody.c_str()); @@ -212,7 +222,29 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { return NETWORK_ERROR; } applyAuthHeaders(http); - const int httpCode = http.GET(); + std::unique_ptr responseBody; + size_t responseSize = 0; + bool responseTooLarge = false; + bool responseOutOfMemory = false; + const int httpCode = http.GET([&](const uint8_t* data, size_t len) { + // Only HTTP 200 needs its JSON body; other statuses are handled below. + if (http.getStatus() != 200) return true; + if (len > MAX_AUTH_RESPONSE_BYTES - responseSize) { + responseTooLarge = true; + return false; + } + if (!responseBody) { + responseBody = makeUniqueNoThrow(MAX_AUTH_RESPONSE_BYTES + 1); + if (!responseBody) { + responseOutOfMemory = true; + return false; + } + } + std::memcpy(responseBody.get() + responseSize, data, len); + responseSize += len; + responseBody[responseSize] = '\0'; + return true; + }); lastHttpCode = httpCode; lastTransportError = (httpCode < 0) ? httpCode : 0; @@ -222,8 +254,24 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { http.end(); return NETWORK_ERROR; } + if (responseTooLarge) { + LOG_ERR("KOSync", "Auth response exceeded %u bytes (HTTP %d)", static_cast(MAX_AUTH_RESPONSE_BYTES), + httpCode); + http.end(); + return INVALID_AUTH_RESPONSE; + } + if (responseOutOfMemory) { + LOG_ERR("KOSync", "Not enough memory for auth response (HTTP %d)", httpCode); + http.end(); + return LOW_MEMORY; + } if (httpCode == 200) { - const Error result = validateAuthResponse(http.getString().c_str()); + if (!http.responseComplete()) { + LOG_ERR("KOSync", "Auth response incomplete"); + http.end(); + return NETWORK_ERROR; + } + const Error result = validateAuthResponse(responseBody ? responseBody.get() : ""); http.end(); return result; } diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index aa73fc3e912..28bbc311dbf 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -186,6 +186,7 @@ class CrossPointSettings : public PersistableStore { SIDE_DECREASE_FONT, SIDE_ROTATE_COUNTERCLOCKWISE, SIDE_ROTATE_CLOCKWISE, + SIDE_ROTATE_FLIP, }; // Font family options (built-in fonts only; SD card fonts use sdFontFamilyName) diff --git a/src/SettingsList.h b/src/SettingsList.h index 065ea50ddef..96d5d0b5138 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -92,9 +92,10 @@ inline std::string sideButtonOptionLabel(const SettingInfo& setting, const uint8 if (isSideButtonActionSetting(setting) && displayIndex < setting.enumRawValues.size()) { const uint8_t action = setting.enumRawValues[displayIndex]; if (action == CrossPointSettings::SIDE_ROTATE_COUNTERCLOCKWISE || - action == CrossPointSettings::SIDE_ROTATE_CLOCKWISE) { - return std::string(tr(STR_ORIENTATION)) + " (" + - (action == CrossPointSettings::SIDE_ROTATE_CLOCKWISE ? tr(STR_DIR_RIGHT) : tr(STR_DIR_LEFT)) + ")"; + action == CrossPointSettings::SIDE_ROTATE_CLOCKWISE || action == CrossPointSettings::SIDE_ROTATE_FLIP) { + if (action == CrossPointSettings::SIDE_ROTATE_CLOCKWISE) return tr(STR_ROTATE_CW); + if (action == CrossPointSettings::SIDE_ROTATE_FLIP) return tr(STR_ROTATE_FLIP); + return tr(STR_ROTATE_CCW); } } return settingEnumOptionLabel(setting, displayIndex); @@ -563,6 +564,7 @@ inline SettingInfo buildSideButtonActionSetting(const StrId nameId, uint8_t Cros {CrossPointSettings::SIDE_DECREASE_FONT, StrId::STR_DECREASE_FONT_SIZE}, {CrossPointSettings::SIDE_ROTATE_COUNTERCLOCKWISE, StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION}, {CrossPointSettings::SIDE_ROTATE_CLOCKWISE, StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION}, + {CrossPointSettings::SIDE_ROTATE_FLIP, StrId::STR_LONG_PRESS_BEHAVIOR_ORIENTATION}, }; for (const auto& [action, label] : readerActions) { setting.enumValues.push_back(label); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 7efafaf79fe..f04a1349398 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -707,6 +707,7 @@ void HomeActivity::loadCoverGridThumbnails() { for (size_t i = 0; i < recentBooks.size(); ++i) { auto& book = recentBooks[i]; if (book.coverState == RecentBook::CoverState::Missing || !Storage.exists(book.path.c_str())) continue; + const int width = coverGridUi->thumbWidthFor(i); const int height = coverGridUi->thumbHeightFor(i); if (book.coverBmpPath.empty()) { if (FsHelpers::hasEpubExtension(book.path)) { @@ -717,8 +718,16 @@ void HomeActivity::loadCoverGridThumbnails() { if (xtc) book.coverBmpPath = xtc->getThumbBmpPath(); } } - const std::string thumbPath = UITheme::getCoverThumbPath(book.coverBmpPath, height); - if (thumbPath.empty() || Storage.exists(thumbPath.c_str())) continue; + const std::string thumbPath = UITheme::getCoverThumbPath(book.coverBmpPath, width, height, false); + if (thumbPath.empty()) continue; + FsFile thumbFile; + if (Storage.exists(thumbPath.c_str()) && Storage.openFileForRead("HOME", thumbPath, thumbFile)) { + Bitmap thumb(thumbFile); + const bool matches = + thumb.parseHeaders() == BmpReaderError::Ok && thumb.getWidth() == width && thumb.getHeight() == height; + thumbFile.close(); + if (matches) continue; + } if (!showingLoading) { showingLoading = true; popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); @@ -731,7 +740,7 @@ void HomeActivity::loadCoverGridThumbnails() { LOG_ERR("HOME", "Cannot allocate EPUB for cover grid thumbnail"); continue; } - if (!epub->generateThumbBmpFromSource(height, &renderer, SETTINGS.getReaderFontId())) { + if (!epub->generateThumbBmpFromSource(width, height, &renderer, SETTINGS.getReaderFontId())) { LOG_ERR("HOME", "Cannot create cover grid thumbnail: %s", book.path.c_str()); } } else if (FsHelpers::hasXtcExtension(book.path)) { @@ -740,7 +749,7 @@ void HomeActivity::loadCoverGridThumbnails() { LOG_ERR("HOME", "Cannot allocate XTC for cover grid thumbnail"); continue; } - if (xtc->load()) xtc->generateThumbBmp(height); + if (xtc->load()) xtc->generateThumbBmp(static_cast(width), static_cast(height)); } } recentsLoaded = true; diff --git a/src/activities/library/LibraryActivity.cpp b/src/activities/library/LibraryActivity.cpp index 84e9b4645a2..9fe8675fcb8 100644 --- a/src/activities/library/LibraryActivity.cpp +++ b/src/activities/library/LibraryActivity.cpp @@ -662,6 +662,10 @@ void LibraryActivity::loop() { pending = {}; RenderLock lock; actionPopup.handleInput(mappedInput, [this] { requestUpdate(); }); + // OptionPopup selects on press. Its matching release belongs to the popup, + // even when choosing a sort option keeps us in the Library activity. + if (!actionPopup.isActive() && mappedInput.isPressed(MappedInputManager::Button::Confirm)) + ignoreConfirmRelease = true; return; } latchInput(); @@ -674,6 +678,9 @@ void LibraryActivity::loop() { requestUpdate(); } if (ignoreConfirmRelease || longPressFired) { + // A popup may have suppressed this release. Consume that suppression while + // ignoring the opening press so it cannot affect the next Select. + (void)mappedInput.wasReleased(MappedInputManager::Button::Confirm); if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) { ignoreConfirmRelease = false; longPressFired = false; diff --git a/src/activities/library/LibrarySettingsActivity.cpp b/src/activities/library/LibrarySettingsActivity.cpp index 7ad9f0118e5..400e6ee46bb 100644 --- a/src/activities/library/LibrarySettingsActivity.cpp +++ b/src/activities/library/LibrarySettingsActivity.cpp @@ -87,6 +87,9 @@ void LibrarySettingsActivity::loop() { return; } if (ignoreConfirmRelease) { + // Consume the menu selection's release even when this screen ignores it. + // Otherwise its one-shot suppression can swallow the next quick Select. + (void)mappedInput.wasReleased(MappedInputManager::Button::Confirm); if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) ignoreConfirmRelease = false; return; } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 174cbc18bfa..cb5de226bf5 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -3396,6 +3396,10 @@ void EpubReaderActivity::loop() { side.action == CrossPointSettings::SIDE_ROTATE_CLOCKWISE)); requestUpdate(); break; + case CrossPointSettings::SIDE_ROTATE_FLIP: + applyOrientation(ReaderUtils::flippedOrientation(SETTINGS.orientation)); + requestUpdate(); + break; case CrossPointSettings::IGNORE: break; default: { diff --git a/src/activities/reader/EpubReaderDrawerActivity.cpp b/src/activities/reader/EpubReaderDrawerActivity.cpp index cc42ca0e29a..297f16dde53 100644 --- a/src/activities/reader/EpubReaderDrawerActivity.cpp +++ b/src/activities/reader/EpubReaderDrawerActivity.cpp @@ -2426,17 +2426,23 @@ void EpubReaderDrawerActivity::renderPreviewContents(const ReaderSettingsDraft& if (CROSSINK_APP_READER_SAMPLE_PREVIEW) { const auto& metrics = UITheme::getInstance().getMetrics(); const int labelTextHeight = renderer.getTextHeight(UI_10_FONT_ID); + const int noteHeight = previewSettings.textAntiAliasing ? labelTextHeight + 2 : 0; const char* name = previewSettings.sdFontFamilyName[0] ? previewSettings.sdFontFamilyName.data() : (previewSettings.fontFamily == 0 ? tr(STR_LEXEND_DECA) : tr(STR_BITTER)); char label[128]; std::snprintf(label, sizeof(label), "%s \"%s\", %upt", tr(STR_PREVIEW), name, previewSettings.readerFontPointSize); - const int separatorY = preview.bottom() - metrics.previewPadding - labelTextHeight - 4; + const int labelY = preview.bottom() - metrics.previewPadding - labelTextHeight - noteHeight; + const int separatorY = labelY - 4; renderer.drawLine(preview.x, separatorY, preview.right() - 1, separatorY, ReaderUtils::readerForegroundBlack()); renderer.beginTextClip(preview.x, preview.y, preview.width, preview.height); - renderer.drawText(UI_10_FONT_ID, preview.x + metrics.previewPadding, - preview.bottom() - metrics.previewPadding - labelTextHeight, label, + renderer.drawText(UI_10_FONT_ID, preview.x + metrics.previewPadding, labelY, label, ReaderUtils::readerForegroundBlack()); + if (previewSettings.textAntiAliasing) { + std::snprintf(label, sizeof(label), "%s: %s", tr(STR_TEXT_AA), tr(STR_PREVIEW_UNAVAILABLE)); + renderer.drawText(SMALL_FONT_ID, preview.x + metrics.previewPadding, labelY + labelTextHeight + 6, label, + ReaderUtils::readerForegroundBlack()); + } renderer.endTextClip(); renderer.drawLine(preview.x, preview.bottom() - 1, preview.right() - 1, preview.bottom() - 1, ReaderUtils::readerForegroundBlack()); @@ -2492,7 +2498,9 @@ void EpubReaderDrawerActivity::renderSamplePreviewText(const ReaderSettingsDraft if (!previewModel || !previewModel->valid()) return; const auto& metrics = UITheme::getInstance().getMetrics(); const fui::Rect area = previewBounds(); - const int labelHeight = renderer.getTextHeight(UI_10_FONT_ID) + metrics.previewPadding + 8; + const int labelTextHeight = renderer.getTextHeight(UI_10_FONT_ID); + const int labelHeight = + labelTextHeight + (settings.textAntiAliasing ? labelTextHeight + 2 : 0) + metrics.previewPadding + 8; const int textHeight = std::max(0, area.height - labelHeight - metrics.previewPadding); // The sample is a short page: show top AND bottom margins proportionally // to its height, while horizontal margins and font sizes remain actual pixels. @@ -2813,7 +2821,7 @@ void EpubReaderDrawerActivity::render(RenderLock&&) { } previousDrawerEdge = drawerEdge; int previewFontId = -1; - // Keep prewarmed glyphs resident through the BW and optional grayscale passes. + // Keep prewarmed glyphs resident through the BW and optional touch grayscale passes. std::optional previewPrewarmScope; bool previewRendered = !CROSSINK_APP_READER_SAMPLE_PREVIEW && renderPreview(previewFontId, previewPrewarmScope); uiReady = false; @@ -2867,7 +2875,10 @@ void EpubReaderDrawerActivity::render(RenderLock&&) { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4, true); } renderer.displayBuffer(HalDisplay::FAST_REFRESH); - if (shouldRenderReaderDrawerAntiAliasing(previewRendered, draft.textAntiAliasing, + // Button menus repaint the sample on every navigation step. A grayscale pass + // here would add a second panel refresh and flash the preview each time. + if (!CROSSINK_APP_READER_SAMPLE_PREVIEW && + shouldRenderReaderDrawerAntiAliasing(previewRendered, draft.textAntiAliasing, ReaderUtils::readerForegroundBlack()) && !sdFontSystem.fontUsesMonochromeRaster(renderer, previewFontId, draft.sdFontFamilyName.data())) { renderPreviewWithAntiAliasing(previewFontId); diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 8f7027b7f89..71419cb3467 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -102,6 +102,10 @@ inline uint8_t rotatedOrientation(const uint8_t orientation, const bool clockwis : (orientation + CrossPointSettings::ORIENTATION_COUNT - 1) % CrossPointSettings::ORIENTATION_COUNT; } +inline uint8_t flippedOrientation(const uint8_t orientation) { + return (orientation + 2) % CrossPointSettings::ORIENTATION_COUNT; +} + struct PageTurnResult { bool prev; bool next; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index ac86a6c1a5f..7ffb29d8233 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -300,6 +300,9 @@ void TxtReaderActivity::loop() { case CrossPointSettings::SIDE_ROTATE_CLOCKWISE: handleTwoFingerRotation(side.action == CrossPointSettings::SIDE_ROTATE_CLOCKWISE); break; + case CrossPointSettings::SIDE_ROTATE_FLIP: + applyReaderOrientation(ReaderUtils::flippedOrientation(SETTINGS.orientation)); + break; case CrossPointSettings::SIDE_PREVIOUS_CHAPTER: case CrossPointSettings::SIDE_NEXT_CHAPTER: case CrossPointSettings::IGNORE: @@ -439,7 +442,11 @@ bool TxtReaderActivity::handleTwoFingerSwipeAction(const CrossPointSettings::TWO } bool TxtReaderActivity::handleTwoFingerRotation(const bool clockwise) { - SETTINGS.orientation = ReaderUtils::rotatedOrientation(SETTINGS.orientation, clockwise); + return applyReaderOrientation(ReaderUtils::rotatedOrientation(SETTINGS.orientation, clockwise)); +} + +bool TxtReaderActivity::applyReaderOrientation(const uint8_t orientation) { + SETTINGS.orientation = orientation; SETTINGS.saveToFile(); { RenderLock lock(*this); diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 34e2296435e..61d84c2df8f 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -77,6 +77,7 @@ class TxtReaderActivity final : public Activity { void cycleReaderFont(); void rebuildTextLayout(); void openReaderMenu(); + bool applyReaderOrientation(uint8_t orientation); #if CROSSINK_APP_CAP_TOUCH bool handlePinchFontResize(); void resetPinchFontGesture(); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 6ac2c8c9893..0a249e1635b 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -422,6 +422,7 @@ void XtcReaderActivity::loop() { case CrossPointSettings::SIDE_DECREASE_FONT: case CrossPointSettings::SIDE_ROTATE_COUNTERCLOCKWISE: case CrossPointSettings::SIDE_ROTATE_CLOCKWISE: + case CrossPointSettings::SIDE_ROTATE_FLIP: case CrossPointSettings::IGNORE: // XTC pages are pre-rendered; font size and rotation do not apply. break; diff --git a/src/components/CoverGridHomeUi.cpp b/src/components/CoverGridHomeUi.cpp index 6d02c346f54..f0b6e4440be 100644 --- a/src/components/CoverGridHomeUi.cpp +++ b/src/components/CoverGridHomeUi.cpp @@ -41,9 +41,14 @@ void CoverGridHomeUi::refreshCoverPaths() { void CoverGridHomeUi::refreshCoverPath(size_t index) { if (index >= books->size() || index >= coverPaths.size()) return; coverCache.invalidate(index); - coverPaths[index] = thumbHeights[index] > 0 - ? UITheme::getCoverThumbPath((*books)[index].coverBmpPath, thumbHeights[index]) - : std::string(); + coverPaths[index] = + thumbWidths[index] > 0 && thumbHeights[index] > 0 + ? UITheme::getCoverThumbPath((*books)[index].coverBmpPath, thumbWidths[index], thumbHeights[index], false) + : std::string(); +} + +int CoverGridHomeUi::thumbWidthFor(size_t index) const { + return index < thumbWidths.size() && thumbWidths[index] > 0 ? thumbWidths[index] : THUMB_HEIGHT * 2 / 3; } int CoverGridHomeUi::thumbHeightFor(size_t index) const { @@ -52,12 +57,12 @@ int CoverGridHomeUi::thumbHeightFor(size_t index) const { bool CoverGridHomeUi::takeThumbHeightsChanged() { return std::exchange(thumbHeightsChanged, false); } -void CoverGridHomeUi::noteThumbHeight(size_t index, int slotWidth, int slotHeight) { +void CoverGridHomeUi::noteThumbSize(size_t index, int slotWidth, int slotHeight) { if (index >= thumbHeights.size()) return; - // Thumbs cover a (0.6*h, h) target box, so a height of max(h, w*5/3) makes - // every cover overfill the slot; the cover renderer crops the overflow (full bleed). - const int height = std::max({1, slotHeight, slotWidth * 5 / 3 + 2}); - if (thumbHeights[index] != height) { + const int width = std::max(1, slotWidth); + const int height = std::max(1, slotHeight); + if (thumbWidths[index] != width || thumbHeights[index] != height) { + thumbWidths[index] = width; thumbHeights[index] = height; thumbHeightsChanged = true; refreshCoverPath(index); @@ -170,7 +175,7 @@ void CoverGridHomeUi::drawCurrent(UiScreen& screen, fui::Rect rect, const int co const int maxCoverWidth = rect.width / GRID_COLUMNS - 2 * COVER_CELL_INSET; card.coverSize.height = std::max(1, std::min(std::min(rect.height, coverRowHeight) - 12, maxCoverWidth * 3 / 2)); card.coverSize.width = std::max(1, card.coverSize.height * 2 / 3); - noteThumbHeight(0, card.coverSize.width, card.coverSize.height); + noteThumbSize(0, card.coverSize.width, card.coverSize.height); gridBounds = layoutGrid(screen.body()); rect.x = gridBounds.x; rect.width = gridBounds.width; @@ -210,7 +215,7 @@ void CoverGridHomeUi::drawGrid(UiScreen& screen) { grid.cellStyles = card.styles; grid.labelHeight = 0; grid.labelGap = 0; - for (size_t i = 1; i < thumbHeights.size(); ++i) noteThumbHeight(i, grid.coverSize.width, grid.coverSize.height); + for (size_t i = 1; i < thumbHeights.size(); ++i) noteThumbSize(i, grid.coverSize.width, grid.coverSize.height); grid.scrollIndicator = false; grid.itemProvider = [](uint16_t index, void*) { return fui::coverGridItem(nullptr, index + 1); }; grid.coverPainterUserData = this; diff --git a/src/components/CoverGridHomeUi.h b/src/components/CoverGridHomeUi.h index f246a03e61e..06d0799dfce 100644 --- a/src/components/CoverGridHomeUi.h +++ b/src/components/CoverGridHomeUi.h @@ -24,8 +24,9 @@ class CoverGridHomeUi final : public UiAppHost<16, 1> { void refreshCoverPaths(); void setSelection(int selection) { selected = selection; } int selectedAction(const MappedInputManager& input); - // Exact generation height for a slot, recorded during draw. Thumbs must be - // generated at the drawn size: rescaling a dithered 1-bit image aliases badly. + // Exact generation size for a slot, recorded during draw. Rescaling a + // dithered 1-bit image aliases badly. + int thumbWidthFor(size_t index) const; int thumbHeightFor(size_t index) const; bool takeThumbHeightsChanged(); @@ -41,12 +42,13 @@ class CoverGridHomeUi final : public UiAppHost<16, 1> { void drawTabs(UiScreen& screen, freeink::ui::Rect rect); bool paintFramedCover(freeink::ui::DrawTarget& target, freeink::ui::Rect rect, size_t index); void refreshCoverPath(size_t index); - void noteThumbHeight(size_t index, int slotWidth, int slotHeight); + void noteThumbSize(size_t index, int slotWidth, int slotHeight); HomeCoverCache coverCache; GfxRenderer& renderer; const std::vector* books = nullptr; std::array coverPaths; + std::array thumbWidths{}; std::array thumbHeights{}; bool thumbHeightsChanged = false; int selected = 0; diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index dcaf88aa969..cc0b4a13a01 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -218,7 +218,8 @@ std::string UITheme::getCoverThumbPath(const std::string& coverBmpPath, int cove return getCoverThumbPath(coverBmpPath, coverWidth, coverHeight); } -std::string UITheme::getCoverThumbPath(const std::string& coverBmpPath, int width, int height) { +std::string UITheme::getCoverThumbPath(const std::string& coverBmpPath, int width, int height, + bool allowLegacyFallback) { if (width <= 0 || height <= 0) { return ""; } @@ -253,7 +254,7 @@ std::string UITheme::getCoverThumbPath(const std::string& coverBmpPath, int widt std::string legacyPath = thumbPath; legacyPath.replace(pos, kHeightPlaceholderLength, std::to_string(height)); thumbPath.replace(pos, kHeightPlaceholderLength, std::to_string(width) + "x" + std::to_string(height)); - if (!Storage.exists(thumbPath.c_str()) && Storage.exists(legacyPath.c_str())) { + if (allowLegacyFallback && !Storage.exists(thumbPath.c_str()) && Storage.exists(legacyPath.c_str())) { return legacyPath; } } diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 29eee353f8e..aa6e779ec59 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -47,7 +47,8 @@ class UITheme { // - a legacy height-only template containing one [HEIGHT] placeholder. // No scaling is done here. Returns an empty string for invalid dimensions or // unsupported placeholder templates. - static std::string getCoverThumbPath(const std::string& coverBmpPath, int width, int height); + static std::string getCoverThumbPath(const std::string& coverBmpPath, int width, int height, + bool allowLegacyFallback = true); static UIIcon getFileIcon(const std::string& filename); static int getStatusBarHeight(); static int getProgressBarHeight(); diff --git a/src/simulator/SimulatorSmokeTest.cpp b/src/simulator/SimulatorSmokeTest.cpp index 6c16bdb82a9..d0c859103da 100644 --- a/src/simulator/SimulatorSmokeTest.cpp +++ b/src/simulator/SimulatorSmokeTest.cpp @@ -470,6 +470,27 @@ class SimulatorSmokeTest { verifyLibraryChoice("longPwrBtn", ShortcutOptionCatalog::PowerButton); for (const char* key : {"sideButtonUpShort", "sideButtonUpLong", "sideButtonDownShort", "sideButtonDownLong"}) { verifyLibraryChoice(key, ShortcutOptionCatalog::SideButton); + const auto setting = std::find_if(allSettings.begin(), allSettings.end(), + [key](const SettingInfo& candidate) { return settingKeyIs(candidate, key); }); + for (const auto [action, suffix] : + {std::pair{CrossPointSettings::SIDE_ROTATE_COUNTERCLOCKWISE, StrId::STR_ROTATE_CCW}, + std::pair{CrossPointSettings::SIDE_ROTATE_CLOCKWISE, StrId::STR_ROTATE_CW}, + std::pair{CrossPointSettings::SIDE_ROTATE_FLIP, StrId::STR_ROTATE_FLIP}}) { + const auto choice = std::find(setting->enumRawValues.begin(), setting->enumRawValues.end(), action); + const std::string expected = I18N.get(suffix); + if (choice == setting->enumRawValues.end() || + sideButtonOptionLabel(*setting, static_cast(choice - setting->enumRawValues.begin())) != + expected) { + fail("Orientation shortcut is missing or mislabeled in %s", key); + } + } + } + for (uint8_t orientation = 0; orientation < CrossPointSettings::ORIENTATION_COUNT; ++orientation) { + if (ReaderUtils::flippedOrientation(ReaderUtils::flippedOrientation(orientation)) != orientation || + ReaderUtils::flippedOrientation(orientation) != + ReaderUtils::rotatedOrientation(ReaderUtils::rotatedOrientation(orientation, true), true)) { + fail("Flip must turn the screen 180 degrees from every orientation"); + } } verifyLibraryChoice("powerChordAction", ShortcutOptionCatalog::ButtonChord); verifyLibraryChoice("longPressMenuAction", ShortcutOptionCatalog::LongPress);