From 6712e8cb1e0b49b06d1a7d1ec2f1c013d273fc04 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sat, 12 Jul 2025 10:37:39 -0600 Subject: [PATCH] feat: add image uploader settings import/export functionality (#6284) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/CMakeLists.txt | 2 + src/util/Expected.hpp | 2 + src/util/ImageUploader.cpp | 135 ++++++++++++++++++ src/util/ImageUploader.hpp | 25 ++++ .../settingspages/ExternalToolsPage.cpp | 90 ++++++++++++ .../settingspages/ExternalToolsPage.hpp | 2 +- 7 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 src/util/ImageUploader.cpp create mode 100644 src/util/ImageUploader.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 07fcc70b..1031748e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Minor: Made nicknames searchable in the Settings dialog search bar. (#5886) - Minor: Added hotkey Action for opening account selector. (#6192) - 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: Automatic streamer mode detection now works from Flatpak. (#6250) - Bugfix: Don't create native messaging manifest file if browser directory doesn't exist. (#6116) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0c1ee130..ed17108a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -526,6 +526,8 @@ set(SOURCE_FILES util/FuzzyConvert.hpp util/Helpers.cpp util/Helpers.hpp + util/ImageUploader.cpp + util/ImageUploader.hpp util/IncognitoBrowser.cpp util/IncognitoBrowser.hpp util/IpcQueue.cpp diff --git a/src/util/Expected.hpp b/src/util/Expected.hpp index 87c46fa1..d4784235 100644 --- a/src/util/Expected.hpp +++ b/src/util/Expected.hpp @@ -2,6 +2,8 @@ #include +#include + class QString; namespace chatterino { diff --git a/src/util/ImageUploader.cpp b/src/util/ImageUploader.cpp new file mode 100644 index 00000000..9020ecef --- /dev/null +++ b/src/util/ImageUploader.cpp @@ -0,0 +1,135 @@ +#include "util/ImageUploader.hpp" + +#include "singletons/Settings.hpp" + +#include +#include + +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 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 diff --git a/src/util/ImageUploader.hpp b/src/util/ImageUploader.hpp new file mode 100644 index 00000000..35b26339 --- /dev/null +++ b/src/util/ImageUploader.hpp @@ -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 validateImportJson(const QString &clipboardText); + +} // namespace imageuploader::detail + +} // namespace chatterino diff --git a/src/widgets/settingspages/ExternalToolsPage.cpp b/src/widgets/settingspages/ExternalToolsPage.cpp index 3bac77d6..62b66096 100644 --- a/src/widgets/settingspages/ExternalToolsPage.cpp +++ b/src/widgets/settingspages/ExternalToolsPage.cpp @@ -1,20 +1,84 @@ #include "widgets/settingspages/ExternalToolsPage.hpp" #include "singletons/Settings.hpp" +#include "util/Clipboard.hpp" #include "util/Helpers.hpp" +#include "util/ImageUploader.hpp" #include "util/StreamLink.hpp" #include "widgets/settingspages/SettingWidget.hpp" #include #include +#include +#include +#include #include +#include +#include namespace chatterino { +namespace { + inline const QStringList STREAMLINK_QUALITY = { "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() : view(GeneralPageView::withoutNavigation(this)) { @@ -135,6 +199,32 @@ void ExternalToolsPage::initLayout(GeneralPageView &layout) SettingWidget::lineEdit("Deletion link", s.imageUploaderDeletionLink) ->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(); diff --git a/src/widgets/settingspages/ExternalToolsPage.hpp b/src/widgets/settingspages/ExternalToolsPage.hpp index 316ae07d..cb6e903f 100644 --- a/src/widgets/settingspages/ExternalToolsPage.hpp +++ b/src/widgets/settingspages/ExternalToolsPage.hpp @@ -15,7 +15,7 @@ public: private: void initLayout(GeneralPageView &layout); - GeneralPageView *view; + GeneralPageView *view{}; }; } // namespace chatterino