Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8ad98e1
feat: add clipping functionality to the reader
cagliostro1991 Apr 22, 2026
26958a6
fix: resolve CI failures for clang-format and cppcheck
cagliostro1991 Apr 22, 2026
4d9e306
fix: address coderabbitai review for clipping feature
cagliostro1991 Apr 22, 2026
198b9d7
fix: swedish translations (#1762)
steka Apr 26, 2026
02822e0
feat: include short SHA in CROSSPOINT_VERSION (#1728)
osteotek Apr 27, 2026
3338695
feat: enable pio build cache (#1769)
Uri-Tauber Apr 27, 2026
b8a5152
feat(theme): add roundedraff theme and fix sleep cover crop grid arti…
bunsoootchi Apr 27, 2026
ef98d44
fix: python requirements files (#1768)
steka Apr 27, 2026
741dd89
fix: cap per-side horizontal CSS inset at 2em (#1694)
Apr 27, 2026
59c8d39
feat: enhance logging for mutex operations in HalStorage and Activity…
cagliostro1991 Apr 28, 2026
eb92f2e
Merge remote-tracking branch 'origin/master' into feature/epub-text-c…
cagliostro1991 Apr 28, 2026
0a211b5
fix: update clipping format to match Kindle-compatible standards
cagliostro1991 Apr 28, 2026
bd279ad
fix: improve error logging and revert page on load failure in ClipSel…
cagliostro1991 Apr 28, 2026
281752c
fix: use canonical Kindle 'Your Highlight' phrasing and apply clang-f…
cagliostro1991 Apr 28, 2026
92346ca
fix: use std::string for header/location and single-buffer write in C…
cagliostro1991 Apr 28, 2026
e9ab936
merge: add PR #1742 clipping support to CrossInk
Louieza23 Apr 28, 2026
88ad649
feat: add save clipping power-button action
Louieza23 Apr 28, 2026
846a620
feat: improve clipping selector controls
Louieza23 Apr 28, 2026
08efb36
feat: store clippings in per-book files
Louieza23 Apr 28, 2026
48cfb66
refine clipping UX for PR review
Louieza23 May 2, 2026
a59d708
refine clipping text layout
Louieza23 May 2, 2026
c517777
refine clipping entry format
Louieza23 May 2, 2026
f773c2c
fix clipping page selection state
Louieza23 May 3, 2026
ee06284
Merge upstream/main into clipping PR branch
Louieza23 May 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/Epub/Epub/blocks/TextBlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class TextBlock final : public Block {
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
const BlockStyle& getBlockStyle() const { return blockStyle; }
const std::vector<std::string>& getWords() const { return words; }
const std::vector<int16_t>& getWordXpos() const { return wordXpos; }
const std::vector<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; }
bool isEmpty() override { return words.empty(); }
size_t wordCount() const { return words.size(); }
// given a renderer works out where to break the words into lines
Expand Down
70 changes: 70 additions & 0 deletions lib/GfxRenderer/GfxRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,41 @@ void GfxRenderer::fillRectDither(const int x, const int y, const int width, cons
}
}

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) {
return;
}

const int rr = radius - 1;
const int rr2 = rr * rr;
for (int dy = 0; dy < radius; dy++) {
for (int dx = 0; dx < radius; dx++) {
const int tx = rr - dx;
const int ty = rr - dy;
if (tx * tx + ty * ty > rr2) {
if (color == Color::White || color == Color::Black) {
bool state = color == Color::Black;
drawPixel(x + dx, y + dy, state); // top-left
drawPixel(x + width - 1 - dx, y + dy, state); // top-right
drawPixel(x + dx, y + height - 1 - dy, state); // bottom-left
drawPixel(x + width - 1 - dx, y + height - 1 - dy, state); // bottom-right
} else if (color == Color::LightGray) {
drawPixelDither<Color::LightGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::LightGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
} else if (color == Color::DarkGray) {
drawPixelDither<Color::DarkGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::DarkGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
}
}
}
}
}
Comment on lines +507 to +540
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp radius before mirroring the corner mask.

This helper assumes radius fits inside the rectangle. If it doesn't, the mirrored writes cross past the opposite edge and start touching pixels outside the requested box. Since this is a public drawing helper, it should defensively cap the radius first.

Suggested fix
 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) {
+  if (width <= 0 || height <= 0 || radius <= 0 || color == Color::Clear) {
     return;
   }
 
-  const int rr = radius - 1;
+  const int clippedRadius = std::min({radius, width / 2, height / 2});
+  if (clippedRadius <= 0) {
+    return;
+  }
+
+  const int rr = clippedRadius - 1;
   const int rr2 = rr * rr;
-  for (int dy = 0; dy < radius; dy++) {
-    for (int dx = 0; dx < radius; dx++) {
+  for (int dy = 0; dy < clippedRadius; dy++) {
+    for (int dx = 0; dx < clippedRadius; dx++) {
       const int tx = rr - dx;
       const int ty = rr - dy;
       if (tx * tx + ty * ty > rr2) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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) {
return;
}
const int rr = radius - 1;
const int rr2 = rr * rr;
for (int dy = 0; dy < radius; dy++) {
for (int dx = 0; dx < radius; dx++) {
const int tx = rr - dx;
const int ty = rr - dy;
if (tx * tx + ty * ty > rr2) {
if (color == Color::White || color == Color::Black) {
bool state = color == Color::Black;
drawPixel(x + dx, y + dy, state); // top-left
drawPixel(x + width - 1 - dx, y + dy, state); // top-right
drawPixel(x + dx, y + height - 1 - dy, state); // bottom-left
drawPixel(x + width - 1 - dx, y + height - 1 - dy, state); // bottom-right
} else if (color == Color::LightGray) {
drawPixelDither<Color::LightGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::LightGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
} else if (color == Color::DarkGray) {
drawPixelDither<Color::DarkGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::DarkGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
}
}
}
}
}
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
const int radius, const Color color) const {
if (width <= 0 || height <= 0 || radius <= 0 || color == Color::Clear) {
return;
}
const int clippedRadius = std::min({radius, width / 2, height / 2});
if (clippedRadius <= 0) {
return;
}
const int rr = clippedRadius - 1;
const int rr2 = rr * rr;
for (int dy = 0; dy < clippedRadius; dy++) {
for (int dx = 0; dx < clippedRadius; dx++) {
const int tx = rr - dx;
const int ty = rr - dy;
if (tx * tx + ty * ty > rr2) {
if (color == Color::White || color == Color::Black) {
bool state = color == Color::Black;
drawPixel(x + dx, y + dy, state); // top-left
drawPixel(x + width - 1 - dx, y + dy, state); // top-right
drawPixel(x + dx, y + height - 1 - dy, state); // bottom-left
drawPixel(x + width - 1 - dx, y + height - 1 - dy, state); // bottom-right
} else if (color == Color::LightGray) {
drawPixelDither<Color::LightGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::LightGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
} else if (color == Color::DarkGray) {
drawPixelDither<Color::DarkGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::DarkGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
}
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/GfxRenderer/GfxRenderer.cpp` around lines 507 - 540, The code in
maskRoundedRectOutsideCorners can write past the opposite edge when radius is
larger than the rectangle; clamp radius first (e.g., compute int r =
std::min(radius, std::min(width, height) / 2) and use r for the early return and
subsequent math instead of the raw radius) so all mirrored writes (the loops
using rr/rr2 and the drawPixel/drawPixelDither calls) stay inside the box;
update the function to use r, rr = r-1, rr2 = rr*rr and keep the existing
color-handling logic with drawPixel and drawPixelDither.


template <Color color>
void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir) const {
if (maxRadius <= 0) return;
Expand Down Expand Up @@ -908,6 +943,41 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
display.displayBuffer(refreshMode, fadingFix);
}

void GfxRenderer::displayWindow(int x, int y, int width, int height) const {
int phyX, phyY, phyW, phyH;
switch (orientation) {
case Portrait:
phyX = x;
phyY = panelHeight - y - height;
phyW = width;
phyH = height;
break;
case LandscapeClockwise:
phyX = panelWidth - x - width;
phyY = panelHeight - y - height;
phyW = width;
phyH = height;
break;
case PortraitInverted:
phyX = panelWidth - x - width;
phyY = y;
phyW = width;
phyH = height;
break;
case LandscapeCounterClockwise:
default:
phyX = x;
phyY = y;
phyW = width;
phyH = height;
break;
}
// Align to 8-pixel (byte) boundaries required by e-ink panel DMA
const int alignedX = (phyX / 8) * 8;
const int alignedW = ((phyX + phyW + 7) / 8) * 8 - alignedX;
display.displayWindow(alignedX, phyY, alignedW, phyH);
}
Comment on lines +946 to +979
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle 90° orientations when converting partial-update rectangles.

The current transform only works for 0°/180° cases. rotateCoordinates() rotates Portrait and PortraitInverted by 90°, so the physical origin must be recomputed from the opposite axis and width/height must swap. As written, portrait-mode partial refreshes will hit the wrong panel area and can leave stale pixels behind.

Suggested fix
 void GfxRenderer::displayWindow(int x, int y, int width, int height) const {
   int phyX, phyY, phyW, phyH;
   switch (orientation) {
     case Portrait:
-      phyX = x;
-      phyY = panelHeight - y - height;
-      phyW = width;
-      phyH = height;
+      phyX = y;
+      phyY = panelHeight - x - width;
+      phyW = height;
+      phyH = width;
       break;
     case LandscapeClockwise:
       phyX = panelWidth - x - width;
       phyY = panelHeight - y - height;
       phyW = width;
       phyH = height;
       break;
     case PortraitInverted:
-      phyX = panelWidth - x - width;
-      phyY = y;
-      phyW = width;
-      phyH = height;
+      phyX = panelWidth - y - height;
+      phyY = x;
+      phyW = height;
+      phyH = width;
       break;
     case LandscapeCounterClockwise:
     default:
       phyX = x;
       phyY = y;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/GfxRenderer/GfxRenderer.cpp` around lines 946 - 979, The displayWindow
transform in GfxRenderer::displayWindow only handles 0°/180° correctly; for 90°
rotations (LandscapeClockwise and LandscapeCounterClockwise) you must recompute
the physical origin using the opposite axis and swap width/height before
alignment so partial-update rects map to the correct panel area. Update the case
handling for LandscapeClockwise and LandscapeCounterClockwise to set phyX/phyY
using panelHeight/panelWidth as needed and set phyW = height and phyH = width
(i.e., swap dims), then perform the 8-pixel alignment using those swapped
dimensions before calling display.displayWindow; ensure the logic matches
rotateCoordinates()’s rotation conventions.


std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
const EpdFontFamily::Style style) const {
if (!text || maxWidth <= 0) return "";
Expand Down
5 changes: 3 additions & 2 deletions lib/GfxRenderer/GfxRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ class GfxRenderer {
int getScreenWidth() const;
int getScreenHeight() const;
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region
// void displayWindow(int x, int y, int width, int height) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region (x and w must be multiples of 8)
void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const;
void clearScreen(uint8_t color = 0xFF) const;
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
Expand All @@ -105,6 +105,7 @@ class GfxRenderer {
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool state) const;
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool roundTopLeft,
bool roundTopRight, bool roundBottomLeft, bool roundBottomRight, bool state) const;
void maskRoundedRectOutsideCorners(int x, int y, int width, int height, int radius, Color color = Color::White) const;
void fillRect(int x, int y, int width, int height, bool state = true) const;
void fillRectDither(int x, int y, int width, int height, Color color) const;
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, Color color) const;
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/belarusian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ STR_GO_TO_PERCENT: "Перайсці да %"
STR_GO_HOME_BUTTON: "На галоўную"
STR_SYNC_PROGRESS: "Сінхранізаваць прагрэс"
STR_DELETE_CACHE: "Выдаліць кэш кнігі"
STR_SAVE_CLIPPING: "Захаваць урывак"
STR_CHAPTER_PREFIX: "Раздзел:"
STR_PAGES_SEPARATOR: "стар. |"
STR_BOOK_PREFIX: "Кніга:"
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/catalan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Ves al %"
STR_GO_HOME_BUTTON: "Ves a l'inici"
STR_SYNC_PROGRESS: "Sincronitza el progrés"
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
STR_SAVE_CLIPPING: "Desa la selecció"
STR_DELETE: "Esborra"
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
STR_CHAPTER_PREFIX: "Capítol: "
Expand Down
2 changes: 2 additions & 0 deletions lib/I18n/translations/czech.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ STR_FILTER_CONTRAST: "Kontrast"
STR_UI_THEME: "Šablona rozhraní"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
Expand Down Expand Up @@ -231,6 +232,7 @@ STR_GO_TO_PERCENT: "Přejít na %"
STR_GO_HOME_BUTTON: "Přejít Domů"
STR_SYNC_PROGRESS: "Průběh synchronizace"
STR_DELETE_CACHE: "Smazat mezipaměť knihy"
STR_SAVE_CLIPPING: "Uložit výběr"
STR_DELETE: "Smazat"
STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "stránek |"
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/danish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Gå til %"
STR_GO_HOME_BUTTON: "Gå til start"
STR_SYNC_PROGRESS: "Synkroniser fremskridt"
STR_DELETE_CACHE: "Slet bogcache"
STR_SAVE_CLIPPING: "Gem klip"
STR_DELETE: "Slet"
STR_DISPLAY_QR: "Vis side som QR"
STR_CHAPTER_PREFIX: "Kapitel: "
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/dutch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Ga naar %"
STR_GO_HOME_BUTTON: "Naar Home"
STR_SYNC_PROGRESS: "Voortgang synchroniseren"
STR_DELETE_CACHE: "Boekcache verwijderen"
STR_SAVE_CLIPPING: "Selectie opslaan"
STR_DELETE: "Verwijder"
STR_DISPLAY_QR: "Pagina als QR tonen"
STR_CHAPTER_PREFIX: "Hoofdstuk: "
Expand Down
3 changes: 3 additions & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ STR_BATTERY: "Battery"
STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
Expand Down Expand Up @@ -268,6 +269,8 @@ STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress"
STR_DELETE_CACHE: "Delete Book Cache"
STR_SAVE_CLIPPING: "Create Clipping"
STR_CLIPPING_SAVE_FAILED: "Failed to save clipping"
STR_DELETE: "Delete"
STR_DISPLAY_QR: "Show page as QR"
STR_CHAPTER_PREFIX: "Chapter: "
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/finnish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ STR_GO_TO_PERCENT: "Siirry kohtaan %"
STR_GO_HOME_BUTTON: "Siirry kotiin"
STR_SYNC_PROGRESS: "Synkronoi edistyminen"
STR_DELETE_CACHE: "Poista kirjan välimuisti"
STR_SAVE_CLIPPING: "Tallenna leike"
STR_CHAPTER_PREFIX: "Luku: "
STR_PAGES_SEPARATOR: " sivua | "
STR_BOOK_PREFIX: "Kirja: "
Expand Down
2 changes: 2 additions & 0 deletions lib/I18n/translations/french.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ STR_BATTERY: "Batterie"
STR_UI_THEME: "Thème interface"
STR_THEME_CLASSIC: "Classique"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Correction lisibilité au soleil"
STR_REMAP_FRONT_BUTTONS: "Configurer boutons façade"
Expand Down Expand Up @@ -254,6 +255,7 @@ STR_GO_TO_PERCENT: "Aller à %"
STR_GO_HOME_BUTTON: "Retour Accueil"
STR_SYNC_PROGRESS: "Synchro progression"
STR_DELETE_CACHE: "Supprimer cache livre"
STR_SAVE_CLIPPING: "Enregistrer la sélection"
STR_DELETE: "Supprimer"
STR_DISPLAY_QR: "Afficher la page en QR"
STR_CHAPTER_PREFIX: "Chapitre : "
Expand Down
2 changes: 2 additions & 0 deletions lib/I18n/translations/german.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ STR_BATTERY: "Batterie"
STR_UI_THEME: "System-Design"
STR_THEME_CLASSIC: "Klassisch"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
Expand Down Expand Up @@ -255,6 +256,7 @@ STR_GO_TO_PERCENT: "Gehe zu %"
STR_GO_HOME_BUTTON: "Zum Anfang"
STR_SYNC_PROGRESS: "Fortschritt synchronisieren"
STR_DELETE_CACHE: "Buch-Cache leeren"
STR_SAVE_CLIPPING: "Auswahl speichern"
STR_DISPLAY_QR: "Seite als QR anzeigen"
STR_DELETE: "Löschen"
STR_REMOVE: "Entfernen"
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/hungarian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Ugrás %-ra"
STR_GO_HOME_BUTTON: "Főoldalra"
STR_SYNC_PROGRESS: "Haladás szinkronizálása"
STR_DELETE_CACHE: "Könyv gyorsítótár törlése"
STR_SAVE_CLIPPING: "Kijelölés mentése"
STR_DELETE: "Törlés"
STR_DISPLAY_QR: "Oldal megjelenítése QR-ként"
STR_CHAPTER_PREFIX: "Fejezet: "
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/italian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ STR_SYNC_PROGRESS: "Sincronizza avanzamento"
STR_DELETE_CACHE: "Elimina cache libro"
STR_DELETE: "Elimina"
STR_DISPLAY_QR: "Mostra pagina come QR"
STR_SAVE_CLIPPING: "Salva selezione"
STR_CHAPTER_PREFIX: "Capitolo: "
STR_PAGES_SEPARATOR: " pagine | "
STR_BOOK_PREFIX: "Libro: "
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/kazakh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ STR_GO_TO_PERCENT: "%-ке өту"
STR_GO_HOME_BUTTON: "Басты бетке өту"
STR_SYNC_PROGRESS: "Үлгерімді синхрондау"
STR_DELETE_CACHE: "Кітап кэшін жою"
STR_SAVE_CLIPPING: "Үзінді сақтау"
STR_CHAPTER_PREFIX: "Тарау: "
Comment on lines 232 to 234
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find which translation files already define STR_SAVE_CLIPPING
rg -n --glob 'lib/I18n/translations/*.ya?ml' '^\s*STR_SAVE_CLIPPING\s*:' lib/I18n/translations || true

# Fail if any locale file is missing the key
missing=0
for f in lib/I18n/translations/*.ya?ml; do
  if ! rg -n '^\s*STR_SAVE_CLIPPING\s*:' "$f" >/dev/null 2>&1; then
    echo "MISSING: $f"
    missing=1
  fi
done

if [ "$missing" -ne 0 ]; then
  echo "One or more locale files are missing STR_SAVE_CLIPPING."
  exit 1
fi

Repository: uxjulia/CrossInk

Length of output: 155


🏁 Script executed:

# List all translation files in the expected directory
find lib/I18n/translations -name '*.yaml' -o -name '*.yml' | sort

Repository: uxjulia/CrossInk

Length of output: 875


🏁 Script executed:

# For each translation file, check if STR_SAVE_CLIPPING exists
for f in $(find lib/I18n/translations -name '*.yaml' -o -name '*.yml' | sort); do
  echo "=== $f ==="
  if rg -n 'STR_SAVE_CLIPPING' "$f"; then
    echo "FOUND"
  else
    echo "NOT FOUND"
  fi
done

Repository: uxjulia/CrossInk

Length of output: 2104


Kazakh translation is correct, but Vietnamese locale is missing STR_SAVE_CLIPPING.

STR_SAVE_CLIPPING: "Үзінді сақтау" is properly added to kazakh.yaml with valid YAML syntax. However, verification shows that vietnamese.yaml is the only locale file missing this key while all other 22 locales have it. Add the Vietnamese translation to maintain localization completeness and prevent fallback/missing-key behavior for Vietnamese users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/I18n/translations/kazakh.yaml` around lines 232 - 234, Add the missing
STR_SAVE_CLIPPING key to vietnamese.yaml so it matches other locales; use the
same key name STR_SAVE_CLIPPING and provide the Vietnamese translation (e.g.,
"Lưu đoạn trích" or the project's approved phrasing), ensuring valid YAML syntax
and placement alongside STR_DELETE_CACHE and STR_CHAPTER_PREFIX entries to keep
locales consistent.

STR_PAGES_SEPARATOR: " бет | "
STR_BOOK_PREFIX: "Кітап: "
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/lithuanian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Eiti į %"
STR_GO_HOME_BUTTON: "Pradžia"
STR_SYNC_PROGRESS: "Sinchr. progresą"
STR_DELETE_CACHE: "Trinti talpyklą"
STR_SAVE_CLIPPING: "Išsaugoti ištrauką"
STR_DELETE: "Trinti"
STR_DISPLAY_QR: "QR kodas"
STR_CHAPTER_PREFIX: "Sk: "
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/polish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Idź do %"
STR_GO_HOME_BUTTON: "Wróć do głównego ekranu"
STR_SYNC_PROGRESS: "Postęp synchronizacji"
STR_DELETE_CACHE: "Usuń pamięć podręczną książek"
STR_SAVE_CLIPPING: "Zapisz fragment"
STR_DELETE: "Usuń"
STR_DISPLAY_QR: "Pokaż stronę jako kod QR"
STR_CHAPTER_PREFIX: "Rozdział: "
Expand Down
2 changes: 2 additions & 0 deletions lib/I18n/translations/portuguese.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ STR_FILTER_CONTRAST: "Contraste"
STR_UI_THEME: "Tema da interface"
STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
Expand Down Expand Up @@ -231,6 +232,7 @@ STR_GO_TO_PERCENT: "Ir para %"
STR_GO_HOME_BUTTON: "Ir para o início"
STR_SYNC_PROGRESS: "Sincronizar progresso"
STR_DELETE_CACHE: "Excluir cache do livro"
STR_SAVE_CLIPPING: "Salvar trecho"
STR_DELETE: "Excluir"
STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: "páginas |"
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/romanian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Săriţi la %"
STR_GO_HOME_BUTTON: "Acasă"
STR_SYNC_PROGRESS: "Progres sincronizare"
STR_DELETE_CACHE: "Ştergere cache cărţi"
STR_SAVE_CLIPPING: "Salvează selecţia"
STR_DELETE: "Ştergeți"
STR_DISPLAY_QR: "Afişați pagina ca cod QR"
STR_CHAPTER_PREFIX: "Capitol: "
Expand Down
2 changes: 2 additions & 0 deletions lib/I18n/translations/russian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ STR_BATTERY: "Батарея"
STR_UI_THEME: "Тема интерфейса"
STR_THEME_CLASSIC: "Классическая"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
Expand Down Expand Up @@ -257,6 +258,7 @@ STR_GO_TO_PERCENT: "Перейти к %"
STR_GO_HOME_BUTTON: "На главную"
STR_SYNC_PROGRESS: "Синхронизировать прогресс"
STR_DELETE_CACHE: "Удалить кэш книги"
STR_SAVE_CLIPPING: "Сохранить фрагмент"
STR_DELETE: "Удалить"
STR_CHAPTER_PREFIX: "Глава: "
STR_DISPLAY_QR: "Показать страницу в виде QR-кода"
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/slovenian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ STR_GO_TO_PERCENT: "Pojdi na %"
STR_GO_HOME_BUTTON: "Pojdi domov"
STR_SYNC_PROGRESS: "Sinhroniziraj napredek"
STR_DELETE_CACHE: "Izbriši predpomnilnik knjige"
STR_SAVE_CLIPPING: "Shrani odlomek"
STR_DELETE: "Izbriši"
STR_DISPLAY_QR: "Prikaži stran kot QR"
STR_CHAPTER_PREFIX: "Poglavje: "
Expand Down
2 changes: 2 additions & 0 deletions lib/I18n/translations/spanish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ STR_UI_THEME: "Interfaz"
STR_THEME_CLASSIC: "Clásico"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extendido"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_SUNLIGHT_FADING_FIX: "Corrección de desvanecimiento"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_OPDS_BROWSER: "Navegador OPDS"
Expand Down Expand Up @@ -254,6 +255,7 @@ STR_GO_TO_PERCENT: "Ir a %"
STR_GO_HOME_BUTTON: "Volver al menú Inicio"
STR_SYNC_PROGRESS: "Sincronizar progreso de lectura"
STR_DELETE_CACHE: "Borrar caché del libro"
STR_SAVE_CLIPPING: "Guardar selección"
STR_DELETE: "Borrar"
STR_DISPLAY_QR: "Mostrar página como QR"
STR_CHAPTER_PREFIX: "Cap.: "
Expand Down
9 changes: 8 additions & 1 deletion lib/I18n/translations/swedish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ STR_BATTERY: "Batteri"
STR_UI_THEME: "Användargränssnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
Expand Down Expand Up @@ -258,6 +259,7 @@ STR_GO_TO_PERCENT: "Gå till %"
STR_GO_HOME_BUTTON: "Gå Hem"
STR_SYNC_PROGRESS: "Synkroniseringsframsteg"
STR_DELETE_CACHE: "Radera bokcache"
STR_SAVE_CLIPPING: "Spara klipp"
STR_DELETE: "Radera"
STR_DISPLAY_QR: "Visa sida som QR-kod"
STR_CHAPTER_PREFIX: "Kapitel:"
Expand Down Expand Up @@ -294,6 +296,12 @@ STR_FOOTNOTES: "Fotnoter"
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
STR_LINK: "[länk]"
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
STR_ADD_SERVER: "Lägg till server"
STR_SERVER_NAME: "Servernamn"
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
STR_DELETE_SERVER: "Ta bort server"
STR_DELETE_CONFIRM: "Vill du ta bort den här servern?"
STR_OPDS_SERVERS: "OPDS-servrar"
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_CRASH_TITLE: "Systemkrasch"
Expand All @@ -318,6 +326,5 @@ STR_KB_HINT_URL_SNIPPETS: "Tryck på URL för URL-fragment"
STR_REMAP_FRONT_BUTTONS_READER: "Ändra frontknappar"
STR_READING_STATS: "Lässtatistik"
STR_BOOKMARKS: "Bokmärken"
STR_OPDS_SERVERS: "OPDS-servrar"
STR_LONG_PRESS_MENU_ACTION: "Menyåtgärd vid långtryck"
STR_CYCLE_PAGE_TURN: "Automatisk vändning"
1 change: 1 addition & 0 deletions lib/I18n/translations/turkish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ STR_GO_TO_PERCENT: "%'ye git"
STR_GO_HOME_BUTTON: "Ana Sayfaya Git"
STR_SYNC_PROGRESS: "Okuma İlerlemesini Senkronize Et"
STR_DELETE_CACHE: "Kitap Önbelleğini Sil"
STR_SAVE_CLIPPING: "Seçimi Kaydet"
STR_CHAPTER_PREFIX: "Bölüm: "
STR_PAGES_SEPARATOR: " sayfa | "
STR_BOOK_PREFIX: "Kitap: "
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/ukrainian.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ STR_GO_TO_PERCENT: "Перейти до %"
STR_GO_HOME_BUTTON: "На головну"
STR_SYNC_PROGRESS: "Прогрес синхронізації"
STR_DELETE_CACHE: "Видалити кеш книги"
STR_SAVE_CLIPPING: "Зберегти уривок"
STR_DELETE: "Видалити"
STR_DISPLAY_QR: "Показати сторінку як QR-код"
STR_CHAPTER_PREFIX: "Розділ: "
Expand Down
10 changes: 10 additions & 0 deletions lib/hal/HalDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
}

void HalDisplay::displayWindow(int x, int y, int w, int h) {
if (x < 0) x = 0;
if (y < 0) y = 0;
if (w <= 0 || h <= 0 || x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT) return;
if (x + w > DISPLAY_WIDTH) w = DISPLAY_WIDTH - x;
if (y + h > DISPLAY_HEIGHT) h = DISPLAY_HEIGHT - y;
einkDisplay.displayWindow(static_cast<uint16_t>(x), static_cast<uint16_t>(y), static_cast<uint16_t>(w),
static_cast<uint16_t>(h));
Comment on lines +61 to +68
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clip top/left overflow instead of shifting the window.

When x or y is negative, this resets the origin to 0 but leaves w/h unchanged. A request like (-10, 5, 20, 8) then refreshes 20 px from 0 instead of the visible 10 px intersection, which can redraw the wrong region during partial updates.

Suggested fix
 void HalDisplay::displayWindow(int x, int y, int w, int h) {
-  if (x < 0) x = 0;
-  if (y < 0) y = 0;
+  if (x < 0) {
+    w += x;
+    x = 0;
+  }
+  if (y < 0) {
+    h += y;
+    y = 0;
+  }
   if (w <= 0 || h <= 0 || x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT) return;
   if (x + w > DISPLAY_WIDTH) w = DISPLAY_WIDTH - x;
   if (y + h > DISPLAY_HEIGHT) h = DISPLAY_HEIGHT - y;
   einkDisplay.displayWindow(static_cast<uint16_t>(x), static_cast<uint16_t>(y), static_cast<uint16_t>(w),
                             static_cast<uint16_t>(h));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/hal/HalDisplay.cpp` around lines 61 - 68, The clipping logic in
HalDisplay::displayWindow incorrectly shifts negative x/y to 0 without reducing
w/h, causing the wrong region to be drawn; update HalDisplay::displayWindow to
compute the clipped origin and size by subtracting the amount clipped on the
left/top (e.g., if x<0, reduce w by -x and set x=0; similarly for y), then clamp
w/h to the remaining visible area and return early if resulting w<=0 or h<=0
before calling einkDisplay.displayWindow with the adjusted uint16_t values;
ensure you still handle right/bottom overflow (x+w>DISPLAY_WIDTH,
y+h>DISPLAY_HEIGHT) after the left/top clipping.

}

void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
Expand Down
1 change: 1 addition & 0 deletions lib/hal/HalDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class HalDisplay {
bool fromProgmem = false) const;

void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
void displayWindow(int x, int y, int w, int h);
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);

// Power management
Expand Down
Loading