From f65fd08e6511d90a9827fcbcb9f58c03824a1982 Mon Sep 17 00:00:00 2001 From: luisangelsm Date: Sat, 21 Mar 2026 20:45:23 +0100 Subject: [PATCH] Make flow ribbons themeable --- YACReaderLibrary/comic_flow_widget.cpp | 1 + YACReaderLibrary/images.qrc | 4 +- YACReaderLibrary/themes/builtin_classic.json | 3 + YACReaderLibrary/themes/builtin_dark.json | 3 + YACReaderLibrary/themes/builtin_light.json | 3 + YACReaderLibrary/themes/theme.h | 6 +- YACReaderLibrary/themes/theme_factory.cpp | 31 ++++- common/rhi/yacreader_flow_rhi.cpp | 130 +++++++++++++++---- common/rhi/yacreader_flow_rhi.h | 9 ++ images/readRibbon.png | Bin 2382 -> 0 bytes images/readRibbon.svg | 24 ++++ images/readingRibbon.png | Bin 2181 -> 0 bytes images/readingRibbon.svg | 19 +++ 13 files changed, 201 insertions(+), 32 deletions(-) delete mode 100644 images/readRibbon.png create mode 100644 images/readRibbon.svg delete mode 100644 images/readingRibbon.png create mode 100644 images/readingRibbon.svg 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 4dcd32e214e95372050a17ef1c07097110d18222..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2382 zcmV-U39iBuAOSP&pW?9uO9Sow&?60x=GVLOn%aSx8erG!Qa{vEa4at7>0g zUe#P8OAlcjjkCC-p8R6K|LmTApNFg_C>T|*M9CCT3Gvbk>sS0xYM0hw2#vRBNI#HI&CZ{U)`cooL@lBMbl z>(TOP4xJ2{bR<&P3_(L6Xb1#>pdkpdksV1^VYj4Km~sr9u>oG5z>Wt|)hZF!Ag zQ07I+;$g5*It43V@_dCMDinD>!w?aQJl|oU>=h8kEoBRYY3@a*vH-?l4lqPHU!fmz z(CT`oSMMZ@NER?k35-goP*Iir1)horU+&T8Un$lj)ob6K+q5f z_7F785CnpTK(L2k55WvU%OyiFMKD9qilK+}hvPjbJmEH2USC^r|B8x|RmEAdEMVWb z0*80^c~9Bs-+$jf;BR~9j+zIbs;hh&f#lKRiW>R+AK!tm2LoMX75=Y{q;@_8&>usQ zgy)Ur)tgIXsT50A9$lZT^H3{_xJ)ZB~O~sV5hG z@zn69_Th%XNa7k<9itxRfK(7q5MVuBf#~)x?_2g(b%}i$sw{);@_4+9!f=z-QQ}@; zM17RHwho_-!gt{}@LL38p!Eb}tsBm`_K*M%Gl3ta8KRY~ak@Oleg}R7zlCjJ4>7>L z9SSz@>h=Ck7K6kCG~fXm{0V3)n#!WhU7?4s()Z!L#Pe-K``%qy)A!`fi=RW4m1Fb% zX(V)n!1#>=5v17=VPL4tb)>nX!JywukZrX{IFlhM`rXOw+1cwoel8GoiK@Vao67WdF+@!wm$+Wd??yma1_&2uv_A zRPNGIx&D!$zZs|7nA#@n1NH^`EX27Aud;eVvDU{=UD?GbDKC8u=#;71&Wnc6tsuW2*f$&lmMr2& zhl7W<{p~(X#|<9pJ}xt|g)tKpHdChU4~_wjrO+j-U@8lgR6QuEt63?jT%?A|H2bs) zjL72S=f|S1?FCd|1pxv|>a%{|K?=i9uhvYJlx>bs(kU>N9#i9+E2>{CgaUIE0>);d zZd{SXRZdCi^$XQzoB(IHlj6Xz`vEB3UBOtxfwlRcn9OSr=?=zPDGWE08M!K{xqwNh zz~GqtaEx%QaLh$1dppJ=f4vERybxj_M(!#M~8yVTRTS% z8Z2w`0cG+y;n?9E3On0tKuADI?Hd?vIy@L?&tQTupD^^AeKOs|Gp$8VMz>EItgkhU2M;_g9)Hi%kInN@`2zbxO)iS!*Pev=fu$ z0_(mIjBRgPV~zqdyobQ;QVPSJ;{;(Lp=2~M3FijqXvVE!LuD^a(miw5wZ~=51Yx1U zq@9?AbA)q+bIz0O&2AUp4F{Sxwi`Y{SV$-tDKI!^ICrCv%(;g^^ZwZ#KJsawFOyrX zR)CSU0wd?%BH1M6k);)LzBN3jha3RL&R%b0i}*^)bOlCa@$vJ4sO!CzHFHv6IcEqc zsT~(Rd)srn)oQaSl(Y(rT$tV7c&dKKU9~yi8qP^&jYO0EWc1q5p8Ks68 zrak@pk4pnI@D}ek6lRH-90s zKqLyJgt+tJ+VZ7QMGZ9#_?yT$;}TOR(>3q;1cQg7>Oj$ynXP|3KGqkGx876ftoFo` zXX<;sKhRPaW5zj?8`ECx6bAhJiY3Hrh?mp%3ccbZ6hNHrM3Z>q^Qd{}+E-FK^CT&n2~N5mr7=#hS;P+v zwI6D#L1jMIba%|6(;$VOZqXXQ_Lt3)HvOmfrvlEZh6J+_yKVZvvq&P0oTgi}xZ3Gw z)23b6lJC&jSc-X-7*GnAUI-)e=@ukD*HIP*c{0J36WZzS4OjVI)i>V{c zM$}9OGH6+WnKh=4Oh@WyhJrC>E(^_k`&WPg0HX#fUvSPh0{{R307*qoM6N<$f^*ML AfdBvi 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 476912ef207b7d9f8963558d66f11fe3fa8ddcdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2181 zcmV;02zvL4P)RCwC#U0rMxRTQ56X}2v{OoYUk zm>6G7j1QVQ6x`!l<XI=eu{dsU!i8f-A}6 zKxUzv4|vBQ!X+38$~*zOi45I}5Rn8AGq+$MBy)sl+*JN4k~B<+L4=!=QLti(Z^nux zMRmSHX(s%bImn9C@a0~4@zy<&Ubd@nVsxt%<>mwxGAq7vr1Vg6k?SeAF!Kn@`q>9& z56)$q5oS?=6y0k zvK@tjc#_P7NEXBZ_QE4X$EXO=O_EH5zp^qZA2pqDTTE6!NDfy?ykksU)X|fAAR92J zEZt&S59ELl$$}Wb0fYcKhIgzJ2oXdG07CL8Wg%xFLI4l~LI4l~LI9{NLNpoSo20U!i~05;SR6cI!S0NINO0Wc3iga9A} zDGNY^fDi!l5D)@Jkg@;}f|LayLZGq$q%5Q?0E8fAA!Q*#01%SzvKJ`}5dwe^s4M^x z0zv?&EJO$ZLZGq$LiI)Rp5WrYTIJDzStVrRl`{do#N27kW8(|hR`ojo1d9_>bUA@rvrmsLMM#{1oQAWl`yCd5V z{jRjJ-CN9th$DX5$7~8Cs=4o$e!KF`q7{XCMoM0#tlj~=>*Wohs;k{v58LhMcH2bA zRAvezto8gQwQT?Tl2tRNx$cAP%`h%q*ZiD}8*I0aS%BG1ZhhR0eLB}i*V1?ByZKLu z?i{;tS#5gZgP|SF!Civm`*?&HX={jY_lwuicj&ux4=430X+oUR?z$67-HU5OU$f01 zANc|9@Ig*Ogr_W-Zx8WveZuuYx(D6M;e0r^s8jL&@i*nb`s&CpYYrcZPSKX;nLTnli{3{QDQ`j zv8^$pTY7?8#kS?e%bljc997gid$jh2pN6U!8J7hzf;?3j^+FNIFsEekm=PI9T^SJX zNzb5X(KAuhE$l*v+VtG}LzRM(GK%+rMVh)(Z8Vc23#9Z3vbc+$NzZmlQFp{VtN@~sbOi48)rKD2Pe!5WX6DzPiOo3feJ2$lM&>jb-YSSN|TgC3QUg=5MA2-;X>cC={ZnfS&tARV^Kw@>S~YH zZBkNJ^+K02HzN}7qnd~jpQE#Z(sKSEwW_|XWL36F$!ZPpUsjtIZHkj|Lx_x2l~g*! zBvN41rXaPE+UmD!4J_9xtFcWz{n96)Z%nc_T_u%nv?Nksaal}lrnXP02-$m5se5%x zs3ya*HXEeG+evMwKIBwoWx^w*q_!QDcU2#Yo*TykVK&LID)zBN3zOwPM2@jE7FapY z_qLr1%(nm1Mr4%LM7AG38?#&1W|EYIvX~95=9>drsA8FYve=dyLP~1Rx=iZ`8-UAf&xZ^S@FVuDbTWl1g@gFqfo^YhjZ5Mt!8d zW{nWaSjn2dLu;C|HnfA~+Dt4E=9-wq3zMvm)K}_rD!#H^W!0Zj>gI3E_yVDmQkbOz zqdrsL9kZ52#rt4`a$rN9Jf2sr`9Ma}3XJtV{%UyM%=BLkd($Q3Te@E&$gxXs#VHUQWCxzCI!~hAFkN?M$z)=rKzofrKGa1 z2ekIZpM|R!8L7T%%|}wkrNGE1Y(bQ&O9E9aS2ITxtB2^R}s8Oa?MUxJ-gY6BO9+;5_A5uXhHt zmS-OEKH+go1$1m6q#wS}5ns(1F_1+bE}sIlAj$L#$26lZlG= zWFo^%if%!Qq0FRI)5FQ2zpuEP*>q-8 z`0*%@hylLfX6LKcd?rTBG~~+?W_K~0#;lYdQ~6*H2y?dK-za-cc~RpdKFkS-@`o4_ zZ)g$~?^NrGYN4rA9tlN!Kf-;ih*4yo7#d$RRD&*@H*~k0r^_I@UT!f8e*wQPCRzV_ zf7s!oYEXomcm$$i!ps|FB)8mR_|?PDjaYUOQ@+b02O4?MEu7Z`(?Q0#Yl>v@r`uE> zG_x3XCAf*pVulJL(j(@MK6TyUCRRlYNRJr!6aZ4je*z2uy9!eNKIi)C00000NkvXX Hu0mjf3A6ob 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