diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 5db05cae..51e284c5 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -122,19 +122,43 @@ A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin. -1. Install the plugin in Calibre: - - - Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin. - - - Download the zip file. - - - Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file. +#### Installing the Plugin in Calibre -2. On the device: File Transfer -> Calibre Wireless, then join a network. +If you don't already have the plugin installed: -3. Make sure your computer is on the same Wi-Fi network. +1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin. +2. Download the zip file. +3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file. +4. Restart Calibre. -4. In Calibre, click "Send to device" to transfer books. +#### Configuring the CrossPoint Plugin in Calibre +1. In Calibre select Preferences. +2. In the Preferences dialog select Plugins. +3. In Plugins search for "crosspoint". +4. Click on "Customize plugin". +5. Update the value for "Host" to match the IP for your device. +6. Leave the other settings as they are. +7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks` +8. Restart Calibre. + +Image + +#### Uploading Books + +To upload a book using the CrossPoint plugin in Calibre: + +1. On the device: File Transfer -> Calibre Wireless, then join a network. +2. Select one or more books. +3. Right-click on that selection. +4. Select "Send to Device" > "Send to main memory" + +The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder. + +Image + +#### Removing a Book + +Books cannot be removed from your device through Calibre. Use the web interface instead. ### 3.6 Settings diff --git a/lib/FsHelpers/FsHelpers.cpp b/lib/FsHelpers/FsHelpers.cpp index 1e3c68a6..6b315633 100644 --- a/lib/FsHelpers/FsHelpers.cpp +++ b/lib/FsHelpers/FsHelpers.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace FsHelpers { @@ -36,12 +37,14 @@ std::string decodeUriEscapes(const std::string& path) { } std::string normalisePath(const std::string& path) { - std::vector components; - std::string component; + std::vector components; + components.reserve(8); // Eight nested folders is more than we might expect - for (const auto c : path) { - if (c == '/') { - if (!component.empty()) { + size_t start = 0; + for (size_t i = 0; i <= path.length(); ++i) { + if (i == path.length() || path[i] == '/') { + if (i > start) { + std::string_view component(path.data() + start, i - start); if (component == "..") { if (!components.empty()) { components.pop_back(); @@ -49,23 +52,28 @@ std::string normalisePath(const std::string& path) { } else { components.push_back(component); } - component.clear(); } - } else { - component += c; + start = i + 1; } } - if (!component.empty()) { - components.push_back(component); + if (components.empty()) { + return ""; + } + + size_t total_len = 0; + for (const auto& c : components) { + total_len += c.length() + 1; } std::string result; - for (const auto& c : components) { - if (!result.empty()) { - result += "/"; + result.reserve(total_len - 1); + + for (size_t i = 0; i < components.size(); ++i) { + if (i > 0) { + result += '/'; } - result += c; + result.append(components[i].data(), components[i].length()); } return result; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index ddd5f1e4..b024fab8 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -661,8 +661,10 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con } void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { - for (int fillY = y; fillY < y + height; fillY++) { - drawLine(x, fillY, x + width - 1, fillY, state); + if (state) { + fillRectImpl(x, y, width, height); + } else { + fillRectImpl(x, y, width, height); } } @@ -694,26 +696,194 @@ void GfxRenderer::drawPixelDither(const int x, const int y) con } void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const { - if (color == Color::Clear) { - } else if (color == Color::Black) { - fillRect(x, y, width, height, true); - } else if (color == Color::White) { - fillRect(x, y, width, height, false); - } else if (color == Color::LightGray) { - for (int fillY = y; fillY < y + height; fillY++) { - for (int fillX = x; fillX < x + width; fillX++) { - drawPixelDither(fillX, fillY); + switch (color) { + case Color::Clear: + break; + case Color::Black: + fillRectImpl(x, y, width, height); + break; + case Color::White: + fillRectImpl(x, y, width, height); + break; + case Color::LightGray: + fillRectImpl(x, y, width, height); + break; + case Color::DarkGray: + fillRectImpl(x, y, width, height); + break; + } +} + +template +void GfxRenderer::fillRectImpl(const int x, const int y, const int width, const int height) const { + if constexpr (C == Color::Clear) return; + if (width <= 0 || height <= 0) return; + if (fontCacheManager_ && fontCacheManager_->isScanning()) return; + + // Clip in logical space. + const int screenW = getScreenWidth(); + const int screenH = getScreenHeight(); + const int lx0 = std::max(0, x); + const int ly0 = std::max(0, y); + const int lx1 = std::min(screenW, x + width); + const int ly1 = std::min(screenH, y + height); + if (lx0 >= lx1 || ly0 >= ly1) return; + + // Rotate the two opposing logical corners into physical-framebuffer space. + // The bounding rect in physical space is the rect we need to fill — rotation + // is rigid (no shear/stretch) so the bbox of the two corners IS the rect. + int paX, paY, pbX, pbY; + rotateCoordinates(orientation, lx0, ly0, &paX, &paY, panelWidth, panelHeight); + rotateCoordinates(orientation, lx1 - 1, ly1 - 1, &pbX, &pbY, panelWidth, panelHeight); + + const int phyX0 = std::min(paX, pbX); + const int phyX1 = std::max(paX, pbX); // inclusive + int phyY0 = std::min(paY, pbY); + int phyY1 = std::max(paY, pbY); + + // Strip mode: clip Y range to the active band and redirect writes. + uint8_t* target = getWriteTarget(); + const int originY = getWriteOriginY(); + const int writeRows = getWriteRows(); + phyY0 = std::max(phyY0, originY); + phyY1 = std::min(phyY1, originY + writeRows - 1); + if (phyY0 > phyY1) return; + + // Bit/byte layout: MSB-first within a byte, so phyX → bit (7 - (phyX & 7)). + // Head and tail masks cover only the in-rect bits of the first/last byte. + const int byteStart = phyX0 >> 3; + const int byteEnd = phyX1 >> 3; // inclusive + const uint8_t headMask = static_cast(0xFFu >> (phyX0 & 7)); + const uint8_t tailMask = static_cast(0xFFu << (7 - (phyX1 & 7))); + const int32_t panelStride = static_cast(panelWidthBytes); + + if constexpr (C == Color::Black || C == Color::White) { + // Solid fill. Framebuffer: 0 = black, 1 = white. + const uint8_t fillByte = (C == Color::Black) ? 0x00u : 0xFFu; + for (int py = phyY0; py <= phyY1; ++py) { + uint8_t* row = target + static_cast(py - originY) * panelStride; + if (byteStart == byteEnd) { + const uint8_t mask = headMask & tailMask; + if constexpr (C == Color::Black) { + row[byteStart] &= static_cast(~mask); + } else { + row[byteStart] |= mask; + } + } else { + if constexpr (C == Color::Black) { + row[byteStart] &= static_cast(~headMask); + if (byteEnd > byteStart + 1) { + memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1); + } + row[byteEnd] &= static_cast(~tailMask); + } else { + row[byteStart] |= headMask; + if (byteEnd > byteStart + 1) { + memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1); + } + row[byteEnd] |= tailMask; + } } } - } else if (color == Color::DarkGray) { - for (int fillY = y; fillY < y + height; fillY++) { - for (int fillX = x; fillX < x + width; fillX++) { - drawPixelDither(fillX, fillY); + } else { + // Dither (LightGray / DarkGray). Both patterns have period 2 in logical + // (x, y), so per physical row we precompute one byte that represents the + // pattern across an 8-pixel stretch — every full byte in the row uses + // that same value. + // + // dlxPerPhyX / dlyPerPhyX: how logical (x, y) change as phyX increments + // along a physical row. Derived from inverting rotateCoordinates. + int dlxPerPhyX = 0, dlyPerPhyX = 0; + switch (orientation) { + case Portrait: + dlxPerPhyX = 0; + dlyPerPhyX = 1; + break; + case PortraitInverted: + dlxPerPhyX = 0; + dlyPerPhyX = -1; + break; + case LandscapeClockwise: + dlxPerPhyX = -1; + dlyPerPhyX = 0; + break; + case LandscapeCounterClockwise: + dlxPerPhyX = 1; + dlyPerPhyX = 0; + break; + } + + // The dither pattern has period 2 in logical space, and each orientation + // maps py to logical coords with a fixed parity relationship. The + // blackMask byte therefore repeats with period 2 in py. Precompute both + // variants outside the row loop to eliminate the per-row switch + 8-bit + // construction loop. + uint8_t blackMasks[2]; + for (int parityIdx = 0; parityIdx < 2; ++parityIdx) { + const int samplePy = phyY0 + parityIdx; + int lxBase = 0, lyBase = 0; + switch (orientation) { + case Portrait: + lxBase = panelHeight - 1 - samplePy; + lyBase = byteStart * 8; + break; + case PortraitInverted: + lxBase = samplePy; + lyBase = panelWidth - 1 - byteStart * 8; + break; + case LandscapeClockwise: + lxBase = panelWidth - 1 - byteStart * 8; + lyBase = panelHeight - 1 - samplePy; + break; + case LandscapeCounterClockwise: + lxBase = byteStart * 8; + lyBase = samplePy; + break; + } + uint8_t mask = 0; + for (int b = 0; b < 8; ++b) { + const int lx = lxBase + b * dlxPerPhyX; + const int ly = lyBase + b * dlyPerPhyX; + bool isBlack; + if constexpr (C == Color::LightGray) { + isBlack = ((lx & 1) == 0) && ((ly & 1) == 0); + } else { // DarkGray + isBlack = (((lx + ly) & 1) == 0); + } + if (isBlack) mask |= static_cast(1u << (7 - b)); + } + blackMasks[samplePy & 1] = mask; + } + + for (int py = phyY0; py <= phyY1; ++py) { + const uint8_t blackMask = blackMasks[py & 1]; + const uint8_t whiteMask = static_cast(~blackMask); + + // Dither writes BOTH inks (the slow path called drawPixel for every + // pixel — setting or clearing — so we must do the same). Inside the + // rect mask: write whiteMask (1s where white, 0s where black). Outside + // the rect mask: leave the framebuffer untouched. + uint8_t* row = target + static_cast(py - originY) * panelStride; + if (byteStart == byteEnd) { + const uint8_t rectMask = headMask & tailMask; + row[byteStart] = static_cast((row[byteStart] & ~rectMask) | (rectMask & whiteMask)); + } else { + row[byteStart] = static_cast((row[byteStart] & ~headMask) | (headMask & whiteMask)); + if (byteEnd > byteStart + 1) { + // Period 2, so every full byte in this row is exactly whiteMask. + memset(row + byteStart + 1, whiteMask, byteEnd - byteStart - 1); + } + row[byteEnd] = static_cast((row[byteEnd] & ~tailMask) | (tailMask & whiteMask)); } } } } +template void GfxRenderer::fillRectImpl(int, int, int, int) const; +template void GfxRenderer::fillRectImpl(int, int, int, int) const; +template void GfxRenderer::fillRectImpl(int, int, int, int) const; +template void GfxRenderer::fillRectImpl(int, int, int, int) const; + void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height, const int radius, const Color color) const { if (radius <= 0 || color == Color::Clear) { diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 471c0a8f..ae000bb2 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -81,6 +81,12 @@ class GfxRenderer { void drawPixelDither(int x, int y) const; template void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const; + // Byte-aligned, orientation-specialized rectangle fill. Rotates the rect's + // two opposing corners into physical-framebuffer space once, then walks each + // physical row with head-mask / middle memset / tail-mask byte writes — no + // per-pixel rotation, no per-pixel RMW. + template + void fillRectImpl(int x, int y, int width, int height) const; public: explicit GfxRenderer(HalDisplay& halDisplay) diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 3ba890b8..de79ac91 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -126,6 +126,7 @@ STR_PAGE_TURN: "Перагортванне" STR_PORTRAIT: "Партрэт" STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_INVERTED: "Інверсія" +STR_ORIENTATION_INVERTED: "Партрэт 180°" STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_PREV_NEXT: "Назад/Наперад" STR_NEXT_PREV: "Наперад/Назад" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 7f3680cb..c1bf6c6b 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -136,6 +136,7 @@ STR_PAGE_TURN: "Canvi de pàgina" STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Horitzontal horari" STR_INVERTED: "Invertit" +STR_ORIENTATION_INVERTED: "Vertical 180°" STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_PREV_NEXT: "Anterior/Següent" STR_NEXT_PREV: "Següent/Anterior" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index c46b5ab9..696cdd33 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -131,6 +131,7 @@ STR_PAGE_TURN: "Otáčení stránek" STR_PORTRAIT: "Na výšku" STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček" STR_INVERTED: "Invertovaný" +STR_ORIENTATION_INVERTED: "Na výšku 180°" STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček" STR_PREV_NEXT: "Předchozí/Další" STR_NEXT_PREV: "Další/Předchozí" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index e6f96b45..cd4ba1ba 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -136,6 +136,7 @@ STR_PAGE_TURN: "Sideskift" STR_PORTRAIT: "Portræt" STR_LANDSCAPE_CW: "Liggende med uret" STR_INVERTED: "Inverteret" +STR_ORIENTATION_INVERTED: "Portræt 180°" STR_LANDSCAPE_CCW: "Liggende mod uret" STR_PREV_NEXT: "Forrige/Næste" STR_NEXT_PREV: "Næste/Forrige" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index d9461477..ee4ca64e 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -135,7 +135,8 @@ STR_SLEEP: "Slaap" STR_PAGE_TURN: "Pagina omslaan" STR_PORTRAIT: "Staand" STR_LANDSCAPE_CW: "Liggend (rechtsom)" -STR_INVERTED: "Omgekeerd" +STR_INVERTED: "Geïnverteerd" +STR_ORIENTATION_INVERTED: "Staand 180°" STR_LANDSCAPE_CCW: "Liggend (linksom)" STR_PREV_NEXT: "Vorige/Volgende" STR_NEXT_PREV: "Volgende/Vorige" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 96ebbdd3..ecd7b0e0 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -139,6 +139,7 @@ STR_FORCE_REFRESH: "Refresh Screen" STR_PORTRAIT: "Portrait" STR_LANDSCAPE_CW: "Landscape CW" STR_INVERTED: "Inverted" +STR_ORIENTATION_INVERTED: "Portrait 180°" STR_LANDSCAPE_CCW: "Landscape CCW" STR_PREV_NEXT: "Prev/Next" STR_NEXT_PREV: "Next/Prev" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 3ccaa39c..ebe120c1 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -130,7 +130,8 @@ STR_SLEEP: "Lepotila" STR_PAGE_TURN: "Sivunkääntö" STR_PORTRAIT: "Pysty" STR_LANDSCAPE_CW: "Vaaka myötäpäivään" -STR_INVERTED: "Käännetty" +STR_INVERTED: "Käänteinen" +STR_ORIENTATION_INVERTED: "Pysty 180°" STR_LANDSCAPE_CCW: "Vaaka vastapäivään" STR_PREV_NEXT: "Edell/Seur" STR_NEXT_PREV: "Seur/Edell" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 9d9e32cd..db4db7ec 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -136,6 +136,7 @@ STR_PAGE_TURN: "Page suivante" STR_PORTRAIT: "Portrait" STR_LANDSCAPE_CW: "Paysage" STR_INVERTED: "Inversé" +STR_ORIENTATION_INVERTED: "Portrait 180°" STR_LANDSCAPE_CCW: "Paysage inversé" STR_PREV_NEXT: "Préc/Suiv" STR_NEXT_PREV: "Suiv/Préc" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index a0f57fdd..a7c1ad08 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -70,7 +70,7 @@ STR_ORIENTATION: "Leseausrichtung" STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten" STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck" -STR_LONG_PRESS_BEHAVIOR_OFF: "AUS" +STR_LONG_PRESS_BEHAVIOR_OFF: "Aus" STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern" STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich" @@ -79,8 +79,9 @@ STR_FONT_SIZE: "Schriftgröße" STR_LINE_SPACING: "Lese-Zeilenabstand" STR_SCREEN_MARGIN: "Lese-Seitenränder" STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung" +STR_LONG_PRESS_MENU: "Menütaste lang drücken" STR_HYPHENATION: "Silbentrennung" -STR_TIME_TO_SLEEP: "Standby nach" +STR_TIME_TO_SLEEP: "Standby-Modus nach" STR_REFRESH_FREQ: "Anti-Ghosting nach" STR_KOREADER_SYNC: "KOReader-Synchr." STR_CHECK_UPDATES: "Nach Updates suchen" @@ -129,7 +130,8 @@ STR_PAGE_TURN: "Umblättern" STR_FORCE_REFRESH: "Bildschirm regenerieren" STR_PORTRAIT: "Hochformat" STR_LANDSCAPE_CW: "Querformat rechts" -STR_INVERTED: "Hochformat 180°" +STR_INVERTED: "Invertiert" +STR_ORIENTATION_INVERTED: "Hochformat 180°" STR_LANDSCAPE_CCW: "Querformat links" STR_PREV_NEXT: "Zurück/Weiter" STR_NEXT_PREV: "Weiter/Zurück" diff --git a/lib/I18n/translations/hebrew.yaml b/lib/I18n/translations/hebrew.yaml index 43a0beac..7d0835cf 100644 --- a/lib/I18n/translations/hebrew.yaml +++ b/lib/I18n/translations/hebrew.yaml @@ -134,7 +134,8 @@ STR_PAGE_TURN: "העברת דף" STR_FORCE_REFRESH: "רענון מסך מלא" STR_PORTRAIT: "לאורך" STR_LANDSCAPE_CW: "לרוחב (ימינה)" -STR_INVERTED: "הפוך" +STR_INVERTED: "היפוך צבעים" +STR_ORIENTATION_INVERTED: "לאורך 180°" STR_LANDSCAPE_CCW: "לרוחב (שמאלה)" STR_PREV_NEXT: "הקודם/הבא" STR_NEXT_PREV: "הבא/הקודם" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 14a5358e..b6f10036 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -132,7 +132,8 @@ STR_SLEEP: "Alvás" STR_PAGE_TURN: "Lapozás" STR_PORTRAIT: "Álló" STR_LANDSCAPE_CW: "Fekvő jobbra" -STR_INVERTED: "Fordított" +STR_INVERTED: "Invertált" +STR_ORIENTATION_INVERTED: "Álló 180°" STR_LANDSCAPE_CCW: "Fekvő balra" STR_PREV_NEXT: "Előző/Következő" STR_NEXT_PREV: "Következő/Előző" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 9ef433b6..f799e256 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -137,7 +137,8 @@ STR_PAGE_TURN: "Cambio pagina" STR_FORCE_REFRESH: "Refresh" STR_PORTRAIT: "Verticale" STR_LANDSCAPE_CW: "Orizzontale Dx" -STR_INVERTED: "Capovolto" +STR_INVERTED: "Invertito" +STR_ORIENTATION_INVERTED: "Verticale 180°" STR_LANDSCAPE_CCW: "Orizzontale Sx" STR_PREV_NEXT: "Prec/Succ" STR_NEXT_PREV: "Succ/Prec" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index dfccc77b..067aaa56 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -126,7 +126,8 @@ STR_SLEEP: "Ұйқы" STR_PAGE_TURN: "Бет аудару" STR_PORTRAIT: "Тік бағдар" STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)" -STR_INVERTED: "Төңкерілген" +STR_INVERTED: "Инверсия" +STR_ORIENTATION_INVERTED: "Тік бағдар 180°" STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)" STR_PREV_NEXT: "Алдыңғы/Келесі" STR_NEXT_PREV: "Келесі/Алдыңғы" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index d3c43ed3..f79a3222 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -132,7 +132,8 @@ STR_SLEEP: "Miegas" STR_PAGE_TURN: "Versti psl." STR_PORTRAIT: "Stačias" STR_LANDSCAPE_CW: "Gulsčias (P)" -STR_INVERTED: "Apverstas" +STR_INVERTED: "Invertuotas" +STR_ORIENTATION_INVERTED: "Stačias 180°" STR_LANDSCAPE_CCW: "Gulsčias (A)" STR_PREV_NEXT: "Atgal/Pirmyn" STR_NEXT_PREV: "Pirmyn/Atgal" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index ba46e715..0cb8f698 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -136,7 +136,8 @@ STR_PAGE_TURN: "Nast. str." STR_FORCE_REFRESH: "Odśwież ekran" STR_PORTRAIT: "Pionowo" STR_LANDSCAPE_CW: "Poziomo P" -STR_INVERTED: "Odwrócony" +STR_INVERTED: "Inwersja" +STR_ORIENTATION_INVERTED: "Pionowo 180°" STR_LANDSCAPE_CCW: "Poziomo L" STR_PREV_NEXT: "Poprz./Nast." STR_NEXT_PREV: "Nast./Poprz." diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 516651ab..7b68383c 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -131,6 +131,7 @@ STR_PAGE_TURN: "Virar página" STR_PORTRAIT: "Retrato" STR_LANDSCAPE_CW: "Paisagem H" STR_INVERTED: "Invertido" +STR_ORIENTATION_INVERTED: "Retrato 180°" STR_LANDSCAPE_CCW: "Paisagem AH" STR_PREV_NEXT: "Ant/Próx" STR_NEXT_PREV: "Próx/Ant" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 67052d10..0fbae3c4 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -136,6 +136,7 @@ STR_PAGE_TURN: "Răsfoire pagină" STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Orizontal dreapta" STR_INVERTED: "Invers" +STR_ORIENTATION_INVERTED: "Vertical 180°" STR_LANDSCAPE_CCW: "Orizontal stânga" STR_PREV_NEXT: "Înainte/Înapoi" STR_NEXT_PREV: "Înapoi/Înainte" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index ff1187d4..bac25717 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -140,6 +140,7 @@ STR_FORCE_REFRESH: "Обновление экрана" STR_PORTRAIT: "Портрет" STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_INVERTED: "Инверсия" +STR_ORIENTATION_INVERTED: "Портрет 180°" STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_PREV_NEXT: "Назад/Вперёд" STR_NEXT_PREV: "Вперёд/Назад" diff --git a/lib/I18n/translations/slovak.yaml b/lib/I18n/translations/slovak.yaml index b3210422..f13d5b3e 100644 --- a/lib/I18n/translations/slovak.yaml +++ b/lib/I18n/translations/slovak.yaml @@ -137,7 +137,8 @@ STR_PAGE_TURN: "Otáčanie stránok" STR_FORCE_REFRESH: "Obnoviť obrazovku" STR_PORTRAIT: "Na výšku" STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek" -STR_INVERTED: "Obrátený" +STR_INVERTED: "Invertovaný" +STR_ORIENTATION_INVERTED: "Na výšku 180°" STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek" STR_PREV_NEXT: "Predchádzajúca/Nasledujúca" STR_NEXT_PREV: "Nasledujúca/Predchádzajúca" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index c01ebc14..317bcb43 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -132,7 +132,8 @@ STR_SLEEP: "Spanje" STR_PAGE_TURN: "Obračanje strani" STR_PORTRAIT: "Pokončno" STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)" -STR_INVERTED: "Obrnjeno" +STR_INVERTED: "Invertirano" +STR_ORIENTATION_INVERTED: "Pokončno 180°" STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)" STR_PREV_NEXT: "Nazaj/Naprej" STR_NEXT_PREV: "Naprej/Nazaj" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 8156f891..0daeac75 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -138,6 +138,7 @@ STR_FORCE_REFRESH: "Refrescar pant." STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Horizontal (horario)" STR_INVERTED: "Invertido" +STR_ORIENTATION_INVERTED: "Al revés" STR_LANDSCAPE_CCW: "Horizontal (antihorario)" STR_PREV_NEXT: "Ant./Sig." STR_NEXT_PREV: "Sig./Ant." diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index ad588f19..19105830 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -138,6 +138,7 @@ STR_FORCE_REFRESH: "Uppdatera skärmen" STR_PORTRAIT: "Porträtt" STR_LANDSCAPE_CW: "Landskap medurs" STR_INVERTED: "Inverterad" +STR_ORIENTATION_INVERTED: "Porträtt 180°" STR_LANDSCAPE_CCW: "Landskap moturs" STR_PREV_NEXT: "Förra/Nästa" STR_NEXT_PREV: "Nästa/Förra" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 0faa13ec..ac6dd32c 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -130,7 +130,8 @@ STR_SLEEP: "Uyku" STR_PAGE_TURN: "Sayfa Çevirme" STR_PORTRAIT: "Dikey" STR_LANDSCAPE_CW: "Yatay (Saat Yönü)" -STR_INVERTED: "Ters" +STR_INVERTED: "Negatif" +STR_ORIENTATION_INVERTED: "Dikey 180°" STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)" STR_PREV_NEXT: "Önceki/Sonraki" STR_NEXT_PREV: "Sonraki/Önceki" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index c60de68f..6a34e1a9 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -137,7 +137,8 @@ STR_PAGE_TURN: "Наст. сторінка" STR_FORCE_REFRESH: "Оновити екран" STR_PORTRAIT: "Книжкова" STR_LANDSCAPE_CW: "Альбом. за год." -STR_INVERTED: "Перевернутий" +STR_INVERTED: "Інверсія" +STR_ORIENTATION_INVERTED: "Книжкова 180°" STR_LANDSCAPE_CCW: "Альбом. проти год." STR_PREV_NEXT: "Попер/Наст" STR_NEXT_PREV: "Наст/Попер" diff --git a/lib/I18n/translations/valencian.yaml b/lib/I18n/translations/valencian.yaml index 6dd28a25..89a7d962 100644 --- a/lib/I18n/translations/valencian.yaml +++ b/lib/I18n/translations/valencian.yaml @@ -141,6 +141,7 @@ STR_PAGE_TURN: "Canvi de pàgina" STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Horitzontal horari" STR_INVERTED: "Invertit" +STR_ORIENTATION_INVERTED: "Vertical 180°" STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_PREV_NEXT: "Anterior/Següent" STR_NEXT_PREV: "Següent/Anterior" diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index 2551b275..e73385fc 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -137,7 +137,8 @@ STR_PAGE_TURN: "Lật trang" STR_FORCE_REFRESH: "Làm tươi màn hình" STR_PORTRAIT: "Dọc" STR_LANDSCAPE_CW: "Ngang (thuận)" -STR_INVERTED: "Lật ngược" +STR_INVERTED: "Đảo màu" +STR_ORIENTATION_INVERTED: "Dọc 180°" STR_LANDSCAPE_CCW: "Ngang (ngược)" STR_PREV_NEXT: "Trước/Sau" STR_NEXT_PREV: "Sau/Trước" diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index e3a5b644..f90e85c0 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -82,6 +82,17 @@ void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* m } void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) { + // X3: a HALF fallback means the caller wants a clean base (e.g. the sleep + // cover, a full-screen swap from arbitrary prior content). Without this, the + // X3 grayscale base takes its gentle differential happy path and the prior + // home/reader frame ghosts through the soft aa_pre_bw_mid waveform. Forcing a + // resync makes displayGrayscaleBase clear first, matching displayBuffer(HALF). + // The reader's FAST path is deliberately left on the differential path so + // per-page grayscale stays cheap. + if (gpio.deviceIsX3() && fallback == RefreshMode::HALF_REFRESH) { + einkDisplay.requestResync(1); + } + einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h index 9b35254a..7b21a72f 100644 --- a/lib/hal/HalDisplay.h +++ b/lib/hal/HalDisplay.h @@ -55,9 +55,9 @@ class HalDisplay { void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h); // Display the framebuffer as the base frame for a grayscale overlay that - // follows. X3 uses the OEM differential base waveform ("AA-pre-BW(mid)"); - // other panels display normally with `fallback` mode (previous behavior). - // Deliberately does NOT force the X3 resync that displayBuffer(HALF) does. + // follows. On X3, HALF fallback first requests a resync to match + // displayBuffer(HALF); FAST fallback keeps the OEM differential base waveform + // ("AA-pre-BW(mid)"). Other panels display normally with `fallback` mode. void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false); void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); diff --git a/platformio.ini b/platformio.ini index abb5e322..0fae0516 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,7 +4,7 @@ build_cache_dir = .cache extra_configs = platformio.local.ini [crosspoint] -version = 1.4.0 +version = 1.4.1 [base] platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip diff --git a/src/SettingsList.h b/src/SettingsList.h index 695e8eec..a0e43cc8 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -151,9 +151,10 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist StrId::STR_CAT_READER), SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, - {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, - "orientation", StrId::STR_CAT_READER), + SettingInfo::Enum( + StrId::STR_ORIENTATION, &CrossPointSettings::orientation, + {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_ORIENTATION_INVERTED, StrId::STR_LANDSCAPE_CCW}, + "orientation", StrId::STR_CAT_READER), SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing", StrId::STR_CAT_READER), SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 3d2df14e..5948fd9a 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -83,9 +83,10 @@ ProgressRange getPageProgressRange(const std::shared_ptr& epub, const int return {epub->calculateProgress(spineIndex, start), epub->calculateProgress(spineIndex, end)}; } -bool bookmarkMatchesProgress(const BookmarkEntry& bookmark, const SavedProgressPosition& progress, +bool bookmarkMatchesProgress(const BookmarkEntry& bookmark, const int spineIndex, const int page, const int pageCount, const ProgressRange& pageRange) { - if (bookmark.xpath == progress.xpath) { + if (bookmark.computedSpineIndex == spineIndex && bookmark.computedChapterPageCount == pageCount && + bookmark.computedChapterProgress == page) { return true; } @@ -1311,10 +1312,12 @@ void EpubReaderActivity::addBookmark() { const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, currentPage, pageCount); const size_t bookmarkCountBeforeToggle = cachedBookmarks.size(); - cachedBookmarks.erase( - std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(), - [&](const BookmarkEntry& b) { return bookmarkMatchesProgress(b, progress, pageRange); }), - cachedBookmarks.end()); + cachedBookmarks.erase(std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(), + [&](const BookmarkEntry& b) { + return bookmarkMatchesProgress(b, currentSpineIndex, currentPage, pageCount, + pageRange); + }), + cachedBookmarks.end()); if (cachedBookmarks.size() != bookmarkCountBeforeToggle) { bookmarkRemoved = true; currentPageBookmarked = false; @@ -1350,11 +1353,10 @@ void EpubReaderActivity::updateBookmarkFlag() { currentPageBookmarked = false; return; } - SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition()); const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount); currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) { - return bookmarkMatchesProgress(b, progress, pageRange); + return bookmarkMatchesProgress(b, currentSpineIndex, section->currentPage, section->pageCount, pageRange); }); } diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 15b2d569..735f1477 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -803,13 +803,6 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c renderer.fillRect(barMarginLeft, progressBarY, barWidth, barHeight, true); } - // Draw Bookmark - if (showStatusBarTextLane && isPageBookmarked) { - const int bookmarkY = textY + 5; - drawBookmarkStatusIcon(renderer, leftClusterX, bookmarkY); - leftClusterWidth += bookmarkStatusIconWidth + bookmarkStatusIconGap; - } - // Draw Battery const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; @@ -848,6 +841,15 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c } } + // Draw Bookmark + if (showStatusBarTextLane && isPageBookmarked) { + const int bookmarkGap = leftClusterWidth > 0 ? bookmarkStatusIconGap : 0; + const int bookmarkX = leftClusterX + leftClusterWidth + bookmarkGap; + const int bookmarkY = textY + 5; + drawBookmarkStatusIcon(renderer, bookmarkX, bookmarkY); + leftClusterWidth += bookmarkStatusIconWidth + bookmarkGap; + } + // Draw Title if (!title.empty()) { textY -= textYOffset; diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index bec1ba33..8572b3e1 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -80,11 +80,11 @@ } .breadcrumb-inline .sep { margin: 0 6px; - color: var(--border-color); + color: var(--label-color); } .breadcrumb-inline .current { color: var(--title-color); - font-weight: 500; + font-weight: 600; } .nav-links { margin: 20px 0; @@ -960,12 +960,6 @@ align-items: center; margin-bottom: 12px; } - .contents-title { - font-size: 1.1em; - font-weight: 600; - color: var(--title-color); - margin: 0; - } .summary-inline { color: var(--label-color); font-size: 0.9em; @@ -1296,9 +1290,6 @@ flex-wrap: wrap; gap: 4px; } - .contents-title { - font-size: 1em; - } .summary-inline { font-size: 0.8em; } @@ -1501,7 +1492,6 @@