From 1db14423194acfeb707f2514256fe910de68bf01 Mon Sep 17 00:00:00 2001 From: Uri Tauber Date: Tue, 23 Jun 2026 21:55:01 +0300 Subject: [PATCH] fix: several bookmarks UX improvments (#2372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR enhances the EPUB reader's bookmark system with two complementary improvements: a per-page bookmark indicator icon and toggle behavior on the existing long-press action. --- ### What Changed **Bookmark Toggle (was: add-only)** The long-press Confirm action now toggles bookmarks rather than always adding. `addBookmark()` checks whether a bookmark with the same xpath already exists in the in-memory cache: - If found → removes it and shows "Bookmark removed." - If not found → adds it and shows "Bookmark added." A new `STR_BOOKMARK_REMOVED` translation string was added to support the removal message. **Bookmark Icon Indicator** A `BookmarkIcon` is now drawn at the top-right corner of the page whenever the current page has a bookmark. `updateBookmarkFlag()` is called at render time to determine whether the current page is bookmarked. **In-Memory Bookmark Cache** Bookmarks are now loaded into `cachedBookmarks` on `onEnter()` rather than being re-read from disk on every toggle. All subsequent add/remove operations work against this cache and flush to disk, avoiding redundant file reads on each bookmark action. **Faster bookmarks list** Previously, calculating "page X/Y" for each entry required decompressing the entire spine item. We now persist `si`/`pc`/`pp` (spine index, page count, and page progress) in the bookmark JSON when saving, and restore them when loading. This avoids the expensive `toCrossPoint()` loop in `onEnter()`, significantly reducing the cost of initializing the bookmarks list. --- ### Files Changed - `EpubReaderActivity.cpp` — `addBookmark()` toggle logic, `updateBookmarkFlag()` (new), icon rendering in `renderContents()`, cache initialization in `onEnter()` - `EpubReaderActivity.h` — new fields: `currentPageBookmarked`, `bookmarkRemoved`, `cachedBookmarks`; new method declaration `updateBookmarkFlag()` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_ --------- Co-authored-by: Julia Nguyen --- lib/I18n/translations/belarusian.yaml | 3 + lib/I18n/translations/catalan.yaml | 5 +- lib/I18n/translations/czech.yaml | 3 + lib/I18n/translations/danish.yaml | 3 + lib/I18n/translations/dutch.yaml | 3 + lib/I18n/translations/english.yaml | 5 +- lib/I18n/translations/finnish.yaml | 3 + lib/I18n/translations/french.yaml | 3 + lib/I18n/translations/german.yaml | 5 +- lib/I18n/translations/hebrew.yaml | 5 +- lib/I18n/translations/hungarian.yaml | 3 + lib/I18n/translations/italian.yaml | 7 +- lib/I18n/translations/kazakh.yaml | 3 + lib/I18n/translations/lithuanian.yaml | 3 + lib/I18n/translations/polish.yaml | 3 + lib/I18n/translations/portuguese.yaml | 3 + lib/I18n/translations/romanian.yaml | 3 + lib/I18n/translations/russian.yaml | 5 +- lib/I18n/translations/slovak.yaml | 11 +- lib/I18n/translations/slovenian.yaml | 3 + lib/I18n/translations/spanish.yaml | 5 +- lib/I18n/translations/swedish.yaml | 5 +- lib/I18n/translations/turkish.yaml | 3 + lib/I18n/translations/ukrainian.yaml | 5 +- lib/I18n/translations/valencian.yaml | 5 +- lib/I18n/translations/vietnamese.yaml | 5 +- src/CrossPointSettings.h | 4 +- src/JsonSettingsIO.cpp | 6 + src/activities/reader/EpubReaderActivity.cpp | 140 +++++++++++++----- src/activities/reader/EpubReaderActivity.h | 6 + .../reader/EpubReaderBookmarksActivity.cpp | 30 ++-- .../reader/EpubReaderMenuActivity.cpp | 14 +- .../reader/EpubReaderMenuActivity.h | 5 +- src/components/icons/bookmark.h | 5 + src/components/themes/BaseTheme.cpp | 42 +++++- src/components/themes/BaseTheme.h | 2 +- 36 files changed, 272 insertions(+), 92 deletions(-) diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 8ce3e024..3ba890b8 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -176,6 +176,9 @@ STR_EXIT: "« Выхад" STR_HOME: "« Галоўная" STR_SELECT: "Абраць" STR_TOGGLE: "Выбар" +STR_TOGGLE_BOOKMARK: "Пераключыць закладку" +STR_BOOKMARK_REMOVED: "Закладка выдалена." +STR_HOLD_OPEN_TO_DELETE: "Утрымлівайце Адкрыць, каб выдаліць" STR_CONFIRM: "Пацв." STR_CANCEL: "Адмена" STR_CONNECT: "Падкл." diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index a3c527ef..7f3680cb 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -175,8 +175,7 @@ STR_DOWNLOADING: "S'està baixant..." STR_DOWNLOAD_FAILED: "Ha fallat la baixada" STR_ERROR_MSG: "Error:" STR_UNNAMED: "Sense nom" -STR_HOLD_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar" -STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre." +STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar" STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor" STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" @@ -192,6 +191,7 @@ STR_HOME: "« Inici" STR_SELECT: "Selecciona" STR_SELECTED: "Seleccionat" STR_TOGGLE: "Canvia" +STR_TOGGLE_BOOKMARK: "Commuta punt de llibre" STR_CONFIRM: "Confirma" STR_CANCEL: "Cancel·la" STR_CONNECT: "Connecta" @@ -236,6 +236,7 @@ STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps" STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals" STR_BOOKMARKS: "Punts de llibre" STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre." +STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre." STR_OPDS_BROWSER: "Navegador OPDS" STR_COVER_CUSTOM: "Portada + Personalitzat" STR_QUICK_RESUME: "Represa ràpida" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index f3836652..c46b5ab9 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -181,6 +181,9 @@ STR_EXIT: "« Konec" STR_HOME: "« Domů" STR_SELECT: "Vybrat" STR_TOGGLE: "Přepnout" +STR_TOGGLE_BOOKMARK: "Přepnout záložku" +STR_BOOKMARK_REMOVED: "Záložka odstraněna." +STR_HOLD_OPEN_TO_DELETE: "Podržte Otevřít pro smazání" STR_CONFIRM: "Potvrdit" STR_CANCEL: "Zrušit" STR_CONNECT: "Připojit" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 2d05aeb9..e6f96b45 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -187,6 +187,9 @@ STR_HOME: "« Hjem" STR_SELECT: "Vælg" STR_SELECTED: "Valgt" STR_TOGGLE: "Skift" +STR_TOGGLE_BOOKMARK: "Skift bogmærke" +STR_BOOKMARK_REMOVED: "Bogmærke fjernet." +STR_HOLD_OPEN_TO_DELETE: "Hold Åbn nede for at slette" STR_CONFIRM: "Bekræft" STR_CANCEL: "Annuller" STR_CONNECT: "Forbind" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 4f445be7..d9461477 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -187,6 +187,9 @@ STR_HOME: "« Home" STR_SELECT: "Kies" STR_SELECTED: "Geselecteerd" STR_TOGGLE: "Wissel" +STR_TOGGLE_BOOKMARK: "Bladwijzer wisselen" +STR_BOOKMARK_REMOVED: "Bladwijzer verwijderd." +STR_HOLD_OPEN_TO_DELETE: "Houd Openen ingedrukt om te verwijderen" STR_CONFIRM: "Bevestig" STR_CANCEL: "Annuleer" STR_CONNECT: "Verbind" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 0b530755..96ebbdd3 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -179,8 +179,7 @@ STR_DOWNLOADING: "Downloading..." STR_DOWNLOAD_FAILED: "Download failed" STR_ERROR_MSG: "Error:" STR_UNNAMED: "Unnamed" -STR_HOLD_CONFIRM_TO_DELETE: "Hold Confirm to Delete" -STR_BOOKMARK_INSTRUCTIONS: "Hold Confirm from the reader to create a bookmark." +STR_HOLD_OPEN_TO_DELETE: "Hold Open to Delete" STR_NO_SERVER_URL: "No server URL configured" STR_FETCH_FEED_FAILED: "Failed to fetch feed" STR_PARSE_FEED_FAILED: "Failed to parse feed" @@ -198,6 +197,7 @@ STR_HOME: "« Home" STR_SELECT: "Select" STR_SELECTED: "Selected" STR_TOGGLE: "Toggle" +STR_TOGGLE_BOOKMARK: "Toggle Bookmark" STR_CONFIRM: "Confirm" STR_CANCEL: "Cancel" STR_CONNECT: "Connect" @@ -260,6 +260,7 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_BOOKMARKS: "Bookmarks" STR_BOOKMARK_ADDED: "Bookmark added." +STR_BOOKMARK_REMOVED: "Bookmark removed." STR_OPDS_BROWSER: "OPDS Browser" STR_SEARCH: "Search" STR_COVER_CUSTOM: "Cover + Custom" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 7401587d..3ccaa39c 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -181,6 +181,9 @@ STR_EXIT: "« Poistu" STR_HOME: "« Koti" STR_SELECT: "Valitse" STR_TOGGLE: "Vaihda" +STR_TOGGLE_BOOKMARK: "Vaihda kirjanmerkki" +STR_BOOKMARK_REMOVED: "Kirjanmerkki poistettu." +STR_HOLD_OPEN_TO_DELETE: "Pidä Avaa painettuna poistaaksesi" STR_CONFIRM: "Vahvista" STR_CANCEL: "Peruuta" STR_CONNECT: "Yhdistä" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 867e5714..9d9e32cd 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -187,6 +187,9 @@ STR_HOME: "« Accueil" STR_SELECT: "OK" STR_SELECTED: "Sélectionné" STR_TOGGLE: "Modifier" +STR_TOGGLE_BOOKMARK: "Basculer le marque-page" +STR_BOOKMARK_REMOVED: "Marque-page supprimé." +STR_HOLD_OPEN_TO_DELETE: "Maintenir Ouvrir pour supprimer" STR_CONFIRM: "Confirmer" STR_CANCEL: "Annuler" STR_CONNECT: "Connecter" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index a8e680e0..a0f57fdd 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -168,8 +168,7 @@ STR_DOWNLOADING: "Herunterladen…" STR_DOWNLOAD_FAILED: "Ladefehler" STR_ERROR_MSG: "Fehler:" STR_UNNAMED: "Unbenannt" -STR_HOLD_CONFIRM_TO_DELETE: "Halte Bestätigen zum Löschen" -STR_BOOKMARK_INSTRUCTIONS: "Halte Bestätigen im Lesemodus um ein Lesezeichen anzulegen." +STR_HOLD_OPEN_TO_DELETE: "Halte Öffnen zum Löschen" STR_NO_SERVER_URL: "Keine Server-URL konfiguriert" STR_FETCH_FEED_FAILED: "Feedfehler" STR_PARSE_FEED_FAILED: "Feed-Format ungültig" @@ -187,6 +186,7 @@ STR_HOME: "« Start" STR_SELECT: "Auswahl" STR_SELECTED: "Ausgewählt" STR_TOGGLE: "Ändern" +STR_TOGGLE_BOOKMARK: "Lesezeichen umschalten" STR_CONFIRM: "Bestätigen" STR_CANCEL: "Abbrechen" STR_CONNECT: "Verbinden" @@ -250,6 +250,7 @@ STR_QUICK_RESUME_TIMEOUT: "Schnelles Fortsetzen nach Timeout" STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen" STR_BOOKMARKS: "Lesezeichen" STR_BOOKMARK_ADDED: "Lesezeichen hinzugefügt." +STR_BOOKMARK_REMOVED: "Lesezeichen entfernt." STR_SEARCH: "Suche" STR_OPDS_BROWSER: "OPDS-Browser" STR_COVER_CUSTOM: "Cover + Eigenes" diff --git a/lib/I18n/translations/hebrew.yaml b/lib/I18n/translations/hebrew.yaml index f519e0b8..210ca2ad 100644 --- a/lib/I18n/translations/hebrew.yaml +++ b/lib/I18n/translations/hebrew.yaml @@ -189,6 +189,7 @@ STR_HOME: "מסך הבית »" STR_SELECT: "בחר" STR_SELECTED: "נבחר" STR_TOGGLE: "בחר" +STR_TOGGLE_BOOKMARK: "הוסף/הסר סימנייה" STR_CONFIRM: "אישור" STR_CANCEL: "ביטול" STR_CONNECT: "התחבר" @@ -360,8 +361,7 @@ STR_FRONT_BTN_FOLLOW_ORIENTATION: "התאמת לחצנים קדמיים לכיו STR_REMOVE_READ_FROM_RECENTS: "הסר ספרים שנקראו מרשימת האחרונים" STR_MOVE_FINISHED_TO_READ: "העבר ספרים שהסתיימו לתיקיית 'נקראו'" STR_DISABLED: "מבוטל" -STR_HOLD_CONFIRM_TO_DELETE: "החזק לחוץ על אישור כדי למחוק" -STR_BOOKMARK_INSTRUCTIONS: "החזק לחוץ על כפתור אישור בזמן הקריאה כדי להוסיף סימנייה" +STR_HOLD_OPEN_TO_DELETE: "לחיצה ארוכה על 'פתח' כדי למחוק" STR_CLOCK: "שעון" STR_CLOCK_UTC_OFFSET: "הפרש זמן UTC" STR_CLOCK_FORMAT: "תבנית השעון" @@ -379,6 +379,7 @@ STR_CLOCK_SYNC_NO_WIFI_HINT: "התחבר תחילה לרשת אלחוטית, ו STR_CLOCK_SYNCED: "השעון סונכרן" STR_BOOKMARKS: "סימניות" STR_BOOKMARK_ADDED: "הסימנייה התווספה" +STR_BOOKMARK_REMOVED: "הסימנייה הוסרה" STR_QUICK_RESUME: "חזרה מהירה" STR_REMOVE_FROM_RECENTS: "להסיר מרשימת הספרים האחרונים?" STR_CONFIRM_DELETE_BOOKMARK: "למחוק סימנייה זו?" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 033c7bf2..14a5358e 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -184,6 +184,9 @@ STR_HOME: "« Főoldal" STR_SELECT: "Kiválasztás" STR_SELECTED: "Kiválasztva" STR_TOGGLE: "Váltás" +STR_TOGGLE_BOOKMARK: "Könyvjelző váltása" +STR_BOOKMARK_REMOVED: "Könyvjelző eltávolítva." +STR_HOLD_OPEN_TO_DELETE: "Tartsa lenyomva a Megnyitás gombot a törléshez" STR_CONFIRM: "Megerősítés" STR_CANCEL: "Mégse" STR_CONNECT: "Csatlakozás" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index acd7cc50..9ef433b6 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -192,6 +192,7 @@ STR_HOME: "« Home" STR_SELECT: "Seleziona" STR_SELECTED: "Selezionato" STR_TOGGLE: "Cambia" +STR_TOGGLE_BOOKMARK: "Attiva/disattiva segnalibro" STR_CONFIRM: "Conferma" STR_CANCEL: "Annulla" STR_CONNECT: "Connetti" @@ -357,9 +358,9 @@ STR_FIRMWARE_WRITE_FAILED: "Aggiornamento firmware non riuscito" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Non spegnere il dispositivo!" STR_RECOVERY_MODE: "Modalità ripristino" STR_RECOVERY_MODE_HINT: "Metti firmware.bin nella scheda SD e selezionalo" -STR_BOOKMARK_INSTRUCTIONS: "Tieni premuto Conferma nel lettore per creare un segnalibro" STR_BOOKMARKS: "Segnalibri" STR_BOOKMARK_ADDED: "Segnalibro aggiunto" +STR_BOOKMARK_REMOVED: "Segnalibro rimosso" STR_CONFIRM_DELETE_BOOKMARK: "Eliminare questo segnalibro?" STR_CLOCK_SYNC_NO_WIFI_HINT: "Connettiti prima al Wi-Fi e poi riprova" STR_CLOCK_SYNC_OK: "Orologio sincronizzato" @@ -374,10 +375,10 @@ STR_CLOCK_SYNCING: "Sincronizzazione con il server NTP..." STR_CLOCK_SYNC_FAIL: "Sincronizzazione non riuscita" STR_CLOCK_SYNC_NOW: "Sincronizza l'orologio adesso" STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso" -STR_HOLD_CONFIRM_TO_DELETE: "Tieni premuto Conferma per cancellare" +STR_HOLD_OPEN_TO_DELETE: "Tieni premuto Apri per eliminare" STR_NEXT_FIELD: "Succ." STR_CURRENT_TIME: "Ora attuale: " STR_DISABLED: "Disattivato" STR_BOOKMARK_OPTION: "Segnalibro" STR_KOSYNC: "KOSync" -STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note" \ No newline at end of file +STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 6508bf77..dfccc77b 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -177,6 +177,9 @@ STR_EXIT: "« Шығу" STR_HOME: "« Басты" STR_SELECT: "Таңдау" STR_TOGGLE: "Ауыстыру" +STR_TOGGLE_BOOKMARK: "Бетбелгіні ауыстыру" +STR_BOOKMARK_REMOVED: "Бетбелгі жойылды." +STR_HOLD_OPEN_TO_DELETE: "Жою үшін Ашу түймесін ұстап тұрыңыз" STR_CONFIRM: "Растау" STR_CANCEL: "Болдырмау" STR_CONNECT: "Қосылу" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 42bcd64e..d3c43ed3 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -184,6 +184,9 @@ STR_HOME: "« Pradžia" STR_SELECT: "Rinktis" STR_SELECTED: "Pasirinkta" STR_TOGGLE: "Keisti" +STR_TOGGLE_BOOKMARK: "Perjungti žymę" +STR_BOOKMARK_REMOVED: "Žymė pašalinta." +STR_HOLD_OPEN_TO_DELETE: "Laikykite Atidaryti, kad ištrintumėte" STR_CONFIRM: "Gerai" STR_CANCEL: "Atšaukti" STR_CONNECT: "Jungtis" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 0a384260..ba46e715 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -191,6 +191,9 @@ STR_HOME: "« Home" STR_SELECT: "Wybierz" STR_SELECTED: "Wybrano" STR_TOGGLE: "Zmień" +STR_TOGGLE_BOOKMARK: "Przełącz zakładkę" +STR_BOOKMARK_REMOVED: "Zakładka usunięta." +STR_HOLD_OPEN_TO_DELETE: "Przytrzymaj Otwórz, aby usunąć" STR_CONFIRM: "Potwierdź" STR_CANCEL: "Anuluj" STR_CONNECT: "Połącz" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 2f5511d0..516651ab 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -181,6 +181,9 @@ STR_EXIT: "« Sair" STR_HOME: "« Início" STR_SELECT: "Escolher" STR_TOGGLE: "Alternar" +STR_TOGGLE_BOOKMARK: "Alternar marcador" +STR_BOOKMARK_REMOVED: "Marcador removido." +STR_HOLD_OPEN_TO_DELETE: "Mantenha Abrir pressionado para excluir" STR_CONFIRM: "Confirmar" STR_CANCEL: "Cancelar" STR_CONNECT: "Conectar" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index e62c5f76..67052d10 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -187,6 +187,9 @@ STR_HOME: "« Acasă" STR_SELECT: "Selectează" STR_SELECTED: "Selectat" STR_TOGGLE: "Schimbă" +STR_TOGGLE_BOOKMARK: "Comută marcajul" +STR_BOOKMARK_REMOVED: "Marcaj eliminat." +STR_HOLD_OPEN_TO_DELETE: "Țineți apăsat Deschideți pentru a șterge" STR_CONFIRM: "Confirmă" STR_CANCEL: "Anulare" STR_CONNECT: "Conectare" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index ae7e9f67..ff1187d4 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -180,8 +180,7 @@ STR_DOWNLOADING: "Загрузка..." STR_DOWNLOAD_FAILED: "Ошибка загрузки" STR_ERROR_MSG: "Ошибка:" STR_UNNAMED: "Без имени" -STR_HOLD_CONFIRM_TO_DELETE: "Удерживайте ОТКРЫТЬ для удаления закладки" -STR_BOOKMARK_INSTRUCTIONS: "Удерживайте ВЫБРАТЬ для добавления закладки" +STR_HOLD_OPEN_TO_DELETE: "Удерживайте Открыть для удаления" STR_NO_SERVER_URL: "URL сервера не настроен" STR_FETCH_FEED_FAILED: "Не удалось получить ленту" STR_PARSE_FEED_FAILED: "Не удалось обработать ленту" @@ -199,6 +198,7 @@ STR_HOME: "« Главная" STR_SELECT: "Выбрать" STR_SELECTED: "Выбран" STR_TOGGLE: "Выбор" +STR_TOGGLE_BOOKMARK: "Переключить закладку" STR_CONFIRM: "Подтв." STR_CANCEL: "Отмена" STR_CONNECT: "Подкл." @@ -261,6 +261,7 @@ STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания" STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки" STR_BOOKMARKS: "Закладки" STR_BOOKMARK_ADDED: "Закладка добавлена" +STR_BOOKMARK_REMOVED: "Закладка удалена" STR_OPDS_BROWSER: "OPDS браузер" STR_SEARCH: "Поиск" STR_COVER_CUSTOM: "Обложка + Свой" diff --git a/lib/I18n/translations/slovak.yaml b/lib/I18n/translations/slovak.yaml index ad329a6a..b3210422 100644 --- a/lib/I18n/translations/slovak.yaml +++ b/lib/I18n/translations/slovak.yaml @@ -176,8 +176,7 @@ STR_DOWNLOADING: "Sťahovanie..." STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo" STR_ERROR_MSG: "Chyba:" STR_UNNAMED: "Nepomenované" -STR_HOLD_CONFIRM_TO_DELETE: "Podrž potvrdiť pre vymazanie" -STR_BOOKMARK_INSTRUCTIONS: "Podrž tlačidlo Potvrdiť pre vytvorenie záložky." +STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie" STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera" STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo" STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo" @@ -194,7 +193,8 @@ STR_EXIT: "« Koniec" STR_HOME: "« Domov" STR_SELECT: "Vybrať" STR_SELECTED: "Vybrané" -STR_TOGGLE: "Prepnúť" +STR_TOGGLE: "Prepnúť" +STR_TOGGLE_BOOKMARK: "Prepnúť záložku" STR_CONFIRM: "Potvrdiť" STR_CANCEL: "Zrušiť" STR_CONNECT: "Pripojiť" @@ -255,8 +255,9 @@ STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku" STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá" -STR_BOOKMARKS: "Záložky" -STR_BOOKMARK_ADDED: "Záložka pridaná." +STR_BOOKMARKS: "Záložky" +STR_BOOKMARK_ADDED: "Záložka pridaná." +STR_BOOKMARK_REMOVED: "Záložka odstránená." STR_OPDS_BROWSER: "Prehliadač OPDS" STR_SEARCH: "Hľadať" STR_COVER_CUSTOM: "Obálka + Vlastné" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index 109240e3..c01ebc14 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -184,6 +184,9 @@ STR_HOME: "« Domov" STR_SELECT: "Izberi" STR_SELECTED: "Izbrano" STR_TOGGLE: "Preklopi" +STR_TOGGLE_BOOKMARK: "Preklopi zaznamek" +STR_BOOKMARK_REMOVED: "Zaznamek odstranjen." +STR_HOLD_OPEN_TO_DELETE: "Držite Odpri za brisanje" STR_CONFIRM: "Potrdi" STR_CANCEL: "Prekliči" STR_CONNECT: "Poveži" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index b56bdf6a..8156f891 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -178,8 +178,7 @@ STR_DOWNLOADING: "Descargando..." STR_DOWNLOAD_FAILED: "Fallo de descarga" STR_ERROR_MSG: "Error:" STR_UNNAMED: "Sin nombre" -STR_HOLD_CONFIRM_TO_DELETE: "Mantenga pulsado Confirmar para borrar" -STR_BOOKMARK_INSTRUCTIONS: "Mantenga pulsado Confirmar en el lector para crear un marcador." +STR_HOLD_OPEN_TO_DELETE: "Mantenga pulsado Abrir para borrar" STR_NO_SERVER_URL: "No se configuró URL de servidor" STR_FETCH_FEED_FAILED: "Fallo al obtener el feed" STR_PARSE_FEED_FAILED: "Fallo al procesar el feed" @@ -197,6 +196,7 @@ STR_HOME: "« Inicio" STR_SELECT: "Selecc." STR_SELECTED: "Seleccionado" STR_TOGGLE: "Cambiar" +STR_TOGGLE_BOOKMARK: "Alternar marcador" STR_CONFIRM: "Confirmar" STR_CANCEL: "Cancelar" STR_CONNECT: "Conectar" @@ -260,6 +260,7 @@ STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo" STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales" STR_BOOKMARKS: "Marcadores" STR_BOOKMARK_ADDED: "Marcador añadido." +STR_BOOKMARK_REMOVED: "Marcador eliminado." STR_OPDS_BROWSER: "Navegador OPDS" STR_SEARCH: "Buscar" STR_COVER_CUSTOM: "Portada + Pers." diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index a0750943..ad588f19 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -176,8 +176,7 @@ STR_DOWNLOADING: "Laddar ner…" STR_DOWNLOAD_FAILED: "Nedladdning misslyckades" STR_ERROR_MSG: "Fel:" STR_UNNAMED: "Ej namngiven" -STR_HOLD_CONFIRM_TO_DELETE: "Håll ned Bekräfta för att radera" -STR_BOOKMARK_INSTRUCTIONS: "Håll Bekräfta i läsaren för att skapa ett bokmärke." +STR_HOLD_OPEN_TO_DELETE: "Håll ned Öppna för att radera" STR_NO_SERVER_URL: "Ingen serveradress konfigurerad" STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde" STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde" @@ -195,6 +194,7 @@ STR_HOME: "« Hem" STR_SELECT: "Välj " STR_SELECTED: "Vald" STR_TOGGLE: "Växla" +STR_TOGGLE_BOOKMARK: "Växla bokmärke" STR_CONFIRM: "Bekräfta" STR_CANCEL: "Avbryt" STR_CONNECT: "Anslut" @@ -258,6 +258,7 @@ STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout" STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" STR_BOOKMARKS: "Bokmärken" STR_BOOKMARK_ADDED: "Bokmärke tillagt." +STR_BOOKMARK_REMOVED: "Bokmärke borttaget." STR_OPDS_BROWSER: "OPDS-webbläsare" STR_SEARCH: "Sök" STR_COVER_CUSTOM: "Omslag + Valfri" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 197e5f9e..0faa13ec 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -181,6 +181,9 @@ STR_EXIT: "« Çıkış" STR_HOME: "« Ana Sayfa" STR_SELECT: "Seç" STR_TOGGLE: "Değiştir" +STR_TOGGLE_BOOKMARK: "Yer imini değiştir" +STR_BOOKMARK_REMOVED: "Yer imi kaldırıldı." +STR_HOLD_OPEN_TO_DELETE: "Silmek için Aç düğmesini basılı tutun" STR_CONFIRM: "Onayla" STR_CANCEL: "İptal" STR_CONNECT: "Bağlan" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 6b69fab0..c60de68f 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -176,8 +176,7 @@ STR_DOWNLOADING: "Завантаження..." STR_DOWNLOAD_FAILED: "Завантаження не вдалося" STR_ERROR_MSG: "Помилка:" STR_UNNAMED: "Без назви" -STR_HOLD_CONFIRM_TO_DELETE: "Утрим. Підтвердити, щоб Видалити" -STR_BOOKMARK_INSTRUCTIONS: "Утрим. Підтвердити в книзі, щоб створ. закладку" +STR_HOLD_OPEN_TO_DELETE: "Утримуйте Відкрити, щоб видалити" STR_NO_SERVER_URL: "URL сервера не налаштовано" STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку" STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку" @@ -195,6 +194,7 @@ STR_HOME: "« Додому" STR_SELECT: "Вибрати" STR_SELECTED: "Вибрано" STR_TOGGLE: "Обрати" +STR_TOGGLE_BOOKMARK: "Перемкнути закладку" STR_CONFIRM: "Підтвердити" STR_CANCEL: "Скасувати" STR_CONNECT: "Приєдн." @@ -257,6 +257,7 @@ STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на со STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки" STR_BOOKMARKS: "Закладки" STR_BOOKMARK_ADDED: "Закладку додано" +STR_BOOKMARK_REMOVED: "Закладку видалено" STR_OPDS_BROWSER: "Браузер OPDS" STR_SEARCH: "Пошук" STR_COVER_CUSTOM: "Обкл. + власне" diff --git a/lib/I18n/translations/valencian.yaml b/lib/I18n/translations/valencian.yaml index a4c2f147..6dd28a25 100644 --- a/lib/I18n/translations/valencian.yaml +++ b/lib/I18n/translations/valencian.yaml @@ -180,8 +180,7 @@ STR_DOWNLOADING: "S'està baixant..." STR_DOWNLOAD_FAILED: "Ha fallat la baixada" STR_ERROR_MSG: "Error:" STR_UNNAMED: "Sense nom" -STR_HOLD_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar" -STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre." +STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar" STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor" STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" @@ -197,6 +196,7 @@ STR_HOME: "« Inici" STR_SELECT: "Selecciona" STR_SELECTED: "Seleccionat" STR_TOGGLE: "Canvia" +STR_TOGGLE_BOOKMARK: "Canvia punt de llibre" STR_CONFIRM: "Confirma" STR_CANCEL: "Cancel·la" STR_CONNECT: "Connecta" @@ -240,6 +240,7 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol" STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals" STR_BOOKMARKS: "Punts de llibre" STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre." +STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre." STR_OPDS_BROWSER: "Navegador OPDS" STR_COVER_CUSTOM: "Portada + Personalitzat" STR_QUICK_RESUME: "Represa ràpida" diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index 38a23450..2551b275 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -176,8 +176,7 @@ STR_DOWNLOADING: "Đang tải về..." STR_DOWNLOAD_FAILED: "Tải về thất bại" STR_ERROR_MSG: "Lỗi:" STR_UNNAMED: "Không tên" -STR_HOLD_CONFIRM_TO_DELETE: "Giữ Xác nhận để xóa" -STR_BOOKMARK_INSTRUCTIONS: "Giữ Xác nhận trong trình đọc để tạo dấu trang." +STR_HOLD_OPEN_TO_DELETE: "Giữ Mở để xóa" STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ" STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp" STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp" @@ -195,6 +194,7 @@ STR_HOME: "« Thư viện" STR_SELECT: "Chọn" STR_SELECTED: "Đã chọn" STR_TOGGLE: "Bật/Tắt" +STR_TOGGLE_BOOKMARK: "Bật/tắt dấu trang" STR_CONFIRM: "Xác nhận" STR_CANCEL: "Hủy" STR_CONNECT: "Kết nối" @@ -257,6 +257,7 @@ STR_SUNLIGHT_FADING_FIX: "Khắc phục mờ dưới nắng" STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước" STR_BOOKMARKS: "Dấu trang" STR_BOOKMARK_ADDED: "Đã thêm dấu trang." +STR_BOOKMARK_REMOVED: "Đã xóa dấu trang." STR_OPDS_BROWSER: "Trình duyệt OPDS" STR_SEARCH: "Tìm kiếm" STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index d001f066..af274e3c 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -238,8 +238,8 @@ class CrossPointSettings { // Long-press page turn button behavior uint8_t longPressButtonBehavior = OFF; // Long-press Confirm function in EPUB reader (cycles through LONG_PRESS_MENU_FUNCTION values). - // Defaults to Bookmark to preserve the upstream long-press-Confirm-adds-bookmark behavior. - uint8_t longPressMenuFunction = LP_MENU_BOOKMARK; + // Defaults to Disabled so shortcut-based bookmark toggling remains opt-in. + uint8_t longPressMenuFunction = LP_MENU_DISABLED; // UI Theme uint8_t uiTheme = LYRA; // Sunlight fading compensation diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index 4b9e0fc6..6f0a51d2 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -423,6 +423,9 @@ bool JsonSettingsIO::saveBookmarks(const std::vector& bookmarks, obj["xpath"] = bookmark.xpath; obj["percentage"] = bookmark.percentage; obj["summary"] = bookmark.summary; + obj["si"] = bookmark.computedSpineIndex; + obj["pc"] = bookmark.computedChapterPageCount; + obj["pp"] = bookmark.computedChapterProgress; } String json; @@ -447,6 +450,9 @@ bool JsonSettingsIO::loadBookmarks(std::vector& bookmarks, const bookmark.xpath = obj["xpath"] | std::string(""); bookmark.percentage = obj["percentage"] | static_cast(0); bookmark.summary = obj["summary"] | std::string(""); + bookmark.computedSpineIndex = obj["si"] | static_cast(0); + bookmark.computedChapterPageCount = obj["pc"] | static_cast(0); + bookmark.computedChapterProgress = obj["pp"] | static_cast(0); } LOG_DBG("BKM", "Loaded %zu bookmarks from file", bookmarks.size()); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index e1328787..3d2df14e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -41,6 +41,8 @@ namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() // pages per minute, first item is 1 to prevent division by zero if accessed constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12}; +constexpr size_t initialBookmarkCacheCapacity = 16; +constexpr float bookmarkProgressEpsilon = 0.0001f; int clampPercent(int percent) { if (percent < 0) { @@ -63,6 +65,35 @@ bool isInReadFolder(const std::string& path) { return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/'; } +struct ProgressRange { + float start; + float end; +}; + +ProgressRange getPageProgressRange(const std::shared_ptr& epub, const int spineIndex, const int page, + const int pageCount) { + if (pageCount <= 1) { + return {epub->calculateProgress(spineIndex, 0.0f), epub->calculateProgress(spineIndex, 1.0f)}; + } + + const float step = 1.0f / static_cast(pageCount - 1); + const float anchor = std::clamp(static_cast(page) * step, 0.0f, 1.0f); + const float start = std::max(0.0f, anchor - (step * 0.5f)); + const float end = std::min(1.0f, anchor + (step * 0.5f)); + return {epub->calculateProgress(spineIndex, start), epub->calculateProgress(spineIndex, end)}; +} + +bool bookmarkMatchesProgress(const BookmarkEntry& bookmark, const SavedProgressPosition& progress, + const ProgressRange& pageRange) { + if (bookmark.xpath == progress.xpath) { + return true; + } + + const float bookmarkProgress = std::clamp(bookmark.percentage, 0.0f, 1.0f); + return bookmarkProgress + bookmarkProgressEpsilon >= pageRange.start && + bookmarkProgress - bookmarkProgressEpsilon <= pageRange.end; +} + // Pick a non-colliding destination path inside /Read/ for a finished book. // Mirrors the suffixing scheme used elsewhere: "name.epub" -> "name (2).epub", etc. std::string buildReadFolderDestination(const std::string& srcPath) { @@ -165,6 +196,8 @@ void EpubReaderActivity::onEnter() { APP_STATE.saveToFile(); RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath()); + loadCachedBookmarks(); + // Trigger first update requestUpdate(); } @@ -281,7 +314,7 @@ void EpubReaderActivity::loop() { const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); startActivityForResult(std::make_unique( renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, - SETTINGS.orientation, !currentPageFootnotes.empty()), + SETTINGS.orientation, !currentPageFootnotes.empty(), !cachedBookmarks.empty()), [this](const ActivityResult& result) { // Always apply orientation change even if the menu was cancelled const auto& menu = std::get(result.data); @@ -500,6 +533,7 @@ void EpubReaderActivity::jumpToPercent(int percent) { void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { auto progressChangeResultHandler = [this](const ActivityResult& result) { + loadCachedBookmarks(); if (!result.isCancelled) { const auto& sync = std::get(result.data); if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { @@ -615,6 +649,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction progressChangeResultHandler); break; } + case EpubReaderMenuActivity::MenuAction::TOGGLE_BOOKMARK: { + addBookmark(); + break; + } } } @@ -902,6 +940,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { return; } + updateBookmarkFlag(); + { auto p = section->loadPageFromSectionFile(); if (!p) { @@ -933,7 +973,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { } if (showBookmarkMessage) { - GUI.drawPopup(renderer, tr(STR_BOOKMARK_ADDED)); + GUI.drawPopup(renderer, bookmarkRemoved ? tr(STR_BOOKMARK_REMOVED) : tr(STR_BOOKMARK_ADDED)); } } @@ -1172,7 +1212,7 @@ void EpubReaderActivity::renderStatusBar() const { title = epub->getTitle(); } - GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset); + GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked); } void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { @@ -1234,11 +1274,31 @@ void EpubReaderActivity::restoreSavedPosition() { requestUpdate(); } +void EpubReaderActivity::loadCachedBookmarks() { + cachedBookmarks.clear(); + if (cachedBookmarks.capacity() < initialBookmarkCacheCapacity) { + cachedBookmarks.reserve(initialBookmarkCacheCapacity); + } + if (!epub) { + currentPageBookmarked = false; + return; + } + + const std::string bmPath = BookmarkUtil::getBookmarkPath(epub->getPath()); + if (Storage.exists(bmPath.c_str())) { + String json = Storage.readFile(bmPath.c_str()); + if (!json.isEmpty()) { + JsonSettingsIO::loadBookmarks(cachedBookmarks, json.c_str()); + } + } + updateBookmarkFlag(); +} + void EpubReaderActivity::addBookmark() { if (!section || !epub) { return; } - LOG_DBG("ERS", "Adding bookmark at spine %d, page %d", currentSpineIndex, section ? section->currentPage : -1); + LOG_DBG("ERS", "Toggle bookmark at spine %d, page %d", currentSpineIndex, section ? section->currentPage : -1); int currentPage; int pageCount; { @@ -1247,45 +1307,57 @@ void EpubReaderActivity::addBookmark() { currentPage = section->currentPage; } - std::string pageText; - if (currentPage >= 0 && currentPage < pageCount) { - pageText = section->getTextFromSectionFile(); + SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition()); + 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()); + if (cachedBookmarks.size() != bookmarkCountBeforeToggle) { + bookmarkRemoved = true; + currentPageBookmarked = false; + } else { + std::string pageText; + if (currentPage >= 0 && currentPage < pageCount) { + pageText = section->getTextFromSectionFile(); + } + BookmarkEntry entry; + entry.percentage = progress.percentage; + entry.xpath = progress.xpath; + entry.summary = BookmarkUtil::sanitizeBookmarkSummary(pageText); + entry.computedSpineIndex = currentSpineIndex; + entry.computedChapterPageCount = pageCount; + entry.computedChapterProgress = currentPage; + cachedBookmarks.insert(cachedBookmarks.begin(), entry); + bookmarkRemoved = false; + currentPageBookmarked = true; } - SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition()); - - BookmarkEntry entry; - entry.percentage = progress.percentage; - entry.xpath = progress.xpath; - entry.summary = BookmarkUtil::sanitizeBookmarkSummary(pageText); - - // Add bookmark const std::string path = BookmarkUtil::getBookmarkPath(epub->getPath()); - LOG_DBG("ERS", "Bookmark path: %s", path.c_str()); const std::string bookmarksDir = BookmarkUtil::getBookmarksDir(); Storage.mkdir(bookmarksDir.c_str()); - std::vector bookmarks; - if (Storage.exists(path.c_str())) { - LOG_DBG("ERS", "Existing bookmark file found, loading bookmarks"); - String json = Storage.readFile(path.c_str()); - if (!json.isEmpty()) { - JsonSettingsIO::loadBookmarks(bookmarks, json.c_str()); - } - } else { - LOG_DBG("ERS", "No existing bookmark file, starting with empty bookmark list"); + const bool ok = JsonSettingsIO::saveBookmarks(cachedBookmarks, path.c_str()); + if (!ok) { + LOG_ERR("ERS", "Failed to save bookmarks to: %s", path.c_str()); } - bookmarks.insert(bookmarks.begin(), entry); - LOG_DBG("ERS", "Saving bookmark to file: %s", path.c_str()); - const bool ok = JsonSettingsIO::saveBookmarks(bookmarks, path.c_str()); - if (ok) { - showBookmarkMessage = true; - } else { - LOG_ERR("ERS", "Failed to save bookmark to: %s", path.c_str()); - } - requestUpdate(); } +void EpubReaderActivity::updateBookmarkFlag() { + if (!section || !epub || cachedBookmarks.empty()) { + 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); + }); +} + ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const { ScreenshotInfo info; info.readerType = ScreenshotInfo::ReaderType::Epub; diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index d1b46225..7e2a4ad2 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -5,6 +5,7 @@ #include +#include "BookmarkEntry.h" #include "EpubReaderMenuActivity.h" #include "ProgressMapper.h" #include "activities/Activity.h" @@ -34,6 +35,9 @@ class EpubReaderActivity final : public Activity { bool automaticPageTurnActive = false; bool showBookmarkMessage = false; bool ignoreNextConfirmRelease = false; + bool currentPageBookmarked = false; + bool bookmarkRemoved = false; // true when last toggle removed (controls popup text) + std::vector cachedBookmarks; // Tracks whether this book is currently removed from Recent Books by the // removeReadBooksFromRecents feature (set at End-of-Book, cleared if paged back in). bool recentsEntryRemoved = false; @@ -66,7 +70,9 @@ class EpubReaderActivity final : public Activity { void applyOrientation(uint8_t orientation); void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void pageTurn(bool isForwardTurn); + void loadCachedBookmarks(); void addBookmark(); + void updateBookmarkFlag(); // Footnote navigation void navigateToHref(const std::string& href, bool savePosition = false); diff --git a/src/activities/reader/EpubReaderBookmarksActivity.cpp b/src/activities/reader/EpubReaderBookmarksActivity.cpp index 93c54ade..0b1f3f30 100644 --- a/src/activities/reader/EpubReaderBookmarksActivity.cpp +++ b/src/activities/reader/EpubReaderBookmarksActivity.cpp @@ -39,14 +39,6 @@ void EpubReaderBookmarksActivity::onEnter() { bookmarks.shrink_to_fit(); } else { JsonSettingsIO::loadBookmarks(bookmarks, json.c_str()); - - // pre-compute bookmark page values for quicker rendering - for (auto& bookmark : bookmarks) { - CrossPointPosition pos = ProgressMapper::toCrossPoint(epub, {bookmark.xpath, bookmark.percentage}, renderer); - bookmark.computedSpineIndex = pos.spineIndex; - bookmark.computedChapterPageCount = pos.totalPages; - bookmark.computedChapterProgress = pos.pageNumber; - } } } else { LOG_DBG("EPB", "No bookmark file found at %s, starting with empty bookmarks", path.c_str()); @@ -93,6 +85,14 @@ void EpubReaderBookmarksActivity::loop() { selectorIndex--; } + if (bookmarks.empty()) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); + return; + } + requestUpdate(); confirmingDelete = DELETE_MODE_OFF; return; @@ -186,9 +186,12 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) { auto bookmark = bookmarks.at(confirmingDelete >= DELETE_MODE_DISPLAY ? selectorIndex : index); auto tocIndex = epub->getTocIndexForSpineIndex(bookmark.computedSpineIndex); auto tocTitle = (tocIndex >= 0) ? (epub->getTocItem(tocIndex)).title : tr(STR_UNNAMED); - return std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - " + - std::to_string(bookmark.computedChapterProgress + 1) + "/" + - std::to_string(bookmark.computedChapterPageCount) + " - " + tocTitle; + std::string subtitle = std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - "; + if (bookmark.computedChapterPageCount > 0) { + subtitle += std::to_string(bookmark.computedChapterProgress + 1) + "/" + + std::to_string(bookmark.computedChapterPageCount) + " - "; + } + return subtitle + tocTitle; }; const auto getBookmarkIcon = [isPortrait](int index) { // only enabled icon in portrait mode due to limitation with rotating icons for other orientations @@ -208,11 +211,8 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) { getBookmarkTitle, getBookmarkSubtitle, getBookmarkIcon); GUI.drawHelpText(renderer, Rect{contentX, pageHeight - hintGutterBottom, contentWidth, LINE_HEIGHT}, - tr(STR_HOLD_CONFIRM_TO_DELETE)); + tr(STR_HOLD_OPEN_TO_DELETE)); } - } else { - GUI.drawHelpText(renderer, Rect{contentX, LINE_HEIGHT * 2, contentWidth, LINE_HEIGHT}, - tr(STR_BOOKMARK_INSTRUCTIONS)); } const auto backLabel = confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_CANCEL) : tr(STR_BACK); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 634db50c..69e1fccd 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -10,23 +10,27 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, - const bool hasFootnotes) + const bool hasFootnotes, const bool hasBookmarks) : Activity("EpubReaderMenu", renderer, mappedInput), - menuItems(buildMenuItems(hasFootnotes)), + menuItems(buildMenuItems(hasFootnotes, hasBookmarks)), title(title), pendingOrientation(currentOrientation), currentPage(currentPage), totalPages(totalPages), bookProgressPercent(bookProgressPercent) {} -std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { +std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, + bool hasBookmarks) { std::vector items; - items.reserve(11); + items.reserve(12); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); if (hasFootnotes) { items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); } - items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS}); + if (hasBookmarks) { + items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS}); + } + items.push_back({MenuAction::TOGGLE_BOOKMARK, StrId::STR_TOGGLE_BOOKMARK}); items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}); items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 43272f48..285ab76e 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -18,6 +18,7 @@ class EpubReaderMenuActivity final : public Activity { AUTO_PAGE_TURN, ROTATE_SCREEN, BOOKMARKS, + TOGGLE_BOOKMARK, SCREENSHOT, DISPLAY_QR, GO_HOME, @@ -27,7 +28,7 @@ class EpubReaderMenuActivity final : public Activity { explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, - const uint8_t currentOrientation, const bool hasFootnotes); + const uint8_t currentOrientation, const bool hasFootnotes, bool hasBookmarks); void onEnter() override; void onExit() override; @@ -40,7 +41,7 @@ class EpubReaderMenuActivity final : public Activity { StrId labelId; }; - static std::vector buildMenuItems(bool hasFootnotes); + static std::vector buildMenuItems(bool hasFootnotes, bool hasBookmarks); // Fixed menu layout const std::vector menuItems; diff --git a/src/components/icons/bookmark.h b/src/components/icons/bookmark.h index 9f99e03c..6343332d 100644 --- a/src/components/icons/bookmark.h +++ b/src/components/icons/bookmark.h @@ -10,3 +10,8 @@ static const uint8_t BookmarkIcon[] = { 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// size: 16x16 +static const uint8_t BookmarkStatusIcon[] = {0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, + 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, + 0x0F, 0x78, 0x0E, 0x38, 0x0C, 0x18, 0x08, 0x08, 0x00, 0x00}; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index caa7e6cc..ff7e927d 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -13,6 +13,7 @@ #include "I18n.h" #include "RecentBooksStore.h" #include "components/UITheme.h" +#include "components/icons/bookmark.h" #include "fontIds.h" // Internal constants @@ -20,6 +21,27 @@ namespace { constexpr int homeMenuMargin = 20; constexpr int homeMarginTop = 30; constexpr int subtitleY = 738; +constexpr int bookmarkStatusIconWidth = 16; +constexpr int bookmarkStatusIconHeight = 14; +constexpr int bookmarkStatusIconGap = 4; +constexpr int bookmarkStatusIconTopCrop = 2; + +bool statusBarTextLaneVisible() { + return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || + SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery || + (SETTINGS.statusBarClock && halClock.isAvailable()); +} + +void drawBookmarkStatusIcon(const GfxRenderer& renderer, const int x, const int y) { + constexpr int bytesPerRow = bookmarkStatusIconWidth / 8; + for (int row = 0; row < bookmarkStatusIconHeight; ++row) { + for (int col = 0; col < bookmarkStatusIconWidth; ++col) { + const uint8_t byte = BookmarkStatusIcon[(row + bookmarkStatusIconTopCrop) * bytesPerRow + col / 8]; + const uint8_t mask = 1U << (7 - (col % 8)); + renderer.drawPixel(x + col, y + row, (byte & mask) != 0); + } + } +} } // namespace @@ -727,11 +749,12 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, std::string title, const int paddingBottom, const int textYOffset, - const bool fillMargin) const { + const bool fillMargin, const bool isPageBookmarked) const { auto metrics = UITheme::getInstance().getMetrics(); int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, &orientedMarginLeft); + const bool showStatusBarTextLane = statusBarTextLaneVisible(); // Draw Progress Text const auto screenHeight = renderer.getScreenHeight(); @@ -777,14 +800,24 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c renderer.fillRect(barMarginLeft, progressBarY, barWidth, barHeight, true); } + // Draw Bookmark + const int leftClusterX = metrics.statusBarHorizontalMargin + orientedMarginLeft + 1; + const bool showBookmarkIcon = showStatusBarTextLane && isPageBookmarked; + const int bookmarkReserveWidth = showBookmarkIcon ? (bookmarkStatusIconWidth + bookmarkStatusIconGap) : 0; + if (showBookmarkIcon) { + const int bookmarkY = textY + 5; + drawBookmarkStatusIcon(renderer, leftClusterX, bookmarkY); + } + // Draw Battery const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; + int leftClusterWidth = bookmarkReserveWidth; if (SETTINGS.statusBarBattery) { GUI.drawBatteryLeft(renderer, - Rect{metrics.statusBarHorizontalMargin + orientedMarginLeft + 1, textY, metrics.batteryWidth, - metrics.batteryHeight}, + Rect{leftClusterX + bookmarkReserveWidth, textY, metrics.batteryWidth, metrics.batteryHeight}, showBatteryPercentage); + leftClusterWidth += showBatteryPercentage ? 50 : 20; } // Draw Clock (X3 only — DS3231 RTC) @@ -808,8 +841,7 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c const int rendererableScreenWidth = renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight; - const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0; - const int titleMarginLeft = batterySize + 30; + const int titleMarginLeft = leftClusterWidth + 30; const int clockReserve = clockTextWidth > 0 ? (clockTextWidth + 10) : 0; const int titleMarginRight = progressTextWidth + clockReserve + 30; diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index fbb3feeb..39adb758 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -210,7 +210,7 @@ class BaseTheme { virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const; void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, std::string title, const int paddingBottom = 0, const int textYOffset = 0, - const bool fillMargin = true) const; + const bool fillMargin = true, const bool isPageBookmarked = false) const; void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, int contentStartX = 0, int contentWidth = 0) const;