mirror of
https://github.com/YACReader/yacreader
synced 2026-04-12 15:49:53 -04:00
Make flow ribbons themeable
This commit is contained in:
@ -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)
|
||||
|
||||
@ -78,8 +78,8 @@
|
||||
<file>../images/notCover.png</file>
|
||||
<file>../images/library_dialogs/openLibrary.svg</file>
|
||||
<file>../images/metadata_dialog/previousCoverPage.svg</file>
|
||||
<file>../images/readingRibbon.png</file>
|
||||
<file>../images/readRibbon.png</file>
|
||||
<file>../images/readingRibbon.svg</file>
|
||||
<file>../images/readRibbon.svg</file>
|
||||
<file>../images/metadata_dialog/resetCover.svg</file>
|
||||
<file>../images/search_result.svg</file>
|
||||
<file>../images/serverConfigBackground.svg</file>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"comicFlow": {
|
||||
"backgroundColor": "#000000",
|
||||
"readMainColor": "#db4725",
|
||||
"readTickColor": "#8a2c17",
|
||||
"readingColor": "#e6b90f",
|
||||
"textColor": "#4c4c4c"
|
||||
},
|
||||
"comicsViewTable": {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"comicFlow": {
|
||||
"backgroundColor": "#111111",
|
||||
"readMainColor": "#db4725",
|
||||
"readTickColor": "#8a2c17",
|
||||
"readingColor": "#e6b90f",
|
||||
"textColor": "#888888"
|
||||
},
|
||||
"comicsViewTable": {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"comicFlow": {
|
||||
"backgroundColor": "#dcdcdc",
|
||||
"readMainColor": "#db4725",
|
||||
"readTickColor": "#8a2c17",
|
||||
"readingColor": "#e6b90f",
|
||||
"textColor": "#303030"
|
||||
},
|
||||
"comicsViewTable": {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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")) {
|
||||
|
||||
@ -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<QRhiTexture> &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<QRhiTexture> 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)
|
||||
{
|
||||
|
||||
@ -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<float> &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();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
24
images/readRibbon.svg
Normal file
24
images/readRibbon.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 136">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #0ff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #f0f;
|
||||
filter: url(#drop-shadow-1);
|
||||
}
|
||||
</style>
|
||||
<filter id="drop-shadow-1" x="0" y="-1" width="100" height="134" filterUnits="userSpaceOnUse">
|
||||
<feOffset dx="0" dy="1"/>
|
||||
<feGaussianBlur result="blur" stdDeviation="2"/>
|
||||
<feFlood flood-color="#000" flood-opacity=".33"/>
|
||||
<feComposite in2="blur" operator="in"/>
|
||||
<feComposite in="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<path class="cls-2" d="M90.69,125.14l-38.73-33.45c-1.13-.97-2.8-.97-3.92,0L9.31,125.14c-1.3,1.12-3.31.2-3.31-1.51V8c0-2.21,1.79-4,4-4h80c2.21,0,4,1.79,4,4v115.63c0,1.71-2.01,2.63-3.31,1.51Z"/>
|
||||
<polygon class="cls-1" points="34.29 52 40 46.29 46 52.29 61 37.29 66.71 43 46 63.71 34.29 52"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 988 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
19
images/readingRibbon.svg
Normal file
19
images/readingRibbon.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 136">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f0f;
|
||||
filter: url(#drop-shadow-2);
|
||||
}
|
||||
</style>
|
||||
<filter id="drop-shadow-2" x="0" y="-1" width="100" height="134" filterUnits="userSpaceOnUse">
|
||||
<feOffset dx="0" dy="1"/>
|
||||
<feGaussianBlur result="blur" stdDeviation="2"/>
|
||||
<feFlood flood-color="#000" flood-opacity=".33"/>
|
||||
<feComposite in2="blur" operator="in"/>
|
||||
<feComposite in="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<path class="cls-1" d="M90.69,125.14l-38.73-33.45c-1.13-.97-2.8-.97-3.92,0L9.31,125.14c-1.3,1.12-3.31.2-3.31-1.51V8c0-2.21,1.79-4,4-4h80c2.21,0,4,1.79,4,4v115.63c0,1.71-2.01,2.63-3.31,1.51Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
Reference in New Issue
Block a user