diff --git a/YACReaderLibrary/comic_flow_widget.cpp b/YACReaderLibrary/comic_flow_widget.cpp
index b75fdb52..0c2b603e 100644
--- a/YACReaderLibrary/comic_flow_widget.cpp
+++ b/YACReaderLibrary/comic_flow_widget.cpp
@@ -24,6 +24,7 @@ void ComicFlowWidget::applyTheme(const Theme &theme)
{
setBackgroundColor(theme.comicFlow.backgroundColor);
setTextColor(theme.comicFlow.textColor);
+ flow->setRibbonImages(theme.comicFlow.readPixmap.toImage(), theme.comicFlow.readingPixmap.toImage());
}
void ComicFlowWidget::setBackgroundColor(const QColor &color)
diff --git a/YACReaderLibrary/images.qrc b/YACReaderLibrary/images.qrc
index 1f898934..b221181e 100644
--- a/YACReaderLibrary/images.qrc
+++ b/YACReaderLibrary/images.qrc
@@ -78,8 +78,8 @@
../images/notCover.png
../images/library_dialogs/openLibrary.svg
../images/metadata_dialog/previousCoverPage.svg
- ../images/readingRibbon.png
- ../images/readRibbon.png
+ ../images/readingRibbon.svg
+ ../images/readRibbon.svg
../images/metadata_dialog/resetCover.svg
../images/search_result.svg
../images/serverConfigBackground.svg
diff --git a/YACReaderLibrary/themes/builtin_classic.json b/YACReaderLibrary/themes/builtin_classic.json
index e3dc64ac..10c301cd 100644
--- a/YACReaderLibrary/themes/builtin_classic.json
+++ b/YACReaderLibrary/themes/builtin_classic.json
@@ -1,6 +1,9 @@
{
"comicFlow": {
"backgroundColor": "#000000",
+ "readMainColor": "#db4725",
+ "readTickColor": "#8a2c17",
+ "readingColor": "#e6b90f",
"textColor": "#4c4c4c"
},
"comicsViewTable": {
diff --git a/YACReaderLibrary/themes/builtin_dark.json b/YACReaderLibrary/themes/builtin_dark.json
index b625814d..2f5a4f41 100644
--- a/YACReaderLibrary/themes/builtin_dark.json
+++ b/YACReaderLibrary/themes/builtin_dark.json
@@ -1,6 +1,9 @@
{
"comicFlow": {
"backgroundColor": "#111111",
+ "readMainColor": "#db4725",
+ "readTickColor": "#8a2c17",
+ "readingColor": "#e6b90f",
"textColor": "#888888"
},
"comicsViewTable": {
diff --git a/YACReaderLibrary/themes/builtin_light.json b/YACReaderLibrary/themes/builtin_light.json
index cf6916ea..25d88e86 100644
--- a/YACReaderLibrary/themes/builtin_light.json
+++ b/YACReaderLibrary/themes/builtin_light.json
@@ -1,6 +1,9 @@
{
"comicFlow": {
"backgroundColor": "#dcdcdc",
+ "readMainColor": "#db4725",
+ "readTickColor": "#8a2c17",
+ "readingColor": "#e6b90f",
"textColor": "#303030"
},
"comicsViewTable": {
diff --git a/YACReaderLibrary/themes/theme.h b/YACReaderLibrary/themes/theme.h
index 9ade45fa..54ba16ad 100644
--- a/YACReaderLibrary/themes/theme.h
+++ b/YACReaderLibrary/themes/theme.h
@@ -92,9 +92,11 @@ struct MetadataScraperDialogThemeTemplates {
QSize rowIconSize = QSize(8, 7);
};
-struct ComicFlowColors {
+struct ComicFlowTheme {
QColor backgroundColor;
QColor textColor;
+ QPixmap readPixmap;
+ QPixmap readingPixmap;
};
struct ComicsViewTableThemeTemplates {
@@ -484,7 +486,7 @@ struct Theme {
ThemeMeta meta;
QJsonObject sourceJson;
- ComicFlowColors comicFlow;
+ ComicFlowTheme comicFlow;
MetadataScraperDialogTheme metadataScraperDialog;
HelpAboutDialogTheme helpAboutDialog;
WhatsNewDialogTheme whatsNewDialog;
diff --git a/YACReaderLibrary/themes/theme_factory.cpp b/YACReaderLibrary/themes/theme_factory.cpp
index a65359dc..22f21611 100644
--- a/YACReaderLibrary/themes/theme_factory.cpp
+++ b/YACReaderLibrary/themes/theme_factory.cpp
@@ -330,10 +330,18 @@ struct WhatsNewDialogParams {
QColor headerDecorationColor;
};
+struct ComicFlowParams {
+ QColor backgroundColor;
+ QColor textColor;
+ QColor readMainColor; // Main ribbon color for read state (#f0f in readRibbon.svg)
+ QColor readTickColor; // Tick color for read state (#0ff in readRibbon.svg)
+ QColor readingColor; // Main ribbon color for reading state (#f0f in readingRibbon.svg)
+};
+
struct ThemeParams {
ThemeMeta meta;
- ComicFlowColors comicFlowColors;
+ ComicFlowParams comicFlowParams;
MetadataScraperDialogParams metadataScraperDialogParams;
HelpAboutDialogTheme helpAboutDialogParams;
EmptyContainerParams emptyContainerParams;
@@ -361,10 +369,22 @@ Theme makeTheme(const ThemeParams ¶ms)
Theme theme;
// Comic Flow
- const auto &cf = params.comicFlowColors;
+ const auto &cf = params.comicFlowParams;
theme.comicFlow.backgroundColor = cf.backgroundColor;
theme.comicFlow.textColor = cf.textColor;
+ {
+ const qreal dpr = qApp->devicePixelRatio();
+ // readRibbon: #f0f (main) + #0ff (tick)
+ theme.comicFlow.readPixmap = renderSvgToPixmap(
+ recoloredSvgToThemeFile(":/images/readRibbon.svg", cf.readMainColor, cf.readTickColor, params.meta.id),
+ 100, 136, dpr);
+ // readingRibbon: #f0f (main)
+ theme.comicFlow.readingPixmap = renderSvgToPixmap(
+ recoloredSvgToThemeFile(":/images/readingRibbon.svg", cf.readingColor, params.meta.id),
+ 100, 136, dpr);
+ }
+
// MetadataScraperDialog
const auto &msd = params.metadataScraperDialogParams;
const auto &t = msd.t;
@@ -918,8 +938,11 @@ Theme makeTheme(const QJsonObject &json)
if (json.contains("comicFlow")) {
const auto o = json["comicFlow"].toObject();
- p.comicFlowColors.backgroundColor = colorFromJson(o, "backgroundColor", p.comicFlowColors.backgroundColor);
- p.comicFlowColors.textColor = colorFromJson(o, "textColor", p.comicFlowColors.textColor);
+ p.comicFlowParams.backgroundColor = colorFromJson(o, "backgroundColor", p.comicFlowParams.backgroundColor);
+ p.comicFlowParams.textColor = colorFromJson(o, "textColor", p.comicFlowParams.textColor);
+ p.comicFlowParams.readMainColor = colorFromJson(o, "readMainColor", p.comicFlowParams.readMainColor);
+ p.comicFlowParams.readTickColor = colorFromJson(o, "readTickColor", p.comicFlowParams.readTickColor);
+ p.comicFlowParams.readingColor = colorFromJson(o, "readingColor", p.comicFlowParams.readingColor);
}
if (json.contains("metadataScraperDialog")) {
diff --git a/common/rhi/yacreader_flow_rhi.cpp b/common/rhi/yacreader_flow_rhi.cpp
index 5d1a7405..7db94666 100644
--- a/common/rhi/yacreader_flow_rhi.cpp
+++ b/common/rhi/yacreader_flow_rhi.cpp
@@ -138,28 +138,8 @@ void YACReaderFlow3D::initialize(QRhiCommandBuffer *cb)
qDebug() << "YACReaderFlow3D: Created defaultTexture" << defaultImage.size();
}
-#ifdef YACREADER_LIBRARY
- // Initialize mark textures
- if (!scene.markTexture) {
- QImage markImage = QImage(":/images/readRibbon.png").convertToFormat(QImage::Format_RGBA8888);
- if (!markImage.isNull()) {
- scene.markTexture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, markImage.size(), 1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips));
- scene.markTexture->create();
- getResourceBatch()->uploadTexture(scene.markTexture.get(), markImage);
- getResourceBatch()->generateMips(scene.markTexture.get());
- }
- }
-
- if (!scene.readingTexture) {
- QImage readingImage = QImage(":/images/readingRibbon.png").convertToFormat(QImage::Format_RGBA8888);
- if (!readingImage.isNull()) {
- scene.readingTexture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, readingImage.size(), 1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips));
- scene.readingTexture->create();
- getResourceBatch()->uploadTexture(scene.readingTexture.get(), readingImage);
- getResourceBatch()->generateMips(scene.readingTexture.get());
- }
- }
-#endif
+ if (ribbonTexturesDirty || (!readRibbonImage.isNull() && !scene.markTexture) || (!readingRibbonImage.isNull() && !scene.readingTexture))
+ syncRibbonTextures(getResourceBatch());
// Create vertex buffer (quad geometry)
if (!scene.vertexBuffer) {
@@ -373,9 +353,35 @@ void YACReaderFlow3D::ensurePipeline()
void YACReaderFlow3D::render(QRhiCommandBuffer *cb)
{
- if (!m_rhi || numObjects == 0)
+ if (!m_rhi)
return;
+ QRhiResourceUpdateBatch *batch = scene.resourceUpdates;
+ scene.resourceUpdates = nullptr;
+
+ auto ensureBatch = [this, &batch]() {
+ if (!batch)
+ batch = m_rhi->nextResourceUpdateBatch();
+ return batch;
+ };
+ auto deferBatch = [this, &batch]() {
+ if (batch)
+ scene.resourceUpdates = batch;
+ };
+
+#ifdef YACREADER_LIBRARY
+ if (ribbonTexturesDirty || (!readRibbonImage.isNull() && !scene.markTexture) || (!readingRibbonImage.isNull() && !scene.readingTexture))
+ syncRibbonTextures(ensureBatch());
+#endif
+
+ // Even without draw calls, pending uploads still have to reach the GPU.
+ // Otherwise recreated ribbon textures would exist but never receive pixels.
+ if (numObjects == 0) {
+ if (batch)
+ cb->resourceUpdate(batch);
+ return;
+ }
+
const QSize outputSize = renderTarget()->pixelSize();
const QColor clearColor = backgroundColor;
@@ -552,6 +558,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb)
ensureUniformBufferCapacity(draws.size());
if (!scene.uniformBuffer) {
+ deferBatch();
qWarning() << "YACReaderFlow3D: No uniform buffer available for rendering";
return;
}
@@ -560,6 +567,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb)
ensurePipeline();
if (!scene.pipeline) {
+ deferBatch();
qWarning() << "YACReaderFlow3D: No pipeline available for rendering";
return;
}
@@ -569,6 +577,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb)
if (!scene.instanceBuffer || scene.instanceBuffer->size() < requiredInstanceSize) {
scene.instanceBuffer.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, requiredInstanceSize));
if (!scene.instanceBuffer->create()) {
+ deferBatch();
qWarning() << "YACReaderFlow3D: Failed to create instance buffer of size" << requiredInstanceSize;
return;
}
@@ -576,7 +585,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb)
// === PHASE 1: PREPARE (BEFORE PASS) ===
// Update ALL uniform and instance data for ALL draws in one batch
- QRhiResourceUpdateBatch *batch = m_rhi->nextResourceUpdateBatch();
+ batch = ensureBatch();
// Process pending texture uploads
if (!pendingTextureUploads.isEmpty()) {
@@ -603,6 +612,7 @@ void YACReaderFlow3D::render(QRhiCommandBuffer *cb)
// === PHASE 2: RENDER (DURING PASS) ===
cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, batch);
+ batch = nullptr;
cb->setGraphicsPipeline(scene.pipeline.get());
cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height()));
@@ -762,6 +772,65 @@ void YACReaderFlow3D::executeDrawWithOffset(QRhiCommandBuffer *cb, QRhiTexture *
cb->draw(6);
}
+void YACReaderFlow3D::removeCachedShaderBindings(QRhiTexture *texture)
+{
+ if (!texture)
+ return;
+
+ auto it = scene.shaderBindingsCache.find(texture);
+ if (it != scene.shaderBindingsCache.end()) {
+ delete it.value();
+ scene.shaderBindingsCache.erase(it);
+ }
+}
+
+void YACReaderFlow3D::syncRibbonTextures(QRhiResourceUpdateBatch *batch)
+{
+#ifndef YACREADER_LIBRARY
+ Q_UNUSED(batch);
+ ribbonTexturesDirty = false;
+#else
+ if (!m_rhi || !batch)
+ return;
+
+ bool allTexturesReady = true;
+
+ auto syncTexture = [this, batch, &allTexturesReady](std::unique_ptr &texture, const QImage &image, const char *name) {
+ if (image.isNull()) {
+ if (texture) {
+ removeCachedShaderBindings(texture.get());
+ texture.reset();
+ }
+ return;
+ }
+
+ if (texture && !ribbonTexturesDirty)
+ return;
+
+ if (texture) {
+ removeCachedShaderBindings(texture.get());
+ texture.reset();
+ }
+
+ std::unique_ptr newTexture(m_rhi->newTexture(QRhiTexture::RGBA8, image.size(), 1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips));
+ if (!newTexture->create()) {
+ qWarning() << "YACReaderFlow3D: Failed to create" << name << "ribbon texture";
+ allTexturesReady = false;
+ return;
+ }
+
+ batch->uploadTexture(newTexture.get(), image);
+ batch->generateMips(newTexture.get());
+ texture = std::move(newTexture);
+ };
+
+ syncTexture(scene.markTexture, readRibbonImage, "read");
+ syncTexture(scene.readingTexture, readingRibbonImage, "reading");
+
+ ribbonTexturesDirty = !allTexturesReady;
+#endif
+}
+
void YACReaderFlow3D::releaseResources()
{
scene.reset();
@@ -1278,6 +1347,19 @@ void YACReaderFlow3D::setTextColor(const QColor &color)
update();
}
+void YACReaderFlow3D::setRibbonImages(const QImage &readImage, const QImage &readingImage)
+{
+ readRibbonImage = readImage.convertToFormat(QImage::Format_RGBA8888);
+ readingRibbonImage = readingImage.convertToFormat(QImage::Format_RGBA8888);
+#ifdef YACREADER_LIBRARY
+ ribbonTexturesDirty = true;
+#else
+ ribbonTexturesDirty = false;
+#endif
+
+ update();
+}
+
// Event handlers
void YACReaderFlow3D::wheelEvent(QWheelEvent *event)
{
diff --git a/common/rhi/yacreader_flow_rhi.h b/common/rhi/yacreader_flow_rhi.h
index ca7184dd..a1062bbb 100644
--- a/common/rhi/yacreader_flow_rhi.h
+++ b/common/rhi/yacreader_flow_rhi.h
@@ -179,6 +179,10 @@ protected:
QColor backgroundColor;
QColor textColor;
+ QImage readRibbonImage;
+ QImage readingRibbonImage;
+ bool ribbonTexturesDirty = false;
+
/*** System info ***/
float viewRotate;
@@ -202,6 +206,8 @@ protected:
// Helper methods
QRhiTexture *createTextureFromImage(QRhiCommandBuffer *cb, const QImage &image);
+ void removeCachedShaderBindings(QRhiTexture *texture);
+ void syncRibbonTextures(QRhiResourceUpdateBatch *batch);
void updateUniformBuffer(QRhiCommandBuffer *cb, const UniformData &data);
void prepareMarkInstanceData(const YACReader3DImageRHI &image, QVector &data);
void ensureUniformBufferCapacity(int requiredSlots);
@@ -260,6 +266,9 @@ public slots:
void setBackgroundColor(const QColor &color);
void setTextColor(const QColor &color);
+ // Ribbon image setters (for themed SVG rasterized images)
+ void setRibbonImages(const QImage &readImage, const QImage &readingImage);
+
virtual void updateImageData() = 0;
void reset();
diff --git a/images/readRibbon.png b/images/readRibbon.png
deleted file mode 100644
index 4dcd32e2..00000000
Binary files a/images/readRibbon.png and /dev/null differ
diff --git a/images/readRibbon.svg b/images/readRibbon.svg
new file mode 100644
index 00000000..7eced57e
--- /dev/null
+++ b/images/readRibbon.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/images/readingRibbon.png b/images/readingRibbon.png
deleted file mode 100644
index 476912ef..00000000
Binary files a/images/readingRibbon.png and /dev/null differ
diff --git a/images/readingRibbon.svg b/images/readingRibbon.svg
new file mode 100644
index 00000000..1cd07eaa
--- /dev/null
+++ b/images/readingRibbon.svg
@@ -0,0 +1,19 @@
+
+
\ No newline at end of file