diff --git a/lib/EpdFont/EpdFont.cpp b/lib/EpdFont/EpdFont.cpp index 5b770462..6d777a3e 100644 --- a/lib/EpdFont/EpdFont.cpp +++ b/lib/EpdFont/EpdFont.cpp @@ -17,6 +17,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star int cursorX = startX; const int cursorY = startY; + int lastBaseX = startX; + int lastBaseAdvance = 0; + int lastBaseTop = 0; + bool hasBaseGlyph = false; + constexpr int MIN_COMBINING_GAP_PX = 1; uint32_t cp; while ((cp = utf8NextCodepoint(reinterpret_cast(&string)))) { const EpdGlyph* glyph = getGlyph(cp); @@ -30,11 +35,30 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star continue; } - *minX = std::min(*minX, cursorX + glyph->left); - *maxX = std::max(*maxX, cursorX + glyph->left + glyph->width); - *minY = std::min(*minY, cursorY + glyph->top - glyph->height); - *maxY = std::max(*maxY, cursorY + glyph->top); - cursorX += glyph->advanceX; + const bool isCombining = utf8IsCombiningMark(cp); + int raiseBy = 0; + if (isCombining && hasBaseGlyph) { + const int currentGap = glyph->top - glyph->height - lastBaseTop; + if (currentGap < MIN_COMBINING_GAP_PX) { + raiseBy = MIN_COMBINING_GAP_PX - currentGap; + } + } + + const int glyphBaseX = (isCombining && hasBaseGlyph) ? (lastBaseX + lastBaseAdvance / 2) : cursorX; + const int glyphBaseY = cursorY - raiseBy; + + *minX = std::min(*minX, glyphBaseX + glyph->left); + *maxX = std::max(*maxX, glyphBaseX + glyph->left + glyph->width); + *minY = std::min(*minY, glyphBaseY + glyph->top - glyph->height); + *maxY = std::max(*maxY, glyphBaseY + glyph->top); + + if (!isCombining) { + lastBaseX = cursorX; + lastBaseAdvance = glyph->advanceX; + lastBaseTop = glyph->top; + hasBaseGlyph = true; + cursorX += glyph->advanceX; + } } } diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index edc01a84..d620d203 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -100,6 +100,15 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo for (size_t i = 0; i < lineCount; ++i) { extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine); } + + // Remove consumed words so size() reflects only remaining words + if (lineCount > 0) { + const size_t consumed = lineBreakIndices[lineCount - 1]; + words.erase(words.begin(), words.begin() + consumed); + wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed); + wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed); + forceBreakAfter.erase(forceBreakAfter.begin(), forceBreakAfter.begin() + consumed); + } } std::vector ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) { @@ -392,11 +401,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl words.insert(words.begin() + wordIndex + 1, remainder); wordStyles.insert(wordStyles.begin() + wordIndex + 1, style); - // The remainder inherits whatever continuation status the original word had with the word after it. - const bool originalContinuedToNext = wordContinues[wordIndex]; - // The original word (now prefix) does NOT continue to remainder (hyphen separates them) - wordContinues[wordIndex] = false; - wordContinues.insert(wordContinues.begin() + wordIndex + 1, originalContinuedToNext); + // Preserve the prefix's attach-to-previous flag; allow a break between prefix and remainder. + wordContinues.insert(wordContinues.begin() + wordIndex + 1, false); // Forced break belongs to the original whole word; transfer it to the remainder (last part). if (!forceBreakAfter.empty()) { diff --git a/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp b/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp index 0a6b7a92..15791ae0 100644 --- a/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp +++ b/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp @@ -174,6 +174,213 @@ std::vector collectCodepoints(const std::string& word) { while (*ptr != 0) { const unsigned char* current = ptr; const uint32_t cp = utf8NextCodepoint(&ptr); + // If this is a combining diacritic (e.g., U+0301 = acute) and there's + // a previous base character that can be composed into a single + // precomposed Unicode scalar (Latin-1 / Latin-Extended), do that + // composition here. This provides lightweight NFC-like behavior for + // common Western European diacritics (acute, grave, circumflex, tilde, + // diaeresis, cedilla) without pulling in a full Unicode normalization + // library. + if (!cps.empty()) { + uint32_t prev = cps.back().value; + uint32_t composed = 0; + switch (cp) { + case 0x0300: // grave + switch (prev) { + case 0x0041: + composed = 0x00C0; + break; // A -> À + case 0x0061: + composed = 0x00E0; + break; // a -> à + case 0x0045: + composed = 0x00C8; + break; // E -> È + case 0x0065: + composed = 0x00E8; + break; // e -> è + case 0x0049: + composed = 0x00CC; + break; // I -> Ì + case 0x0069: + composed = 0x00EC; + break; // i -> ì + case 0x004F: + composed = 0x00D2; + break; // O -> Ò + case 0x006F: + composed = 0x00F2; + break; // o -> ò + case 0x0055: + composed = 0x00D9; + break; // U -> Ù + case 0x0075: + composed = 0x00F9; + break; // u -> ù + default: + break; + } + break; + case 0x0301: // acute + switch (prev) { + case 0x0041: + composed = 0x00C1; + break; // A -> Á + case 0x0061: + composed = 0x00E1; + break; // a -> á + case 0x0045: + composed = 0x00C9; + break; // E -> É + case 0x0065: + composed = 0x00E9; + break; // e -> é + case 0x0049: + composed = 0x00CD; + break; // I -> Í + case 0x0069: + composed = 0x00ED; + break; // i -> í + case 0x004F: + composed = 0x00D3; + break; // O -> Ó + case 0x006F: + composed = 0x00F3; + break; // o -> ó + case 0x0055: + composed = 0x00DA; + break; // U -> Ú + case 0x0075: + composed = 0x00FA; + break; // u -> ú + case 0x0059: + composed = 0x00DD; + break; // Y -> Ý + case 0x0079: + composed = 0x00FD; + break; // y -> ý + default: + break; + } + break; + case 0x0302: // circumflex + switch (prev) { + case 0x0041: + composed = 0x00C2; + break; // A -> Â + case 0x0061: + composed = 0x00E2; + break; // a -> â + case 0x0045: + composed = 0x00CA; + break; // E -> Ê + case 0x0065: + composed = 0x00EA; + break; // e -> ê + case 0x0049: + composed = 0x00CE; + break; // I -> Î + case 0x0069: + composed = 0x00EE; + break; // i -> î + case 0x004F: + composed = 0x00D4; + break; // O -> Ô + case 0x006F: + composed = 0x00F4; + break; // o -> ô + case 0x0055: + composed = 0x00DB; + break; // U -> Û + case 0x0075: + composed = 0x00FB; + break; // u -> û + default: + break; + } + break; + case 0x0303: // tilde + switch (prev) { + case 0x0041: + composed = 0x00C3; + break; // A -> Ã + case 0x0061: + composed = 0x00E3; + break; // a -> ã + case 0x004E: + composed = 0x00D1; + break; // N -> Ñ + case 0x006E: + composed = 0x00F1; + break; // n -> ñ + default: + break; + } + break; + case 0x0308: // diaeresis/umlaut + switch (prev) { + case 0x0041: + composed = 0x00C4; + break; // A -> Ä + case 0x0061: + composed = 0x00E4; + break; // a -> ä + case 0x0045: + composed = 0x00CB; + break; // E -> Ë + case 0x0065: + composed = 0x00EB; + break; // e -> ë + case 0x0049: + composed = 0x00CF; + break; // I -> Ï + case 0x0069: + composed = 0x00EF; + break; // i -> ï + case 0x004F: + composed = 0x00D6; + break; // O -> Ö + case 0x006F: + composed = 0x00F6; + break; // o -> ö + case 0x0055: + composed = 0x00DC; + break; // U -> Ü + case 0x0075: + composed = 0x00FC; + break; // u -> ü + case 0x0059: + composed = 0x0178; + break; // Y -> Ÿ + case 0x0079: + composed = 0x00FF; + break; // y -> ÿ + default: + break; + } + break; + case 0x0327: // cedilla + switch (prev) { + case 0x0043: + composed = 0x00C7; + break; // C -> Ç + case 0x0063: + composed = 0x00E7; + break; // c -> ç + default: + break; + } + break; + default: + break; + } + + if (composed != 0) { + cps.back().value = composed; + continue; // skip pushing the combining mark itself + } + } + cps.push_back({cp, static_cast(current - base)}); } diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 61cea685..8eadebe7 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -174,12 +174,14 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode } } - if constexpr (rotation == TextRotation::Rotated90CW) { - *cursorY -= glyph->advanceX; - } else if constexpr (rotation == TextRotation::Rotated90CCW) { - *cursorY += glyph->advanceX; - } else { - *cursorX += glyph->advanceX; + if (!utf8IsCombiningMark(cp)) { + if constexpr (rotation == TextRotation::Rotated90CW) { + *cursorY -= glyph->advanceX; + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + *cursorY += glyph->advanceX; + } else { + *cursorX += glyph->advanceX; + } } } @@ -241,6 +243,11 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha const EpdFontFamily::Style style) const { int yPos = y + getFontAscenderSize(fontId); int xpos = x; + int lastBaseX = x; + int lastBaseY = yPos; + int lastBaseAdvance = 0; + int lastBaseTop = 0; + bool hasBaseGlyph = false; // cannot draw a NULL / empty string if (text == nullptr || *text == '\0') { @@ -253,9 +260,43 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha return; } const auto& font = fontIt->second; + constexpr int MIN_COMBINING_GAP_PX = 1; uint32_t cp; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + if (utf8IsCombiningMark(cp) && hasBaseGlyph) { + const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); + if (!combiningGlyph) { + combiningGlyph = font.getGlyph(REPLACEMENT_GLYPH, style); + } + + int raiseBy = 0; + if (combiningGlyph) { + const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop; + if (currentGap < MIN_COMBINING_GAP_PX) { + raiseBy = MIN_COMBINING_GAP_PX - currentGap; + } + } + + int combiningX = lastBaseX + lastBaseAdvance / 2; + int combiningY = lastBaseY - raiseBy; + renderChar(font, cp, &combiningX, &combiningY, black, style); + continue; + } + + const EpdGlyph* glyph = font.getGlyph(cp, style); + if (!glyph) { + glyph = font.getGlyph(REPLACEMENT_GLYPH, style); + } + + if (!utf8IsCombiningMark(cp)) { + lastBaseX = xpos; + lastBaseY = yPos; + lastBaseAdvance = glyph ? glyph->advanceX : 0; + lastBaseTop = glyph ? glyph->top : 0; + hasBaseGlyph = true; + } + renderChar(font, cp, &xpos, &yPos, black, style); } } @@ -963,6 +1004,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFo int width = 0; const auto& font = fontIt->second; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + if (utf8IsCombiningMark(cp)) { + continue; + } const EpdGlyph* glyph = font.getGlyph(cp, style); if (!glyph) glyph = font.getGlyph(REPLACEMENT_GLYPH, style); if (glyph) width += glyph->advanceX; @@ -1016,9 +1060,48 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y int xPos = x; int yPos = y; + int lastBaseX = x; + int lastBaseY = y; + int lastBaseAdvance = 0; + int lastBaseTop = 0; + bool hasBaseGlyph = false; + constexpr int MIN_COMBINING_GAP_PX = 1; uint32_t cp; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + if (utf8IsCombiningMark(cp) && hasBaseGlyph) { + const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); + if (!combiningGlyph) { + combiningGlyph = font.getGlyph(REPLACEMENT_GLYPH, style); + } + + int raiseBy = 0; + if (combiningGlyph) { + const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop; + if (currentGap < MIN_COMBINING_GAP_PX) { + raiseBy = MIN_COMBINING_GAP_PX - currentGap; + } + } + + int combiningX = lastBaseX - raiseBy; + int combiningY = lastBaseY - lastBaseAdvance / 2; + renderCharImpl(*this, renderMode, font, cp, &combiningX, &combiningY, black, style); + continue; + } + + const EpdGlyph* glyph = font.getGlyph(cp, style); + if (!glyph) { + glyph = font.getGlyph(REPLACEMENT_GLYPH, style); + } + + if (!utf8IsCombiningMark(cp)) { + lastBaseX = xPos; + lastBaseY = yPos; + lastBaseAdvance = glyph ? glyph->advanceX : 0; + lastBaseTop = glyph ? glyph->top : 0; + hasBaseGlyph = true; + } + renderCharImpl(*this, renderMode, font, cp, &xPos, &yPos, black, style); } } @@ -1040,9 +1123,48 @@ void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int int xPos = x; int yPos = y; + int lastBaseX = x; + int lastBaseY = y; + int lastBaseAdvance = 0; + int lastBaseTop = 0; + bool hasBaseGlyph = false; + constexpr int MIN_COMBINING_GAP_PX = 1; uint32_t cp; while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + if (utf8IsCombiningMark(cp) && hasBaseGlyph) { + const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); + if (!combiningGlyph) { + combiningGlyph = font.getGlyph(REPLACEMENT_GLYPH, style); + } + + int raiseBy = 0; + if (combiningGlyph) { + const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop; + if (currentGap < MIN_COMBINING_GAP_PX) { + raiseBy = MIN_COMBINING_GAP_PX - currentGap; + } + } + + int combiningX = lastBaseX + raiseBy; + int combiningY = lastBaseY + lastBaseAdvance / 2; + renderCharImpl(*this, renderMode, font, cp, &combiningX, &combiningY, black, style); + continue; + } + + const EpdGlyph* glyph = font.getGlyph(cp, style); + if (!glyph) { + glyph = font.getGlyph(REPLACEMENT_GLYPH, style); + } + + if (!utf8IsCombiningMark(cp)) { + lastBaseX = xPos; + lastBaseY = yPos; + lastBaseAdvance = glyph ? glyph->advanceX : 0; + lastBaseTop = glyph ? glyph->top : 0; + hasBaseGlyph = true; + } + renderCharImpl(*this, renderMode, font, cp, &xPos, &yPos, black, style); } } diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index 8285f3a8..76678b5c 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -13,6 +13,7 @@ extern const char* const STRINGS_CZ[]; extern const char* const STRINGS_PO[]; extern const char* const STRINGS_RU[]; extern const char* const STRINGS_SV[]; +extern const char* const STRINGS_RO[]; } // namespace i18n_strings // Language enum @@ -25,6 +26,7 @@ enum class Language : uint8_t { PORTUGUESE = 5, RUSSIAN = 6, SWEDISH = 7, + ROMANIAN = 8, _COUNT }; @@ -419,6 +421,8 @@ inline const char* const* getStringArray(Language lang) { return i18n_strings::STRINGS_RU; case Language::SWEDISH: return i18n_strings::STRINGS_SV; + case Language::ROMANIAN: + return i18n_strings::STRINGS_RO; default: return i18n_strings::STRINGS_EN; } diff --git a/lib/I18n/I18nStrings.h b/lib/I18n/I18nStrings.h index b2c14ea3..83cffed6 100644 --- a/lib/I18n/I18nStrings.h +++ b/lib/I18n/I18nStrings.h @@ -15,5 +15,6 @@ extern const char* const STRINGS_CZ[]; extern const char* const STRINGS_PO[]; extern const char* const STRINGS_RU[]; extern const char* const STRINGS_SV[]; +extern const char* const STRINGS_RO[]; } // namespace i18n_strings diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 5e71563e..b3ad4281 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Nedávné knihy" STR_NO_RECENT_BOOKS: "Žádné nedávné knihy" STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre" STR_FORGET_AND_REMOVE: "Zapomenout síť a odstranit uložené heslo?" -STR_FORGET_BUTTON: "Zapomenout na síť" +STR_FORGET_BUTTON: "Zapomenout" STR_CALIBRE_STARTING: "Spuštění Calibre..." STR_CALIBRE_SETUP: "Nastavení" STR_CALIBRE_STATUS: "Stav" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 6182a04a..dd20d71d 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Recent Books" STR_NO_RECENT_BOOKS: "No recent books" STR_CALIBRE_DESC: "Use Calibre wireless device transfers" STR_FORGET_AND_REMOVE: "Forget network and remove saved password?" -STR_FORGET_BUTTON: "Forget network" +STR_FORGET_BUTTON: "Forget" STR_CALIBRE_STARTING: "Starting Calibre..." STR_CALIBRE_SETUP: "Setup" STR_CALIBRE_STATUS: "Status" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 052ca7aa..ea00e876 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Livres récents" STR_NO_RECENT_BOOKS: "Aucun livre récent" STR_CALIBRE_DESC: "Utiliser les transferts sans fil Calibre" STR_FORGET_AND_REMOVE: "Oublier le réseau et supprimer le mot de passe enregistré ?" -STR_FORGET_BUTTON: "Oublier le réseau" +STR_FORGET_BUTTON: "Oublier" STR_CALIBRE_STARTING: "Démarrage de Calibre..." STR_CALIBRE_SETUP: "Configuration" STR_CALIBRE_STATUS: "Statut" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 8125afa4..862e3d17 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Zuletzt gelesen" STR_NO_RECENT_BOOKS: "Keine Bücher" STR_CALIBRE_DESC: "Calibre-Übertragung (WLAN)" STR_FORGET_AND_REMOVE: "WLAN entfernen & Passwort löschen?" -STR_FORGET_BUTTON: "WLAN entfernen" +STR_FORGET_BUTTON: "Entfernen" STR_CALIBRE_STARTING: "Calibre starten…" STR_CALIBRE_SETUP: "Installation" STR_CALIBRE_STATUS: "Status" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index a5444512..a90f2395 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Livros recentes" STR_NO_RECENT_BOOKS: "Sem livros recentes" STR_CALIBRE_DESC: "Usar transferências sem fio Calibre" STR_FORGET_AND_REMOVE: "Esquecer a rede e remover a senha salva?" -STR_FORGET_BUTTON: "Esquecer rede" +STR_FORGET_BUTTON: "Esquecer" STR_CALIBRE_STARTING: "Iniciando Calibre..." STR_CALIBRE_SETUP: "Configuração" STR_CALIBRE_STATUS: "Status" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml new file mode 100644 index 00000000..90acc6a7 --- /dev/null +++ b/lib/I18n/translations/romanian.yaml @@ -0,0 +1,318 @@ +_language_name: "Română" +_language_code: "ROMANIAN" +_order: "8" + +STR_CROSSPOINT: "CrossPoint" +STR_BOOTING: "PORNEŞTE" +STR_SLEEPING: "REPAUS" +STR_ENTERING_SLEEP: "Intră în repaus..." +STR_BROWSE_FILES: "Răsfoieşte fişierele" +STR_FILE_TRANSFER: "Transfer de fişiere" +STR_SETTINGS_TITLE: "Setări" +STR_CALIBRE_LIBRARY: "Biblioteca Calibre" +STR_CONTINUE_READING: "Continuă lectura" +STR_NO_OPEN_BOOK: "Nicio carte deschisă" +STR_START_READING: "Începeţi lectura" +STR_BOOKS: "Cărţi" +STR_NO_BOOKS_FOUND: "Nicio carte găsită" +STR_SELECT_CHAPTER: "Selectaţi capitolul" +STR_NO_CHAPTERS: "Niciun capitol" +STR_END_OF_BOOK: "Sfârşitul cărţii" +STR_EMPTY_CHAPTER: "Capitol gol" +STR_INDEXING: "Indexează..." +STR_MEMORY_ERROR: "Eroare de memorie" +STR_PAGE_LOAD_ERROR: "Eroare la încărcarea paginii" +STR_EMPTY_FILE: "Fişier gol" +STR_OUT_OF_BOUNDS: "Eroare: În afara limitelor" +STR_LOADING: "Se încarcă..." +STR_LOADING_POPUP: "Se încarcă..." +STR_LOAD_XTC_FAILED: "Eroare la încărcarea XTC" +STR_LOAD_TXT_FAILED: "Eroare la încărcarea TXT" +STR_LOAD_EPUB_FAILED: "Eroare la încărcarea EPUB" +STR_SD_CARD_ERROR: "Eroare la cardul SD" +STR_WIFI_NETWORKS: "Reţele WiFi" +STR_NO_NETWORKS: "Nu s-au găsit reţele" +STR_NETWORKS_FOUND: "%zu reţele găsite" +STR_SCANNING: "Scanează..." +STR_CONNECTING: "Se conectează..." +STR_CONNECTED: "Conectat!" +STR_CONNECTION_FAILED: "Conexiune eşuată" +STR_CONNECTION_TIMEOUT: "Timp de conectare depăşit" +STR_FORGET_NETWORK: "Uitaţi reţeaua?" +STR_SAVE_PASSWORD: "Salvaţi parola?" +STR_REMOVE_PASSWORD: "Ştergeţi parola salvată?" +STR_PRESS_OK_SCAN: "Apăsaţi OK pentru a scana din nou" +STR_PRESS_ANY_CONTINUE: "Apăsaţi orice buton pentru a continua" +STR_SELECT_HINT: "STÂNGA/DREAPTA: Selectaţi | OK: Confirmaţi" +STR_HOW_CONNECT: "Cum doriţi să vă conectaţi?" +STR_JOIN_NETWORK: "Conectaţi-vă la o reţea" +STR_CREATE_HOTSPOT: "Creaţi un hotspot" +STR_JOIN_DESC: "Conectaţi-vă la o reţea WiFi existentă" +STR_HOTSPOT_DESC: "Creaţi un hotspot WiFi" +STR_STARTING_HOTSPOT: "Hotspot porneşte..." +STR_HOTSPOT_MODE: "Mod Hotspot" +STR_CONNECT_WIFI_HINT: "Conectaţi-vă dispozitivul la această reţea WiFi" +STR_OPEN_URL_HINT: "Deschideţi acest URL în browserul dvs." +STR_OR_HTTP_PREFIX: "sau http://" +STR_SCAN_QR_HINT: "sau scanaţi codul QR cu telefonul dvs.:" +STR_CALIBRE_WIRELESS: "Calibre Wireless" +STR_CALIBRE_WEB_URL: "Calibre URL" +STR_CONNECT_WIRELESS: "Conectaţi-vă ca dispozitiv wireless" +STR_NETWORK_LEGEND: "* = Criptat | + = Salvat" +STR_MAC_ADDRESS: "Adresă MAC:" +STR_CHECKING_WIFI: "Verificare WiFi..." +STR_ENTER_WIFI_PASSWORD: "Introduceţi parola WiFi" +STR_ENTER_TEXT: "Introduceţi textul" +STR_TO_PREFIX: "la " +STR_CALIBRE_DISCOVERING: "Descoperă Calibre..." +STR_CALIBRE_CONNECTING_TO: "Se conectează la " +STR_CALIBRE_CONNECTED_TO: "Conectat la " +STR_CALIBRE_WAITING_COMMANDS: "Se aşteaptă comenzi..." +STR_CONNECTION_FAILED_RETRYING: "(Conexiune eşuată, se reîncearcă)" +STR_CALIBRE_DISCONNECTED: "Calibre deconectat" +STR_CALIBRE_WAITING_TRANSFER: "Se aşteaptă transfer..." +STR_CALIBRE_TRANSFER_HINT: "Dacă transferul eşuează, activaţi\\n'Ignoraţi spaţiul liber' în setările\\nplugin-ului SmartDevice din Calibre." +STR_CALIBRE_RECEIVING: "Se primeşte: " +STR_CALIBRE_RECEIVED: "Primite: " +STR_CALIBRE_WAITING_MORE: "Se aşteaptă mai multe..." +STR_CALIBRE_FAILED_CREATE_FILE: "Creare fişier eşuată" +STR_CALIBRE_PASSWORD_REQUIRED: "Necesită parolă" +STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer întrerupt" +STR_CALIBRE_INSTRUCTION_1: "1) Instalaţi plugin-ul CrossPoint Reader" +STR_CALIBRE_INSTRUCTION_2: "2) Fiţi în aceeaşi reţea WiFi" +STR_CALIBRE_INSTRUCTION_3: "3) În Calibre: \"Trimiteţi la dispozitiv\"" +STR_CALIBRE_INSTRUCTION_4: "\"Păstraţi acest ecran deschis în timpul trimiterii\"" +STR_CAT_DISPLAY: "Ecran" +STR_CAT_READER: "Lectură" +STR_CAT_CONTROLS: "Controale" +STR_CAT_SYSTEM: "Sistem" +STR_SLEEP_SCREEN: "Ecran de repaus" +STR_SLEEP_COVER_MODE: "Mod ecran de repaus cu copertă" +STR_STATUS_BAR: "Bara de stare" +STR_HIDE_BATTERY: "Ascunde procentul bateriei" +STR_EXTRA_SPACING: "Spaţiere suplimentară între paragrafe" +STR_TEXT_AA: "Anti-Aliasing text" +STR_SHORT_PWR_BTN: "Apăsare scurtă întrerupător" +STR_ORIENTATION: "Orientare lectură" +STR_FRONT_BTN_LAYOUT: "Aspect butoane frontale" +STR_SIDE_BTN_LAYOUT: "Aspect butoane laterale (lectură)" +STR_LONG_PRESS_SKIP: "Sărire capitol la apăsare lungă" +STR_FONT_FAMILY: "Familie font lectură" +STR_EXT_READER_FONT: "Font lectură extern" +STR_EXT_CHINESE_FONT: "Font lectură" +STR_EXT_UI_FONT: "Font meniu" +STR_FONT_SIZE: "Dimensiune font" +STR_LINE_SPACING: "Spaţiere între rânduri" +STR_ASCII_LETTER_SPACING: "Spaţiere litere ASCII " +STR_ASCII_DIGIT_SPACING: "Spaţiere cifre ASCII" +STR_CJK_SPACING: "Spaţiere CJK" +STR_COLOR_MODE: "Mod culoare" +STR_SCREEN_MARGIN: "Margine ecran lectură" +STR_PARA_ALIGNMENT: "Aliniere paragrafe reader" +STR_HYPHENATION: "Silabisire" +STR_TIME_TO_SLEEP: "Timp până la repaus" +STR_REFRESH_FREQ: "Frecvenţă reîmprospătare" +STR_CALIBRE_SETTINGS: "Setări Calibre" +STR_KOREADER_SYNC: "Sincronizare KOReader" +STR_CHECK_UPDATES: "Căutaţi actualizări" +STR_LANGUAGE: "Limbă" +STR_SELECT_WALLPAPER: "Selectaţi imaginea de fundal" +STR_CLEAR_READING_CACHE: "Goliţi cache-ul de lectură" +STR_CALIBRE: "Calibre" +STR_USERNAME: "Utilizator" +STR_PASSWORD: "Parolă" +STR_SYNC_SERVER_URL: "URL server sincronizare" +STR_DOCUMENT_MATCHING: "Corespondenţă document" +STR_AUTHENTICATE: "Autentificare" +STR_KOREADER_USERNAME: "Nume utilizator KOReader" +STR_KOREADER_PASSWORD: "Parolă KOReader" +STR_FILENAME: "Nume fişier" +STR_BINARY: "Fişier binar" +STR_SET_CREDENTIALS_FIRST: "Vă rugăm să setaţi mai întâi acreditările" +STR_WIFI_CONN_FAILED: "Conexiune WiFi eşuată" +STR_AUTHENTICATING: "Se autentifică..." +STR_AUTH_SUCCESS: "Autentificare reuşită!" +STR_KOREADER_AUTH: "Autentificare KOReader" +STR_SYNC_READY: "Sincronizare KOReader gata de utilizare" +STR_AUTH_FAILED: "Autentificare eşuată" +STR_DONE: "Gata" +STR_CLEAR_CACHE_WARNING_1: "Aceasta va şterge tot cache-ul de lectură." +STR_CLEAR_CACHE_WARNING_2: "Tot progresul de lectură va fi pierdut!" +STR_CLEAR_CACHE_WARNING_3: "Cărţile vor trebui reindexate" +STR_CLEAR_CACHE_WARNING_4: "când vor fi deschise din nou." +STR_CLEARING_CACHE: "Se şterge cache-ul..." +STR_CACHE_CLEARED: "Cache şters" +STR_ITEMS_REMOVED: "elemente eliminate" +STR_FAILED_LOWER: "eşuat" +STR_CLEAR_CACHE_FAILED: "ştergerea cache-ului a eşuat" +STR_CHECK_SERIAL_OUTPUT: "Verificaţi ieşirea serială pentru detalii" +STR_DARK: "Întunecat" +STR_LIGHT: "Luminos" +STR_CUSTOM: "Personalizat" +STR_COVER: "Copertă" +STR_NONE_OPT: "Niciunul" +STR_FIT: "Potrivit" +STR_CROP: "Decupat" +STR_NO_PROGRESS: "Fără progres" +STR_FULL_OPT: "Complet" +STR_NEVER: "Niciodată" +STR_IN_READER: "În lectură" +STR_ALWAYS: "Întotdeauna" +STR_IGNORE: "Ignoră" +STR_SLEEP: "Repaus" +STR_PAGE_TURN: "Răsfoire pagină" +STR_PORTRAIT: "Vertical" +STR_LANDSCAPE_CW: "Orizontal dreapta" +STR_INVERTED: "Invers" +STR_LANDSCAPE_CCW: "Orizontal stânga" +STR_FRONT_LAYOUT_BCLR: "Înapoi, Cnfrm, St, Dr" +STR_FRONT_LAYOUT_LRBC: "St, Dr, Înapoi, Cnfrm" +STR_FRONT_LAYOUT_LBCR: "St, Înapoi, Cnfrm, Dr" +STR_PREV_NEXT: "Înainte/Înapoi" +STR_NEXT_PREV: "Înapoi/Înainte" +STR_BOOKERLY: "Bookerly" +STR_NOTO_SANS: "Noto Sans" +STR_OPEN_DYSLEXIC: "Open Dyslexic" +STR_SMALL: "Mic" +STR_MEDIUM: "Mediu" +STR_LARGE: "Mare" +STR_X_LARGE: "Foarte mare" +STR_TIGHT: "Strâns" +STR_NORMAL: "Normal" +STR_WIDE: "Larg" +STR_JUSTIFY: "Aliniere" +STR_ALIGN_LEFT: "Stânga" +STR_CENTER: "Centru" +STR_ALIGN_RIGHT: "Dreapta" +STR_MIN_1: "1 min" +STR_MIN_5: "5 min" +STR_MIN_10: "10 min" +STR_MIN_15: "15 min" +STR_MIN_30: "30 min" +STR_PAGES_1: "1 pagină" +STR_PAGES_5: "5 pagini" +STR_PAGES_10: "10 pagini" +STR_PAGES_15: "15 pagini" +STR_PAGES_30: "30 pagini" +STR_UPDATE: "Actualizare" +STR_CHECKING_UPDATE: "Se verifică actualizările..." +STR_NEW_UPDATE: "Nouă actualizare disponibilă!" +STR_CURRENT_VERSION: "Versiune curentă: " +STR_NEW_VERSION: "Noua versiune: " +STR_UPDATING: "Se actualizează..." +STR_NO_UPDATE: "Nicio actualizare disponibilă" +STR_UPDATE_FAILED: "Actualizare eşuată" +STR_UPDATE_COMPLETE: "Actualizare completă" +STR_POWER_ON_HINT: "Apăsaţi şi menţineţi apăsat întrerupătorul pentru a porni din nou" +STR_EXTERNAL_FONT: "Font extern" +STR_BUILTIN_DISABLED: "Încorporat (Dezactivat)" +STR_NO_ENTRIES: "Niciun rezultat găsit" +STR_DOWNLOADING: "Se descarcă..." +STR_DOWNLOAD_FAILED: "Descărcare eşuată" +STR_ERROR_MSG: "Eroare:" +STR_UNNAMED: "Fără nume" +STR_NO_SERVER_URL: "Niciun URL de server configurat" +STR_FETCH_FEED_FAILED: "Eşec la preluarea feed-ului" +STR_PARSE_FEED_FAILED: "Eşec la analizarea feed-ului" +STR_NETWORK_PREFIX: "Reţea: " +STR_IP_ADDRESS_PREFIX: "Adresă IP: " +STR_SCAN_QR_WIFI_HINT: "sau scanaţi codul QR cu telefonul pentru a vă conecta la Wifi." +STR_ERROR_GENERAL_FAILURE: "Eroare: Eşec general" +STR_ERROR_NETWORK_NOT_FOUND: "Eroare: Reţea negăsită" +STR_ERROR_CONNECTION_TIMEOUT: "Eroare: Timp de conectare depăşit" +STR_SD_CARD: "Card SD" +STR_BACK: "« Înapoi" +STR_EXIT: "« Ieşire" +STR_HOME: "« Acasă" +STR_SAVE: "« Salvare" +STR_SELECT: "Selectează" +STR_TOGGLE: "Schimbă" +STR_CONFIRM: "Confirmă" +STR_CANCEL: "Anulare" +STR_CONNECT: "Conectare" +STR_OPEN: "Deschidere" +STR_DOWNLOAD: "Descarcă" +STR_RETRY: "Reîncercare" +STR_YES: "Da" +STR_NO: "Nu" +STR_STATE_ON: "Pornit" +STR_STATE_OFF: "Oprit" +STR_SET: "Setare" +STR_NOT_SET: "Neconfigurat" +STR_DIR_LEFT: "Stânga" +STR_DIR_RIGHT: "Dreapta" +STR_DIR_UP: "Sus" +STR_DIR_DOWN: "Jos" +STR_CAPS_ON: "CAPS" +STR_CAPS_OFF: "caps" +STR_OK_BUTTON: "OK" +STR_ON_MARKER: "[ON]" +STR_SLEEP_COVER_FILTER: "Filtru ecran de repaus" +STR_FILTER_CONTRAST: "Contrast" +STR_STATUS_BAR_FULL_PERCENT: "Complet cu procentaj" +STR_STATUS_BAR_FULL_BOOK: "Complet cu bara de carte" +STR_STATUS_BAR_BOOK_ONLY: "Doar bara de carte" +STR_STATUS_BAR_FULL_CHAPTER: "Complet cu bara de capitol" +STR_UI_THEME: "Tema UI" +STR_THEME_CLASSIC: "Clasic" +STR_THEME_LYRA: "Lyra" +STR_SUNLIGHT_FADING_FIX: "Corecţie estompare lumină" +STR_REMAP_FRONT_BUTTONS: "Remapare butoane frontale" +STR_OPDS_BROWSER: "Browser OPDS" +STR_COVER_CUSTOM: "Copertă + Personalizat" +STR_RECENTS: "Recente" +STR_MENU_RECENT_BOOKS: "Cărţi recente" +STR_NO_RECENT_BOOKS: "Nicio carte recentă" +STR_CALIBRE_DESC: "Utilizaţi transferurile wireless ale dispozitivului Calibre" +STR_FORGET_AND_REMOVE: "Uitaţi reţeaua şi eliminaţi parola salvată?" +STR_FORGET_BUTTON: "Uitaţi" +STR_CALIBRE_STARTING: "Pornirea Calibre..." +STR_CALIBRE_SETUP: "Configurare" +STR_CALIBRE_STATUS: "Stare" +STR_CLEAR_BUTTON: "ştergere" +STR_DEFAULT_VALUE: "Implicit" +STR_REMAP_PROMPT: "Apăsaţi un buton frontal pentru fiecare rol" +STR_UNASSIGNED: "Neatribuit" +STR_ALREADY_ASSIGNED: "Deja atribuit" +STR_REMAP_RESET_HINT: "Buton lateral Sus: Resetaţi la aspectul implicit" +STR_REMAP_CANCEL_HINT: "Buton lateral Jos: Anulaţi remaparea" +STR_HW_BACK_LABEL: "Înapoi (butonul 1)" +STR_HW_CONFIRM_LABEL: "Confirmare (butonul 2)" +STR_HW_LEFT_LABEL: "Stânga (butonul 3)" +STR_HW_RIGHT_LABEL: "Dreapta (butonul 4)" +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_CHAPTER_PREFIX: "Capitol: " +STR_PAGES_SEPARATOR: " pagini | " +STR_BOOK_PREFIX: "Carte: " +STR_KBD_SHIFT: "shift" +STR_KBD_SHIFT_CAPS: "SHIFT" +STR_KBD_LOCK: "LOCK" +STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL" +STR_PERCENT_STEP_HINT: "Stânga/Dreapta: 1% Sus/Jos: 10%" +STR_SYNCING_TIME: "Timp de sincronizare..." +STR_CALC_HASH: "Calcularea hash-ului documentului..." +STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului" +STR_FETCH_PROGRESS: "Preluarea progresului de la distanţă..." +STR_UPLOAD_PROGRESS: "Încărcarea progresului..." +STR_NO_CREDENTIALS_MSG: "Nicio acreditare configurată" +STR_KOREADER_SETUP_HINT: "Configuraţi contul KOReader în setări" +STR_PROGRESS_FOUND: "Progres găsit!" +STR_REMOTE_LABEL: "Remote:" +STR_LOCAL_LABEL: "Local:" +STR_PAGE_OVERALL_FORMAT: "Pagina %d, %.2f%% din total" +STR_PAGE_TOTAL_OVERALL_FORMAT: "Pagina %d/%d, %.2f%% din total" +STR_DEVICE_FROM_FORMAT: " De la: %s" +STR_APPLY_REMOTE: "Aplică progresul remote" +STR_UPLOAD_LOCAL: "Încărcaţi progresul local" +STR_NO_REMOTE_MSG: "Niciun progres remote găsit" +STR_UPLOAD_PROMPT: "Încărcaţi poziţia curentă?" +STR_UPLOAD_SUCCESS: "Progres încărcat!" +STR_SYNC_FAILED_MSG: "Sincronizare eşuată" +STR_SECTION_PREFIX: "Secţiune " +STR_UPLOAD: "Încărcare" +STR_BOOK_S_STYLE: "Stilul cărţii" +STR_EMBEDDED_STYLE: "Stil încorporat" +STR_OPDS_SERVER_URL: "URL server OPDS" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index a2de5f84..2cd37e09 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Недавние книги" STR_NO_RECENT_BOOKS: "Нет недавних книг" STR_CALIBRE_DESC: "Использовать беспроводную передачу Calibre" STR_FORGET_AND_REMOVE: "Забыть сеть и удалить сохранённый пароль?" -STR_FORGET_BUTTON: "Забыть сеть" +STR_FORGET_BUTTON: "Забыть" STR_CALIBRE_STARTING: "Запуск Calibre..." STR_CALIBRE_SETUP: "Настройка" STR_CALIBRE_STATUS: "Статус" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 6e0f9b2e..d61941a4 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Libros recientes" STR_NO_RECENT_BOOKS: "No hay libros recientes" STR_CALIBRE_DESC: "Utilice las transferencias dispositivos inalámbricos de calibre" STR_FORGET_AND_REMOVE: "Olvidar la red y eliminar la contraseña guardada?" -STR_FORGET_BUTTON: "Olvidar la red" +STR_FORGET_BUTTON: "Olvidar" STR_CALIBRE_STARTING: "Iniciando calibre..." STR_CALIBRE_SETUP: "Configuración" STR_CALIBRE_STATUS: "Estado" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 02cd97a7..57bfb20d 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -267,7 +267,7 @@ STR_MENU_RECENT_BOOKS: "Senaste böckerna" STR_NO_RECENT_BOOKS: "Inga senaste böcker" STR_CALIBRE_DESC: "Använd Calibres trådlösa enhetsöverföring" STR_FORGET_AND_REMOVE: "Glöm nätverk och ta bort sparat lösenord?" -STR_FORGET_BUTTON: "Glöm nätverk" +STR_FORGET_BUTTON: "Glöm" STR_CALIBRE_STARTING: "Starar Calibre…" STR_CALIBRE_SETUP: "Inställning" STR_CALIBRE_STATUS: "Status" diff --git a/lib/Utf8/Utf8.h b/lib/Utf8/Utf8.h index 23d63a4e..cce7c0d6 100644 --- a/lib/Utf8/Utf8.h +++ b/lib/Utf8/Utf8.h @@ -9,3 +9,11 @@ uint32_t utf8NextCodepoint(const unsigned char** string); size_t utf8RemoveLastChar(std::string& str); // Truncate string by removing N UTF-8 codepoints from the end. void utf8TruncateChars(std::string& str, size_t numChars); + +// Returns true for Unicode combining diacritical marks that should not advance the cursor. +inline bool utf8IsCombiningMark(const uint32_t cp) { + return (cp >= 0x0300 && cp <= 0x036F) // Combining Diacritical Marks + || (cp >= 0x1DC0 && cp <= 0x1DFF) // Combining Diacritical Marks Supplement + || (cp >= 0x20D0 && cp <= 0x20FF) // Combining Diacritical Marks for Symbols + || (cp >= 0xFE20 && cp <= 0xFE2F); // Combining Half Marks +} diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 8b44d7d1..ed64215e 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -196,6 +196,15 @@ std::string getFileName(std::string filename) { return filename.substr(0, pos); } +std::string getFileExtension(std::string filename) { + if (filename.back() == '/') { + return ""; + } + const auto pos = filename.rfind('.'); + if (pos == std::string::npos) return ""; + return filename.substr(pos); +} + void MyLibraryActivity::render(Activity::RenderLock&&) { renderer.clearScreen(); @@ -214,7 +223,8 @@ void MyLibraryActivity::render(Activity::RenderLock&&) { GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex, [this](int index) { return getFileName(files[index]); }, nullptr, - [this](int index) { return UITheme::getFileIcon(files[index]); }); + [this](int index) { return UITheme::getFileIcon(files[index]); }, + [this](int index) { return getFileExtension(files[index]); }, false); } // Help text