fix: improve image type detection
- use the same approach as status-go to detect the image type, relying on "magic" type matching instead of looking at the file extension (now using C++ and QMime*) - add a little error popup when the user tries to upload an unsupported image type while creating/editing a community - expose all the image related properties from the C++ backend instead of constructing and duplicating them in QML - cleanup some unused/dead code Fixes #16668
This commit is contained in:
parent
e3238b3fd2
commit
623333ab8c
|
@ -1,18 +1,38 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QMimeDatabase>
|
||||||
class QQmlEngine;
|
|
||||||
class QJSEngine;
|
|
||||||
|
|
||||||
class UrlUtils : public QObject
|
class UrlUtils : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QString validImageNameFilters READ validImageNameFilters FINAL CONSTANT) // "*.jpg *.jpe *.jp *.jpeg *.png *.webp *.gif *.svg"
|
||||||
|
Q_PROPERTY(QStringList validPreferredImageExtensions READ validPreferredImageExtensions FINAL CONSTANT) // ["jpg", "png", "webp", "gif", "svg"]
|
||||||
|
Q_PROPERTY(QStringList allValidImageExtensions READ allValidImageExtensions FINAL CONSTANT) // ["jpg", "jpe", "jp", "jpeg", "png", "webp", "gif", "svg"]
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static QObject* qmlInstance(QQmlEngine* engine, QJSEngine* scriptEngine);
|
explicit UrlUtils(QObject* parent = nullptr);
|
||||||
|
|
||||||
Q_INVOKABLE static bool isValidImageUrl(const QUrl &url,
|
Q_INVOKABLE bool isValidImageUrl(const QUrl &url) const;
|
||||||
const QStringList &acceptedExtensions);
|
|
||||||
Q_INVOKABLE static qint64 getFileSize(const QUrl &url);
|
Q_INVOKABLE static qint64 getFileSize(const QUrl &url);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QMimeDatabase m_mimeDb;
|
||||||
|
|
||||||
|
QStringList m_validImageMimeTypes{QStringLiteral("image/jpeg"),
|
||||||
|
QStringLiteral("image/png"),
|
||||||
|
QStringLiteral("image/gif"),
|
||||||
|
QStringLiteral("image/svg")};
|
||||||
|
|
||||||
|
// "*.jpg *.jpe *.jp *.jpeg *.png *.webp *.gif *.svg"
|
||||||
|
QString m_imgFilters;
|
||||||
|
QString validImageNameFilters() const { return m_imgFilters; }
|
||||||
|
|
||||||
|
// ["jpg", "png", "webp", "gif", "svg"]
|
||||||
|
QStringList m_imgExtensions;
|
||||||
|
QStringList validPreferredImageExtensions() const { return m_imgExtensions; }
|
||||||
|
|
||||||
|
// ["jpg", "jpe", "jp", "jpeg", "png", "webp", "gif", "svg"]
|
||||||
|
QStringList m_allImgExtensions;
|
||||||
|
QStringList allValidImageExtensions() const { return m_allImgExtensions; }
|
||||||
};
|
};
|
||||||
|
|
|
@ -71,7 +71,9 @@ void registerStatusQTypes() {
|
||||||
qmlRegisterType<FormattedDoubleProperty>("StatusQ", 0, 1, "FormattedDoubleProperty");
|
qmlRegisterType<FormattedDoubleProperty>("StatusQ", 0, 1, "FormattedDoubleProperty");
|
||||||
|
|
||||||
qmlRegisterSingletonType<ClipboardUtils>("StatusQ", 0, 1, "ClipboardUtils", &ClipboardUtils::qmlInstance);
|
qmlRegisterSingletonType<ClipboardUtils>("StatusQ", 0, 1, "ClipboardUtils", &ClipboardUtils::qmlInstance);
|
||||||
qmlRegisterSingletonType<UrlUtils>("StatusQ", 0, 1, "UrlUtils", &UrlUtils::qmlInstance);
|
qmlRegisterSingletonType<UrlUtils>("StatusQ", 0, 1, "UrlUtils", [](QQmlEngine* engine, QJSEngine*) {
|
||||||
|
return new UrlUtils(engine);
|
||||||
|
});
|
||||||
|
|
||||||
qmlRegisterType<ModelEntry>("StatusQ", 0, 1, "ModelEntry");
|
qmlRegisterType<ModelEntry>("StatusQ", 0, 1, "ModelEntry");
|
||||||
qmlRegisterType<SnapshotObject>("StatusQ", 0, 1, "SnapshotObject");
|
qmlRegisterType<SnapshotObject>("StatusQ", 0, 1, "SnapshotObject");
|
||||||
|
|
|
@ -1,25 +1,40 @@
|
||||||
#include "StatusQ/urlutils.h"
|
#include "StatusQ/urlutils.h"
|
||||||
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QImageReader>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include <algorithm>
|
namespace {
|
||||||
|
constexpr auto webpMime = "image/webp";
|
||||||
QObject* UrlUtils::qmlInstance(QQmlEngine*, QJSEngine*)
|
|
||||||
{
|
|
||||||
return new UrlUtils;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UrlUtils::isValidImageUrl(const QUrl& url, const QStringList& acceptedExtensions)
|
UrlUtils::UrlUtils(QObject *parent): QObject(parent) {
|
||||||
{
|
const auto webpSupported = QImageReader::supportedMimeTypes().contains(webpMime);
|
||||||
const auto strippedUrl = url.url(
|
if (webpSupported)
|
||||||
QUrl::RemoveAuthority | QUrl::RemoveFragment | QUrl::RemoveQuery);
|
m_validImageMimeTypes.append(webpMime);
|
||||||
|
|
||||||
return std::any_of(acceptedExtensions.constBegin(),
|
QStringList imgFilters;
|
||||||
acceptedExtensions.constEnd(),
|
for (const auto& mime: std::as_const(m_validImageMimeTypes)) {
|
||||||
[strippedUrl](const auto & ext) {
|
const auto mimeData = m_mimeDb.mimeTypeForName(mime);
|
||||||
return strippedUrl.endsWith(ext, Qt::CaseInsensitive);
|
imgFilters.append(mimeData.globPatterns());
|
||||||
});
|
m_imgExtensions.append(mimeData.preferredSuffix());
|
||||||
|
m_allImgExtensions.append(mimeData.suffixes());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_imgFilters = imgFilters.join(' ');
|
||||||
|
m_imgFilters.append(QStringLiteral(" "));
|
||||||
|
m_imgFilters.append(m_imgFilters.toUpper()); // include the uppercase extensions too for case sensitive file systems
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UrlUtils::isValidImageUrl(const QUrl &url) const
|
||||||
|
{
|
||||||
|
QString mimeType;
|
||||||
|
if (url.isLocalFile())
|
||||||
|
mimeType = m_mimeDb.mimeTypeForFile(url.toLocalFile(), QMimeDatabase::MatchContent).name();
|
||||||
|
else
|
||||||
|
mimeType = m_mimeDb.mimeTypeForUrl(url).name();
|
||||||
|
|
||||||
|
return m_validImageMimeTypes.contains(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
qint64 UrlUtils::getFileSize(const QUrl& url)
|
qint64 UrlUtils::getFileSize(const QUrl& url)
|
||||||
|
|
|
@ -38,27 +38,6 @@ QtObject {
|
||||||
readonly property bool createCommunityEnabled: localAppSettings.createCommunityEnabled ?? false
|
readonly property bool createCommunityEnabled: localAppSettings.createCommunityEnabled ?? false
|
||||||
readonly property bool testEnvironment: localAppSettings.testEnvironment ?? false
|
readonly property bool testEnvironment: localAppSettings.testEnvironment ?? false
|
||||||
|
|
||||||
// TODO: Could the backend provide directly 2 filtered models??
|
|
||||||
//property var featuredCommunitiesModel: root.communitiesModuleInst.curatedFeaturedCommunities
|
|
||||||
//property var popularCommunitiesModel: root.communitiesModuleInst.curatedPopularCommunities
|
|
||||||
property ListModel tagsModel: ListModel {}//root.notionsTagsModel
|
|
||||||
|
|
||||||
// TO DO: Complete list can be added in backend or here: https://www.notion.so/Category-tags-339b2e699e7c4d36ab0608ab00b99111
|
|
||||||
property ListModel notionsTagsModel : ListModel {
|
|
||||||
ListElement { name: "gaming"; emoji: "🎮"}
|
|
||||||
ListElement { name: "art"; emoji: "🖼️️"}
|
|
||||||
ListElement { name: "crypto"; emoji: "💸"}
|
|
||||||
ListElement { name: "nsfw"; emoji: "🍆"}
|
|
||||||
ListElement { name: "markets"; emoji: "💎"}
|
|
||||||
ListElement { name: "defi"; emoji: "📈"}
|
|
||||||
ListElement { name: "travel"; emoji: "🚁"}
|
|
||||||
ListElement { name: "web3"; emoji: "🗺"}
|
|
||||||
ListElement { name: "sport"; emoji: "🎾"}
|
|
||||||
ListElement { name: "food"; emoji: "🥑"}
|
|
||||||
ListElement { name: "enviroment"; emoji: "☠️"}
|
|
||||||
ListElement { name: "privacy"; emoji: "👻"}
|
|
||||||
}
|
|
||||||
|
|
||||||
property string communityTags: communitiesModuleInst.tags
|
property string communityTags: communitiesModuleInst.tags
|
||||||
|
|
||||||
signal importingCommunityStateChanged(string communityId, int state, string errorMsg)
|
signal importingCommunityStateChanged(string communityId, int state, string errorMsg)
|
||||||
|
|
|
@ -147,11 +147,12 @@ SettingsContentBase {
|
||||||
|
|
||||||
Panel {
|
Panel {
|
||||||
id: panelMembers
|
id: panelMembers
|
||||||
filters: ExpressionFilter {
|
filters: FastExpressionFilter {
|
||||||
readonly property int ownerRole: Constants.memberRole.owner
|
readonly property int ownerRole: Constants.memberRole.owner
|
||||||
readonly property int adminRole: Constants.memberRole.admin
|
readonly property int adminRole: Constants.memberRole.admin
|
||||||
readonly property int tokenMasterRole: Constants.memberRole.tokenMaster
|
readonly property int tokenMasterRole: Constants.memberRole.tokenMaster
|
||||||
expression: model.joined && model.memberRole !== ownerRole && model.memberRole !== adminRole && model.memberRole !== tokenMasterRole
|
expression: model.joined && model.memberRole !== ownerRole && model.memberRole !== adminRole && model.memberRole !== tokenMasterRole
|
||||||
|
expectedRoles: ["joined", "memberRole"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,8 +163,9 @@ SettingsContentBase {
|
||||||
|
|
||||||
Panel {
|
Panel {
|
||||||
id: panelPendingRequests
|
id: panelPendingRequests
|
||||||
filters: ExpressionFilter {
|
filters: FastExpressionFilter {
|
||||||
expression: model.spectated && !model.joined
|
expression: model.spectated && !model.joined
|
||||||
|
expectedRoles: ["joined", "spectated"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.15
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.3
|
||||||
|
|
||||||
|
import StatusQ 0.1
|
||||||
import StatusQ.Components 0.1
|
import StatusQ.Components 0.1
|
||||||
import StatusQ.Controls 0.1
|
import StatusQ.Controls 0.1
|
||||||
import StatusQ.Core 0.1
|
import StatusQ.Core 0.1
|
||||||
import StatusQ.Core.Theme 0.1
|
import StatusQ.Core.Theme 0.1
|
||||||
import StatusQ.Popups 0.1
|
import StatusQ.Popups 0.1
|
||||||
|
import StatusQ.Popups.Dialog 0.1
|
||||||
|
|
||||||
import utils 1.0
|
import utils 1.0
|
||||||
|
|
||||||
|
@ -39,14 +42,37 @@ Item {
|
||||||
|
|
||||||
title: root.imageFileDialogTitle
|
title: root.imageFileDialogTitle
|
||||||
folder: root.userSelectedImage ? imageCropper.source.substr(0, imageCropper.source.lastIndexOf("/")) : shortcuts.pictures
|
folder: root.userSelectedImage ? imageCropper.source.substr(0, imageCropper.source.lastIndexOf("/")) : shortcuts.pictures
|
||||||
nameFilters: [qsTr("Supported image formats (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))]
|
nameFilters: [qsTr("Supported image formats (%1)").arg(UrlUtils.validImageNameFilters)]
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (fileDialog.fileUrls.length > 0) {
|
if (fileDialog.fileUrls.length > 0) {
|
||||||
cropImage(fileDialog.fileUrls[0])
|
const url = fileDialog.fileUrls[0]
|
||||||
|
if (Utils.isValidDragNDropImage(url))
|
||||||
|
cropImage(url)
|
||||||
|
else
|
||||||
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // FileDialog
|
} // FileDialog
|
||||||
|
|
||||||
|
StatusDialog {
|
||||||
|
id: errorDialog
|
||||||
|
title: qsTr("Image format not supported")
|
||||||
|
width: 480
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
StatusBaseText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: qsTr("Format of the image you chose is not supported. Most probably you picked a file that is invalid, corrupted or has a wrong file extension.")
|
||||||
|
}
|
||||||
|
StatusBaseText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.pixelSize: Theme.additionalTextSize
|
||||||
|
text: qsTr("Supported image extensions: %1").arg(UrlUtils.allValidImageExtensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
standardButtons: Dialog.Ok
|
||||||
|
} // StatusDialog
|
||||||
|
|
||||||
StatusModal {
|
StatusModal {
|
||||||
id: imageCropperModal
|
id: imageCropperModal
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
import StatusQ 0.1
|
||||||
|
|
||||||
import utils 1.0
|
import utils 1.0
|
||||||
|
|
||||||
StatusChatImageValidator {
|
StatusChatImageValidator {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
errorMessage: qsTr("Format not supported.")
|
errorMessage: qsTr("Format not supported.")
|
||||||
secondaryErrorMessage: qsTr("Upload %1 only").arg(Constants.acceptedDragNDropImageExtensions.map(ext => ext.replace(".", "").toUpperCase() + "s").join(", "))
|
secondaryErrorMessage: qsTr("Upload %1 only").arg(UrlUtils.validPreferredImageExtensions.map(ext => ext.toUpperCase() + "s").join(", "))
|
||||||
|
|
||||||
onImagesChanged: {
|
onImagesChanged: {
|
||||||
let isValid = true
|
let isValid = true
|
||||||
|
|
|
@ -982,7 +982,7 @@ Rectangle {
|
||||||
folder: shortcuts.pictures
|
folder: shortcuts.pictures
|
||||||
selectMultiple: true
|
selectMultiple: true
|
||||||
nameFilters: [
|
nameFilters: [
|
||||||
qsTr("Image files (%1)").arg(Constants.acceptedDragNDropImageExtensions.map(img => "*" + img).join(" "))
|
qsTr("Image files (%1)").arg(UrlUtils.validImageNameFilters)
|
||||||
]
|
]
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
imageBtn.highlighted = false
|
imageBtn.highlighted = false
|
||||||
|
|
|
@ -955,7 +955,6 @@ QtObject {
|
||||||
readonly property int maxNumberOfPins: 3
|
readonly property int maxNumberOfPins: 3
|
||||||
|
|
||||||
readonly property string dataImagePrefix: "data:image"
|
readonly property string dataImagePrefix: "data:image"
|
||||||
readonly property var acceptedDragNDropImageExtensions: [".png", ".jpg", ".jpeg"]
|
|
||||||
|
|
||||||
readonly property string mentionSpanTag: `<span style="background-color: ${Theme.palette.mentionColor2};"><a style="color:${Theme.palette.mentionColor1};text-decoration:none" href='http://'>`
|
readonly property string mentionSpanTag: `<span style="background-color: ${Theme.palette.mentionColor2};"><a style="color:${Theme.palette.mentionColor1};text-decoration:none" href='http://'>`
|
||||||
|
|
||||||
|
|
|
@ -276,8 +276,7 @@ QtObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidDragNDropImage(url) {
|
function isValidDragNDropImage(url) {
|
||||||
return url.startsWith(Constants.dataImagePrefix) ||
|
return url.startsWith(Constants.dataImagePrefix) || UrlUtils.isValidImageUrl(url)
|
||||||
UrlUtils.isValidImageUrl(url, Constants.acceptedDragNDropImageExtensions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFilesizeValid(img) {
|
function isFilesizeValid(img) {
|
||||||
|
|
Loading…
Reference in New Issue