Merge pull request #243 from jpirnay/fi-rebuildcovers
fix: rebuild covers
This commit is contained in:
@@ -457,9 +457,41 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int effectiveSrcW = progressive ? (srcWidth + 7) / 8 : srcWidth;
|
// Progressive JPEGs must stay at 1/8 (DC-only mode, only safe scale with MCU_SKIP patch).
|
||||||
const int effectiveSrcH = progressive ? (srcHeight + 7) / 8 : srcHeight;
|
// For baseline JPEGs with a target size, pick the largest built-in DCT scale that still
|
||||||
const int decodeFlags = progressive ? JPEG_SCALE_EIGHTH : 0;
|
// keeps the decoded image >= the target — the custom scaler handles the fine remainder.
|
||||||
|
// This avoids decoding millions of pixels that are immediately thrown away.
|
||||||
|
int jpegDecodeFlags = 0;
|
||||||
|
int jpegScaleDenom = 1;
|
||||||
|
if (progressive) {
|
||||||
|
jpegDecodeFlags = JPEG_SCALE_EIGHTH;
|
||||||
|
jpegScaleDenom = 8;
|
||||||
|
} else if (targetWidth > 0 && targetHeight > 0) {
|
||||||
|
// Use max(scaleX, scaleY) as the constraint: the DCT pre-scale must keep BOTH axes
|
||||||
|
// >= target so the fine scaler always downscales (never upscales) on either axis.
|
||||||
|
// This is safe for both crop=true (which uses max scale) and crop=false (uses min scale).
|
||||||
|
const float scaleX = static_cast<float>(targetWidth) / srcWidth;
|
||||||
|
const float scaleY = static_cast<float>(targetHeight) / srcHeight;
|
||||||
|
const float scaleMax = scaleX > scaleY ? scaleX : scaleY;
|
||||||
|
if (scaleMax <= 0.125f) {
|
||||||
|
jpegDecodeFlags = JPEG_SCALE_EIGHTH;
|
||||||
|
jpegScaleDenom = 8;
|
||||||
|
} else if (scaleMax <= 0.25f) {
|
||||||
|
jpegDecodeFlags = JPEG_SCALE_QUARTER;
|
||||||
|
jpegScaleDenom = 4;
|
||||||
|
} else if (scaleMax <= 0.5f) {
|
||||||
|
jpegDecodeFlags = JPEG_SCALE_HALF;
|
||||||
|
jpegScaleDenom = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int effectiveSrcW = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
|
||||||
|
const int effectiveSrcH = (srcHeight + jpegScaleDenom - 1) / jpegScaleDenom;
|
||||||
|
|
||||||
|
if (jpegScaleDenom > 1) {
|
||||||
|
LOG_DBG("JPG", "Using JPEGDEC 1/%d DCT scale: %dx%d -> %dx%d", jpegScaleDenom, srcWidth, srcHeight, effectiveSrcW,
|
||||||
|
effectiveSrcH);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate output dimensions (pre-scale to fit display exactly)
|
// Calculate output dimensions (pre-scale to fit display exactly)
|
||||||
int outWidth = effectiveSrcW;
|
int outWidth = effectiveSrcW;
|
||||||
@@ -487,7 +519,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
scaleY_fp = (static_cast<uint32_t>(effectiveSrcH) << 16) / outHeight;
|
scaleY_fp = (static_cast<uint32_t>(effectiveSrcH) << 16) / outHeight;
|
||||||
needsScaling = true;
|
needsScaling = true;
|
||||||
|
|
||||||
LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", effectiveSrcW, effectiveSrcH, outWidth, outHeight,
|
LOG_DBG("JPG", "Fine-scaling %dx%d -> %dx%d (target %dx%d)", effectiveSrcW, effectiveSrcH, outWidth, outHeight,
|
||||||
targetWidth, targetHeight);
|
targetWidth, targetHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,7 +602,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
||||||
jpeg->setUserPointer(&ctx);
|
jpeg->setUserPointer(&ctx);
|
||||||
|
|
||||||
rc = jpeg->decode(0, 0, decodeFlags);
|
rc = jpeg->decode(0, 0, jpegDecodeFlags);
|
||||||
|
|
||||||
if (rc != 1 || ctx.error) {
|
if (rc != 1 || ctx.error) {
|
||||||
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "Txt.h"
|
#include "Txt.h"
|
||||||
|
|
||||||
|
#include <Bitmap.h>
|
||||||
|
#include <BitmapHelpers.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -184,6 +186,98 @@ bool Txt::generateCoverBmp() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Txt::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
||||||
|
std::string Txt::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||||
|
std::string Txt::getThumbBmpPath(int width, int height) const {
|
||||||
|
return cachePath + "/thumb_" + std::to_string(width) + "x" + std::to_string(height) + ".bmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Txt::generateThumbBmp(int height) const {
|
||||||
|
const std::string destPath = getThumbBmpPath(height);
|
||||||
|
if (Storage.exists(destPath.c_str())) return true;
|
||||||
|
const int width = static_cast<int>(height * 0.6f);
|
||||||
|
if (!generateThumbBmp(width, height)) return false;
|
||||||
|
const std::string srcPath = getThumbBmpPath(width, height);
|
||||||
|
Storage.rename(srcPath.c_str(), destPath.c_str());
|
||||||
|
return Storage.exists(destPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Txt::generateThumbBmp(int width, int height) const {
|
||||||
|
const std::string thumbPath = getThumbBmpPath(width, height);
|
||||||
|
if (Storage.exists(thumbPath.c_str())) return true;
|
||||||
|
|
||||||
|
setupCacheDir();
|
||||||
|
|
||||||
|
FsFile thumbBmp;
|
||||||
|
if (!Storage.openFileForWrite("TXT", thumbPath, thumbBmp)) return false;
|
||||||
|
|
||||||
|
const uint32_t rowSize = (static_cast<uint32_t>(width) + 31) / 32 * 4;
|
||||||
|
BmpHeader bmpHeader;
|
||||||
|
createBmpHeader(&bmpHeader, width, height, BmpRowOrder::TopDown);
|
||||||
|
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeader), sizeof(BmpHeader));
|
||||||
|
|
||||||
|
uint8_t* rowBuffer = static_cast<uint8_t*>(malloc(rowSize));
|
||||||
|
if (!rowBuffer) {
|
||||||
|
thumbBmp.close();
|
||||||
|
Storage.remove(thumbPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches the Lyra "no cover" placeholder: 1px border, white top third, black bottom two thirds.
|
||||||
|
// Book icon (32x32, 1=white/transparent, 0=black) centered in the white area.
|
||||||
|
// In 1-bit BMP: 1=white, 0=black.
|
||||||
|
static const uint8_t kIcon[] = {
|
||||||
|
0xFF, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFE, 0x1F, 0xE0, 0x00, 0x02, 0x1F, 0xE0, 0x00, 0x03,
|
||||||
|
0x1F, 0xE0, 0x00, 0x03, 0x1F, 0xF0, 0x00, 0x01, 0x1F, 0xF0, 0x00, 0x01, 0x1F, 0xF0, 0x00, 0x01, 0x9F, 0xF8, 0x00,
|
||||||
|
0x00, 0x9F, 0xF8, 0x00, 0x00, 0xDF, 0xFC, 0x00, 0x00, 0x6F, 0xFE, 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0x1F, 0xFF,
|
||||||
|
0x80, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x0F, 0xFF, 0x00, 0x00,
|
||||||
|
0x1F, 0xFE, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x6F, 0xF8, 0x00, 0x00, 0xDF, 0xF8, 0x00, 0x00, 0x9F, 0xF0, 0x00,
|
||||||
|
0x01, 0x9F, 0xF0, 0x00, 0x01, 0x1F, 0xE0, 0x00, 0x01, 0x1F, 0xE0, 0x00, 0x03, 0x1F, 0xE0, 0x00, 0x02, 0x1F, 0xE0,
|
||||||
|
0x00, 0x02, 0x1F, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x1F};
|
||||||
|
static constexpr int kIconSize = 32;
|
||||||
|
static constexpr int kIconStride = 4; // bytes per icon row (32 bits)
|
||||||
|
|
||||||
|
const int splitY = height / 3; // white above, black below
|
||||||
|
const int iconX = (width - kIconSize) / 2;
|
||||||
|
const int iconY = (splitY - kIconSize) / 2;
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
const bool blackRegion = (y >= splitY);
|
||||||
|
memset(rowBuffer, blackRegion ? 0x00 : 0xFF, rowSize);
|
||||||
|
|
||||||
|
// 1px border
|
||||||
|
if (y == 0 || y == height - 1) {
|
||||||
|
memset(rowBuffer, 0x00, rowSize);
|
||||||
|
} else {
|
||||||
|
// Left and right border pixels
|
||||||
|
rowBuffer[0] &= 0x7F; // clear MSB (x=0)
|
||||||
|
rowBuffer[(width - 1) / 8] &= ~(0x80u >> ((width - 1) % 8)); // clear x=width-1
|
||||||
|
|
||||||
|
// Overlay icon row if within icon bounds (icon is on white area)
|
||||||
|
const int iconRow = y - iconY;
|
||||||
|
if (!blackRegion && iconRow >= 0 && iconRow < kIconSize && iconX >= 0 && iconX + kIconSize <= width) {
|
||||||
|
// Icon format: 0=dark pixel, 1=white/transparent. In BMP: 0=black, 1=white.
|
||||||
|
// The icon pixels (0=dark) should clear bits in the white BMP region.
|
||||||
|
for (int ix = 0; ix < kIconSize; ix++) {
|
||||||
|
const int iconByte = iconRow * kIconStride + ix / 8;
|
||||||
|
const int iconBit = 7 - (ix % 8);
|
||||||
|
const bool iconDark = !((kIcon[iconByte] >> iconBit) & 1);
|
||||||
|
if (iconDark) {
|
||||||
|
const int bx = iconX + ix;
|
||||||
|
rowBuffer[bx / 8] &= ~(0x80u >> (bx % 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbBmp.write(rowBuffer, rowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(rowBuffer);
|
||||||
|
thumbBmp.close();
|
||||||
|
LOG_DBG("TXT", "Generated surrogate thumb BMP (%dx%d): %s", width, height, thumbPath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
|
bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ class Txt {
|
|||||||
[[nodiscard]] bool generateCoverBmp() const;
|
[[nodiscard]] bool generateCoverBmp() const;
|
||||||
[[nodiscard]] std::string findCoverImage() const;
|
[[nodiscard]] std::string findCoverImage() const;
|
||||||
|
|
||||||
|
// Thumbnail support - generates a surrogate placeholder cover BMP
|
||||||
|
[[nodiscard]] std::string getThumbBmpPath() const;
|
||||||
|
[[nodiscard]] std::string getThumbBmpPath(int height) const;
|
||||||
|
[[nodiscard]] std::string getThumbBmpPath(int width, int height) const;
|
||||||
|
[[nodiscard]] bool generateThumbBmp(int height) const;
|
||||||
|
[[nodiscard]] bool generateThumbBmp(int width, int height) const;
|
||||||
|
|
||||||
// Read content from file
|
// Read content from file
|
||||||
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
|
[[nodiscard]] bool readContent(uint8_t* buffer, size_t offset, size_t length) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <PngToBmpConverter.h>
|
#include <PngToBmpConverter.h>
|
||||||
|
#include <Txt.h>
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@@ -198,8 +199,62 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
|
|
||||||
const auto thumbSizes = GUI.getCoverThumbSizes(coverHeight);
|
const auto thumbSizes = GUI.getCoverThumbSizes(coverHeight);
|
||||||
|
|
||||||
|
// Called at every early-return point where a cover was just written to disk.
|
||||||
|
// Invalidates the carousel frame cache so tryFastHomeRender re-renders the tile
|
||||||
|
// with the new BMP rather than compositing the stale placeholder frame.
|
||||||
|
const auto onCoverGenerated = [this]() {
|
||||||
|
UITheme::getInstance().getMutableTheme().invalidateFrameCache();
|
||||||
|
coverRendered = false;
|
||||||
|
nextRecentCoverIndex++;
|
||||||
|
recentsLoading = false;
|
||||||
|
requestUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
for (; nextRecentCoverIndex < recentBooks.size(); nextRecentCoverIndex++) {
|
for (; nextRecentCoverIndex < recentBooks.size(); nextRecentCoverIndex++) {
|
||||||
RecentBook& book = recentBooks[nextRecentCoverIndex];
|
RecentBook& book = recentBooks[nextRecentCoverIndex];
|
||||||
|
|
||||||
|
// If coverBmpPath was previously cleared (failed cover extraction) or is missing after
|
||||||
|
// a cache wipe, attempt to generate it now using the full load path (buildIfMissing=true).
|
||||||
|
if (book.coverBmpPath.empty() && Storage.exists(book.path.c_str())) {
|
||||||
|
if (FsHelpers::hasEpubExtension(book.path)) {
|
||||||
|
Epub epub(book.path, "/.crosspoint");
|
||||||
|
if (epub.load(true, true)) {
|
||||||
|
const std::string thumbPath = epub.getThumbBmpPath();
|
||||||
|
bool success = thumbSizes.empty() ? epub.generateThumbBmp(coverHeight)
|
||||||
|
: epub.generateThumbBmp(thumbSizes[0].first, thumbSizes[0].second);
|
||||||
|
if (success) {
|
||||||
|
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
|
||||||
|
book.coverBmpPath = thumbPath;
|
||||||
|
LOG_DBG("HOME", "Generated missing cover for %s", book.path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (FsHelpers::hasXtcExtension(book.path)) {
|
||||||
|
Xtc xtc(book.path, "/.crosspoint");
|
||||||
|
if (xtc.load()) {
|
||||||
|
const std::string thumbPath = xtc.getThumbBmpPath();
|
||||||
|
bool success = thumbSizes.empty() ? xtc.generateThumbBmp(coverHeight)
|
||||||
|
: xtc.generateThumbBmp(thumbSizes[0].first, thumbSizes[0].second);
|
||||||
|
if (success) {
|
||||||
|
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
|
||||||
|
book.coverBmpPath = thumbPath;
|
||||||
|
LOG_DBG("HOME", "Generated missing cover for %s", book.path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
|
||||||
|
Txt txt(book.path, "/.crosspoint");
|
||||||
|
const std::string thumbPath = txt.getThumbBmpPath();
|
||||||
|
bool success = thumbSizes.empty() ? txt.generateThumbBmp(coverHeight)
|
||||||
|
: txt.generateThumbBmp(thumbSizes[0].first, thumbSizes[0].second);
|
||||||
|
if (success) {
|
||||||
|
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
|
||||||
|
book.coverBmpPath = thumbPath;
|
||||||
|
LOG_DBG("HOME", "Generated surrogate cover for %s", book.path.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCoverGenerated();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!book.coverBmpPath.empty()) {
|
if (!book.coverBmpPath.empty()) {
|
||||||
// Sidecar covers (JPG/PNG paths stored directly) must be converted to BMP thumbnails
|
// Sidecar covers (JPG/PNG paths stored directly) must be converted to BMP thumbnails
|
||||||
// and the stored coverBmpPath updated to the cache path with [WIDTH]x[HEIGHT] placeholder.
|
// and the stored coverBmpPath updated to the cache path with [WIDTH]x[HEIGHT] placeholder.
|
||||||
@@ -242,10 +297,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
// Don't permanently clear the path on failure — keep the raw sidecar path
|
// Don't permanently clear the path on failure — keep the raw sidecar path
|
||||||
// so the next home visit can retry (e.g. after more memory becomes available).
|
// so the next home visit can retry (e.g. after more memory becomes available).
|
||||||
}
|
}
|
||||||
coverRendered = false;
|
onCoverGenerated();
|
||||||
nextRecentCoverIndex++;
|
|
||||||
recentsLoading = false;
|
|
||||||
requestUpdate();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +318,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
bool success = true;
|
bool success = true;
|
||||||
if (FsHelpers::hasEpubExtension(book.path)) {
|
if (FsHelpers::hasEpubExtension(book.path)) {
|
||||||
Epub epub(book.path, "/.crosspoint");
|
Epub epub(book.path, "/.crosspoint");
|
||||||
epub.load(false, true);
|
epub.load(true, true);
|
||||||
for (const auto& sz : thumbSizes) {
|
for (const auto& sz : thumbSizes) {
|
||||||
const std::string path = UITheme::getCoverThumbPath(book.coverBmpPath, sz.first, sz.second);
|
const std::string path = UITheme::getCoverThumbPath(book.coverBmpPath, sz.first, sz.second);
|
||||||
if (!Storage.exists(path.c_str())) success = epub.generateThumbBmp(sz.first, sz.second) && success;
|
if (!Storage.exists(path.c_str())) success = epub.generateThumbBmp(sz.first, sz.second) && success;
|
||||||
@@ -279,15 +331,18 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
if (!Storage.exists(path.c_str())) success = xtc.generateThumbBmp(sz.first, sz.second) && success;
|
if (!Storage.exists(path.c_str())) success = xtc.generateThumbBmp(sz.first, sz.second) && success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
|
||||||
|
Txt txt(book.path, "/.crosspoint");
|
||||||
|
for (const auto& sz : thumbSizes) {
|
||||||
|
const std::string path = UITheme::getCoverThumbPath(book.coverBmpPath, sz.first, sz.second);
|
||||||
|
if (!Storage.exists(path.c_str())) success = txt.generateThumbBmp(sz.first, sz.second) && success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
|
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
|
||||||
book.coverBmpPath = "";
|
book.coverBmpPath = "";
|
||||||
}
|
}
|
||||||
coverRendered = false;
|
onCoverGenerated();
|
||||||
nextRecentCoverIndex++;
|
|
||||||
recentsLoading = false;
|
|
||||||
requestUpdate();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -295,16 +350,13 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
if (!Storage.exists(coverPath.c_str())) {
|
if (!Storage.exists(coverPath.c_str())) {
|
||||||
if (FsHelpers::hasEpubExtension(book.path)) {
|
if (FsHelpers::hasEpubExtension(book.path)) {
|
||||||
Epub epub(book.path, "/.crosspoint");
|
Epub epub(book.path, "/.crosspoint");
|
||||||
epub.load(false, true);
|
epub.load(true, true);
|
||||||
bool success = epub.generateThumbBmp(coverHeight);
|
bool success = epub.generateThumbBmp(coverHeight);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
|
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
|
||||||
book.coverBmpPath = "";
|
book.coverBmpPath = "";
|
||||||
}
|
}
|
||||||
coverRendered = false;
|
onCoverGenerated();
|
||||||
nextRecentCoverIndex++;
|
|
||||||
recentsLoading = false;
|
|
||||||
requestUpdate();
|
|
||||||
return;
|
return;
|
||||||
} else if (FsHelpers::hasXtcExtension(book.path)) {
|
} else if (FsHelpers::hasXtcExtension(book.path)) {
|
||||||
Xtc xtc(book.path, "/.crosspoint");
|
Xtc xtc(book.path, "/.crosspoint");
|
||||||
@@ -314,12 +366,18 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
|
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
|
||||||
book.coverBmpPath = "";
|
book.coverBmpPath = "";
|
||||||
}
|
}
|
||||||
coverRendered = false;
|
onCoverGenerated();
|
||||||
nextRecentCoverIndex++;
|
|
||||||
recentsLoading = false;
|
|
||||||
requestUpdate();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
|
||||||
|
Txt txt(book.path, "/.crosspoint");
|
||||||
|
bool success = txt.generateThumbBmp(coverHeight);
|
||||||
|
if (!success) {
|
||||||
|
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, "");
|
||||||
|
book.coverBmpPath = "";
|
||||||
|
}
|
||||||
|
onCoverGenerated();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,6 +483,11 @@ void HomeActivity::loop() {
|
|||||||
const bool isCarousel = (GUI.getHomeNavigation() == HomeNavigation::Carousel);
|
const bool isCarousel = (GUI.getHomeNavigation() == HomeNavigation::Carousel);
|
||||||
|
|
||||||
if (isCarousel) {
|
if (isCarousel) {
|
||||||
|
if (firstRenderDone && !recentsLoaded && !recentsLoading) {
|
||||||
|
loadRecentCovers(UITheme::getInstance().getMetrics().homeCoverHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const int bookCount = static_cast<int>(recentBooks.size());
|
const int bookCount = static_cast<int>(recentBooks.size());
|
||||||
const int menuItemCount = static_cast<int>(menuEntries.size());
|
const int menuItemCount = static_cast<int>(menuEntries.size());
|
||||||
const bool inCarouselRow = (selectorIndex < bookCount);
|
const bool inCarouselRow = (selectorIndex < bookCount);
|
||||||
@@ -521,9 +584,6 @@ void HomeActivity::render(RenderLock&&) {
|
|||||||
if (!firstRenderDone) {
|
if (!firstRenderDone) {
|
||||||
firstRenderDone = true;
|
firstRenderDone = true;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
} else if (!recentsLoaded && !recentsLoading) {
|
|
||||||
recentsLoading = true;
|
|
||||||
loadRecentCovers(metrics.homeCoverHeight);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -568,9 +628,6 @@ void HomeActivity::render(RenderLock&&) {
|
|||||||
if (!firstRenderDone) {
|
if (!firstRenderDone) {
|
||||||
firstRenderDone = true;
|
firstRenderDone = true;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
} else if (!recentsLoaded && !recentsLoading) {
|
|
||||||
recentsLoading = true;
|
|
||||||
loadRecentCovers(getHomeCoverRenderHeight(computeHomeScreenLayout(metrics, contentRect.height, menuCount)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ void MdReaderActivity::onEnter() {
|
|||||||
auto fileName = filePath.substr(filePath.rfind('/') + 1);
|
auto fileName = filePath.substr(filePath.rfind('/') + 1);
|
||||||
APP_STATE.openEpubPath = filePath;
|
APP_STATE.openEpubPath = filePath;
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
RECENT_BOOKS.addBook(filePath, fileName, "", "", ReaderActivity::sidecarCoverPath(filePath));
|
const std::string txtSidecar = ReaderActivity::sidecarCoverPath(filePath);
|
||||||
|
const std::string txtCover = txtSidecar.empty() ? txt->getThumbBmpPath() : txtSidecar;
|
||||||
|
RECENT_BOOKS.addBook(filePath, fileName, "", "", txtCover);
|
||||||
|
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,9 @@ void TxtReaderActivity::onEnter() {
|
|||||||
auto fileName = filePath.substr(filePath.rfind('/') + 1);
|
auto fileName = filePath.substr(filePath.rfind('/') + 1);
|
||||||
APP_STATE.openEpubPath = filePath;
|
APP_STATE.openEpubPath = filePath;
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
RECENT_BOOKS.addBook(filePath, fileName, "", "", ReaderActivity::sidecarCoverPath(filePath));
|
const std::string txtSidecar = ReaderActivity::sidecarCoverPath(filePath);
|
||||||
|
const std::string txtCover = txtSidecar.empty() ? txt->getThumbBmpPath() : txtSidecar;
|
||||||
|
RECENT_BOOKS.addBook(filePath, fileName, "", "", txtCover);
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
LOG_DBG("THEME", "Rendering bmp");
|
LOG_DBG("THEME", "Rendering bmp");
|
||||||
|
|
||||||
// Draw the cover image (bookWidth and bookHeight already match image aspect ratio)
|
renderer.fillRect(bookX, bookY, bookWidth, bookHeight, false);
|
||||||
renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight);
|
renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight);
|
||||||
|
|
||||||
// Draw border around the card
|
// Draw border around the card
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
static_cast<float>(tileWidth - 2 * hPaddingInSelection) / static_cast<float>(coverHeight);
|
static_cast<float>(tileWidth - 2 * hPaddingInSelection) / static_cast<float>(coverHeight);
|
||||||
const float cropX = std::max(0.0f, 1.0f - (tileRatio / ratio));
|
const float cropX = std::max(0.0f, 1.0f - (tileRatio / ratio));
|
||||||
|
|
||||||
|
// Clear tile to white before drawing: 1-bit BMPs only draw dark pixels,
|
||||||
|
// leaving white pixels transparent — any stale dark content shows through.
|
||||||
|
renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
|
tileWidth - 2 * hPaddingInSelection, coverHeight, false);
|
||||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
tileWidth - 2 * hPaddingInSelection, coverHeight, cropX);
|
tileWidth - 2 * hPaddingInSelection, coverHeight, cropX);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -338,6 +338,7 @@ void LyraCarouselTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect,
|
|||||||
const float bmpRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
const float bmpRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||||
const float tileRatio = static_cast<float>(maxW) / static_cast<float>(maxH);
|
const float tileRatio = static_cast<float>(maxW) / static_cast<float>(maxH);
|
||||||
const float cropX = (bmpRatio > tileRatio) ? (1.0f - tileRatio / bmpRatio) : 0.0f;
|
const float cropX = (bmpRatio > tileRatio) ? (1.0f - tileRatio / bmpRatio) : 0.0f;
|
||||||
|
renderer.fillRect(x, y, maxW, maxH, false);
|
||||||
renderer.drawBitmap(bitmap, x, y, maxW, maxH, cropX, 0.0f);
|
renderer.drawBitmap(bitmap, x, y, maxW, maxH, cropX, 0.0f);
|
||||||
// Clear only the pixels outside the arc in each corner.
|
// Clear only the pixels outside the arc in each corner.
|
||||||
// The arc centre for the top-left corner is (x+r, y+r). A pixel at
|
// The arc centre for the top-left corner is (x+r, y+r). A pixel at
|
||||||
|
|||||||
@@ -568,6 +568,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
coverWidth = bitmap.getWidth();
|
coverWidth = bitmap.getWidth();
|
||||||
|
renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, coverHeight, false);
|
||||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
||||||
coverHeight);
|
coverHeight);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user