Krita Source Code Documentation
Loading...
Searching...
No Matches
KisSupporterBundleWidget.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: GPL-3.0-or-later
3 */
7#include <KisStorageModel.h>
8#include <QCoreApplication>
9#include <QCryptographicHash>
10#include <QMessageBox>
11#include <QNetworkReply>
12#include <QProgressDialog>
13#include <QTimer>
14#include <kis_assert.h>
15#include <kis_debug.h>
16
18 const KisSupporterBundle &bundle,
19 const QVector<KisSupporterProduct> &bundleProducts,
20 QWidget *parent)
21 : QWidget(parent)
22 , m_fetcher(fetcher)
23 , m_bundle(bundle)
24{
25 setupUi(this);
26
27 QStringList contentList;
28 addContentEntry(contentList, QStringLiteral("paintoppresets"), [](int count) {
29 return i18np("%1 brush preset", "%1 brush presets", count);
30 });
31 addContentEntry(contentList, QStringLiteral("brushes"), [](int count) {
32 return i18np("%1 brush tips", "%1 brush tips", count);
33 });
34 addContentEntry(contentList, QStringLiteral("patterns"), [](int count) {
35 return i18np("%1 texture patterns", "%1 texture patterns", count);
36 });
37 addContentEntry(contentList, QStringLiteral("workspaces"), [](int count) {
38 return i18np("%1 workspace layout", "%1 workspace layouts", count);
39 });
40
41 QStringList productList;
42 for (const KisSupporterProduct &product : bundleProducts) {
43 switch (product.availability) {
44 case KisSupporterProductAvailability::Missing:
45 // Product is neither available for purchase nor does the user own
46 // it, so we'll pretend like it doesn't exist.
47 break;
48 case KisSupporterProductAvailability::Available:
49 productList.append(product.title);
50 break;
51 case KisSupporterProductAvailability::Owned:
52 if (product.type == KisSupporterProductType::Subscription) {
53 productList.append(i18n("%1 (subscribed)").arg(product.title));
54 } else {
55 productList.append(i18n("%1 (owned)").arg(product.title));
56 }
57 m_productOwned = true;
58 break;
59 }
60 }
61
62 if (m_bundle.thumbnail.isNull()) {
63 m_lblPreview->hide();
64 } else {
65 m_lblPreview->setPixmap(m_bundle.thumbnail);
66 }
67
68 QString author = m_bundle.author.toHtmlEscaped();
69 if (!m_bundle.source.isEmpty()) {
70 author = QStringLiteral("<a href=\"%1\">%2</a>").arg(m_bundle.source.toHtmlEscaped(), author);
71 }
72 m_lblTitle->setText(QStringLiteral("<strong>%1</strong> by %2").arg(m_bundle.title.toHtmlEscaped(), author));
73 m_lblDescription->setText(m_bundle.description);
74
75 // The margins on this stuff is ginormous by default, cut that down.
76 QString productsText = QStringLiteral(
77 "<style>\n"
78 "ul { margin-top: 0px; margin-bottom: 4px; margin-left: -10px; }\n"
79 "li { margin: 0px; padding: 0px; }\n"
80 "</style>");
81 if (!contentList.isEmpty()) {
82 productsText.append(
83 i18nc("in-app purchases, prefix for a list of what a bundle contains", "Contains:").toHtmlEscaped());
84 productsText.append(QStringLiteral("<ul>"));
85 for (const QString &content : contentList) {
86 productsText.append(QStringLiteral("<li>%1</li>").arg(content.toHtmlEscaped()));
87 }
88 productsText.append(QStringLiteral("</ul>"));
89 }
90
91 if (productList.isEmpty()) {
92 productsText.append(i18nc("in-app purchases, a bundle that the user can't acquire", "Not available."));
93 } else {
94 productsText.append(
95 i18nc("in-app purchases, prefix for a list of which purchases make a bundle available", "Available with:")
96 .toHtmlEscaped());
97 productsText.append(QStringLiteral("<ul>"));
98 for (const QString &product : productList) {
99 productsText.append(QStringLiteral("<li>%1</li>").arg(product.toHtmlEscaped()));
100 }
101 productsText.append(QStringLiteral("</ul>"));
102 }
103
104 m_lblProducts->setText(productsText);
105
108 if (!m_canImport) {
109 m_stkDownload->setCurrentWidget(m_pgDone);
110 } else if (m_productOwned) {
111 m_stkDownload->setCurrentWidget(m_pgGet);
112 } else {
113 m_stkDownload->setCurrentWidget(m_pgSupport);
114 }
115
116 connect(m_btnGet, &QPushButton::clicked, this, &KisSupporterBundleWidget::slotStartDownload);
117
119 if (androidDonations) {
120 connect(m_btnSupport, &QPushButton::clicked, androidDonations, &KisAndroidDonations::slotStartDonationFlow);
121 } else {
122 m_btnSupport->setEnabled(false);
123 }
124}
125
127{
131
132 m_progressDlg = new QProgressDialog(this);
133 m_progressDlg->setAutoClose(false);
134 m_progressDlg->setAutoReset(false);
135 m_progressDlg->setModal(true);
136 m_progressDlg->setMinimumDuration(0);
137 m_progressDlg->setRange(0, 0);
138 m_progressDlg->setLabelText(i18n("Downloading bundle…"));
139 m_progressDlg->setWindowTitle(i18n("%1").arg(m_bundle.title));
140 m_progressDlg->show();
141
142 QNetworkReply *reply = m_fetcher->fetchBundle(m_bundle);
143 connect(reply, &QNetworkReply::downloadProgress, this, &KisSupporterBundleWidget::slotUpdateDownloadProgress);
144 connect(m_progressDlg, &QProgressDialog::canceled, reply, &QNetworkReply::abort);
145 connect(reply, &QNetworkReply::finished, this, [=] {
147 reply->deleteLater();
148 });
149}
150
151void KisSupporterBundleWidget::slotUpdateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
152{
153 if (m_progressDlg) {
154 if (bytesTotal <= 0) {
155 m_progressDlg->setRange(0, 0);
156 } else {
157 m_progressDlg->setRange(0, 100);
158 m_progressDlg->setValue(qRound(qreal(bytesTotal) / qreal(bytesReceived) * 100.0));
159 }
160 }
161}
162
164 const QString &key,
165 const std::function<QString(int)> &formatFn) const
166{
167 int count = m_bundle.resourceCountByMediaType.value(key, 0);
168 if (count > 0) {
169 outContentList.append(formatFn(count));
170 }
171}
172
174{
175 if (reply->error() == QNetworkReply::OperationCanceledError) {
176 showFinishedDownload(false, true, QString());
177 return;
178 }
179
180 QString errorMessage;
181 if (KisSupporterBundlesFetcher::hasNetworkReplyError(reply, errorMessage)) {
182 showFinishedDownload(false, false, errorMessage);
183 return;
184 }
185
187 QCoreApplication::processEvents();
188
189 QByteArray bytes = reply->readAll();
190 long long sizeInBytes = bytes.size();
191 if (sizeInBytes != m_bundle.sizeInBytes) {
192 warnResources << "Bundle" << m_bundle.fileName << "size of" << sizeInBytes << "does not match expected"
194 showFinishedDownload(false, false, i18n("Invalid bundle size"));
195 return;
196 }
197
198 QString hash = QCryptographicHash::hash(bytes, QCryptographicHash::Md5).toHex();
199 if (hash != bundle().checksum) {
200 warnResources << "Bundle" << m_bundle.fileName << "hash" << hash << "does not match expected"
202 showFinishedDownload(false, false, i18n("Invalid bundle checksum"));
203 return;
204 }
205
206 m_progressDlg->setLabelText(i18n("Activating bundle…"));
208 QCoreApplication::processEvents();
209
211 QGuiApplication::setOverrideCursor(Qt::WaitCursor);
212 bool importOk = storageModel->importStorageData(m_bundle.fileName, KisStorageModel::None, bytes);
213 QGuiApplication::restoreOverrideCursor();
214
215 if (!importOk) {
216 showFinishedDownload(false, false, i18n("Failed to import bundle"));
217 return;
218 }
219
220 showFinishedDownload(true, false, QString());
221}
222
223void KisSupporterBundleWidget::showFinishedDownload(bool success, bool cancel, const QString &errorMessage)
224{
225 if (success) {
226 m_btnGet->clearFocus(); // Prevent auto-scrolling to the next button.
228 m_canImport = false;
229 m_stkDownload->setCurrentWidget(m_pgDone);
230 } else if (cancel) {
232 m_btnGet->setEnabled(true);
233 m_stkDownload->setCurrentWidget(m_pgGet);
234 } else {
235 // Delay this a bit to avoid excessively fast retries and to make it
236 // clear to the user that a retry is actually happening. Otherwise the
237 // download progress bar may flash so quickly that it seems like it's
238 // not doing anything at all.
239 QTimer::singleShot(1000, [=] {
241 QMessageBox::warning(this, i18n("Download failed"), i18n("Bundle download failed: %1").arg(errorMessage));
242 m_stkDownload->setCurrentWidget(m_pgGet);
243 });
244 }
245}
246
248{
249 if (m_progressDlg) {
250 m_progressDlg->close();
251 m_progressDlg = nullptr;
252 }
253}
static KisAndroidDonations * instance()
static KisStorageModel * instance()
bool importStorageData(const QString &filename, StorageImportOption importOption, const QByteArray &data) const
bool canImportStorage(const QString &filename) const
void handleDownloadFinished(QNetworkReply *reply)
void showFinishedDownload(bool success, bool cancel, const QString &errorMessage)
KisSupporterBundleWidget(KisSupporterBundlesFetcher *fetcher, const KisSupporterBundle &bundle, const QVector< KisSupporterProduct > &bundleProducts, QWidget *parent=nullptr)
const KisSupporterBundle & bundle() const
QPointer< KisSupporterBundlesFetcher > m_fetcher
void addContentEntry(QStringList &outContentList, const QString &key, const std::function< QString(int)> &formatFn) const
void slotUpdateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
static bool hasNetworkReplyError(QNetworkReply *reply, QString &outErrorMessage)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define warnResources
Definition kis_debug.h:85
QHash< QString, int > resourceCountByMediaType