Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/mod_uploader
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/mod_uploader
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/mod_uploader')
-rw-r--r--source/mod_uploader/CMakeLists.txt32
-rw-r--r--source/mod_uploader/StarModUploader.cpp394
-rw-r--r--source/mod_uploader/StarModUploader.hpp59
-rw-r--r--source/mod_uploader/StarSPlainTextEdit.cpp21
-rw-r--r--source/mod_uploader/StarSPlainTextEdit.hpp28
-rw-r--r--source/mod_uploader/StarStringConversion.hpp20
-rw-r--r--source/mod_uploader/main.cpp29
7 files changed, 583 insertions, 0 deletions
diff --git a/source/mod_uploader/CMakeLists.txt b/source/mod_uploader/CMakeLists.txt
new file mode 100644
index 0000000..b80feac
--- /dev/null
+++ b/source/mod_uploader/CMakeLists.txt
@@ -0,0 +1,32 @@
+INCLUDE_DIRECTORIES (
+ ${STAR_EXTERN_INCLUDES}
+ ${STAR_CORE_INCLUDES}
+ ${STAR_BASE_INCLUDES}
+ )
+
+FIND_PACKAGE (Qt5Core)
+FIND_PACKAGE (Qt5Widgets)
+
+SET (CMAKE_INCLUDE_CURRENT_DIR ON)
+SET (CMAKE_AUTOMOC ON)
+
+SET (star_mod_uploader_HEADERS
+ StarStringConversion.hpp
+ StarModUploader.hpp
+ StarSPlainTextEdit.hpp
+ )
+
+SET (star_mod_uploader_SOURCES
+ StarModUploader.cpp
+ StarSPlainTextEdit.cpp
+ main.cpp
+ )
+
+ADD_EXECUTABLE (mod_uploader WIN32
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base>
+ ${star_mod_uploader_HEADERS} ${star_mod_uploader_SOURCES})
+QT5_USE_MODULES (mod_uploader Core Gui Widgets)
+TARGET_LINK_LIBRARIES (mod_uploader ${STAR_EXT_LIBS} ${STEAM_API_LIBRARY})
+
+SET (CMAKE_AUTOMOC OFF)
+SET (CMAKE_INCLUDE_CURRENT_DIR OFF)
diff --git a/source/mod_uploader/StarModUploader.cpp b/source/mod_uploader/StarModUploader.cpp
new file mode 100644
index 0000000..c8c86e6
--- /dev/null
+++ b/source/mod_uploader/StarModUploader.cpp
@@ -0,0 +1,394 @@
+#include <QApplication>
+#include <QFileDialog>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QProgressDialog>
+#include <QMessageBox>
+
+#include "StarModUploader.hpp"
+#include "StarFile.hpp"
+#include "StarThread.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarPackedAssetSource.hpp"
+#include "StarStringConversion.hpp"
+
+namespace Star {
+
+ModUploader::ModUploader()
+ : QMainWindow() {
+
+ auto selectDirectoryButton = new QPushButton("Select Mod Directory");
+ m_directoryLabel = new QLabel();
+ m_reloadButton = new QPushButton("Reload");
+ m_nameEditor = new QLineEdit();
+ m_titleEditor = new QLineEdit();
+ m_authorEditor = new QLineEdit();
+ m_versionEditor = new QLineEdit();
+ m_descriptionEditor = new SPlainTextEdit();
+ m_previewImageLabel = new QLabel();
+ auto selectPreviewImageButton = new QPushButton("Select");
+ m_modIdLabel = new QLabel();
+ auto resetModIdButton = new QPushButton("Reset Steam Mod Information");
+ auto userAgreementLabel = new QLabel("By submitting this item, you agree to the <a href=\"http://steamcommunity.com/sharedfiles/workshoplegalagreement\">workshop terms of service</a>");
+ auto uploadButton = new QPushButton("Upload to Steam!");
+
+ m_categorySelectors.set("Armor and Clothes", new QCheckBox("Armor and Clothes"));
+ m_categorySelectors.set("Character Improvements", new QCheckBox("Character Improvements"));
+ m_categorySelectors.set("Cheats and God Items", new QCheckBox("Cheats and God Items"));
+ m_categorySelectors.set("Crafting and Building", new QCheckBox("Crafting and Building"));
+ m_categorySelectors.set("Dungeons", new QCheckBox("Dungeons"));
+ m_categorySelectors.set("Food and Farming", new QCheckBox("Food and Farming"));
+ m_categorySelectors.set("Furniture and Objects", new QCheckBox("Furniture and Objects"));
+ m_categorySelectors.set("In-Game Tools", new QCheckBox("In-Game Tools"));
+ m_categorySelectors.set("Mechanics", new QCheckBox("Mechanics"));
+ m_categorySelectors.set("Miscellaneous", new QCheckBox("Miscellaneous"));
+ m_categorySelectors.set("Musical Instruments and Songs", new QCheckBox("Musical Instruments and Songs"));
+ m_categorySelectors.set("NPCs and Creatures", new QCheckBox("NPCs and Creatures"));
+ m_categorySelectors.set("Planets and Environments", new QCheckBox("Planets and Environments"));
+ m_categorySelectors.set("Quests", new QCheckBox("Quests"));
+ m_categorySelectors.set("Species", new QCheckBox("Species"));
+ m_categorySelectors.set("Ships", new QCheckBox("Ships"));
+ m_categorySelectors.set("User Interface", new QCheckBox("User Interface"));
+ m_categorySelectors.set("Vehicles and Mounts", new QCheckBox("Vehicles and Mounts"));
+ m_categorySelectors.set("Weapons", new QCheckBox("Weapons"));
+
+ m_modIdLabel->setOpenExternalLinks(true);
+ userAgreementLabel->setOpenExternalLinks(true);
+
+ connect(selectDirectoryButton, SIGNAL(clicked()), this, SLOT(selectDirectory()));
+ connect(m_reloadButton, SIGNAL(clicked()), this, SLOT(loadDirectory()));
+ connect(selectPreviewImageButton, SIGNAL(clicked()), this, SLOT(selectPreview()));
+ connect(m_nameEditor, SIGNAL(editingFinished()), this, SLOT(writeMetadata()));
+ connect(m_titleEditor, SIGNAL(editingFinished()), this, SLOT(writeMetadata()));
+ connect(m_authorEditor, SIGNAL(editingFinished()), this, SLOT(writeMetadata()));
+ connect(m_versionEditor, SIGNAL(editingFinished()), this, SLOT(writeMetadata()));
+ connect(m_descriptionEditor, SIGNAL(editingFinished()), this, SLOT(writeMetadata()));
+ connect(resetModIdButton, SIGNAL(clicked()), this, SLOT(resetModId()));
+ connect(uploadButton, SIGNAL(clicked()), this, SLOT(uploadToSteam()));
+
+ for (auto pair : m_categorySelectors) {
+ connect(pair.second, SIGNAL(clicked()), this, SLOT(writeMetadata()));
+ }
+
+ auto loadDirectoryLayout = new QHBoxLayout();
+ loadDirectoryLayout->addWidget(selectDirectoryButton);
+ loadDirectoryLayout->addWidget(m_directoryLabel, 1);
+ loadDirectoryLayout->addWidget(m_reloadButton);
+
+ QGridLayout* editorLeftLayout = new QGridLayout();
+ editorLeftLayout->addWidget(new QLabel("Name"), 0, 0);
+ editorLeftLayout->addWidget(m_nameEditor, 0, 1, 1, 2);
+
+ editorLeftLayout->addWidget(new QLabel("Title"), 1, 0);
+ editorLeftLayout->addWidget(m_titleEditor, 1, 1, 1, 2);
+
+ editorLeftLayout->addWidget(new QLabel("Author"), 2, 0);
+ editorLeftLayout->addWidget(m_authorEditor, 2, 1, 1, 2);
+
+ editorLeftLayout->addWidget(new QLabel("Version"), 3, 0);
+ editorLeftLayout->addWidget(m_versionEditor, 3, 1, 1, 2);
+
+ editorLeftLayout->addWidget(new QLabel("Description"), 4, 0);
+ editorLeftLayout->addWidget(m_descriptionEditor, 4, 1, 1, 2);
+
+ editorLeftLayout->addWidget(new QLabel("Preview Image"), 5, 0);
+ editorLeftLayout->addWidget(m_previewImageLabel, 5, 1);
+ editorLeftLayout->addWidget(selectPreviewImageButton, 5, 2);
+
+ editorLeftLayout->addWidget(new QLabel("Mod ID"), 6, 0);
+ editorLeftLayout->addWidget(m_modIdLabel, 6, 1);
+ editorLeftLayout->addWidget(resetModIdButton, 6, 2);
+
+ editorLeftLayout->addWidget(userAgreementLabel, 7, 0, 1, 3, Qt::AlignCenter);
+ editorLeftLayout->addWidget(uploadButton, 8, 0, 1, 3);
+
+ editorLeftLayout->setColumnStretch(1, 1);
+
+ QVBoxLayout* categoryLayout = new QVBoxLayout();
+ categoryLayout->addWidget(new QLabel("Categories"));
+ auto categoryKeys = m_categorySelectors.keys();
+ categoryKeys.sort();
+ for (auto k : categoryKeys) {
+ categoryLayout->addWidget(m_categorySelectors[k]);
+ }
+ categoryLayout-> addWidget(new QWidget(), 1);
+
+ QHBoxLayout* editorLayout = new QHBoxLayout();
+ editorLayout->addLayout(editorLeftLayout, 1);
+ editorLayout->addLayout(categoryLayout);
+
+ m_editorSection = new QWidget();
+ m_editorSection->setLayout(editorLayout);
+
+ auto mainLayout = new QVBoxLayout();
+ mainLayout->addLayout(loadDirectoryLayout);
+ mainLayout->addWidget(m_editorSection);
+
+ auto* centralWidget = new QWidget(this);
+ centralWidget->setLayout(mainLayout);
+ setCentralWidget(centralWidget);
+
+ m_reloadButton->setEnabled(false);
+ m_editorSection->setEnabled(false);
+
+ setWindowTitle("Steam Mod Uploader");
+ resize(1000, 600);
+}
+
+void ModUploader::selectDirectory() {
+ QString dir = QFileDialog::getExistingDirectory(this, "Select the top-level mod directory");
+ m_modDirectory = toSString(dir);
+
+ loadDirectory();
+}
+
+void ModUploader::loadDirectory() {
+ QProgressDialog progress("Loading mod directory...", "", 0, 0, this);
+ progress.setWindowModality(Qt::WindowModal);
+ progress.setCancelButton(nullptr);
+ progress.setAutoReset(false);
+ progress.show();
+
+ if (m_modDirectory && !File::isDirectory(*m_modDirectory))
+ m_modDirectory.reset();
+
+ if (!m_modDirectory) {
+ m_reloadButton->setEnabled(false);
+ m_directoryLabel->setText("");
+ m_editorSection->setEnabled(false);
+ m_assetSource.reset();
+ return;
+ }
+
+ m_reloadButton->setEnabled(true);
+ m_directoryLabel->setText(toQString(*m_modDirectory));
+ m_editorSection->setEnabled(true);
+ m_assetSource = DirectoryAssetSource(*m_modDirectory);
+
+ JsonObject metadata = m_assetSource->metadata();
+ m_nameEditor->setText(toQString(metadata.value("name", "").toString()));
+ m_titleEditor->setText(toQString(metadata.value("friendlyName", "").toString()));
+ m_authorEditor->setText(toQString(metadata.value("author", "").toString()));
+ m_versionEditor->setText(toQString(metadata.value("version", "").toString()));
+ m_descriptionEditor->setPlainText(toQString(metadata.value("description", "").toString()));
+
+ for (auto pair : m_categorySelectors)
+ pair.second->setChecked(false);
+
+ auto tagString = metadata.value("tags", "").toString();
+ auto tagList = tagString.split('|');
+ for (auto tag : tagList) {
+ if (m_categorySelectors.contains(tag))
+ m_categorySelectors[tag]->setChecked(true);
+ }
+
+ String modId = metadata.value("steamContentId", "").toString();
+ m_modIdLabel->setText(toQString(strf("<a href=\"steam://url/CommunityFilePage/%s\">%s</a>", modId, modId)));
+
+ String previewFile = File::relativeTo(*m_modDirectory, "_previewimage");
+ if (File::isFile(previewFile)) {
+ m_modPreview = QImage(toQString(previewFile), "PNG");
+ m_previewImageLabel->setPixmap(QPixmap::fromImage(m_modPreview));
+ } else {
+ m_modPreview = {};
+ m_previewImageLabel->setPixmap({});
+ }
+}
+
+void ModUploader::selectPreview() {
+ QString image = QFileDialog::getOpenFileName(this, "Select a mod preview image", "", "Images (*.png *.jpg)");
+
+ m_modPreview = {};
+ m_previewImageLabel->setPixmap({});
+
+ if (!image.isEmpty()) {
+ if (m_modPreview.load(image))
+ m_previewImageLabel->setPixmap(QPixmap::fromImage(m_modPreview));
+ else
+ QMessageBox::critical(this, "Error", "Could not load preview image");
+ }
+
+ writePreview();
+}
+
+void ModUploader::writeMetadata() {
+ if (!m_assetSource)
+ return;
+
+ auto metadata = m_assetSource->metadata();
+ auto setMetadata = [&metadata](String const& key, String const& value) {
+ if (value.empty())
+ metadata.remove(key);
+ else
+ metadata[key] = value;
+ };
+
+ setMetadata("name", toSString(m_nameEditor->text()));
+ setMetadata("friendlyName", toSString(m_titleEditor->text()));
+ setMetadata("author", toSString(m_authorEditor->text()));
+ setMetadata("version", toSString(m_versionEditor->text()));
+ setMetadata("description", toSString(m_descriptionEditor->toPlainText()));
+
+ auto tagList = StringList();
+ for (auto pair : m_categorySelectors) {
+ if (pair.second->isChecked())
+ tagList.append(pair.first);
+ }
+ auto tagString = tagList.join("|");
+ setMetadata("tags", tagString);
+
+ m_assetSource->setMetadata(metadata);
+}
+
+void ModUploader::writePreview() {
+ if (m_modPreview.isNull())
+ return;
+
+ String modPreviewFile = File::relativeTo(*m_modDirectory, "_previewimage");
+ m_modPreview.save(toQString(modPreviewFile), "PNG");
+}
+
+void ModUploader::resetModId() {
+ m_modIdLabel->setText("");
+ auto metadata = m_assetSource->metadata();
+ metadata.remove("steamContentId");
+ m_assetSource->setMetadata(metadata);
+}
+
+void ModUploader::uploadToSteam() {
+ if (!m_modDirectory)
+ return;
+
+ QProgressDialog progress("Uploading to Steam...", "", 0, 0, this);
+ progress.setWindowModality(Qt::WindowModal);
+ progress.setCancelButton(nullptr);
+ progress.setAutoReset(false);
+ progress.show();
+
+ if (m_assetSource->assetPaths().empty()) {
+ QMessageBox::critical(this, "Error", "Cannot upload, mod has no content");
+ return;
+ }
+
+ m_steamItemCreateResult = {};
+ m_steamItemSubmitResult = {};
+
+ JsonObject metadata = m_assetSource->metadata();
+ String modIdString = metadata.value("steamContentId", "").toString();
+ if (modIdString.empty()) {
+ CCallResult<ModUploader, CreateItemResult_t> callResultCreate;
+ callResultCreate.Set(SteamUGC()->CreateItem(SteamUtils()->GetAppID(), k_EWorkshopFileTypeCommunity),
+ this, &ModUploader::onSteamCreateItem);
+
+ progress.setLabelText("Creating new Steam UGC Item");
+ while (!m_steamItemCreateResult) {
+ QApplication::processEvents();
+ SteamAPI_RunCallbacks();
+ Thread::sleep(20);
+ }
+
+ if (m_steamItemCreateResult->second) {
+ QMessageBox::critical(this, "Error", "There was an IO error creating a new Steam UGC item");
+ return;
+ }
+
+ if (m_steamItemCreateResult->first.m_bUserNeedsToAcceptWorkshopLegalAgreement) {
+ QMessageBox::critical(this, "Error", "The current Steam user has not agreed to the workshop legal agreement");
+ return;
+ }
+
+ if (m_steamItemCreateResult->first.m_eResult == k_EResultInsufficientPrivilege) {
+ QMessageBox::critical(this, "Error", "Insufficient privileges to create a new Steam UGC item");
+ return;
+ }
+
+ if (m_steamItemCreateResult->first.m_eResult != k_EResultOK) {
+ QMessageBox::critical(this, "Error", strf("Error creating new Steam UGC Item (%s)", m_steamItemCreateResult->first.m_eResult).c_str());
+ return;
+ }
+
+ modIdString = toString(m_steamItemCreateResult->first.m_nPublishedFileId);
+ String modUrl = strf("steam://url/CommunityFilePage/%s", modIdString);
+
+ metadata.set("steamContentId", modIdString);
+ metadata.set("link", modUrl);
+ m_assetSource->setMetadata(metadata);
+
+ m_modIdLabel->setText(toQString(strf("<a href=\"%s\">%s</a>", modUrl, modIdString)));
+ }
+
+ String steamUploadDir = File::temporaryDirectory();
+ auto progressCallback = [&progress](size_t i, size_t total, String, String assetPath) {
+ progress.setLabelText(toQString(strf("Packing '%s'", assetPath)));
+ progress.setMaximum(total);
+ progress.setValue(i);
+ QApplication::processEvents();
+ };
+
+ String packedPath = File::relativeTo(steamUploadDir, "contents.pak");
+ PackedAssetSource::build(*m_assetSource, packedPath, {}, progressCallback);
+
+ PublishedFileId_t modId = lexicalCast<PublishedFileId_t>(modIdString);
+
+ UGCUpdateHandle_t updateHandle = SteamUGC()->StartItemUpdate(SteamUtils()->GetAppID(), modId);
+ SteamUGC()->SetItemTitle(updateHandle, toSString(m_titleEditor->text()).utf8Ptr());
+ SteamUGC()->SetItemDescription(updateHandle, toSString(m_descriptionEditor->toPlainText()).utf8Ptr());
+ if (!m_modPreview.isNull())
+ SteamUGC()->SetItemPreview(updateHandle, File::relativeTo(*m_modDirectory, "_previewimage").utf8Ptr());
+ SteamUGC()->SetItemContent(updateHandle, steamUploadDir.utf8Ptr());
+
+ // construct tags
+ auto tagList = StringList();
+ for (auto entry : m_categorySelectors.pairs()) {
+ if (entry.second->isChecked())
+ tagList.append(entry.first);
+ }
+ const char** tagStrings = new const char*[tagList.size()];
+ for (int i = 0; i < tagList.size(); ++i) {
+ tagStrings[i] = tagList[i].utf8Ptr();
+ }
+ SteamUGC()->SetItemTags(updateHandle, &SteamParamStringArray_t{tagStrings, (int32_t)tagList.size()});
+
+ CCallResult<ModUploader, SubmitItemUpdateResult_t> callResultSubmit;
+ callResultSubmit.Set(SteamUGC()->SubmitItemUpdate(updateHandle, nullptr),
+ this, &ModUploader::onSteamSubmitItem);
+
+ progress.setLabelText("Updating Steam UGC Item");
+ while (!m_steamItemSubmitResult) {
+ uint64 bytesProcessed;
+ uint64 bytesTotal;
+ SteamUGC()->GetItemUpdateProgress(updateHandle, &bytesProcessed, &bytesTotal);
+ progress.setMaximum(bytesTotal);
+ progress.setValue(bytesProcessed);
+ QApplication::processEvents();
+ SteamAPI_RunCallbacks();
+ Thread::sleep(20);
+ }
+
+ File::removeDirectoryRecursive(steamUploadDir);
+
+ if (m_steamItemSubmitResult->second) {
+ QMessageBox::critical(this, "Error", "There was an IO error submitting changes to the Steam UGC item");
+ return;
+ }
+
+ if (m_steamItemSubmitResult->first.m_bUserNeedsToAcceptWorkshopLegalAgreement) {
+ QMessageBox::critical(this, "Error", "The current Steam user has not agreed to the workshop legal agreement");
+ return;
+ }
+
+ if (m_steamItemSubmitResult->first.m_eResult != k_EResultOK) {
+ QMessageBox::critical(this, "Error", strf("Error submitting changes to the Steam UGC item (%s)", m_steamItemSubmitResult->first.m_eResult).c_str());
+ return;
+ }
+}
+
+void ModUploader::onSteamCreateItem(CreateItemResult_t* result, bool ioFailure) {
+ m_steamItemCreateResult = make_pair(*result, ioFailure);
+}
+
+void ModUploader::onSteamSubmitItem(SubmitItemUpdateResult_t* result, bool ioFailure) {
+ m_steamItemSubmitResult = make_pair(*result, ioFailure);
+}
+
+}
diff --git a/source/mod_uploader/StarModUploader.hpp b/source/mod_uploader/StarModUploader.hpp
new file mode 100644
index 0000000..603b42f
--- /dev/null
+++ b/source/mod_uploader/StarModUploader.hpp
@@ -0,0 +1,59 @@
+#ifndef STAR_MOD_UPLOADER
+#define STAR_MOD_UPLOADER
+
+#include <QMainWindow>
+#include <QLabel>
+#include <QListWidget>
+#include <QLineEdit>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QCheckBox>
+
+#include "steam/steam_api.h"
+
+#include "StarDirectoryAssetSource.hpp"
+#include "StarSPlainTextEdit.hpp"
+
+namespace Star {
+
+class ModUploader : public QMainWindow {
+ Q_OBJECT
+public:
+ ModUploader();
+
+private slots:
+ void selectDirectory();
+ void loadDirectory();
+ void selectPreview();
+ void writeMetadata();
+ void writePreview();
+ void resetModId();
+ void uploadToSteam();
+
+private:
+ void onSteamCreateItem(CreateItemResult_t* result, bool ioFailure);
+ void onSteamSubmitItem(SubmitItemUpdateResult_t* result, bool ioFailure);
+
+ QPushButton* m_reloadButton;
+ QLabel* m_directoryLabel;
+ QLineEdit* m_nameEditor;
+ QLineEdit* m_titleEditor;
+ QLineEdit* m_authorEditor;
+ QLineEdit* m_versionEditor;
+ SPlainTextEdit* m_descriptionEditor;
+ QLabel* m_previewImageLabel;
+ QLabel* m_modIdLabel;
+ QWidget* m_editorSection;
+ HashMap<String, QCheckBox*> m_categorySelectors;
+
+ Maybe<String> m_modDirectory;
+ Maybe<DirectoryAssetSource> m_assetSource;
+ QImage m_modPreview;
+
+ Maybe<pair<CreateItemResult_t, bool>> m_steamItemCreateResult;
+ Maybe<pair<SubmitItemUpdateResult_t, bool>> m_steamItemSubmitResult;
+};
+
+}
+
+#endif
diff --git a/source/mod_uploader/StarSPlainTextEdit.cpp b/source/mod_uploader/StarSPlainTextEdit.cpp
new file mode 100644
index 0000000..32bdfb3
--- /dev/null
+++ b/source/mod_uploader/StarSPlainTextEdit.cpp
@@ -0,0 +1,21 @@
+#include "StarSPlainTextEdit.hpp"
+
+namespace Star {
+
+SPlainTextEdit::SPlainTextEdit(QWidget* parent)
+ : QPlainTextEdit(parent), m_changed(false) {
+
+ connect(this, SIGNAL(textChanged()), this, SLOT(wasChanged()));
+}
+
+void SPlainTextEdit::focusOutEvent(QFocusEvent* e) {
+ QPlainTextEdit::focusOutEvent(e);
+ m_changed = false;
+ emit editingFinished();
+}
+
+void SPlainTextEdit::wasChanged() {
+ m_changed = true;
+}
+
+}
diff --git a/source/mod_uploader/StarSPlainTextEdit.hpp b/source/mod_uploader/StarSPlainTextEdit.hpp
new file mode 100644
index 0000000..9ebc5f8
--- /dev/null
+++ b/source/mod_uploader/StarSPlainTextEdit.hpp
@@ -0,0 +1,28 @@
+#ifndef STAR_SPLAIN_TEXT_EDIT_HPP
+#define STAR_SPLAIN_TEXT_EDIT_HPP
+
+#include <QPlainTextEdit>
+
+namespace Star {
+
+class SPlainTextEdit : public QPlainTextEdit {
+ Q_OBJECT
+public:
+ SPlainTextEdit(QWidget* parent = nullptr);
+
+signals:
+ void editingFinished();
+
+protected:
+ void focusOutEvent(QFocusEvent* e);
+
+private slots:
+ void wasChanged();
+
+private:
+ bool m_changed;
+};
+
+}
+
+#endif
diff --git a/source/mod_uploader/StarStringConversion.hpp b/source/mod_uploader/StarStringConversion.hpp
new file mode 100644
index 0000000..020398f
--- /dev/null
+++ b/source/mod_uploader/StarStringConversion.hpp
@@ -0,0 +1,20 @@
+#ifndef STAR_STRING_CONVERSION
+#define STAR_STRING_CONVERSION
+
+#include <QString>
+
+#include "StarString.hpp"
+
+namespace Star {
+
+inline String toSString(QString const& str) {
+ return String(str.toUtf8().data());
+}
+
+inline QString toQString(String const& str) {
+ return QString::fromUtf8(str.utf8Ptr(), str.utf8Size());
+}
+
+}
+
+#endif
diff --git a/source/mod_uploader/main.cpp b/source/mod_uploader/main.cpp
new file mode 100644
index 0000000..978039a
--- /dev/null
+++ b/source/mod_uploader/main.cpp
@@ -0,0 +1,29 @@
+#include <QApplication>
+#include <QMessageBox>
+
+#include "steam/steam_api.h"
+
+#include "StarModUploader.hpp"
+#include "StarStringConversion.hpp"
+
+using namespace Star;
+
+int main(int argc, char** argv) {
+ QApplication app(argc, argv);
+
+ if (!SteamAPI_Init()) {
+ QMessageBox::critical(nullptr, "Error", "Could not initialize Steam API");
+ return 1;
+ }
+
+ ModUploader modUploader;
+ modUploader.show();
+
+ try {
+ return app.exec();
+ } catch (std::exception const& e) {
+ QMessageBox::critical(nullptr, "Error", toQString(strf("Exception caught: %s\n", outputException(e, true))));
+ coutf("Exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}