feat: add image uploader settings import/export functionality (#6284)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
- Minor: Made nicknames searchable in the Settings dialog search bar. (#5886)
|
- Minor: Made nicknames searchable in the Settings dialog search bar. (#5886)
|
||||||
- Minor: Added hotkey Action for opening account selector. (#6192)
|
- Minor: Added hotkey Action for opening account selector. (#6192)
|
||||||
- Minor: Add a setting to change the emote and badge thumbnail size. (#6126)
|
- Minor: Add a setting to change the emote and badge thumbnail size. (#6126)
|
||||||
|
- Minor: Add an import and export button to image uploader settings. (#6284)
|
||||||
- Bugfix: Commands are no longer tab-completable in the middle of messages. (#6273)
|
- Bugfix: Commands are no longer tab-completable in the middle of messages. (#6273)
|
||||||
- Bugfix: Automatic streamer mode detection now works from Flatpak. (#6250)
|
- Bugfix: Automatic streamer mode detection now works from Flatpak. (#6250)
|
||||||
- Bugfix: Don't create native messaging manifest file if browser directory doesn't exist. (#6116)
|
- Bugfix: Don't create native messaging manifest file if browser directory doesn't exist. (#6116)
|
||||||
|
|||||||
@@ -526,6 +526,8 @@ set(SOURCE_FILES
|
|||||||
util/FuzzyConvert.hpp
|
util/FuzzyConvert.hpp
|
||||||
util/Helpers.cpp
|
util/Helpers.cpp
|
||||||
util/Helpers.hpp
|
util/Helpers.hpp
|
||||||
|
util/ImageUploader.cpp
|
||||||
|
util/ImageUploader.hpp
|
||||||
util/IncognitoBrowser.cpp
|
util/IncognitoBrowser.cpp
|
||||||
util/IncognitoBrowser.hpp
|
util/IncognitoBrowser.hpp
|
||||||
util/IpcQueue.cpp
|
util/IpcQueue.cpp
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <nonstd/expected.hpp>
|
#include <nonstd/expected.hpp>
|
||||||
|
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
class QString;
|
class QString;
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#include "util/ImageUploader.hpp"
|
||||||
|
|
||||||
|
#include "singletons/Settings.hpp"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace chatterino::imageuploader::detail {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QStringList parseHeaders(const QJsonObject &headersObj)
|
||||||
|
{
|
||||||
|
QStringList headerLines;
|
||||||
|
for (auto it = headersObj.begin(); it != headersObj.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it.value().isString())
|
||||||
|
{
|
||||||
|
headerLines.append(
|
||||||
|
QString("%1: %2").arg(it.key(), it.value().toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headerLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QJsonObject exportSettings(const Settings &s)
|
||||||
|
{
|
||||||
|
QJsonObject settingsObj;
|
||||||
|
settingsObj["Version"] = "1.0.0";
|
||||||
|
settingsObj["Name"] = "Chatterino Image Uploader Settings";
|
||||||
|
settingsObj["RequestMethod"] = "POST";
|
||||||
|
settingsObj["RequestURL"] = s.imageUploaderUrl.getValue();
|
||||||
|
settingsObj["Body"] = "MultipartFormData";
|
||||||
|
settingsObj["FileFormName"] = s.imageUploaderFormField.getValue();
|
||||||
|
settingsObj["URL"] = s.imageUploaderLink.getValue();
|
||||||
|
settingsObj["DeletionURL"] = s.imageUploaderDeletionLink.getValue();
|
||||||
|
|
||||||
|
QString headers = s.imageUploaderHeaders.getValue();
|
||||||
|
if (!headers.isEmpty())
|
||||||
|
{
|
||||||
|
QJsonObject headersObj;
|
||||||
|
QStringList headerLines = headers.split('\n', Qt::SkipEmptyParts);
|
||||||
|
for (const QString &line : headerLines)
|
||||||
|
{
|
||||||
|
QStringList parts = line.split(':', Qt::SkipEmptyParts);
|
||||||
|
if (parts.size() >= 2)
|
||||||
|
{
|
||||||
|
QString key = parts[0].trimmed();
|
||||||
|
QString value = parts.mid(1).join(':').trimmed();
|
||||||
|
headersObj[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!headersObj.isEmpty())
|
||||||
|
{
|
||||||
|
settingsObj["Headers"] = headersObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool importSettings(const QJsonObject &settingsObj, Settings &s)
|
||||||
|
{
|
||||||
|
if (!settingsObj.contains("RequestURL") ||
|
||||||
|
!settingsObj["RequestURL"].isString() ||
|
||||||
|
!settingsObj.contains("FileFormName") ||
|
||||||
|
!settingsObj["FileFormName"].isString() ||
|
||||||
|
!settingsObj.contains("URL") || !settingsObj["URL"].isString())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.imageUploaderUrl = settingsObj["RequestURL"].toString();
|
||||||
|
s.imageUploaderFormField = settingsObj["FileFormName"].toString();
|
||||||
|
s.imageUploaderLink = settingsObj["URL"].toString();
|
||||||
|
|
||||||
|
if (settingsObj.contains("DeletionURL") &&
|
||||||
|
settingsObj["DeletionURL"].isString())
|
||||||
|
{
|
||||||
|
s.imageUploaderDeletionLink = settingsObj["DeletionURL"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsObj.contains("Headers") && settingsObj["Headers"].isObject())
|
||||||
|
{
|
||||||
|
QStringList headers = parseHeaders(settingsObj["Headers"].toObject());
|
||||||
|
if (!headers.isEmpty())
|
||||||
|
{
|
||||||
|
s.imageUploaderHeaders = headers.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.imageUploaderEnabled = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpectedStr<QJsonObject> validateImportJson(const QString &clipboardText)
|
||||||
|
{
|
||||||
|
if (clipboardText.isEmpty())
|
||||||
|
{
|
||||||
|
return nonstd::make_unexpected("Clipboard must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc =
|
||||||
|
QJsonDocument::fromJson(clipboardText.toUtf8(), &parseError);
|
||||||
|
|
||||||
|
if (parseError.error != QJsonParseError::NoError)
|
||||||
|
{
|
||||||
|
return nonstd::make_unexpected("Clipboard did not contain valid JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.isObject())
|
||||||
|
{
|
||||||
|
return nonstd::make_unexpected("JSON must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto settingsObj = doc.object();
|
||||||
|
|
||||||
|
if (!settingsObj.contains("Version"))
|
||||||
|
{
|
||||||
|
return nonstd::make_unexpected("JSON must contain the 'Version' key");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingsObj.contains("Name"))
|
||||||
|
{
|
||||||
|
return nonstd::make_unexpected("JSON must contain the 'Name' key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::imageuploader::detail
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "util/Expected.hpp"
|
||||||
|
|
||||||
|
class QJsonObject;
|
||||||
|
class QString;
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Settings;
|
||||||
|
|
||||||
|
namespace imageuploader::detail {
|
||||||
|
|
||||||
|
// Exports current image uploader settings to JSON format.
|
||||||
|
QJsonObject exportSettings(const Settings &s);
|
||||||
|
|
||||||
|
// Imports image uploader settings from JSON into the Settings object.
|
||||||
|
bool importSettings(const QJsonObject &settingsObj, Settings &s);
|
||||||
|
|
||||||
|
// Validates if the clipboard text contains valid JSON and parses it.
|
||||||
|
ExpectedStr<QJsonObject> validateImportJson(const QString &clipboardText);
|
||||||
|
|
||||||
|
} // namespace imageuploader::detail
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
@@ -1,20 +1,84 @@
|
|||||||
#include "widgets/settingspages/ExternalToolsPage.hpp"
|
#include "widgets/settingspages/ExternalToolsPage.hpp"
|
||||||
|
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
|
#include "util/Clipboard.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
|
#include "util/ImageUploader.hpp"
|
||||||
#include "util/StreamLink.hpp"
|
#include "util/StreamLink.hpp"
|
||||||
#include "widgets/settingspages/SettingWidget.hpp"
|
#include "widgets/settingspages/SettingWidget.hpp"
|
||||||
|
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
inline const QStringList STREAMLINK_QUALITY = {
|
inline const QStringList STREAMLINK_QUALITY = {
|
||||||
"Choose", "Source", "High", "Medium", "Low", "Audio only",
|
"Choose", "Source", "High", "Medium", "Low", "Audio only",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void exportImageUploaderSettings(QWidget *parent)
|
||||||
|
{
|
||||||
|
const auto &s = *getSettings();
|
||||||
|
|
||||||
|
QJsonObject settingsObj = imageuploader::detail::exportSettings(s);
|
||||||
|
QJsonDocument doc(settingsObj);
|
||||||
|
crossPlatformCopy(doc.toJson(QJsonDocument::Indented));
|
||||||
|
|
||||||
|
QMessageBox::information(
|
||||||
|
parent, "Settings Exported",
|
||||||
|
"Image uploader settings have been copied to clipboard as JSON.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void importImageUploaderSettings(QWidget *parent)
|
||||||
|
{
|
||||||
|
QString clipboardText = getClipboardText().trimmed();
|
||||||
|
|
||||||
|
auto res = imageuploader::detail::validateImportJson(clipboardText);
|
||||||
|
if (!res)
|
||||||
|
{
|
||||||
|
QMessageBox::warning(
|
||||||
|
parent, "Import Failed",
|
||||||
|
QString("Error validating image uploader import: %1.")
|
||||||
|
.arg(res.error()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &settingsObj = *res;
|
||||||
|
|
||||||
|
int ret = QMessageBox::question(
|
||||||
|
parent, "Import Settings",
|
||||||
|
"This will overwrite your current image uploader settings. Continue?",
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
|
||||||
|
if (ret != QMessageBox::Yes)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &s = *getSettings();
|
||||||
|
if (imageuploader::detail::importSettings(settingsObj, s))
|
||||||
|
{
|
||||||
|
QMessageBox::information(
|
||||||
|
parent, "Import Successful",
|
||||||
|
"Image uploader settings have been imported successfully!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QMessageBox::warning(
|
||||||
|
parent, "Import Failed",
|
||||||
|
"No valid image uploader settings found in the JSON.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
ExternalToolsPage::ExternalToolsPage()
|
ExternalToolsPage::ExternalToolsPage()
|
||||||
: view(GeneralPageView::withoutNavigation(this))
|
: view(GeneralPageView::withoutNavigation(this))
|
||||||
{
|
{
|
||||||
@@ -135,6 +199,32 @@ void ExternalToolsPage::initLayout(GeneralPageView &layout)
|
|||||||
|
|
||||||
SettingWidget::lineEdit("Deletion link", s.imageUploaderDeletionLink)
|
SettingWidget::lineEdit("Deletion link", s.imageUploaderDeletionLink)
|
||||||
->addTo(layout, form);
|
->addTo(layout, form);
|
||||||
|
|
||||||
|
layout.addDescription(
|
||||||
|
"Export your current image uploader settings as JSON to share with "
|
||||||
|
"others, or import settings from clipboard (compatible with ShareX "
|
||||||
|
".sxcu format).");
|
||||||
|
|
||||||
|
auto *buttonLayout = new QHBoxLayout;
|
||||||
|
|
||||||
|
auto *importButton = new QPushButton("Import Settings from Clipboard");
|
||||||
|
importButton->setToolTip(
|
||||||
|
"Import image uploader settings from clipboard JSON");
|
||||||
|
QObject::connect(importButton, &QPushButton::clicked, [this]() {
|
||||||
|
importImageUploaderSettings(this);
|
||||||
|
});
|
||||||
|
buttonLayout->addWidget(importButton);
|
||||||
|
|
||||||
|
auto *exportButton = new QPushButton("Export Settings to Clipboard");
|
||||||
|
exportButton->setToolTip(
|
||||||
|
"Copy current image uploader settings to clipboard as JSON");
|
||||||
|
QObject::connect(exportButton, &QPushButton::clicked, [this]() {
|
||||||
|
exportImageUploaderSettings(this);
|
||||||
|
});
|
||||||
|
buttonLayout->addWidget(exportButton);
|
||||||
|
|
||||||
|
buttonLayout->addStretch();
|
||||||
|
layout.addLayout(buttonLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
layout.addStretch();
|
layout.addStretch();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void initLayout(GeneralPageView &layout);
|
void initLayout(GeneralPageView &layout);
|
||||||
|
|
||||||
GeneralPageView *view;
|
GeneralPageView *view{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|||||||
Reference in New Issue
Block a user