chore: release 1.4.1 (#2447)
Compile Release / build-release (push) Canceled after 0s

## Improvements

* Moved the File Manager breadcrumb into the Contents card header for a
cleaner, more consistent interface.
* Updated the Wireless Transfer section of the User Guide with clearer
instructions.
* Battery status bar indicator no longer changes position when adding or
removing bookmarks.

## Performance

* Optimized path normalization for faster file handling.
* Significantly improved bookmark rendering by removing unnecessary
XPath lookups.
* Optimized dithered rectangle drawing (fillRectDither) using a
byte-aligned rendering implementation, improving display performance on
supported devices.

## Bug Fixes

* Fixed an issue where the Inverted Orientation label was incorrectly
combined with the Color Filter label for Geman localization.
* Fixed excessive ghosting on the X3 cover screen during sleep
This commit is contained in:
Julia
2026-06-26 17:40:04 -04:00
committed by GitHub
37 changed files with 366 additions and 100 deletions
+31 -7
View File
@@ -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. CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
1. Install the plugin in Calibre: #### Installing the Plugin in Calibre
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin. If you don't already have the plugin installed:
- Download the zip file. 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.
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file. #### 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.
2. On the device: File Transfer -> Calibre Wireless, then join a network. <img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
3. Make sure your computer is on the same Wi-Fi network. #### Uploading Books
4. In Calibre, click "Send to device" to transfer 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.
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
#### Removing a Book
Books cannot be removed from your device through Calibre. Use the web interface instead.
### 3.6 Settings ### 3.6 Settings
+22 -14
View File
@@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
#include <string_view>
#include <vector> #include <vector>
namespace FsHelpers { namespace FsHelpers {
@@ -36,12 +37,14 @@ std::string decodeUriEscapes(const std::string& path) {
} }
std::string normalisePath(const std::string& path) { std::string normalisePath(const std::string& path) {
std::vector<std::string> components; std::vector<std::string_view> components;
std::string component; components.reserve(8); // Eight nested folders is more than we might expect
for (const auto c : path) { size_t start = 0;
if (c == '/') { for (size_t i = 0; i <= path.length(); ++i) {
if (!component.empty()) { if (i == path.length() || path[i] == '/') {
if (i > start) {
std::string_view component(path.data() + start, i - start);
if (component == "..") { if (component == "..") {
if (!components.empty()) { if (!components.empty()) {
components.pop_back(); components.pop_back();
@@ -49,23 +52,28 @@ std::string normalisePath(const std::string& path) {
} else { } else {
components.push_back(component); components.push_back(component);
} }
component.clear();
} }
} else { start = i + 1;
component += c;
} }
} }
if (!component.empty()) { if (components.empty()) {
components.push_back(component); return "";
}
size_t total_len = 0;
for (const auto& c : components) {
total_len += c.length() + 1;
} }
std::string result; std::string result;
for (const auto& c : components) { result.reserve(total_len - 1);
if (!result.empty()) {
result += "/"; 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; return result;
+185 -15
View File
@@ -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 { 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++) { if (state) {
drawLine(x, fillY, x + width - 1, fillY, state); fillRectImpl<Color::Black>(x, y, width, height);
} else {
fillRectImpl<Color::White>(x, y, width, height);
} }
} }
@@ -694,26 +696,194 @@ void GfxRenderer::drawPixelDither<Color::DarkGray>(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 { void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
if (color == Color::Clear) { switch (color) {
} else if (color == Color::Black) { case Color::Clear:
fillRect(x, y, width, height, true); break;
} else if (color == Color::White) { case Color::Black:
fillRect(x, y, width, height, false); fillRectImpl<Color::Black>(x, y, width, height);
} else if (color == Color::LightGray) { break;
for (int fillY = y; fillY < y + height; fillY++) { case Color::White:
for (int fillX = x; fillX < x + width; fillX++) { fillRectImpl<Color::White>(x, y, width, height);
drawPixelDither<Color::LightGray>(fillX, fillY); break;
case Color::LightGray:
fillRectImpl<Color::LightGray>(x, y, width, height);
break;
case Color::DarkGray:
fillRectImpl<Color::DarkGray>(x, y, width, height);
break;
}
}
template <Color C>
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<uint8_t>(0xFFu >> (phyX0 & 7));
const uint8_t tailMask = static_cast<uint8_t>(0xFFu << (7 - (phyX1 & 7)));
const int32_t panelStride = static_cast<int32_t>(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<int32_t>(py - originY) * panelStride;
if (byteStart == byteEnd) {
const uint8_t mask = headMask & tailMask;
if constexpr (C == Color::Black) {
row[byteStart] &= static_cast<uint8_t>(~mask);
} else {
row[byteStart] |= mask;
}
} else {
if constexpr (C == Color::Black) {
row[byteStart] &= static_cast<uint8_t>(~headMask);
if (byteEnd > byteStart + 1) {
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
}
row[byteEnd] &= static_cast<uint8_t>(~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++) { } else {
for (int fillX = x; fillX < x + width; fillX++) { // Dither (LightGray / DarkGray). Both patterns have period 2 in logical
drawPixelDither<Color::DarkGray>(fillX, fillY); // (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<uint8_t>(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<uint8_t>(~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<int32_t>(py - originY) * panelStride;
if (byteStart == byteEnd) {
const uint8_t rectMask = headMask & tailMask;
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~rectMask) | (rectMask & whiteMask));
} else {
row[byteStart] = static_cast<uint8_t>((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<uint8_t>((row[byteEnd] & ~tailMask) | (tailMask & whiteMask));
} }
} }
} }
} }
template void GfxRenderer::fillRectImpl<Color::Black>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::White>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::LightGray>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::DarkGray>(int, int, int, int) const;
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height, void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
const int radius, const Color color) const { const int radius, const Color color) const {
if (radius <= 0 || color == Color::Clear) { if (radius <= 0 || color == Color::Clear) {
+6
View File
@@ -81,6 +81,12 @@ class GfxRenderer {
void drawPixelDither(int x, int y) const; void drawPixelDither(int x, int y) const;
template <Color color> template <Color color>
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const; 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 <Color color>
void fillRectImpl(int x, int y, int width, int height) const;
public: public:
explicit GfxRenderer(HalDisplay& halDisplay) explicit GfxRenderer(HalDisplay& halDisplay)
+1
View File
@@ -126,6 +126,7 @@ STR_PAGE_TURN: "Перагортванне"
STR_PORTRAIT: "Партрэт" STR_PORTRAIT: "Партрэт"
STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Інверсія" STR_INVERTED: "Інверсія"
STR_ORIENTATION_INVERTED: "Партрэт 180°"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Наперад" STR_PREV_NEXT: "Назад/Наперад"
STR_NEXT_PREV: "Наперад/Назад" STR_NEXT_PREV: "Наперад/Назад"
+1
View File
@@ -136,6 +136,7 @@ STR_PAGE_TURN: "Canvi de pàgina"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari" STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit" STR_INVERTED: "Invertit"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_PREV_NEXT: "Anterior/Següent" STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
+1
View File
@@ -131,6 +131,7 @@ STR_PAGE_TURN: "Otáčení stránek"
STR_PORTRAIT: "Na výšku" STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček" STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
STR_INVERTED: "Invertovaný" STR_INVERTED: "Invertovaný"
STR_ORIENTATION_INVERTED: "Na výšku 180°"
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček" STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
STR_PREV_NEXT: "Předchozí/Další" STR_PREV_NEXT: "Předchozí/Další"
STR_NEXT_PREV: "Další/Předchozí" STR_NEXT_PREV: "Další/Předchozí"
+1
View File
@@ -136,6 +136,7 @@ STR_PAGE_TURN: "Sideskift"
STR_PORTRAIT: "Portræt" STR_PORTRAIT: "Portræt"
STR_LANDSCAPE_CW: "Liggende med uret" STR_LANDSCAPE_CW: "Liggende med uret"
STR_INVERTED: "Inverteret" STR_INVERTED: "Inverteret"
STR_ORIENTATION_INVERTED: "Portræt 180°"
STR_LANDSCAPE_CCW: "Liggende mod uret" STR_LANDSCAPE_CCW: "Liggende mod uret"
STR_PREV_NEXT: "Forrige/Næste" STR_PREV_NEXT: "Forrige/Næste"
STR_NEXT_PREV: "Næste/Forrige" STR_NEXT_PREV: "Næste/Forrige"
+2 -1
View File
@@ -135,7 +135,8 @@ STR_SLEEP: "Slaap"
STR_PAGE_TURN: "Pagina omslaan" STR_PAGE_TURN: "Pagina omslaan"
STR_PORTRAIT: "Staand" STR_PORTRAIT: "Staand"
STR_LANDSCAPE_CW: "Liggend (rechtsom)" STR_LANDSCAPE_CW: "Liggend (rechtsom)"
STR_INVERTED: "Omgekeerd" STR_INVERTED: "Geïnverteerd"
STR_ORIENTATION_INVERTED: "Staand 180°"
STR_LANDSCAPE_CCW: "Liggend (linksom)" STR_LANDSCAPE_CCW: "Liggend (linksom)"
STR_PREV_NEXT: "Vorige/Volgende" STR_PREV_NEXT: "Vorige/Volgende"
STR_NEXT_PREV: "Volgende/Vorige" STR_NEXT_PREV: "Volgende/Vorige"
+1
View File
@@ -139,6 +139,7 @@ STR_FORCE_REFRESH: "Refresh Screen"
STR_PORTRAIT: "Portrait" STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Landscape CW" STR_LANDSCAPE_CW: "Landscape CW"
STR_INVERTED: "Inverted" STR_INVERTED: "Inverted"
STR_ORIENTATION_INVERTED: "Portrait 180°"
STR_LANDSCAPE_CCW: "Landscape CCW" STR_LANDSCAPE_CCW: "Landscape CCW"
STR_PREV_NEXT: "Prev/Next" STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev" STR_NEXT_PREV: "Next/Prev"
+2 -1
View File
@@ -130,7 +130,8 @@ STR_SLEEP: "Lepotila"
STR_PAGE_TURN: "Sivunkääntö" STR_PAGE_TURN: "Sivunkääntö"
STR_PORTRAIT: "Pysty" STR_PORTRAIT: "Pysty"
STR_LANDSCAPE_CW: "Vaaka myötäpäivään" 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_LANDSCAPE_CCW: "Vaaka vastapäivään"
STR_PREV_NEXT: "Edell/Seur" STR_PREV_NEXT: "Edell/Seur"
STR_NEXT_PREV: "Seur/Edell" STR_NEXT_PREV: "Seur/Edell"
+1
View File
@@ -136,6 +136,7 @@ STR_PAGE_TURN: "Page suivante"
STR_PORTRAIT: "Portrait" STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Paysage" STR_LANDSCAPE_CW: "Paysage"
STR_INVERTED: "Inversé" STR_INVERTED: "Inversé"
STR_ORIENTATION_INVERTED: "Portrait 180°"
STR_LANDSCAPE_CCW: "Paysage inversé" STR_LANDSCAPE_CCW: "Paysage inversé"
STR_PREV_NEXT: "Préc/Suiv" STR_PREV_NEXT: "Préc/Suiv"
STR_NEXT_PREV: "Suiv/Préc" STR_NEXT_PREV: "Suiv/Préc"
+5 -3
View File
@@ -70,7 +70,7 @@ STR_ORIENTATION: "Leseausrichtung"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)" STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck" 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_SKIP: "Kapitel überspringen"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich" 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_LINE_SPACING: "Lese-Zeilenabstand"
STR_SCREEN_MARGIN: "Lese-Seitenränder" STR_SCREEN_MARGIN: "Lese-Seitenränder"
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung" STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
STR_LONG_PRESS_MENU: "Menütaste lang drücken"
STR_HYPHENATION: "Silbentrennung" STR_HYPHENATION: "Silbentrennung"
STR_TIME_TO_SLEEP: "Standby nach" STR_TIME_TO_SLEEP: "Standby-Modus nach"
STR_REFRESH_FREQ: "Anti-Ghosting nach" STR_REFRESH_FREQ: "Anti-Ghosting nach"
STR_KOREADER_SYNC: "KOReader-Synchr." STR_KOREADER_SYNC: "KOReader-Synchr."
STR_CHECK_UPDATES: "Nach Updates suchen" STR_CHECK_UPDATES: "Nach Updates suchen"
@@ -129,7 +130,8 @@ STR_PAGE_TURN: "Umblättern"
STR_FORCE_REFRESH: "Bildschirm regenerieren" STR_FORCE_REFRESH: "Bildschirm regenerieren"
STR_PORTRAIT: "Hochformat" STR_PORTRAIT: "Hochformat"
STR_LANDSCAPE_CW: "Querformat rechts" STR_LANDSCAPE_CW: "Querformat rechts"
STR_INVERTED: "Hochformat 180°" STR_INVERTED: "Invertiert"
STR_ORIENTATION_INVERTED: "Hochformat 180°"
STR_LANDSCAPE_CCW: "Querformat links" STR_LANDSCAPE_CCW: "Querformat links"
STR_PREV_NEXT: "Zurück/Weiter" STR_PREV_NEXT: "Zurück/Weiter"
STR_NEXT_PREV: "Weiter/Zurück" STR_NEXT_PREV: "Weiter/Zurück"
+2 -1
View File
@@ -134,7 +134,8 @@ STR_PAGE_TURN: "העברת דף"
STR_FORCE_REFRESH: "רענון מסך מלא" STR_FORCE_REFRESH: "רענון מסך מלא"
STR_PORTRAIT: "לאורך" STR_PORTRAIT: "לאורך"
STR_LANDSCAPE_CW: "לרוחב (ימינה)" STR_LANDSCAPE_CW: "לרוחב (ימינה)"
STR_INVERTED: "הפוך" STR_INVERTED: יפוך צבעים"
STR_ORIENTATION_INVERTED: "לאורך 180°"
STR_LANDSCAPE_CCW: "לרוחב (שמאלה)" STR_LANDSCAPE_CCW: "לרוחב (שמאלה)"
STR_PREV_NEXT: "הקודם/הבא" STR_PREV_NEXT: "הקודם/הבא"
STR_NEXT_PREV: "הבא/הקודם" STR_NEXT_PREV: "הבא/הקודם"
+2 -1
View File
@@ -132,7 +132,8 @@ STR_SLEEP: "Alvás"
STR_PAGE_TURN: "Lapozás" STR_PAGE_TURN: "Lapozás"
STR_PORTRAIT: "Álló" STR_PORTRAIT: "Álló"
STR_LANDSCAPE_CW: "Fekvő jobbra" STR_LANDSCAPE_CW: "Fekvő jobbra"
STR_INVERTED: "Fordított" STR_INVERTED: "Invertált"
STR_ORIENTATION_INVERTED: "Álló 180°"
STR_LANDSCAPE_CCW: "Fekvő balra" STR_LANDSCAPE_CCW: "Fekvő balra"
STR_PREV_NEXT: "Előző/Következő" STR_PREV_NEXT: "Előző/Következő"
STR_NEXT_PREV: "Következő/Előző" STR_NEXT_PREV: "Következő/Előző"
+2 -1
View File
@@ -137,7 +137,8 @@ STR_PAGE_TURN: "Cambio pagina"
STR_FORCE_REFRESH: "Refresh" STR_FORCE_REFRESH: "Refresh"
STR_PORTRAIT: "Verticale" STR_PORTRAIT: "Verticale"
STR_LANDSCAPE_CW: "Orizzontale Dx" STR_LANDSCAPE_CW: "Orizzontale Dx"
STR_INVERTED: "Capovolto" STR_INVERTED: "Invertito"
STR_ORIENTATION_INVERTED: "Verticale 180°"
STR_LANDSCAPE_CCW: "Orizzontale Sx" STR_LANDSCAPE_CCW: "Orizzontale Sx"
STR_PREV_NEXT: "Prec/Succ" STR_PREV_NEXT: "Prec/Succ"
STR_NEXT_PREV: "Succ/Prec" STR_NEXT_PREV: "Succ/Prec"
+2 -1
View File
@@ -126,7 +126,8 @@ STR_SLEEP: "Ұйқы"
STR_PAGE_TURN: "Бет аудару" STR_PAGE_TURN: "Бет аудару"
STR_PORTRAIT: "Тік бағдар" STR_PORTRAIT: "Тік бағдар"
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)" STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
STR_INVERTED: "Төңкерілген" STR_INVERTED: "Инверсия"
STR_ORIENTATION_INVERTED: "Тік бағдар 180°"
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)" STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
STR_PREV_NEXT: "Алдыңғы/Келесі" STR_PREV_NEXT: "Алдыңғы/Келесі"
STR_NEXT_PREV: "Келесі/Алдыңғы" STR_NEXT_PREV: "Келесі/Алдыңғы"
+2 -1
View File
@@ -132,7 +132,8 @@ STR_SLEEP: "Miegas"
STR_PAGE_TURN: "Versti psl." STR_PAGE_TURN: "Versti psl."
STR_PORTRAIT: "Stačias" STR_PORTRAIT: "Stačias"
STR_LANDSCAPE_CW: "Gulsčias (P)" 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_LANDSCAPE_CCW: "Gulsčias (A)"
STR_PREV_NEXT: "Atgal/Pirmyn" STR_PREV_NEXT: "Atgal/Pirmyn"
STR_NEXT_PREV: "Pirmyn/Atgal" STR_NEXT_PREV: "Pirmyn/Atgal"
+2 -1
View File
@@ -136,7 +136,8 @@ STR_PAGE_TURN: "Nast. str."
STR_FORCE_REFRESH: "Odśwież ekran" STR_FORCE_REFRESH: "Odśwież ekran"
STR_PORTRAIT: "Pionowo" STR_PORTRAIT: "Pionowo"
STR_LANDSCAPE_CW: "Poziomo P" STR_LANDSCAPE_CW: "Poziomo P"
STR_INVERTED: "Odwrócony" STR_INVERTED: "Inwersja"
STR_ORIENTATION_INVERTED: "Pionowo 180°"
STR_LANDSCAPE_CCW: "Poziomo L" STR_LANDSCAPE_CCW: "Poziomo L"
STR_PREV_NEXT: "Poprz./Nast." STR_PREV_NEXT: "Poprz./Nast."
STR_NEXT_PREV: "Nast./Poprz." STR_NEXT_PREV: "Nast./Poprz."
+1
View File
@@ -131,6 +131,7 @@ STR_PAGE_TURN: "Virar página"
STR_PORTRAIT: "Retrato" STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H" STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido" STR_INVERTED: "Invertido"
STR_ORIENTATION_INVERTED: "Retrato 180°"
STR_LANDSCAPE_CCW: "Paisagem AH" STR_LANDSCAPE_CCW: "Paisagem AH"
STR_PREV_NEXT: "Ant/Próx" STR_PREV_NEXT: "Ant/Próx"
STR_NEXT_PREV: "Próx/Ant" STR_NEXT_PREV: "Próx/Ant"
+1
View File
@@ -136,6 +136,7 @@ STR_PAGE_TURN: "Răsfoire pagină"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Orizontal dreapta" STR_LANDSCAPE_CW: "Orizontal dreapta"
STR_INVERTED: "Invers" STR_INVERTED: "Invers"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Orizontal stânga" STR_LANDSCAPE_CCW: "Orizontal stânga"
STR_PREV_NEXT: "Înainte/Înapoi" STR_PREV_NEXT: "Înainte/Înapoi"
STR_NEXT_PREV: "Înapoi/Înainte" STR_NEXT_PREV: "Înapoi/Înainte"
+1
View File
@@ -140,6 +140,7 @@ STR_FORCE_REFRESH: "Обновление экрана"
STR_PORTRAIT: "Портрет" STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Инверсия" STR_INVERTED: "Инверсия"
STR_ORIENTATION_INVERTED: "Портрет 180°"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Вперёд" STR_PREV_NEXT: "Назад/Вперёд"
STR_NEXT_PREV: "Вперёд/Назад" STR_NEXT_PREV: "Вперёд/Назад"
+2 -1
View File
@@ -137,7 +137,8 @@ STR_PAGE_TURN: "Otáčanie stránok"
STR_FORCE_REFRESH: "Obnoviť obrazovku" STR_FORCE_REFRESH: "Obnoviť obrazovku"
STR_PORTRAIT: "Na výšku" STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek" 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_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca" STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca" STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
+2 -1
View File
@@ -132,7 +132,8 @@ STR_SLEEP: "Spanje"
STR_PAGE_TURN: "Obračanje strani" STR_PAGE_TURN: "Obračanje strani"
STR_PORTRAIT: "Pokončno" STR_PORTRAIT: "Pokončno"
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)" 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_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
STR_PREV_NEXT: "Nazaj/Naprej" STR_PREV_NEXT: "Nazaj/Naprej"
STR_NEXT_PREV: "Naprej/Nazaj" STR_NEXT_PREV: "Naprej/Nazaj"
+1
View File
@@ -138,6 +138,7 @@ STR_FORCE_REFRESH: "Refrescar pant."
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horizontal (horario)" STR_LANDSCAPE_CW: "Horizontal (horario)"
STR_INVERTED: "Invertido" STR_INVERTED: "Invertido"
STR_ORIENTATION_INVERTED: "Al revés"
STR_LANDSCAPE_CCW: "Horizontal (antihorario)" STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
STR_PREV_NEXT: "Ant./Sig." STR_PREV_NEXT: "Ant./Sig."
STR_NEXT_PREV: "Sig./Ant." STR_NEXT_PREV: "Sig./Ant."
+1
View File
@@ -138,6 +138,7 @@ STR_FORCE_REFRESH: "Uppdatera skärmen"
STR_PORTRAIT: "Porträtt" STR_PORTRAIT: "Porträtt"
STR_LANDSCAPE_CW: "Landskap medurs" STR_LANDSCAPE_CW: "Landskap medurs"
STR_INVERTED: "Inverterad" STR_INVERTED: "Inverterad"
STR_ORIENTATION_INVERTED: "Porträtt 180°"
STR_LANDSCAPE_CCW: "Landskap moturs" STR_LANDSCAPE_CCW: "Landskap moturs"
STR_PREV_NEXT: "Förra/Nästa" STR_PREV_NEXT: "Förra/Nästa"
STR_NEXT_PREV: "Nästa/Förra" STR_NEXT_PREV: "Nästa/Förra"
+2 -1
View File
@@ -130,7 +130,8 @@ STR_SLEEP: "Uyku"
STR_PAGE_TURN: "Sayfa Çevirme" STR_PAGE_TURN: "Sayfa Çevirme"
STR_PORTRAIT: "Dikey" STR_PORTRAIT: "Dikey"
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)" 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_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
STR_PREV_NEXT: "Önceki/Sonraki" STR_PREV_NEXT: "Önceki/Sonraki"
STR_NEXT_PREV: "Sonraki/Önceki" STR_NEXT_PREV: "Sonraki/Önceki"
+2 -1
View File
@@ -137,7 +137,8 @@ STR_PAGE_TURN: "Наст. сторінка"
STR_FORCE_REFRESH: "Оновити екран" STR_FORCE_REFRESH: "Оновити екран"
STR_PORTRAIT: "Книжкова" STR_PORTRAIT: "Книжкова"
STR_LANDSCAPE_CW: "Альбом. за год." STR_LANDSCAPE_CW: "Альбом. за год."
STR_INVERTED: "Перевернутий" STR_INVERTED: "Інверсія"
STR_ORIENTATION_INVERTED: "Книжкова 180°"
STR_LANDSCAPE_CCW: "Альбом. проти год." STR_LANDSCAPE_CCW: "Альбом. проти год."
STR_PREV_NEXT: "Попер/Наст" STR_PREV_NEXT: "Попер/Наст"
STR_NEXT_PREV: "Наст/Попер" STR_NEXT_PREV: "Наст/Попер"
+1
View File
@@ -141,6 +141,7 @@ STR_PAGE_TURN: "Canvi de pàgina"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari" STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit" STR_INVERTED: "Invertit"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_PREV_NEXT: "Anterior/Següent" STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
+2 -1
View File
@@ -137,7 +137,8 @@ STR_PAGE_TURN: "Lật trang"
STR_FORCE_REFRESH: "Làm tươi màn hình" STR_FORCE_REFRESH: "Làm tươi màn hình"
STR_PORTRAIT: "Dọc" STR_PORTRAIT: "Dọc"
STR_LANDSCAPE_CW: "Ngang (thuận)" 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_LANDSCAPE_CCW: "Ngang (ngược)"
STR_PREV_NEXT: "Trước/Sau" STR_PREV_NEXT: "Trước/Sau"
STR_NEXT_PREV: "Sau/Trước" STR_NEXT_PREV: "Sau/Trước"
+11
View File
@@ -82,6 +82,17 @@ void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* m
} }
void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) { 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); einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen);
} }
+3 -3
View File
@@ -55,9 +55,9 @@ class HalDisplay {
void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h); 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 // Display the framebuffer as the base frame for a grayscale overlay that
// follows. X3 uses the OEM differential base waveform ("AA-pre-BW(mid)"); // follows. On X3, HALF fallback first requests a resync to match
// other panels display normally with `fallback` mode (previous behavior). // displayBuffer(HALF); FAST fallback keeps the OEM differential base waveform
// Deliberately does NOT force the X3 resync that displayBuffer(HALF) does. // ("AA-pre-BW(mid)"). Other panels display normally with `fallback` mode.
void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false); void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false);
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
+1 -1
View File
@@ -4,7 +4,7 @@ build_cache_dir = .cache
extra_configs = platformio.local.ini extra_configs = platformio.local.ini
[crosspoint] [crosspoint]
version = 1.4.0 version = 1.4.1
[base] [base]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
+3 -2
View File
@@ -151,8 +151,9 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, SettingInfo::Enum(
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, 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), "orientation", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing,
"extraParagraphSpacing", StrId::STR_CAT_READER), "extraParagraphSpacing", StrId::STR_CAT_READER),
+9 -7
View File
@@ -83,9 +83,10 @@ ProgressRange getPageProgressRange(const std::shared_ptr<Epub>& epub, const int
return {epub->calculateProgress(spineIndex, start), epub->calculateProgress(spineIndex, end)}; 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) { const ProgressRange& pageRange) {
if (bookmark.xpath == progress.xpath) { if (bookmark.computedSpineIndex == spineIndex && bookmark.computedChapterPageCount == pageCount &&
bookmark.computedChapterProgress == page) {
return true; return true;
} }
@@ -1311,9 +1312,11 @@ void EpubReaderActivity::addBookmark() {
const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, currentPage, pageCount); const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, currentPage, pageCount);
const size_t bookmarkCountBeforeToggle = cachedBookmarks.size(); const size_t bookmarkCountBeforeToggle = cachedBookmarks.size();
cachedBookmarks.erase( cachedBookmarks.erase(std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(),
std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) {
[&](const BookmarkEntry& b) { return bookmarkMatchesProgress(b, progress, pageRange); }), return bookmarkMatchesProgress(b, currentSpineIndex, currentPage, pageCount,
pageRange);
}),
cachedBookmarks.end()); cachedBookmarks.end());
if (cachedBookmarks.size() != bookmarkCountBeforeToggle) { if (cachedBookmarks.size() != bookmarkCountBeforeToggle) {
bookmarkRemoved = true; bookmarkRemoved = true;
@@ -1350,11 +1353,10 @@ void EpubReaderActivity::updateBookmarkFlag() {
currentPageBookmarked = false; currentPageBookmarked = false;
return; return;
} }
SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition());
const ProgressRange pageRange = const ProgressRange pageRange =
getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount); getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount);
currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) { 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);
}); });
} }
+9 -7
View File
@@ -803,13 +803,6 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
renderer.fillRect(barMarginLeft, progressBarY, barWidth, barHeight, true); 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 // Draw Battery
const bool showBatteryPercentage = const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; 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 // Draw Title
if (!title.empty()) { if (!title.empty()) {
textY -= textYOffset; textY -= textYOffset;
+38 -23
View File
@@ -80,11 +80,11 @@
} }
.breadcrumb-inline .sep { .breadcrumb-inline .sep {
margin: 0 6px; margin: 0 6px;
color: var(--border-color); color: var(--label-color);
} }
.breadcrumb-inline .current { .breadcrumb-inline .current {
color: var(--title-color); color: var(--title-color);
font-weight: 500; font-weight: 600;
} }
.nav-links { .nav-links {
margin: 20px 0; margin: 20px 0;
@@ -960,12 +960,6 @@
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 12px;
} }
.contents-title {
font-size: 1.1em;
font-weight: 600;
color: var(--title-color);
margin: 0;
}
.summary-inline { .summary-inline {
color: var(--label-color); color: var(--label-color);
font-size: 0.9em; font-size: 0.9em;
@@ -1296,9 +1290,6 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
} }
.contents-title {
font-size: 1em;
}
.summary-inline { .summary-inline {
font-size: 0.8em; font-size: 0.8em;
} }
@@ -1501,7 +1492,6 @@
<div class="page-header"> <div class="page-header">
<div class="page-header-left"> <div class="page-header-left">
<h2>📁 File Manager</h2> <h2>📁 File Manager</h2>
<div class="breadcrumb-inline" id="directory-breadcrumbs"></div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
@@ -1523,7 +1513,7 @@
<div class="card"> <div class="card">
<div class="contents-header"> <div class="contents-header">
<h2 class="contents-title">Contents</h2> <div class="breadcrumb-inline" id="directory-breadcrumbs"></div>
<span class="summary-inline" id="folder-summary"></span> <span class="summary-inline" id="folder-summary"></span>
</div> </div>
@@ -1904,19 +1894,44 @@
const breadcrumbs = document.getElementById('directory-breadcrumbs'); const breadcrumbs = document.getElementById('directory-breadcrumbs');
const fileTable = document.getElementById('file-table'); const fileTable = document.getElementById('file-table');
let breadcrumbContent = '<span class="sep">/</span>'; const segments = currentPath.split('/').filter(Boolean);
if (currentPath === '/') { breadcrumbs.replaceChildren();
breadcrumbContent += '<span class="current">🏠</span>';
const appendSep = function() {
const sep = document.createElement('span');
sep.className = 'sep';
sep.textContent = '';
breadcrumbs.appendChild(sep);
};
const appendLink = function(label, href) {
const link = document.createElement('a');
link.href = href;
link.textContent = label;
breadcrumbs.appendChild(link);
};
const appendCurrent = function(label) {
const current = document.createElement('span');
current.className = 'current';
current.textContent = label;
breadcrumbs.appendChild(current);
};
if (segments.length === 0) {
appendCurrent('🏠 Home');
} else { } else {
breadcrumbContent += '<a href="/files">🏠</a>'; appendLink('🏠 Home', '/files');
const pathSegments = currentPath.split('/'); segments.forEach(function(segment, index) {
pathSegments.slice(1, pathSegments.length - 1).forEach(function(segment, index) { appendSep();
breadcrumbContent += '<span class="sep">/</span><a href="/files?path=' + encodeURIComponent(pathSegments.slice(0, index + 2).join('/')) + '">' + escapeHtml(segment) + '</a>'; if (index === segments.length - 1) {
appendCurrent(segment);
} else {
const path = '/' + segments.slice(0, index + 1).join('/');
appendLink(segment, '/files?path=' + encodeURIComponent(path));
}
}); });
breadcrumbContent += '<span class="sep">/</span>';
breadcrumbContent += '<span class="current">' + escapeHtml(pathSegments[pathSegments.length - 1]) + '</span>';
} }
breadcrumbs.innerHTML = breadcrumbContent;
let files = []; let files = [];
try { try {