summaryrefslogtreecommitdiff
path: root/settings/accounts-translations-plugin/plugin.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'settings/accounts-translations-plugin/plugin.cpp')
-rw-r--r--settings/accounts-translations-plugin/plugin.cpp182
1 files changed, 175 insertions, 7 deletions
diff --git a/settings/accounts-translations-plugin/plugin.cpp b/settings/accounts-translations-plugin/plugin.cpp
index 4a1c651..e377b6c 100644
--- a/settings/accounts-translations-plugin/plugin.cpp
+++ b/settings/accounts-translations-plugin/plugin.cpp
@@ -5,10 +5,20 @@
*/
#include <QCoreApplication>
+#include <QCryptographicHash>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
#include <QLocale>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
+#include <QSaveFile>
+#include <QSet>
+#include <QStandardPaths>
#include <QTranslator>
+#include <QUrl>
#include <QtQml>
class AppTranslator : public QTranslator
@@ -27,10 +37,167 @@ public:
}
};
-class MastodonAccountsTranslationsPlugin : public QQmlExtensionPlugin
+class FediverseInstanceIconCache : public QObject
{
Q_OBJECT
- Q_PLUGIN_METADATA(IID "com.jolla.settings.accounts.mastodon")
+public:
+ explicit FediverseInstanceIconCache(QObject *parent = nullptr)
+ : QObject(parent)
+ , m_networkAccessManager(new QNetworkAccessManager(this))
+ {
+ }
+
+ Q_INVOKABLE QString cachedIconPath(const QString &apiHost) const
+ {
+ const QString normalizedHost = normalizeApiHost(apiHost);
+ if (normalizedHost.isEmpty()) {
+ return QString();
+ }
+
+ const QString prefix = iconPrefix(normalizedHost);
+ const QDir dir(cacheDirectory());
+ const QStringList matches = dir.entryList(QStringList() << (prefix + QStringLiteral(".*")), QDir::Files);
+ if (matches.isEmpty()) {
+ return QString();
+ }
+
+ return dir.absoluteFilePath(matches.first());
+ }
+
+ Q_INVOKABLE void cacheIcon(const QString &apiHost, const QString &iconUrl)
+ {
+ const QString normalizedHost = normalizeApiHost(apiHost);
+ const QUrl normalizedUrl = QUrl::fromUserInput(iconUrl);
+ if (normalizedHost.isEmpty() || !normalizedUrl.isValid() || normalizedUrl.scheme().isEmpty()) {
+ emit iconError(apiHost);
+ return;
+ }
+
+ const QString existingPath = cachedIconPath(normalizedHost);
+ if (!existingPath.isEmpty()) {
+ emit iconReady(normalizedHost, existingPath);
+ return;
+ }
+
+ if (m_hostsInFlight.contains(normalizedHost)) {
+ return;
+ }
+
+ QNetworkReply *reply = m_networkAccessManager->get(QNetworkRequest(normalizedUrl));
+ reply->setProperty("apiHost", normalizedHost);
+ connect(reply, &QNetworkReply::finished, this, [this, reply]() { handleReply(reply); });
+ m_hostsInFlight.insert(normalizedHost);
+ }
+
+Q_SIGNALS:
+ void iconReady(const QString &apiHost, const QString &iconPath);
+ void iconError(const QString &apiHost);
+
+private:
+ static QString normalizeApiHost(const QString &rawHost)
+ {
+ QString host = rawHost.trimmed();
+ if (host.isEmpty()) {
+ return QString();
+ }
+ if (!host.startsWith(QLatin1String("https://")) && !host.startsWith(QLatin1String("http://"))) {
+ host.prepend(QStringLiteral("https://"));
+ }
+
+ const QUrl url(host);
+ if (!url.isValid() || url.host().isEmpty()) {
+ return QString();
+ }
+
+ QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment));
+ if (normalized.endsWith(QLatin1Char('/'))) {
+ normalized.chop(1);
+ }
+ return normalized;
+ }
+
+ static QString cacheDirectory()
+ {
+ const QString base = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
+ return base + QStringLiteral("/sailfish-account-fediverse/icons");
+ }
+
+ static QString iconPrefix(const QString &apiHost)
+ {
+ return QString::fromLatin1(QCryptographicHash::hash(apiHost.toUtf8(), QCryptographicHash::Sha1).toHex());
+ }
+
+ static QString extensionForReply(QNetworkReply *reply)
+ {
+ const QString suffix = QFileInfo(reply->url().path()).suffix().trimmed().toLower();
+ if (!suffix.isEmpty()) {
+ return suffix;
+ }
+
+ const QString contentType = QString::fromLatin1(reply->header(QNetworkRequest::ContentTypeHeader).toByteArray()).toLower();
+ if (contentType.contains(QLatin1String("png"))) {
+ return QStringLiteral("png");
+ }
+ if (contentType.contains(QLatin1String("jpeg")) || contentType.contains(QLatin1String("jpg"))) {
+ return QStringLiteral("jpg");
+ }
+ if (contentType.contains(QLatin1String("webp"))) {
+ return QStringLiteral("webp");
+ }
+ if (contentType.contains(QLatin1String("svg"))) {
+ return QStringLiteral("svg");
+ }
+ return QStringLiteral("img");
+ }
+
+ void handleReply(QNetworkReply *reply)
+ {
+ const QString apiHost = reply->property("apiHost").toString();
+ m_hostsInFlight.remove(apiHost);
+
+ const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ const bool success = reply->error() == QNetworkReply::NoError && httpCode >= 200 && httpCode < 300;
+ const QString extension = extensionForReply(reply);
+ const QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ if (!success || data.isEmpty()) {
+ emit iconError(apiHost);
+ return;
+ }
+
+ const QString dirPath = cacheDirectory();
+ QDir dir;
+ if (!dir.mkpath(dirPath)) {
+ emit iconError(apiHost);
+ return;
+ }
+
+ const QString prefix = iconPrefix(apiHost);
+ const QDir cacheDir(dirPath);
+ const QStringList matches = cacheDir.entryList(QStringList() << (prefix + QStringLiteral(".*")), QDir::Files);
+ for (const QString &match : matches) {
+ QFile::remove(cacheDir.absoluteFilePath(match));
+ }
+
+ const QString filePath = cacheDir.absoluteFilePath(prefix + QStringLiteral(".") + extension);
+ QSaveFile file(filePath);
+ if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) {
+ emit iconError(apiHost);
+ return;
+ }
+
+ emit iconReady(apiHost, filePath);
+ }
+
+ QNetworkAccessManager *m_networkAccessManager;
+ QSet<QString> m_hostsInFlight;
+};
+
+class FediverseAccountsTranslationsPlugin : public QQmlExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "com.jolla.settings.accounts.fediverse")
public:
void initializeEngine(QQmlEngine *engine, const char *uri) override
@@ -38,18 +205,19 @@ public:
Q_UNUSED(uri)
AppTranslator *engineeringEnglish = new AppTranslator(engine);
- engineeringEnglish->load("settings-accounts-mastodon_eng_en", "/usr/share/translations");
+ engineeringEnglish->load("settings-accounts-fediverse_eng_en", "/usr/share/translations");
AppTranslator *translator = new AppTranslator(engine);
- translator->load(QLocale(), "settings-accounts-mastodon", "-", "/usr/share/translations");
+ translator->load(QLocale(), "settings-accounts-fediverse", "-", "/usr/share/translations");
}
void registerTypes(const char *uri) override
{
- Q_ASSERT(QLatin1String(uri) == QLatin1String("com.jolla.settings.accounts.mastodon"));
- qmlRegisterUncreatableType<MastodonAccountsTranslationsPlugin>(uri, 1, 0,
- "MastodonTranslationPlugin",
+ Q_ASSERT(QLatin1String(uri) == QLatin1String("com.jolla.settings.accounts.fediverse"));
+ qmlRegisterUncreatableType<FediverseAccountsTranslationsPlugin>(uri, 1, 0,
+ "FediverseTranslationPlugin",
QString());
+ qmlRegisterType<FediverseInstanceIconCache>(uri, 1, 0, "FediverseInstanceIconCache");
}
};