Merge branch 'master' of https://github.com/crosspoint-reader/crosspoint-reader into feat-page-overlay

This commit is contained in:
jpirnay
2026-03-07 10:22:18 +01:00
41 changed files with 924 additions and 193 deletions
+8 -13
View File
@@ -103,14 +103,11 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
pos += strlen(pattern);
const auto endPos = coverPageHtml.find('"', pos);
if (endPos != std::string::npos) {
const auto ref = coverPageHtml.substr(pos, endPos - pos);
const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
// Check if it's an image file
if (ref.length() >= 4) {
const auto ext = ref.substr(ref.length() - 4);
if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") {
imageRef = ref;
break;
}
if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) {
imageRef = ref;
break;
}
}
pos = coverPageHtml.find(pattern, pos);
@@ -541,8 +538,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
return false;
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
if (FsHelpers::hasJpgExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
@@ -575,7 +571,7 @@ bool Epub::generateCoverBmp(bool cropped) const {
return success;
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
if (FsHelpers::hasPngExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
const auto coverPngTempPath = getCachePath() + "/.cover.png";
@@ -629,8 +625,7 @@ bool Epub::generateThumbBmp(int height) const {
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) {
LOG_DBG("EBP", "No known cover image for thumbnail");
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
} else if (FsHelpers::hasJpgExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
@@ -666,7 +661,7 @@ bool Epub::generateThumbBmp(int height) const {
}
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
return success;
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
} else if (FsHelpers::hasPngExtension(coverImageHref)) {
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
const auto coverPngTempPath = getCachePath() + "/.cover.png";
+52 -8
View File
@@ -10,10 +10,10 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 17;
constexpr uint8_t SECTION_FILE_VERSION = 18;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(uint32_t);
sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t);
} // namespace
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
@@ -44,7 +44,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t),
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId);
@@ -56,8 +56,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, hyphenationEnabled);
serialization::writePod(file, embeddedStyle);
serialization::writePod(file, imageRendering);
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
}
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
@@ -239,10 +240,20 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false;
}
// Go back and write LUT offset
file.seek(HEADER_SIZE - sizeof(uint32_t) - sizeof(pageCount));
// Write anchor-to-page map for fragment navigation (e.g. footnote targets)
const uint32_t anchorMapOffset = file.position();
const auto& anchors = visitor.getAnchors();
serialization::writePod(file, static_cast<uint16_t>(anchors.size()));
for (const auto& [anchor, page] : anchors) {
serialization::writeString(file, anchor);
serialization::writePod(file, page);
}
// Patch header with final pageCount, lutOffset, and anchorMapOffset
file.seek(HEADER_SIZE - sizeof(uint32_t) * 2 - sizeof(pageCount));
serialization::writePod(file, pageCount);
serialization::writePod(file, lutOffset);
serialization::writePod(file, anchorMapOffset);
file.close();
if (cssParser) {
cssParser->clear();
@@ -255,7 +266,7 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
return nullptr;
}
file.seek(HEADER_SIZE - sizeof(uint32_t));
file.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
uint32_t lutOffset;
serialization::readPod(file, lutOffset);
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
@@ -267,3 +278,36 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
file.close();
return page;
}
std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) const {
FsFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return std::nullopt;
}
const uint32_t fileSize = f.size();
f.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t anchorMapOffset;
serialization::readPod(f, anchorMapOffset);
if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) {
f.close();
return std::nullopt;
}
f.seek(anchorMapOffset);
uint16_t count;
serialization::readPod(f, count);
for (uint16_t i = 0; i < count; i++) {
std::string key;
uint16_t page;
serialization::readString(f, key);
serialization::readPod(f, page);
if (key == anchor) {
f.close();
return page;
}
}
f.close();
return std::nullopt;
}
+5
View File
@@ -1,6 +1,8 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include "Epub.h"
@@ -37,4 +39,7 @@ class Section {
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, const std::function<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
// Look up the page number for an anchor id from the section cache file.
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
};
@@ -1,5 +1,6 @@
#include "JpegToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <JPEGDEC.h>
@@ -486,9 +487,5 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
}
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension;
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".jpg" || ext == ".jpeg");
return FsHelpers::hasJpgExtension(extension);
}
@@ -1,5 +1,6 @@
#include "PngToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Logging.h>
@@ -391,9 +392,5 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
}
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
std::string ext = extension;
for (auto& c : ext) {
c = tolower(c);
}
return (ext == ".png");
return FsHelpers::hasPngExtension(extension);
}
@@ -133,11 +133,21 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the
// div's margin should be preserved, even though it has no direct text content.
currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle));
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
return;
}
makePages();
}
// Record deferred anchor after previous block is flushed
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
wordsExtractedInBlock = 0;
}
@@ -151,7 +161,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return;
}
// Extract class and style attributes for CSS processing
// Extract class, style, and id attributes
std::string classAttr;
std::string styleAttr;
if (atts != nullptr) {
@@ -160,6 +170,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
classAttr = atts[i + 1];
} else if (strcmp(atts[i], "style") == 0) {
styleAttr = atts[i + 1];
} else if (strcmp(atts[i], "id") == 0) {
// Defer recording until startNewTextBlock, after previous block is flushed to pages
self->pendingAnchorId = atts[i + 1];
}
}
}
@@ -374,6 +387,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (self->currentPage && !self->currentPage->elements.empty() &&
(self->currentPageNextY + displayHeight > self->viewportHeight)) {
self->completePageFn(std::move(self->currentPage));
self->completedPageCount++;
self->currentPage.reset(new Page());
if (!self->currentPage) {
LOG_ERR("EHP", "Failed to create new page");
@@ -990,7 +1004,12 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
// Process last page if there is still text
if (currentTextBlock) {
makePages();
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset();
currentTextBlock.reset();
}
@@ -1003,6 +1022,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
if (currentPageNextY + lineHeight > viewportHeight) {
completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset(new Page());
currentPageNextY = 0;
}
@@ -5,6 +5,7 @@
#include <climits>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "../FootnoteEntry.h"
@@ -69,6 +70,11 @@ class ChapterHtmlSlimParser {
int tableRowIndex = 0;
int tableColIndex = 0;
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0;
std::vector<std::pair<std::string, uint16_t>> anchorData;
std::string pendingAnchorId; // deferred until after previous text block is flushed
// Footnote link tracking
bool insideFootnoteLink = false;
int footnoteLinkDepth = -1;
@@ -119,4 +125,5 @@ class ChapterHtmlSlimParser {
~ChapterHtmlSlimParser() = default;
bool parseAndBuildPages();
void addLineToPage(std::shared_ptr<TextBlock> line);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
};
+43 -1
View File
@@ -1,8 +1,12 @@
#include "FsHelpers.h"
#include <cctype>
#include <cstring>
#include <vector>
std::string FsHelpers::normalisePath(const std::string& path) {
namespace FsHelpers {
std::string normalisePath(const std::string& path) {
std::vector<std::string> components;
std::string component;
@@ -37,3 +41,41 @@ std::string FsHelpers::normalisePath(const std::string& path) {
return result;
}
bool checkFileExtension(std::string_view fileName, const char* extension) {
const size_t extLen = strlen(extension);
if (fileName.length() < extLen) {
return false;
}
const size_t offset = fileName.length() - extLen;
for (size_t i = 0; i < extLen; i++) {
if (tolower(static_cast<unsigned char>(fileName[offset + i])) !=
tolower(static_cast<unsigned char>(extension[i]))) {
return false;
}
}
return true;
}
bool hasJpgExtension(std::string_view fileName) {
return checkFileExtension(fileName, ".jpg") || checkFileExtension(fileName, ".jpeg");
}
bool hasPngExtension(std::string_view fileName) { return checkFileExtension(fileName, ".png"); }
bool hasBmpExtension(std::string_view fileName) { return checkFileExtension(fileName, ".bmp"); }
bool hasGifExtension(std::string_view fileName) { return checkFileExtension(fileName, ".gif"); }
bool hasEpubExtension(std::string_view fileName) { return checkFileExtension(fileName, ".epub"); }
bool hasXtcExtension(std::string_view fileName) {
return checkFileExtension(fileName, ".xtc") || checkFileExtension(fileName, ".xtch");
}
bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(fileName, ".txt"); }
bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); }
} // namespace FsHelpers
+56 -5
View File
@@ -1,7 +1,58 @@
#pragma once
#include <string>
#include <WString.h>
class FsHelpers {
public:
static std::string normalisePath(const std::string& path);
};
#include <string>
#include <string_view>
namespace FsHelpers {
std::string normalisePath(const std::string& path);
/**
* Check if the given filename ends with the specified extension (case-insensitive).
*/
bool checkFileExtension(std::string_view fileName, const char* extension);
inline bool checkFileExtension(const String& fileName, const char* extension) {
return checkFileExtension(std::string_view{fileName.c_str(), fileName.length()}, extension);
}
// Check for either .jpg or .jpeg extension (case-insensitive)
bool hasJpgExtension(std::string_view fileName);
inline bool hasJpgExtension(const String& fileName) {
return hasJpgExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .png extension (case-insensitive)
bool hasPngExtension(std::string_view fileName);
inline bool hasPngExtension(const String& fileName) {
return hasPngExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .bmp extension (case-insensitive)
bool hasBmpExtension(std::string_view fileName);
// Check for .gif extension (case-insensitive)
bool hasGifExtension(std::string_view fileName);
inline bool hasGifExtension(const String& fileName) {
return hasGifExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .epub extension (case-insensitive)
bool hasEpubExtension(std::string_view fileName);
inline bool hasEpubExtension(const String& fileName) {
return hasEpubExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for either .xtc or .xtch extension (case-insensitive)
bool hasXtcExtension(std::string_view fileName);
// Check for .txt extension (case-insensitive)
bool hasTxtExtension(std::string_view fileName);
inline bool hasTxtExtension(const String& fileName) {
return hasTxtExtension(std::string_view{fileName.c_str(), fileName.length()});
}
// Check for .md extension (case-insensitive)
bool hasMarkdownExtension(std::string_view fileName);
} // namespace FsHelpers
+30
View File
@@ -92,6 +92,10 @@ STR_STATUS_BAR: "Barra d'estat"
STR_HIDE_BATTERY: "Oculta el % de bateria"
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
STR_TEXT_AA: "Antialiàsing del text"
STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra"
STR_IMAGES_SUPPRESS: "Suprimir"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura"
STR_FRONT_BTN_LAYOUT: "Disposició dels botons frontals"
@@ -226,6 +230,7 @@ STR_EXIT: "« Surt"
STR_HOME: "« Inici"
STR_SAVE: "« Desa"
STR_SELECT: "Selecciona"
STR_SELECTED: "Seleccionat"
STR_TOGGLE: "Canvia"
STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la"
@@ -235,6 +240,8 @@ STR_DOWNLOAD: "Descarrega"
STR_RETRY: "Nou intent"
STR_YES: "Sí"
STR_NO: "No"
STR_SHOW: "Mostrar"
STR_HIDE: "Amagar"
STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF"
STR_NOT_SET: "No establert"
@@ -247,6 +254,21 @@ STR_CAPS_OFF: "majs"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
STR_BOOK_PROGRESS_PERCENTAGE: "Percentatge de progrés del llibre"
STR_PROGRESS_BAR: "Barra de progrés"
STR_PROGRESS_BAR_THICKNESS: "Gruix de la barra de progrés"
STR_PROGRESS_BAR_THIN: "Fina"
STR_PROGRESS_BAR_MEDIUM: "Mitjana"
STR_PROGRESS_BAR_THICK: "Gruixuda"
STR_BOOK: "Llibre"
STR_CHAPTER: "Capítol"
STR_EXAMPLE_CHAPTER: "Capítol 21"
STR_EXAMPLE_BOOK: "Títol del llibre"
STR_PREVIEW: "Vista prèvia"
STR_TITLE: "Títol"
STR_BATTERY: "Bateria"
STR_UI_THEME: "Tema de la interfície"
STR_THEME_CLASSIC: "Clàssic"
STR_THEME_LYRA: "Lyra"
@@ -279,6 +301,8 @@ 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_DELETE: "Esborra"
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
STR_CHAPTER_PREFIX: "Capítol: "
STR_PAGES_SEPARATOR: " pàgines | "
STR_BOOK_PREFIX: "Llibre: "
@@ -311,3 +335,9 @@ STR_UPLOAD: "Puja"
STR_BOOK_S_STYLE: "Estil del llibre"
STR_EMBEDDED_STYLE: "Estil incrustat"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
STR_FOOTNOTES: "Notes al peu"
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
STR_LINK: "[enllaç]"
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
+5
View File
@@ -92,6 +92,10 @@ STR_STATUS_BAR: "Status Bar"
STR_HIDE_BATTERY: "Ukryj % baterii"
STR_EXTRA_SPACING: "Dodatkowe odstępy paragrafów"
STR_TEXT_AA: "Wygładzanie tekstu"
STR_IMAGES: "Obrazki"
STR_IMAGES_DISPLAY: "Pokazuj"
STR_IMAGES_PLACEHOLDER: "Ramki"
STR_IMAGES_SUPPRESS: "Pomijaj"
STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania"
STR_ORIENTATION: "Układ czytania"
STR_FRONT_BTN_LAYOUT: "Układ przednich przycisków"
@@ -297,6 +301,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_DELETE: "Usuń"
STR_DISPLAY_QR: "Pokaż stronę jako kod QR"
STR_CHAPTER_PREFIX: "Rozdział: "
STR_PAGES_SEPARATOR: " stron | "
+7
View File
@@ -297,7 +297,9 @@ 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_DELETE: "Ştergeți"
STR_DISPLAY_QR: "Afișați pagina ca cod QR"
STR_CHAPTER_PREFIX: "Capitol: "
STR_PAGES_SEPARATOR: " pagini | "
STR_BOOK_PREFIX: "Carte: "
STR_KBD_SHIFT: "shift"
@@ -329,4 +331,9 @@ STR_UPLOAD: "Încărcare"
STR_BOOK_S_STYLE: "Stilul cărţii"
STR_EMBEDDED_STYLE: "Stil încorporat"
STR_OPDS_SERVER_URL: "URL server OPDS"
STR_FOOTNOTES: "Note de subsol"
STR_NO_FOOTNOTES: "Nicio notă de subsol"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Captură ecran"
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
+35 -7
View File
@@ -63,7 +63,7 @@ STR_MAC_ADDRESS: "MAC-adress:"
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
STR_ENTER_WIFI_PASSWORD: "Skriv in WiFi-lösenord"
STR_ENTER_TEXT: "Skriv text"
STR_TO_PREFIX: "till"
STR_TO_PREFIX: "till "
STR_CALIBRE_DISCOVERING: "Söker Calibre…"
STR_CALIBRE_CONNECTING_TO: "Ansluter till"
STR_CALIBRE_CONNECTED_TO: "Ansluten till"
@@ -92,6 +92,10 @@ STR_STATUS_BAR: "Statusrad"
STR_HIDE_BATTERY: "Dölj batteriprocent"
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
STR_TEXT_AA: "Textkantutjämning"
STR_IMAGES: "Bilder"
STR_IMAGES_DISPLAY: "Visa"
STR_IMAGES_PLACEHOLDER: "Platshållare"
STR_IMAGES_SUPPRESS: "Dölj"
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
STR_ORIENTATION: "Läsrikting"
STR_FRONT_BTN_LAYOUT: "Frontknappslayout"
@@ -226,6 +230,7 @@ STR_EXIT: "« Avsluta"
STR_HOME: "« Hem"
STR_SAVE: "« Spara"
STR_SELECT: "Välj "
STR_SELECTED: "Vald"
STR_TOGGLE: "Växla"
STR_CONFIRM: "Bekräfta"
STR_CANCEL: "Avbryt"
@@ -235,6 +240,8 @@ STR_DOWNLOAD: "Ladda ner"
STR_RETRY: "Försök igen"
STR_YES: "Ja"
STR_NO: "Nej"
STR_SHOW: "Visa"
STR_HIDE: "Dölj"
STR_STATE_ON: "PÅ"
STR_STATE_OFF: "AV"
STR_NOT_SET: "Inte inställd"
@@ -247,6 +254,21 @@ STR_CAPS_OFF: "versaler"
STR_OK_BUTTON: "Okej"
STR_SLEEP_COVER_FILTER: "Viloskärmens omslagsfilter"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Anpassa statusfält"
STR_CHAPTER_PAGE_COUNT: "Antal sidor i kapitel"
STR_BOOK_PROGRESS_PERCENTAGE: "Procentuellt bokframsteg"
STR_PROGRESS_BAR: "Framstegsindikator"
STR_PROGRESS_BAR_THICKNESS: "Tjocklek på framstegsindikator"
STR_PROGRESS_BAR_THIN: "Tunn"
STR_PROGRESS_BAR_MEDIUM: "Mellan"
STR_PROGRESS_BAR_THICK: "Tjock"
STR_BOOK: "Bok"
STR_CHAPTER: "Kapitel"
STR_EXAMPLE_CHAPTER: "Kapitel 21"
STR_EXAMPLE_BOOK: "Boktitel"
STR_PREVIEW: "Förhandsgranskning"
STR_TITLE: "Titel"
STR_BATTERY: "Batteri"
STR_UI_THEME: "Användargränssnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
@@ -269,17 +291,18 @@ STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Tryck en frontknapp för var funktion"
STR_UNASSIGNED: "Otilldelad"
STR_ALREADY_ASSIGNED: "Redan tilldelad"
STR_REMAP_RESET_HINT: "Översta sidoknapp: Återställ standardlayout"
STR_REMAP_CANCEL_HINT: "Nedre sidoknapp: Avbryt tilldelning"
STR_HW_BACK_LABEL: "Bak (Första knapp)"
STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knapp)"
STR_HW_LEFT_LABEL: "Vänster (Tredje knapp)"
STR_HW_RIGHT_LABEL: "Höger (Fjärde knapp)"
STR_REMAP_RESET_HINT: "Översta sidoknappen: Återställ standardlayout"
STR_REMAP_CANCEL_HINT: "Nedre sidoknappen: Avbryt tilldelning"
STR_HW_BACK_LABEL: "Bak (Första knappen)"
STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knappen)"
STR_HW_LEFT_LABEL: "Vänster (Tredje knappen)"
STR_HW_RIGHT_LABEL: "Höger (Fjärde knappen)"
STR_GO_TO_PERCENT: "Gå till %"
STR_GO_HOME_BUTTON: "Gå Hem"
STR_SYNC_PROGRESS: "Synkroniseringsframsteg"
STR_DELETE_CACHE: "Radera bokcache"
STR_DELETE: "Radera"
STR_DISPLAY_QR: "Visa sida som QR-kod"
STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " sidor | "
STR_BOOK_PREFIX: "Bok:"
@@ -312,4 +335,9 @@ STR_UPLOAD: "Uppladdning"
STR_BOOK_S_STYLE: "Bokstil"
STR_EMBEDDED_STYLE: "Inbäddad stil"
STR_OPDS_SERVER_URL: "OPDS-serveradress"
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_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
+344
View File
@@ -0,0 +1,344 @@
_language_name: "Türkçe"
_language_code: "TR"
_order: "17"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "BAŞLATILIYOR"
STR_SLEEPING: "UYKU MODU"
STR_ENTERING_SLEEP: "Uyku moduna geçiliyor"
STR_BROWSE_FILES: "Dosyalara Göz At"
STR_FILE_TRANSFER: "Dosya Transferi"
STR_SETTINGS_TITLE: "Ayarlar"
STR_CALIBRE_LIBRARY: "Calibre Kütüphanesi"
STR_CONTINUE_READING: "Okumaya Devam Et"
STR_NO_OPEN_BOOK: "Açık kitap yok"
STR_START_READING: "Aşağıdan okumaya başlayın"
STR_BOOKS: "Kitaplar"
STR_SELECT_CHAPTER: "Bölüm Seç"
STR_NO_CHAPTERS: "Bölüm yok"
STR_END_OF_BOOK: "Kitabın sonu"
STR_EMPTY_CHAPTER: "Boş bölüm"
STR_INDEXING: "Endeksleniyor"
STR_MEMORY_ERROR: "Bellek hatası"
STR_PAGE_LOAD_ERROR: "Sayfa yükleme hatası"
STR_EMPTY_FILE: "Boş dosya"
STR_OUT_OF_BOUNDS: "Sınırların dışında"
STR_LOADING: "Yükleniyor..."
STR_LOADING_POPUP: "Yükleniyor"
STR_LOAD_XTC_FAILED: "XTC yüklenemedi"
STR_LOAD_TXT_FAILED: "TXT yüklenemedi"
STR_LOAD_EPUB_FAILED: "EPUB yüklenemedi"
STR_SD_CARD_ERROR: "SD card hatası"
STR_WIFI_NETWORKS: "WiFi Ağları"
STR_NO_NETWORKS: "Ağ bulunamadı"
STR_NETWORKS_FOUND: "%zu ağ bulundu"
STR_SCANNING: "Tarıyor..."
STR_CONNECTING: "Bağlanıyor..."
STR_CONNECTED: "Bağlandı!"
STR_CONNECTION_FAILED: "Bağlantı Başarısız"
STR_CONNECTION_TIMEOUT: "Bağlantı zaman aşımı"
STR_FORGET_NETWORK: "Ağı Unut?"
STR_SAVE_PASSWORD: "Şifre kaydedilsin mi?"
STR_REMOVE_PASSWORD: "Kayıtlı şifre silinsin mi?"
STR_PRESS_OK_SCAN: "Tekrar taramak için OK'e basın"
STR_PRESS_ANY_CONTINUE: "Devam etmek için bir tuşa basın"
STR_SELECT_HINT: "SOL/SAĞ: Seç | OK: Onayla"
STR_HOW_CONNECT: "Nasıl bağlanmak istersiniz?"
STR_JOIN_NETWORK: "Bir Ağa Katıl"
STR_CREATE_HOTSPOT: "Erişim Noktası Oluştur"
STR_JOIN_DESC: "Mevcut bir WiFi ağına bağlan"
STR_HOTSPOT_DESC: "Başkalarının katılabileceği ağ oluştur"
STR_STARTING_HOTSPOT: "Erişim Noktası Başlatılıyor..."
STR_HOTSPOT_MODE: "Erişim Noktası Modu"
STR_CONNECT_WIFI_HINT: "Cihazınızı bu WiFi ağına bağlayın"
STR_OPEN_URL_HINT: "Tarayıcınızda bu adresi açın"
STR_OR_HTTP_PREFIX: "veya http://"
STR_SCAN_QR_HINT: "veya telefonunuzla QR kodu tarayın:"
STR_CALIBRE_WIRELESS: "Calibre Kablosuz"
STR_CALIBRE_WEB_URL: "Calibre Web Adresi"
STR_CONNECT_WIRELESS: "Kablosuz Cihaz Olarak Bağlan"
STR_NETWORK_LEGEND: "* = Şifreli | + = Kayıtlı"
STR_MAC_ADDRESS: "MAC adresi:"
STR_CHECKING_WIFI: "WiFi kontrol ediliyor..."
STR_ENTER_WIFI_PASSWORD: "WiFi Şifresini Girin"
STR_ENTER_TEXT: "Metin Girin"
STR_TO_PREFIX: "Ağ: "
STR_CALIBRE_DISCOVERING: "Calibre aranıyor..."
STR_CALIBRE_CONNECTING_TO: "Bağlanılıyor: "
STR_CALIBRE_CONNECTED_TO: "Bağlandı: "
STR_CALIBRE_WAITING_COMMANDS: "Komutlar bekleniyor..."
STR_CONNECTION_FAILED_RETRYING: "(Bağlantı başarısız, tekrar deneniyor)"
STR_CALIBRE_DISCONNECTED: "Calibre bağlantısı kesildi"
STR_CALIBRE_WAITING_TRANSFER: "Transfer bekleniyor..."
STR_CALIBRE_TRANSFER_HINT: "Transfer başarısız olursa, Calibre\nSmartDevice eklenti ayarlarından\n'Ignore free space'i etkinleştirin."
STR_CALIBRE_RECEIVING: "Alınıyor: "
STR_CALIBRE_RECEIVED: "Alındı: "
STR_CALIBRE_WAITING_MORE: "Devamı bekleniyor..."
STR_CALIBRE_FAILED_CREATE_FILE: "Dosya oluşturulamadı"
STR_CALIBRE_PASSWORD_REQUIRED: "Şifre gerekli"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer kesintiye uğradı"
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader eklentisini kurun"
STR_CALIBRE_INSTRUCTION_2: "2) Aynı WiFi ağında olun"
STR_CALIBRE_INSTRUCTION_3: "3) Calibre'de: \"Cihaza gönder\""
STR_CALIBRE_INSTRUCTION_4: "\"Gönderim sırasında bu ekranı açık tutun\""
STR_CAT_DISPLAY: "Ekran"
STR_CAT_READER: "Okuyucu"
STR_CAT_CONTROLS: "Kontroller"
STR_CAT_SYSTEM: "Sistem"
STR_SLEEP_SCREEN: "Uyku Ekranı"
STR_SLEEP_COVER_MODE: "Uyku Ekranı Kapak Modu"
STR_STATUS_BAR: "Durum Çubuğu"
STR_HIDE_BATTERY: "Pil Yüzdesini Gizle"
STR_EXTRA_SPACING: "Ekstra Paragraf Boşluğu"
STR_TEXT_AA: "Metin Yumuşatma (AA)"
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü"
STR_FRONT_BTN_LAYOUT: "Ön Tuş Dizilimi"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
STR_LONG_PRESS_SKIP: "Uzun Basışla Bölüm Atla"
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
STR_EXT_READER_FONT: "Harici Okuyucu Yazı Tipi"
STR_EXT_CHINESE_FONT: "Okuyucu Yazı Tipi"
STR_EXT_UI_FONT: "Arayüz Yazı Tipi"
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
STR_LINE_SPACING: "Okuyucu Satır Aralığı"
STR_ASCII_LETTER_SPACING: "ASCII Harf Aralığı"
STR_ASCII_DIGIT_SPACING: "ASCII Rakam Aralığı"
STR_CJK_SPACING: "CJK Aralığı"
STR_COLOR_MODE: "Renk Modu"
STR_SCREEN_MARGIN: "Okuyucu Ekran Kenar Boşluğu"
STR_PARA_ALIGNMENT: "Okuyucu Paragraf Hizalaması"
STR_HYPHENATION: "Hecelerden Ayırma"
STR_TIME_TO_SLEEP: "Uykuya Geçme Süresi"
STR_REFRESH_FREQ: "Yenileme Sıklığı"
STR_CALIBRE_SETTINGS: "Calibre Ayarları"
STR_KOREADER_SYNC: "KOReader Senkronizasyonu"
STR_CHECK_UPDATES: "Güncellemeleri denetle"
STR_LANGUAGE: "Dil"
STR_SELECT_WALLPAPER: "Duvar Kağıdı Seç"
STR_CLEAR_READING_CACHE: "Okuma Önbelleğini Temizle"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Kullanıcı Adı"
STR_PASSWORD: "Şifre"
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
STR_AUTHENTICATE: "Kimlik Doğrula"
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
STR_KOREADER_PASSWORD: "KOReader Şifresi"
STR_FILENAME: "Dosya Adı"
STR_BINARY: "İkili"
STR_SET_CREDENTIALS_FIRST: "Önce kimlik bilgilerini ayarlayın"
STR_WIFI_CONN_FAILED: "WiFi bağlantısı başarısız"
STR_AUTHENTICATING: "Kimlik doğrulanıyor..."
STR_AUTH_SUCCESS: "Kimlik doğrulama başarılı!"
STR_KOREADER_AUTH: "KOReader Doğrulaması"
STR_SYNC_READY: "KOReader senkronizasyonu hazır"
STR_AUTH_FAILED: "Kimlik Doğrulama Başarısız"
STR_DONE: "Tamamlandı"
STR_CLEAR_CACHE_WARNING_1: "Bu işlem tüm önbelleğe alınmış verileri siler."
STR_CLEAR_CACHE_WARNING_2: "Tüm okuma ilerlemesi kaybolacak!"
STR_CLEAR_CACHE_WARNING_3: "Kitapların tekrar açıldığında yeniden"
STR_CLEAR_CACHE_WARNING_4: "endekslenmesi gerekecek."
STR_CLEARING_CACHE: "Önbellek temizleniyor..."
STR_CACHE_CLEARED: "Önbellek Temizlendi"
STR_ITEMS_REMOVED: "öğe kaldırıldı"
STR_FAILED_LOWER: "başarısız"
STR_CLEAR_CACHE_FAILED: "Önbellek temizlenemedi"
STR_CHECK_SERIAL_OUTPUT: "Detaylar için seri çıktıya bakın"
STR_DARK: "Koyu"
STR_LIGHT: "Açık"
STR_CUSTOM: "Özel"
STR_COVER: "Kapak"
STR_NONE_OPT: "Yok"
STR_FIT: "Sığdır"
STR_CROP: "Kırp"
STR_NO_PROGRESS: "İlerleme Yok"
STR_FULL_OPT: "Tam"
STR_NEVER: "Asla"
STR_IN_READER: "Okuyucuda"
STR_ALWAYS: "Her Zaman"
STR_IGNORE: "Yoksay"
STR_SLEEP: "Uyku"
STR_PAGE_TURN: "Sayfa Çevirme"
STR_PORTRAIT: "Dikey"
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
STR_INVERTED: "Ters"
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
STR_FRONT_LAYOUT_BCLR: "Geri, Onayla, Sol, Sağ"
STR_FRONT_LAYOUT_LRBC: "Sol, Sağ, Geri, Onayla"
STR_FRONT_LAYOUT_LBCR: "Sol, Geri, Onayla, Sağ"
STR_PREV_NEXT: "Önceki/Sonraki"
STR_NEXT_PREV: "Sonraki/Önceki"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Küçük"
STR_MEDIUM: "Orta"
STR_LARGE: "Büyük"
STR_X_LARGE: "Çok Büyük"
STR_TIGHT: "Dar"
STR_NORMAL: "Normal"
STR_WIDE: "Geniş"
STR_JUSTIFY: "İki Yana Yasla"
STR_ALIGN_LEFT: "Sola Yasla"
STR_CENTER: "Ortala"
STR_ALIGN_RIGHT: "Sağa Yasla"
STR_MIN_1: "1 dak"
STR_MIN_5: "5 dak"
STR_MIN_10: "10 dak"
STR_MIN_15: "15 dak"
STR_MIN_30: "30 dak"
STR_PAGES_1: "1 sayfa"
STR_PAGES_5: "5 sayfa"
STR_PAGES_10: "10 sayfa"
STR_PAGES_15: "15 sayfa"
STR_PAGES_30: "30 sayfa"
STR_UPDATE: "Güncelle"
STR_CHECKING_UPDATE: "Güncelleme denetleniyor..."
STR_NEW_UPDATE: "Yeni güncelleme mevcut!"
STR_CURRENT_VERSION: "Mevcut Sürüm: "
STR_NEW_VERSION: "Yeni Sürüm: "
STR_UPDATING: "Güncelleniyor..."
STR_NO_UPDATE: "Güncelleme yok"
STR_UPDATE_FAILED: "Güncelleme başarısız"
STR_UPDATE_COMPLETE: "Güncelleme tamamlandı"
STR_POWER_ON_HINT: "Açmak için güç tuşuna basılı tutun"
STR_EXTERNAL_FONT: "Harici Yazı Tipi"
STR_BUILTIN_DISABLED: "Yerleşik (Devre Dışı)"
STR_NO_ENTRIES: "Girdi bulunamadı"
STR_DOWNLOADING: "İndiriliyor..."
STR_DOWNLOAD_FAILED: "İndirme başarısız"
STR_ERROR_MSG: "Hata:"
STR_UNNAMED: "İsimsiz"
STR_NO_SERVER_URL: "Sunucu adresi ayarlanmamış"
STR_FETCH_FEED_FAILED: "Akış alınamadı"
STR_PARSE_FEED_FAILED: "Akış ayrıştırılamadı"
STR_NETWORK_PREFIX: "Ağ: "
STR_IP_ADDRESS_PREFIX: "IP Adresi: "
STR_SCAN_QR_WIFI_HINT: "veya WiFi'ye bağlanmak için QR kodu tarayın."
STR_ERROR_GENERAL_FAILURE: "Hata: Genel hata"
STR_ERROR_NETWORK_NOT_FOUND: "Hata: Ağ bulunamadı"
STR_ERROR_CONNECTION_TIMEOUT: "Hata: Bağlantı zaman aşımı"
STR_SD_CARD: "SD kart"
STR_BACK: "« Geri"
STR_EXIT: "« Çıkış"
STR_HOME: "« Ana Sayfa"
STR_SAVE: "« Kaydet"
STR_SELECT: "Seç"
STR_TOGGLE: "Değiştir"
STR_CONFIRM: "Onayla"
STR_CANCEL: "İptal"
STR_CONNECT: "Bağlan"
STR_OPEN: "Aç"
STR_DOWNLOAD: "İndir"
STR_RETRY: "Tekrar Dene"
STR_YES: "Evet"
STR_NO: "Hayır"
STR_STATE_ON: "AÇIK"
STR_STATE_OFF: "KAPALI"
STR_NOT_SET: "Ayarlanmadı"
STR_DIR_LEFT: "Sol"
STR_DIR_RIGHT: "Sağ"
STR_DIR_UP: "Yukarı"
STR_DIR_DOWN: "Aşağı"
STR_CAPS_ON: "BÜYÜK"
STR_CAPS_OFF: "küçük"
STR_OK_BUTTON: "Tamam"
STR_SLEEP_COVER_FILTER: "Uyku Ekranı Kapak Filtresi"
STR_FILTER_CONTRAST: "Kontrast"
STR_UI_THEME: "Arayüz Teması"
STR_THEME_CLASSIC: "Klasik"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Genişletilmiş"
STR_SUNLIGHT_FADING_FIX: "Güneş Işığı Solma Düzeltmesi"
STR_REMAP_FRONT_BUTTONS: "Ön Tuşları Yeniden Ata"
STR_OPDS_BROWSER: "OPDS Tarayıcı"
STR_COVER_CUSTOM: "Kapak + Özel"
STR_RECENTS: "Son Okunanlar"
STR_MENU_RECENT_BOOKS: "Son Kitaplar"
STR_NO_RECENT_BOOKS: "Son okunan kitap yok"
STR_CALIBRE_DESC: "Calibre kablosuz cihaz transferini kullan"
STR_FORGET_AND_REMOVE: "Ağı unut ve kayıtlı şifreyi sil?"
STR_FORGET_BUTTON: "Unut"
STR_CALIBRE_STARTING: "Calibre Başlatılıyor..."
STR_CALIBRE_SETUP: "Kurulum"
STR_CALIBRE_STATUS: "Durum"
STR_CLEAR_BUTTON: "Temizle"
STR_DEFAULT_VALUE: "Varsayılan"
STR_REMAP_PROMPT: "Her rol için bir ön tuşa basın"
STR_UNASSIGNED: "Atanmamış"
STR_ALREADY_ASSIGNED: "Zaten atanmış"
STR_REMAP_RESET_HINT: "Yan tuş Yukarı: Varsayılan dizilime dön"
STR_REMAP_CANCEL_HINT: "Yan tuş Aşağı: Atamayı iptal et"
STR_HW_BACK_LABEL: "Geri (1. tuş)"
STR_HW_CONFIRM_LABEL: "Onayla (2. tuş)"
STR_HW_LEFT_LABEL: "Sol (3. tuş)"
STR_HW_RIGHT_LABEL: "Sağ (4. tuş)"
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_CHAPTER_PREFIX: "Bölüm: "
STR_PAGES_SEPARATOR: " sayfa | "
STR_BOOK_PREFIX: "Kitap: "
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "ÜST"
STR_KBD_LOCK: "KİLİT"
STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin"
STR_PERCENT_STEP_HINT: "Sol/Sağ: %1 Yukarı/Aşağı: %10"
STR_SYNCING_TIME: "Zaman senkronize ediliyor..."
STR_CALC_HASH: "Belge özeti hesaplanıyor..."
STR_HASH_FAILED: "Belge özeti hesaplanamadı"
STR_FETCH_PROGRESS: "Uzak ilerleme alınıyor..."
STR_UPLOAD_PROGRESS: "İlerleme yükleniyor..."
STR_NO_CREDENTIALS_MSG: "Kimlik bilgisi ayarlanmamış"
STR_KOREADER_SETUP_HINT: "Ayarlar'da KOReader hesabını kurun"
STR_PROGRESS_FOUND: "İlerleme bulundu!"
STR_REMOTE_LABEL: "Uzak:"
STR_LOCAL_LABEL: "Yerel:"
STR_PAGE_OVERALL_FORMAT: "Sayfa %d, genel %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Sayfa %d/%d, genel %.2f%%"
STR_DEVICE_FROM_FORMAT: " Şuradan: %s"
STR_APPLY_REMOTE: "Uzak ilerlemeyi uygula"
STR_UPLOAD_LOCAL: "Yerel ilerlemeyi yükle"
STR_NO_REMOTE_MSG: "Uzak ilerleme bulunamadı"
STR_UPLOAD_PROMPT: "Mevcut konumu yükle?"
STR_UPLOAD_SUCCESS: "İlerleme yüklendi!"
STR_SYNC_FAILED_MSG: "Senkronizasyon başarısız"
STR_SECTION_PREFIX: "Bölüm "
STR_UPLOAD: "Yükle"
STR_BOOK_S_STYLE: "Kitabın Stili"
STR_EMBEDDED_STYLE: "Gömülü Stil"
STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi"
STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: "
STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)"
STR_BATTERY: "Pil"
STR_BOOK: "Kitap"
STR_BOOK_PROGRESS_PERCENTAGE: "Kitap İlerleme Yüzdesi"
STR_CHAPTER: "Bölüm"
STR_CHAPTER_PAGE_COUNT: "Bölüm Sayfa Sayısı"
STR_CUSTOMISE_STATUS_BAR: "Durum Çubuğunu Özelleştir"
STR_DELETE: "Sil"
STR_DISPLAY_QR: "Sayfayı QR olarak göster"
STR_EXAMPLE_BOOK: "Kitap Başlığı"
STR_EXAMPLE_CHAPTER: "Bölüm 21"
STR_FOOTNOTES: "Dipnotlar"
STR_HIDE: "Gizle"
STR_IMAGES: "Görseller"
STR_IMAGES_DISPLAY: "Göster"
STR_IMAGES_PLACEHOLDER: "Yer Tutucu"
STR_IMAGES_SUPPRESS: "Bastır"
STR_LINK: "[bağlantı]"
STR_NO_FILES_FOUND: "Dosya bulunamadı"
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme"
STR_PROGRESS_BAR: "İlerleme Çubuğu"
STR_PROGRESS_BAR_MEDIUM: "Orta"
STR_PROGRESS_BAR_THICK: "Kalın"
STR_PROGRESS_BAR_THICKNESS: "İlerleme Çubuğu Kalınlığı"
STR_PROGRESS_BAR_THIN: "İnce"
STR_SCREENSHOT_BUTTON: "Ekran görüntüsü al"
STR_SELECTED: "Seçili"
STR_SHOW: "Göster"
STR_TITLE: "Başlık"
+39 -5
View File
@@ -1,15 +1,28 @@
#include "Logging.h"
#include <string>
#define MAX_ENTRY_LEN 256
#define MAX_LOG_LINES 16
// Simple ring buffer log, useful for error reporting when we encounter a crash
RTC_NOINIT_ATTR char logMessages[MAX_LOG_LINES][MAX_ENTRY_LEN];
RTC_NOINIT_ATTR size_t logHead = 0;
void addToLogRingBuffer(const char* message) {
// Add the message to the ring buffer, overwriting old messages if necessary
strncpy(logMessages[logHead], message, MAX_ENTRY_LEN - 1);
logMessages[logHead][MAX_ENTRY_LEN - 1] = '\0';
logHead = (logHead + 1) % MAX_LOG_LINES;
}
// Since logging can take a large amount of flash, we want to make the format string as short as possible.
// This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to
// provide the format string for the message itself.
void logPrintf(const char* level, const char* origin, const char* format, ...) {
if (!logSerial) {
return; // Serial not initialized, skip logging
}
va_list args;
va_start(args, format);
char buf[256];
char buf[MAX_ENTRY_LEN];
char* c = buf;
// add the timestamp
{
@@ -43,5 +56,26 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
// add the user message
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
va_end(args);
logSerial.print(buf);
if (logSerial) {
logSerial.print(buf);
}
addToLogRingBuffer(buf);
}
std::string getLastLogs() {
std::string output;
for (size_t i = 0; i < MAX_LOG_LINES; i++) {
size_t idx = (logHead + i) % MAX_LOG_LINES;
if (logMessages[idx][0] != '\0') {
output += logMessages[idx];
}
}
return output;
}
void clearLastLogs() {
for (size_t i = 0; i < MAX_LOG_LINES; i++) {
logMessages[i][0] = '\0';
}
logHead = 0;
}
+5
View File
@@ -2,6 +2,8 @@
#include <HardwareSerial.h>
#include <string>
/*
Define ENABLE_SERIAL_LOG to enable logging
Can be set in platformio.ini build_flags or as a compile definition
@@ -53,6 +55,9 @@ void logPrintf(const char* level, const char* origin, const char* format, ...);
#define LOG_INF(origin, format, ...)
#endif
std::string getLastLogs();
void clearLastLogs();
class MySerialImpl : public Print {
public:
void begin(unsigned long baud) { logSerial.begin(baud); }
+3 -12
View File
@@ -41,7 +41,7 @@ std::string Txt::getTitle() const {
std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath;
// Remove .txt extension
if (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".txt") {
if (FsHelpers::hasTxtExtension(filename)) {
filename = filename.substr(0, filename.length() - 4);
}
@@ -112,14 +112,7 @@ bool Txt::generateCoverBmp() const {
// Setup cache directory
setupCacheDir();
// Get file extension
const size_t len = coverImagePath.length();
const bool isJpg =
(len >= 4 && (coverImagePath.substr(len - 4) == ".jpg" || coverImagePath.substr(len - 4) == ".JPG")) ||
(len >= 5 && (coverImagePath.substr(len - 5) == ".jpeg" || coverImagePath.substr(len - 5) == ".JPEG"));
const bool isBmp = len >= 4 && (coverImagePath.substr(len - 4) == ".bmp" || coverImagePath.substr(len - 4) == ".BMP");
if (isBmp) {
if (FsHelpers::hasBmpExtension(coverImagePath)) {
// Copy BMP file to cache
LOG_DBG("TXT", "Copying BMP cover image to cache");
FsFile src, dst;
@@ -139,9 +132,7 @@ bool Txt::generateCoverBmp() const {
dst.close();
LOG_DBG("TXT", "Copied BMP cover to cache");
return true;
}
if (isJpg) {
} else if (FsHelpers::hasJpgExtension(coverImagePath)) {
// Convert JPG/JPEG to BMP (same approach as Epub)
LOG_DBG("TXT", "Generating BMP from JPG cover image");
FsFile coverJpg, coverBmp;
-10
View File
@@ -144,14 +144,4 @@ inline const char* errorToString(XtcError err) {
}
}
/**
* Check if filename has XTC/XTCH extension
*/
inline bool isXtcExtension(const char* filename) {
if (!filename) return false;
const char* ext = strrchr(filename, '.');
if (!ext) return false;
return (strcasecmp(ext, ".xtc") == 0 || strcasecmp(ext, ".xtch") == 0);
}
} // namespace xtc
+8 -3
View File
@@ -71,10 +71,15 @@ bool ZipFile::loadAllFileStatSlims() {
file.read(&k, 2);
file.seekCur(8);
file.read(&fileStat.localHeaderOffset, 4);
file.read(itemName, nameLen);
itemName[nameLen] = '\0';
fileStatSlimCache.emplace(itemName, fileStat);
if (nameLen < sizeof(itemName)) {
file.read(itemName, nameLen);
itemName[nameLen] = '\0';
fileStatSlimCache.emplace(itemName, fileStat);
} else {
// Skip over oversized entry names to avoid writing past fixed buffer.
file.seekCur(nameLen);
}
// Skip the rest of this entry (extra field + comment)
file.seekCur(m + k);
+140
View File
@@ -0,0 +1,140 @@
#include "HalSystem.h"
#include <string>
#include "Arduino.h"
#include "HalStorage.h"
#include "Logging.h"
#include "esp_debug_helpers.h"
#include "esp_private/esp_cpu_internal.h"
#include "esp_private/esp_system_attr.h"
#include "esp_private/panic_internal.h"
#define MAX_PANIC_STACK_DEPTH 32
RTC_NOINIT_ATTR char panicMessage[256];
RTC_NOINIT_ATTR HalSystem::StackFrame panicStack[MAX_PANIC_STACK_DEPTH];
extern "C" {
void __real_panic_abort(const char* message);
void __real_panic_print_backtrace(const void* frame, int core);
static DRAM_ATTR const char PANIC_REASON_UNKNOWN[] = "(unknown panic reason)";
void IRAM_ATTR __wrap_panic_abort(const char* message) {
if (!message) message = PANIC_REASON_UNKNOWN;
// IRAM-safe bounded copy (strncpy is not IRAM-safe in panic context)
int i = 0;
for (; i < (int)sizeof(panicMessage) - 1 && message[i]; i++) {
panicMessage[i] = message[i];
}
panicMessage[i] = '\0';
__real_panic_abort(message);
}
void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
if (!frame) {
__real_panic_print_backtrace(frame, core);
return;
}
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
panicStack[i].sp = 0;
}
// Copied from components/esp_system/port/arch/riscv/panic_arch.c
uint32_t sp = (uint32_t)((RvExcFrame*)frame)->sp;
const int per_line = 8;
int depth = 0;
for (int x = 0; x < 1024; x += per_line * sizeof(uint32_t)) {
uint32_t* spp = (uint32_t*)(sp + x);
// panic_print_hex(sp + x);
// panic_print_str(": ");
panicStack[depth].sp = sp + x;
for (int y = 0; y < per_line; y++) {
// panic_print_str("0x");
// panic_print_hex(spp[y]);
// panic_print_str(y == per_line - 1 ? "\r\n" : " ");
panicStack[depth].spp[y] = spp[y];
}
depth++;
if (depth >= MAX_PANIC_STACK_DEPTH) {
break;
}
}
__real_panic_print_backtrace(frame, core);
}
}
namespace HalSystem {
void begin() {
// This is mostly for the first boot, we need to initialize the panic info and logs to empty state
// If we reboot from a panic state, we want to keep the panic info until we successfully dump it to the SD card, use
// `clearPanic()` to clear it after dumping
if (!isRebootFromPanic()) {
clearPanic();
}
}
void checkPanic() {
if (isRebootFromPanic()) {
auto panicInfo = getPanicInfo(true);
auto file = Storage.open("/crash_report.txt", O_WRITE | O_CREAT | O_TRUNC);
if (file) {
file.write(panicInfo.c_str(), panicInfo.size());
file.close();
LOG_INF("SYS", "Dumped panic info to SD card");
} else {
LOG_ERR("SYS", "Failed to open crash_report.txt for writing");
}
}
}
void clearPanic() {
panicMessage[0] = '\0';
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
panicStack[i].sp = 0;
}
clearLastLogs();
}
std::string getPanicInfo(bool full) {
if (!full) {
return panicMessage;
} else {
std::string info;
info += "CrossPoint version: " CROSSPOINT_VERSION;
info += "\n\nPanic reason: " + std::string(panicMessage);
info += "\n\nLast logs:\n" + getLastLogs();
info += "\n\nStack memory:\n";
auto toHex = [](uint32_t value) {
char buffer[9];
snprintf(buffer, sizeof(buffer), "%08X", value);
return std::string(buffer);
};
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
if (panicStack[i].sp == 0) {
break;
}
info += "0x" + toHex(panicStack[i].sp) + ": ";
for (size_t j = 0; j < 8; j++) {
info += "0x" + toHex(panicStack[i].spp[j]) + " ";
}
info += "\n";
}
return info;
}
}
bool isRebootFromPanic() {
const auto resetReason = esp_reset_reason();
return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP;
}
} // namespace HalSystem
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <string>
namespace HalSystem {
struct StackFrame {
uint32_t sp;
uint32_t spp[8];
};
void begin();
// Dump panic info to SD card if necessary
void checkPanic();
void clearPanic();
std::string getPanicInfo(bool full = false);
bool isRebootFromPanic();
} // namespace HalSystem