1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
|
// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "avatar.h"
#include "connection.h"
#include "logging_categories_p.h"
#include "jobs/mediathumbnailjob.h"
#include <QtCore/QDir>
#include <QtCore/QStandardPaths>
#include <QtCore/QStringBuilder>
#include <QtGui/QPainter>
using namespace Quotient;
class Q_DECL_HIDDEN Avatar::Private {
public:
explicit Private(Connection* c) : connection(c) {}
~Private()
{
thumbnailRequest.abandon();
uploadRequest.abandon();
}
Q_DISABLE_COPY_MOVE(Private)
QImage get(QSize size, get_callback_t callback) const;
void thumbnailRequestFinished() const;
QString localFile() const;
Connection* connection;
QUrl _url;
// The below are related to image caching, hence mutable
mutable QImage originalImage;
mutable std::vector<std::pair<QSize, QImage>> scaledImages;
mutable QSize largestRequestedSize{};
enum ImageSource : quint8 { Unknown, Cache, Network, Invalid };
mutable ImageSource imageSource = Invalid;
mutable JobHandle<MediaThumbnailJob> thumbnailRequest = nullptr;
mutable JobHandle<UploadContentJob> uploadRequest = nullptr;
mutable std::vector<get_callback_t> callbacks{};
};
Avatar::Avatar(Connection* parent, const QUrl& url) : d(makeImpl<Private>(parent))
{
if (!url.isEmpty())
updateUrl(url);
}
QImage Avatar::get(int dimension, get_callback_t callback) const
{
return d->get({ dimension, dimension }, std::move(callback));
}
QImage Avatar::get(int width, int height, get_callback_t callback) const
{
return d->get({ width, height }, std::move(callback));
}
bool Avatar::upload(const QString& fileName, upload_callback_t callback) const
{
if (isJobPending(d->uploadRequest))
return false;
upload(fileName).then(std::move(callback));
return true;
}
bool Avatar::upload(QIODevice* source, upload_callback_t callback) const
{
if (isJobPending(d->uploadRequest) || !source->isReadable())
return false;
upload(source).then(std::move(callback));
return true;
}
QFuture<QUrl> Avatar::upload(const QString& fileName) const
{
d->uploadRequest = d->connection->uploadFile(fileName);
return d->uploadRequest.responseFuture();
}
QFuture<QUrl> Avatar::upload(QIODevice* source) const
{
d->uploadRequest = d->connection->uploadContent(source);
return d->uploadRequest.responseFuture();
}
bool Avatar::isEmpty() const { return d->_url.isEmpty(); }
QString Avatar::mediaId() const { return d->_url.authority() + d->_url.path(); }
QImage Avatar::Private::get(QSize size, get_callback_t callback) const
{
if (imageSource == Unknown && originalImage.load(localFile())) {
imageSource = Cache;
largestRequestedSize = originalImage.size();
}
// Assuming that all thumbnails for this avatar have the same aspect ratio,
// it's enough for the image requested before to be large enough in at least
// one dimension to be suitable for scaling down to the requested size;
// therefore the new size has to be larger in both dimensions to warrant a
// new request to the server
if ((imageSource == Unknown && !thumbnailRequest)
|| (imageSource != Invalid && size.width() > largestRequestedSize.width()
&& size.height() > largestRequestedSize.height())) {
qCDebug(MAIN) << "Getting avatar from" << _url.toString();
largestRequestedSize = size;
thumbnailRequest.abandon();
if (callback)
callbacks.emplace_back(std::move(callback));
thumbnailRequest = connection->getThumbnail(_url, size);
thumbnailRequest.onResult([this] { thumbnailRequestFinished(); });
// The result of this request will only be returned when get() is
// called next time afterwards
}
if (imageSource == Invalid || originalImage.isNull())
return {};
// NB: because of KeepAspectRatio, scaledImage.size() might not be equal to
// requestedSize - this is why requestedSize is stored separately
for (const auto& [requestedSize, scaledImage] : scaledImages)
if (requestedSize == size)
return scaledImage;
const auto& result = originalImage.scaled(size, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
scaledImages.emplace_back(size, result);
return result;
}
void Avatar::Private::thumbnailRequestFinished() const
{
// NB: The following code preserves _originalImage in case of
// most errors
switch (thumbnailRequest->error()) {
case BaseJob::NoError: break;
case BaseJob::NetworkError:
case BaseJob::NetworkAuthRequired:
case BaseJob::TooManyRequests: // Shouldn't reach here but just in case
case BaseJob::Timeout:
return; // Make another attempt when requested again
default:
// Other errors are likely unrecoverable but just in case,
// check if there's a previous image to fall back to; if
// there is, assume that the error is temporary
if (originalImage.isNull())
imageSource = Invalid; // Can't do much with the rest
return;
}
auto&& img = thumbnailRequest->thumbnail();
if (img.format() == QImage::Format_Invalid) {
qCWarning(MAIN) << "The request for" << _url
<< "was successful but the received image "
"is invalid or unsupported";
return;
}
imageSource = Network;
originalImage = std::move(img);
originalImage.save(localFile());
scaledImages.clear();
for (auto&& n : callbacks)
n();
callbacks.clear();
}
QString Avatar::Private::localFile() const
{
static const auto cachePath = cacheLocation(u"avatars");
return cachePath % _url.authority() % u'_' % _url.fileName() % ".png"_L1;
}
QUrl Avatar::url() const { return d->_url; }
bool Avatar::updateUrl(const QUrl& newUrl)
{
if (newUrl == d->_url)
return false;
if (isUrlValid(newUrl)) {
d->_url = d->connection->makeMediaUrl(newUrl);
d->imageSource = Private::Unknown;
} else {
qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:" << newUrl.toDisplayString();
d->_url.clear();
d->imageSource = Private::Invalid;
}
d->originalImage = {};
d->scaledImages.clear();
d->thumbnailRequest.abandon();
return true;
}
bool Avatar::isUrlValid(const QUrl& u)
{
return u.isValid() && u.scheme() == u"mxc" && u.path().count(u'/') == 1;
}
|