Merge branch 'feat-ordered-list' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "../converters/DitherUtils.h"
|
||||
#include "../converters/DirectPixelWriter.h"
|
||||
#include "../converters/ImageDecoderFactory.h"
|
||||
|
||||
// Cache file format:
|
||||
@@ -66,6 +66,9 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
return false;
|
||||
}
|
||||
|
||||
DirectPixelWriter pw;
|
||||
pw.init(renderer);
|
||||
|
||||
for (int row = 0; row < cachedHeight; row++) {
|
||||
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||
@@ -74,13 +77,14 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
return false;
|
||||
}
|
||||
|
||||
int destY = y + row;
|
||||
const int destY = y + row;
|
||||
pw.beginRow(destY);
|
||||
for (int col = 0; col < cachedWidth; col++) {
|
||||
int byteIdx = col / 4;
|
||||
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
||||
const int byteIdx = col >> 2; // col / 4
|
||||
const int bitShift = 6 - (col & 3) * 2; // MSB first within byte
|
||||
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||
|
||||
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
|
||||
pw.writePixel(x + col, pixelValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalDisplay.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Direct framebuffer writer that eliminates per-pixel overhead from the image
|
||||
// rendering hot path. Pre-computes orientation transform as linear coefficients
|
||||
// and caches render-mode state so the inner loop is: one multiply, one add,
|
||||
// one shift, and one AND per pixel — no branches, no method calls.
|
||||
//
|
||||
// Caller is responsible for ensuring (outX, outY) are within screen bounds.
|
||||
// ImageBlock::render() already validates this before entering the pixel loop,
|
||||
// and the JPEG/PNG callbacks pre-clamp destination ranges to screen bounds.
|
||||
struct DirectPixelWriter {
|
||||
uint8_t* fb;
|
||||
GfxRenderer::RenderMode mode;
|
||||
|
||||
// Orientation is collapsed into a linear transform:
|
||||
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
||||
// phyY = phyYBase + x * phyYStepX + y * phyYStepY
|
||||
int phyXBase, phyYBase;
|
||||
int phyXStepX, phyYStepX; // per logical-X step
|
||||
int phyXStepY, phyYStepY; // per logical-Y step
|
||||
|
||||
// Row-precomputed: the Y-dependent portion of the physical coords
|
||||
int rowPhyXBase, rowPhyYBase;
|
||||
|
||||
void init(GfxRenderer& renderer) {
|
||||
fb = renderer.getFrameBuffer();
|
||||
mode = renderer.getRenderMode();
|
||||
|
||||
switch (renderer.getOrientation()) {
|
||||
case GfxRenderer::Portrait:
|
||||
// phyX = y, phyY = (DISPLAY_HEIGHT-1) - x
|
||||
phyXBase = 0;
|
||||
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
|
||||
phyXStepX = 0;
|
||||
phyYStepX = -1;
|
||||
phyXStepY = 1;
|
||||
phyYStepY = 0;
|
||||
break;
|
||||
case GfxRenderer::LandscapeClockwise:
|
||||
// phyX = (DISPLAY_WIDTH-1) - x, phyY = (DISPLAY_HEIGHT-1) - y
|
||||
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
|
||||
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
|
||||
phyXStepX = -1;
|
||||
phyYStepX = 0;
|
||||
phyXStepY = 0;
|
||||
phyYStepY = -1;
|
||||
break;
|
||||
case GfxRenderer::PortraitInverted:
|
||||
// phyX = (DISPLAY_WIDTH-1) - y, phyY = x
|
||||
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
|
||||
phyYBase = 0;
|
||||
phyXStepX = 0;
|
||||
phyYStepX = 1;
|
||||
phyXStepY = -1;
|
||||
phyYStepY = 0;
|
||||
break;
|
||||
case GfxRenderer::LandscapeCounterClockwise:
|
||||
// phyX = x, phyY = y
|
||||
phyXBase = 0;
|
||||
phyYBase = 0;
|
||||
phyXStepX = 1;
|
||||
phyYStepX = 0;
|
||||
phyXStepY = 0;
|
||||
phyYStepY = 1;
|
||||
break;
|
||||
default:
|
||||
// Fallback to LandscapeCounterClockwise (identity transform)
|
||||
phyXBase = 0;
|
||||
phyYBase = 0;
|
||||
phyXStepX = 1;
|
||||
phyYStepX = 0;
|
||||
phyXStepY = 0;
|
||||
phyYStepY = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call once per row before the column loop.
|
||||
// Pre-computes the Y-dependent portion so writePixel() only needs the X part.
|
||||
inline void beginRow(int logicalY) {
|
||||
rowPhyXBase = phyXBase + logicalY * phyXStepY;
|
||||
rowPhyYBase = phyYBase + logicalY * phyYStepY;
|
||||
}
|
||||
|
||||
// Write a single 2-bit dithered pixel value to the framebuffer.
|
||||
// Must be called after beginRow() for the current row.
|
||||
// No bounds checking — caller guarantees coordinates are valid.
|
||||
inline void writePixel(int logicalX, uint8_t pixelValue) const {
|
||||
// Determine whether to draw based on render mode
|
||||
bool draw;
|
||||
bool state;
|
||||
switch (mode) {
|
||||
case GfxRenderer::BW:
|
||||
draw = (pixelValue < 3);
|
||||
state = true;
|
||||
break;
|
||||
case GfxRenderer::GRAYSCALE_MSB:
|
||||
draw = (pixelValue == 1 || pixelValue == 2);
|
||||
state = false;
|
||||
break;
|
||||
case GfxRenderer::GRAYSCALE_LSB:
|
||||
draw = (pixelValue == 1);
|
||||
state = false;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (!draw) return;
|
||||
|
||||
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
||||
const int phyY = rowPhyYBase + logicalX * phyYStepX;
|
||||
|
||||
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX >> 3);
|
||||
const uint8_t bitMask = 1 << (7 - (phyX & 7));
|
||||
|
||||
if (state) {
|
||||
fb[byteIndex] &= ~bitMask; // Clear bit (draw black)
|
||||
} else {
|
||||
fb[byteIndex] |= bitMask; // Set bit (draw white)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Direct cache writer that eliminates per-pixel overhead from PixelCache::setPixel().
|
||||
// Pre-computes row pointer so the inner loop is just byte index + bit manipulation.
|
||||
//
|
||||
// Caller guarantees coordinates are within cache bounds.
|
||||
struct DirectCacheWriter {
|
||||
uint8_t* buffer;
|
||||
int bytesPerRow;
|
||||
int originX;
|
||||
uint8_t* rowPtr; // Pre-computed for current row
|
||||
|
||||
void init(uint8_t* cacheBuffer, int cacheBytesPerRow, int cacheOriginX) {
|
||||
buffer = cacheBuffer;
|
||||
bytesPerRow = cacheBytesPerRow;
|
||||
originX = cacheOriginX;
|
||||
rowPtr = nullptr;
|
||||
}
|
||||
|
||||
// Call once per row before the column loop.
|
||||
inline void beginRow(int screenY, int cacheOriginY) { rowPtr = buffer + (screenY - cacheOriginY) * bytesPerRow; }
|
||||
|
||||
// Write a 2-bit pixel value. No bounds checking.
|
||||
inline void writePixel(int screenX, uint8_t value) const {
|
||||
const int localX = screenX - originX;
|
||||
const int byteIdx = localX >> 2; // localX / 4
|
||||
const int bitShift = 6 - (localX & 3) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
rowPtr[byteIdx] = (rowPtr[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 4x4 Bayer matrix for ordered dithering
|
||||
@@ -26,15 +25,3 @@ inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
|
||||
if (adjusted < 192) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
// Draw a pixel respecting the current render mode for grayscale support
|
||||
inline void drawPixelWithRenderMode(GfxRenderer& renderer, int x, int y, uint8_t pixelValue) {
|
||||
GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
|
||||
if (renderMode == GfxRenderer::BW && pixelValue < 3) {
|
||||
renderer.drawPixel(x, y, true);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (pixelValue == 1 || pixelValue == 2)) {
|
||||
renderer.drawPixel(x, y, false);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && pixelValue == 1) {
|
||||
renderer.drawPixel(x, y, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
|
||||
#include "DirectPixelWriter.h"
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
@@ -167,10 +168,21 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
|
||||
if (dstYStart >= dstYEnd || dstXStart >= dstXEnd) return 1;
|
||||
|
||||
// Pre-compute orientation and render-mode state once per callback invocation
|
||||
DirectPixelWriter pw;
|
||||
pw.init(renderer);
|
||||
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
|
||||
}
|
||||
|
||||
// === 1:1 fast path: no scaling math ===
|
||||
if (fineScaleFP == FP_ONE) {
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, ctx->config->y);
|
||||
const uint8_t* row = &pixels[(dstY - blockY) * stride];
|
||||
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
|
||||
const int outX = cfgX + dstX;
|
||||
@@ -182,8 +194,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, dithered);
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
@@ -203,6 +215,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, ctx->config->y);
|
||||
const int32_t srcFyFP = dstY * invScaleFP;
|
||||
const int32_t fy = srcFyFP & FP_MASK;
|
||||
const int32_t fyInv = FP_ONE - fy;
|
||||
@@ -239,8 +253,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, dithered);
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
|
||||
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
|
||||
@@ -262,8 +276,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, dithered);
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
|
||||
// Right edge (with X boundary clamping)
|
||||
@@ -288,8 +302,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, dithered);
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
@@ -298,6 +312,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, ctx->config->y);
|
||||
const int32_t srcFyFP = dstY * invScaleFP;
|
||||
int ly = (srcFyFP >> FP_SHIFT) - blockY;
|
||||
if (ly < 0) ly = 0;
|
||||
@@ -319,8 +335,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, dithered);
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
|
||||
#include "DirectPixelWriter.h"
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
@@ -207,6 +208,17 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
bool useDithering = ctx->config->useDithering;
|
||||
bool caching = ctx->caching;
|
||||
|
||||
// Pre-compute orientation and render-mode state once per row
|
||||
DirectPixelWriter pw;
|
||||
pw.init(*ctx->renderer);
|
||||
pw.beginRow(outY);
|
||||
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
|
||||
cw.beginRow(outY, ctx->config->y);
|
||||
}
|
||||
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
|
||||
@@ -222,8 +234,8 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
}
|
||||
drawPixelWithRenderMode(*ctx->renderer, outX, outY, ditheredGray);
|
||||
if (caching) ctx->cache.setPixel(outX, outY, ditheredGray);
|
||||
pw.writePixel(outX, ditheredGray);
|
||||
if (caching) cw.writePixel(outX, ditheredGray);
|
||||
}
|
||||
|
||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||
@@ -356,10 +368,18 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate cache buffer using SCALED dimensions
|
||||
// Allocate cache buffer using SCALED dimensions.
|
||||
// PNG decode is fast enough (~135ms for 400x600) that caching provides minimal benefit
|
||||
// for larger images, while the cache buffer competes with the 44KB PNG decoder for heap.
|
||||
// Skip caching when the buffer would exceed the framebuffer size (48KB).
|
||||
static constexpr size_t PNG_MAX_CACHE_BYTES = 48000;
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||
size_t cacheSize = (size_t)((ctx.dstWidth + 3) / 4) * ctx.dstHeight;
|
||||
if (cacheSize > PNG_MAX_CACHE_BYTES) {
|
||||
LOG_DBG("PNG", "Skipping cache: %zu bytes exceeds PNG limit (%zu)", cacheSize, PNG_MAX_CACHE_BYTES);
|
||||
ctx.caching = false;
|
||||
} else if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
|
||||
ctx.caching = false;
|
||||
}
|
||||
|
||||
@@ -660,6 +660,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(name, "ul") == 0 || strcmp(name, "ol") == 0) {
|
||||
self->listStack.push_back({self->depth, name[0] == 'o', 0});
|
||||
}
|
||||
|
||||
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
||||
const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle(
|
||||
cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth);
|
||||
@@ -728,7 +732,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
self->updateEffectiveInlineStyle();
|
||||
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
char marker[12];
|
||||
if (!self->listStack.empty() && self->listStack.back().isOrdered) {
|
||||
self->listStack.back().counter += 1;
|
||||
snprintf(marker, sizeof(marker), "%d.", self->listStack.back().counter);
|
||||
} else {
|
||||
strcpy(marker, "\xe2\x80\xa2");
|
||||
}
|
||||
self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR);
|
||||
} else if (strcmp(name, "pre") == 0) {
|
||||
// Record depth so characterData can treat \n as a hard line break inside <pre>.
|
||||
// depth has not been incremented yet here; it will be after startElement returns.
|
||||
@@ -1080,6 +1091,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
|
||||
self->depth -= 1;
|
||||
|
||||
// Pop list entries whose ul/ol is now out of scope
|
||||
while (!self->listStack.empty() && self->listStack.back().depth >= self->depth) {
|
||||
self->listStack.pop_back();
|
||||
}
|
||||
|
||||
// Closing a footnote link — create entry from collected text and href
|
||||
if (self->insideFootnoteLink && self->depth == self->footnoteLinkDepth) {
|
||||
if (self->currentFootnoteLinkText[0] != '\0' && self->currentFootnoteLinkHref[0] != '\0') {
|
||||
|
||||
@@ -73,6 +73,13 @@ class ChapterHtmlSlimParser {
|
||||
int tableRowIndex = 0;
|
||||
int tableColIndex = 0;
|
||||
|
||||
struct ListEntry {
|
||||
int depth;
|
||||
bool isOrdered;
|
||||
int counter;
|
||||
};
|
||||
std::vector<ListEntry> listStack;
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -66,6 +66,10 @@ STR_SLEEP_COVER_MODE: "Slaapscherm omslag-modus"
|
||||
STR_HIDE_BATTERY: "Batterij % verbergen"
|
||||
STR_EXTRA_SPACING: "Extra regelafstand alinea"
|
||||
STR_TEXT_AA: "Tekst Anti-Aliasing"
|
||||
STR_IMAGES: "Afbeeldingen"
|
||||
STR_IMAGES_DISPLAY: "Weergave"
|
||||
STR_IMAGES_PLACEHOLDER: "Placeholder"
|
||||
STR_IMAGES_SUPPRESS: "Verbergen"
|
||||
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
|
||||
STR_ORIENTATION: "Leesstand"
|
||||
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
|
||||
@@ -77,6 +81,7 @@ STR_SCREEN_MARGIN: "Schermmarge lezer"
|
||||
STR_PARA_ALIGNMENT: "Uitlijning alinea lezer"
|
||||
STR_HYPHENATION: "Woordafbreking"
|
||||
STR_TIME_TO_SLEEP: "Tijd tot slaapstand"
|
||||
STR_SHOW_HIDDEN_FILES: "Toon verborgen bestanden"
|
||||
STR_REFRESH_FREQ: "Verversingsfrequentie"
|
||||
STR_KOREADER_SYNC: "KOReader Sync"
|
||||
STR_CHECK_UPDATES: "Controleren op updates"
|
||||
@@ -180,6 +185,7 @@ STR_BACK: "« Terug"
|
||||
STR_EXIT: "« Sluit"
|
||||
STR_HOME: "« Home"
|
||||
STR_SELECT: "Kies"
|
||||
STR_SELECTED: "Geselecteerd"
|
||||
STR_TOGGLE: "Wissel"
|
||||
STR_CONFIRM: "Bevestig"
|
||||
STR_CANCEL: "Annuleer"
|
||||
@@ -247,6 +253,7 @@ STR_GO_TO_PERCENT: "Ga naar %"
|
||||
STR_GO_HOME_BUTTON: "Naar Home"
|
||||
STR_SYNC_PROGRESS: "Voortgang synchroniseren"
|
||||
STR_DELETE_CACHE: "Boekcache verwijderen"
|
||||
STR_DELETE: "Verwijder"
|
||||
STR_DISPLAY_QR: "Pagina als QR tonen"
|
||||
STR_CHAPTER_PREFIX: "Hoofdstuk: "
|
||||
STR_PAGES_SEPARATOR: " pagina's | "
|
||||
@@ -281,3 +288,5 @@ STR_FOOTNOTES: "Voetnoten"
|
||||
STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
|
||||
STR_LINK: "[link]"
|
||||
STR_SCREENSHOT_BUTTON: "Screenshot maken"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
|
||||
Reference in New Issue
Block a user