File indexing completed on 2025-01-19 04:46:50

0001 /*
0002    SPDX-FileCopyrightText: 2017 Volker Krause <vkrause@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "itineraryrenderer.h"
0008 #include "itinerarykdeconnecthandler.h"
0009 #include "itinerarymemento.h"
0010 #include "itineraryurlhandler.h"
0011 
0012 #include <MessageViewer/HtmlWriter>
0013 #include <MessageViewer/IconNameCache>
0014 #include <MessageViewer/MessagePartRendererManager>
0015 
0016 #include <GrantleeTheme/GrantleeKi18nLocalizer>
0017 #include <GrantleeTheme/GrantleeThemeEngine>
0018 
0019 #include <KItinerary/Flight>
0020 #include <KItinerary/Reservation>
0021 #include <KItinerary/Ticket>
0022 
0023 #include <KColorScheme>
0024 #include <KTextTemplate/Engine>
0025 #include <KTextTemplate/MetaType>
0026 #include <KTextTemplate/Template>
0027 #include <Prison/Barcode>
0028 
0029 #include <QGuiApplication>
0030 #include <QPalette>
0031 
0032 using namespace KItinerary;
0033 
0034 ItineraryRenderer::ItineraryRenderer() = default;
0035 
0036 void ItineraryRenderer::setKDEConnectHandler(ItineraryKDEConnectHandler *kdeConnect)
0037 {
0038     m_kdeConnect = kdeConnect;
0039 }
0040 
0041 bool ItineraryRenderer::render(const MimeTreeParser::MessagePartPtr &msgPart,
0042                                MessageViewer::HtmlWriter *htmlWriter,
0043                                MessageViewer::RenderContext *context) const
0044 {
0045     Q_UNUSED(context)
0046     const auto mpList = msgPart.dynamicCast<MimeTreeParser::MessagePartList>();
0047     if (!msgPart->isRoot() || !mpList->hasSubParts()) {
0048         return false;
0049     }
0050 
0051     const auto node = mpList->subParts().at(0)->content();
0052     const auto nodeHelper = msgPart->nodeHelper();
0053     if (!nodeHelper || !node) {
0054         return false;
0055     }
0056 
0057     auto memento = dynamic_cast<ItineraryMemento *>(nodeHelper->bodyPartMemento(node->topLevel(), ItineraryMemento::identifier()));
0058     if (!memento || !memento->hasData()) {
0059         return false;
0060     }
0061     const auto extractedData = memento->data();
0062     if (extractedData.isEmpty()) { // hasData() will not be correct for filtered structured data on the first pass through here...
0063         return false;
0064     }
0065 
0066     const auto dir = nodeHelper->createTempDir(QStringLiteral("semantic"));
0067     auto c = MessageViewer::MessagePartRendererManager::self()->createContext();
0068 
0069     QVariantMap style;
0070     style.insert(QStringLiteral("expandIcon"),
0071                  QString(QStringLiteral("file://") + MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quoteexpand.png"))));
0072     style.insert(QStringLiteral("collapseIcon"),
0073                  QString(QStringLiteral("file://") + MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quotecollapse.png"))));
0074     style.insert(QStringLiteral("palette"), QGuiApplication::palette());
0075     style.insert(QStringLiteral("viewScheme"), QVariant::fromValue(KColorScheme(QPalette::Normal, KColorScheme::View)));
0076     c.insert(QStringLiteral("style"), style);
0077 
0078     const bool testMode = qEnvironmentVariableIsSet("BPF_ITINERARY_TESTMODE"); // ensure deterministic results for unit tests
0079     QVariantMap actionState;
0080     actionState.insert(QStringLiteral("canShowCalendar"), memento->startDate().isValid());
0081     actionState.insert(QStringLiteral("canAddToCalendar"), memento->canAddToCalendar());
0082     actionState.insert(QStringLiteral("hasItineraryApp"), ItineraryUrlHandler::hasItineraryApp() || testMode);
0083     if (!testMode) {
0084         const auto devices = m_kdeConnect->devices();
0085         actionState.insert(QStringLiteral("canSendToDevice"), !devices.isEmpty());
0086         if (devices.size() == 1) {
0087             actionState.insert(QStringLiteral("defaultDeviceName"), devices[0].name);
0088             actionState.insert(QStringLiteral("defaultDeviceId"), devices[0].deviceId);
0089         }
0090     }
0091     c.insert(QStringLiteral("actionState"), actionState);
0092 
0093     // Grantlee can't do indexed map/array lookups, so we need to interleave this here already
0094     QVariantList elems;
0095     elems.reserve(extractedData.size());
0096     int ticketTokenId = 0;
0097     for (int i = 0; i < extractedData.size(); ++i) {
0098         QVariantMap data;
0099         QVariantMap state;
0100         const auto d = extractedData.at(i);
0101         state.insert(QStringLiteral("expanded"), d.expanded);
0102         data.insert(QStringLiteral("state"), state);
0103         data.insert(QStringLiteral("groupId"), i);
0104 
0105         QList<QVariant> reservations;
0106         reservations.reserve(d.reservations.count());
0107         for (const auto &r : d.reservations) {
0108             QVariantMap m;
0109             m.insert(QStringLiteral("reservation"), r);
0110 
0111             // generate ticket barcodes
0112             const auto ticket = JsonLd::convert<Reservation>(r).reservedTicket().value<Ticket>();
0113             std::optional<Prison::Barcode> barcode;
0114             switch (ticket.ticketTokenType()) {
0115             case Ticket::AztecCode:
0116                 barcode = Prison::Barcode::create(Prison::Aztec);
0117                 break;
0118             case Ticket::QRCode:
0119                 barcode = Prison::Barcode::create(Prison::QRCode);
0120                 break;
0121             case Ticket::DataMatrix:
0122                 barcode = Prison::Barcode::create(Prison::DataMatrix);
0123                 break;
0124             case Ticket::Code128:
0125                 barcode = Prison::Barcode::create(Prison::Code128);
0126                 break;
0127             case Ticket::PDF417:
0128                 barcode = Prison::Barcode::create(Prison::PDF417);
0129                 break;
0130             default:
0131                 break;
0132             }
0133             if (barcode) {
0134                 const QVariant barcodeContent = ticket.ticketTokenData();
0135                 if (barcodeContent.userType() == QMetaType::QString) {
0136                     barcode->setData(barcodeContent.toString());
0137                 } else {
0138                     barcode->setData(barcodeContent.toByteArray());
0139                 }
0140 
0141                 const auto img = barcode->toImage(barcode->preferredSize(qGuiApp->devicePixelRatio()));
0142                 const QString fileName = dir + QLatin1StringView("/ticketToken") + QString::number(ticketTokenId++) + QLatin1StringView(".png");
0143                 img.save(fileName);
0144                 m.insert(QStringLiteral("ticketToken"), fileName);
0145                 nodeHelper->addTempFile(fileName);
0146             }
0147 
0148             reservations.push_back(m);
0149         }
0150         data.insert(QStringLiteral("reservations"), QVariant::fromValue(reservations));
0151         elems.push_back(data);
0152     }
0153     c.insert(QStringLiteral("data"), elems);
0154 
0155     auto t = MessageViewer::MessagePartRendererManager::self()->loadByName(QStringLiteral("org.kde.messageviewer/itinerary/itinerary.html"));
0156     const_cast<KTextTemplate::Engine *>(t->engine())->addDefaultLibrary(QStringLiteral("kitinerary_ktexttemplate_extension"));
0157     dynamic_cast<GrantleeTheme::Engine *>(const_cast<KTextTemplate::Engine *>(t->engine()))
0158         ->localizer()
0159         ->setApplicationDomain(QByteArrayLiteral("messageviewer_semantic_plugin"));
0160     KTextTemplate::OutputStream s(htmlWriter->stream());
0161     t->render(&s, &c);
0162     qobject_cast<GrantleeTheme::Engine *>(const_cast<KTextTemplate::Engine *>(t->engine()))
0163         ->localizer()
0164         ->setApplicationDomain(QByteArrayLiteral("libmessageviewer"));
0165     return false; // yes, false, we want the rest of the email rendered normally after this
0166 }