From 57e7ceaf0e9e2d421a4a0a9fbfddb57ed650477f Mon Sep 17 00:00:00 2001 From: Big Duckie Date: Fri, 15 Nov 2024 14:22:16 -0700 Subject: [PATCH] Commit of existing code. --- .gitignore | 291 +++++++++++++++ .idea/.gitignore | 8 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/solderPacker.iml | 10 + .idea/vcs.xml | 6 + requirements-cli.txt | 6 + requirements.txt | 13 + solderPacker.py | 147 ++++++++ solderPackerGUI.py | 337 ++++++++++++++++++ wrenchIcon.ico | Bin 0 -> 19106 bytes 12 files changed, 836 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/solderPacker.iml create mode 100644 .idea/vcs.xml create mode 100644 requirements-cli.txt create mode 100644 requirements.txt create mode 100644 solderPacker.py create mode 100644 solderPackerGUI.py create mode 100644 wrenchIcon.ico diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f406b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,291 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm +# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python,pycharm + +config.toml \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..01de0e2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..92e05cf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/solderPacker.iml b/.idea/solderPacker.iml new file mode 100644 index 0000000..74d515a --- /dev/null +++ b/.idea/solderPacker.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/requirements-cli.txt b/requirements-cli.txt new file mode 100644 index 0000000..be635ea --- /dev/null +++ b/requirements-cli.txt @@ -0,0 +1,6 @@ +certifi==2024.8.30 +charset-normalizer==3.4.0 +idna==3.10 +requests==2.32.3 +text-unidecode==1.3 +urllib3==2.2.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..42c96e2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +certifi==2024.8.30 +charset-normalizer==3.4.0 +idna==3.10 +PySide6==6.8.0.2 +PySide6_Addons==6.8.0.2 +PySide6_Essentials==6.8.0.2 +python-slugify==8.0.4 +requests==2.32.3 +shiboken6==6.8.0.2 +text-unidecode==1.3 +toml==0.10.2 +urllib3==2.2.3 +validators==0.34.0 diff --git a/solderPacker.py b/solderPacker.py new file mode 100644 index 0000000..78c2147 --- /dev/null +++ b/solderPacker.py @@ -0,0 +1,147 @@ +import argparse +import logging +import os +import shutil +import zipfile + +import requests + + +class SolderPacker: + def __init__(self, solder_dir: str, solder_url: str, solder_key: str): + self.solder_dir = solder_dir + self.solder_url = solder_url + self.solder_key = solder_key + + def compress_jar(self, mod_slug: str, minecraft_version: str, mod_loader: str, mod_version: str, jar_path: str): + export_path = f"{self.solder_dir}{os.sep}{mod_slug}" + if not os.path.exists(export_path): + os.makedirs(export_path) + zip_path = f"{export_path}{os.sep}{mod_slug}-{minecraft_version}-{mod_loader}-{mod_version}.zip" + if os.path.exists(zip_path): + os.remove(zip_path) + + mod_zip = zipfile.ZipFile( + file=zip_path, + mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) + mod_zip.write(jar_path, f"mods/{mod_slug}-{minecraft_version}-{mod_loader}-{mod_version}.jar") + mod_zip.close() + return zip_path + + def copy_zip(self, mod_slug: str, minecraft_version: str, mod_loader: str, mod_version: str, zip_src): + export_path = f"{self.solder_dir}{os.sep}{mod_slug}" + if not os.path.exists(export_path): + os.makedirs(export_path) + zip_path = f"{export_path}{os.sep}{mod_slug}-{minecraft_version}-{mod_loader}-{mod_version}.zip" + shutil.copy(zip_src, zip_path) + return zip_path + + def register_mod(self, mod_slug: str, mod_name: str, mod_author: str, mod_description: str, mod_url: str): + resp = requests.get(f"{self.solder_url}/api/mod/{mod_slug}") + if resp.status_code == 404: + mod_data = { + "name": mod_slug, + "pretty_name": mod_name, + "author": mod_author, + "description": mod_description, + "link": mod_url + } + requests.post(f"{self.solder_url}/mod/create", cookies={"technic_solder_session": self.solder_key}, + data=mod_data, headers={"Origin": self.solder_url, "X-Requested-With": "XMLHttpRequest"}) + + resp = requests.get(f"{self.solder_url}/api/mod/{mod_slug}") + if resp.status_code == 404: + logging.debug("failed to register mod") + return resp.json() + else: + logging.debug("mod already registered") + + def register_mod_version(self, mod_slug: str, minecraft_version: str, mod_loader: str, mod_version: str): + resp = requests.get(f"{self.solder_url}/api/mod/{mod_slug}") + if resp.status_code != 200: + logging.debug("mod doesn't exist") + return + mod_id = resp.json()["id"] + + resp = requests.get(f"{self.solder_url}/api/mod/{mod_slug}/{mod_version}") + if resp.status_code != 200: + logging.debug("creating mod version") + version_data = { + "mod-id": mod_id, + "add-version": f"{minecraft_version}-{mod_loader}-{mod_version}", + "add-md5": "" + } + resp = requests.post(f"{self.solder_url}/mod/add-version", + cookies={"technic_solder_session": self.solder_key}, + data=version_data, + headers={"Origin": self.solder_url, "X-Requested-With": "XMLHttpRequest"}) + if resp.status_code != 200: + logging.debug("failed to create mod version") + else: + logging.debug("created mod version") + else: + logging.debug("mod version already registered. updating hash") + version_data = { + "version-id": resp.json()["id"], + "md5": "" + } + resp = requests.post(f"{self.solder_url}/mod/rehash", + cookies={"technic_solder_session": self.solder_key}, + data=version_data, + headers={"Origin": self.solder_url, "X-Requested-With": "XMLHttpRequest"}) + if resp.status_code != 200: + logging.debug("failed to rehash mod version") + else: + logging.debug("rehashed mod version") + + +def main(): + try: + argp = argparse.ArgumentParser() + mod_data = argp.add_argument_group(title="Mod Data") + mod_data.add_argument("-s", "--slug", help="Mod Slug") + mod_data.add_argument("-n", "--name", help="Mod Name") + mod_data.add_argument("-v", "--version", help="Mod Version") + mod_data.add_argument("-a", "--author", help="Mod Author") + mod_data.add_argument("-d", "--description", help="Mod Description") + mod_data.add_argument("-u", "--url", help="Mod URL") + + mod_data.add_argument("-m", "--minecraft", type=str, help="Minecraft Version") + mod_data.add_argument("-l", "--loader", type=str, choices=["forge", "fabric", "quilt"], default="forge", + help="Mod Loader") + + argp.add_argument("-p", "--path", required=True, default=os.getcwd(), help="Export Path") + + solder_api = argp.add_argument_group(title="Solder API") + solder_api.add_argument("-sa", "--api", default=os.environ.get("SOLDER_URL"), type=str, + help="Solder instance URL") + solder_api.add_argument("-sk", "--key", default=os.environ.get("SOLDER_KEY"), type=str, help="Solder API Key") + + input_type = argp.add_mutually_exclusive_group(required=True) + input_type.add_argument("-j", "--jar", type=str, help="The jar file to package and upload as a mod.") + input_type.add_argument("-z", "--zip", type=str, help="The zip file to upload.") + + argp.set_defaults() + args = argp.parse_args() + + sp = SolderPacker(args.path, args.api, args.key) + if args.jar: + if not os.path.isfile(args.jar): + print("Malformed JAR path") + exit() + sp.compress_jar(args.slug, args.minecraft, args.loader, args.version, args.jar) + elif args.zip: + if not os.path.isfile(args.zip): + print("Malformed ZIP path") + exit() + sp.copy_zip(args.slug, args.minecraft, args.loader, args.version, args.zip) + + sp.register_mod(args.slug, args.name, args.author, args.description, args.url) + sp.register_mod_version(args.slug, args.minecraft, args.loader, args.version) + + except KeyboardInterrupt: + print("Packing cancelled") + + +if __name__ == '__main__': + main() diff --git a/solderPackerGUI.py b/solderPackerGUI.py new file mode 100644 index 0000000..b798fa3 --- /dev/null +++ b/solderPackerGUI.py @@ -0,0 +1,337 @@ +import json +import logging +import os +import re +import sys +import traceback +import zipfile + +import PySide6.QtCore as QtC +import PySide6.QtGui as QtG +import PySide6.QtWidgets as QtW +import toml +from slugify import slugify + +from solderPacker import SolderPacker + +# Globals +config = {} +base_path = os.path.dirname(os.path.realpath(__file__)) + + +class Form(QtW.QMainWindow): + def __init__(self): + QtW.QMainWindow.__init__(self) + self.setMinimumSize(400, 500) + self.setWindowIcon(QtG.QIcon(f"{base_path}{os.sep}wrenchIcon.ico")) + self.setWindowTitle("Solder Packer") + + self.thread_pool = QtC.QThreadPool() + self.solder_packer = SolderPacker(config["path"], config["url"], config["token"]) + self.jar_path = "" + + menubar = self.menuBar() + options_action = menubar.addAction("Options") + options_action.triggered.connect(self.open_options) + + main_widget = QtW.QWidget() + layout = QtW.QGridLayout() + + mod_title_label = QtW.QLabel("Mod Name") + self.mod_title_edit = QtW.QLineEdit("") + layout.addWidget(mod_title_label, 1, 1) + layout.addWidget(self.mod_title_edit, 1, 2) + + mod_slug_label = QtW.QLabel("Mod Slug") + self.mod_slug_edit = QtW.QLineEdit("") + layout.addWidget(mod_slug_label, 2, 1) + layout.addWidget(self.mod_slug_edit, 2, 2) + + mod_version_label = QtW.QLabel("Mod Version") + self.mod_version_edit = QtW.QLineEdit("") + layout.addWidget(mod_version_label, 3, 1) + layout.addWidget(self.mod_version_edit, 3, 2) + + mod_mc_version_label = QtW.QLabel("MC Version") + self.mod_mc_version_edit = QtW.QLineEdit("") + layout.addWidget(mod_mc_version_label, 4, 1) + layout.addWidget(self.mod_mc_version_edit, 4, 2) + + mod_loader_label = QtW.QLabel("Mod Loader") + self.mod_loader_edit = QtW.QLineEdit("") + layout.addWidget(mod_loader_label, 5, 1) + layout.addWidget(self.mod_loader_edit, 5, 2) + + mod_author_label = QtW.QLabel("Mod Author") + self.mod_author_edit = QtW.QLineEdit("") + layout.addWidget(mod_author_label, 6, 1) + layout.addWidget(self.mod_author_edit, 6, 2) + + mod_url_label = QtW.QLabel("Mod URL") + self.mod_url_edit = QtW.QLineEdit("") + layout.addWidget(mod_url_label, 7, 1) + layout.addWidget(self.mod_url_edit, 7, 2) + + mod_desc_label = QtW.QLabel("Mod Description") + mod_desc_label.setAlignment(QtC.Qt.AlignmentFlag.AlignCenter) + self.mod_desc_edit = QtW.QPlainTextEdit("") + self.mod_desc_edit.setMinimumSize(200, 50) + layout.addWidget(mod_desc_label, 8, 1, 1, 2) + layout.addWidget(self.mod_desc_edit, 9, 1, 1, 2) + + horizontal_line = QtW.QFrame() + horizontal_line.setFrameShape(QtW.QFrame.Shape.HLine) + horizontal_line.setFrameShadow(QtW.QFrame.Shadow.Sunken) + layout.addWidget(horizontal_line, 10, 1, 1, 2) + + self.drop_label = FileDropArea("Drop JAR Here", self) + self.drop_label.setMinimumSize(200, 50) + self.drop_label.setAlignment(QtC.Qt.AlignmentFlag.AlignCenter) + self.drop_label.setFrameStyle(QtW.QFrame.Shadow.Sunken | QtW.QFrame.Shape.StyledPanel) + self.drop_label.dropped.connect(self.file_dropped) + layout.addWidget(self.drop_label, 11, 1, 1, 2) + + self.btn_process = QtW.QPushButton("Process") + self.btn_process.clicked.connect(self.process_click) + layout.addWidget(self.btn_process, 12, 1, 1, 2) + + self.setCentralWidget(main_widget) + main_widget.setLayout(layout) + + def open_options(self): + options_dialog = OptionsDialog(parent=self) + options_dialog.closed.connect(self.options_closed) + options_dialog.show() + + def options_closed(self): + self.solder_packer.solder_dir = config["path"] + self.solder_packer.solder_url = config["url"] + self.solder_packer.solder_key = config["token"] + + def file_dropped(self, file_path: str): + self.jar_path = file_path + if config["extract"]: + jar = zipfile.ZipFile(file_path, "r") + if "META-INF/mods.toml" in jar.namelist(): + # This jar file uses FML + logging.debug("identified mod as forge mod") + mod_toml = toml.loads(jar.read("META-INF/mods.toml").decode("utf-8")) + # This mess is to catch if the mod is using the Jar version for versioning. It extracts it from the + # MANIFEST.MF and places it where it should be in the toml. + if mod_toml["mods"][0]["version"] == "${file.jarVersion}": + mod_toml["mods"][0]["version"] = re.findall( + r"Implementation-Version: (.*)", jar.read("META-INF/MANIFEST.MF").decode("utf-8"))[0] + + # Update form with data + self.mod_title_edit.setText(mod_toml.get("mods", [{}])[0].get("displayName", "")) + self.mod_slug_edit.setText(slugify(mod_toml.get("mods", [{}])[0].get("displayName", ""), + replacements=[["&", "and"], ["$", "s"], ["'", ""]])) + self.mod_version_edit.setText(mod_toml.get("mods", [{}])[0].get("version", "")) + self.mod_loader_edit.setText("forge") + self.mod_author_edit.setText(mod_toml.get("mods", [{}])[0].get("authors", "")) + self.mod_url_edit.setText(mod_toml.get("mods", [{}])[0].get("displayURL", "")) + self.mod_desc_edit.setPlainText(mod_toml.get("mods", [{}])[0].get("description", "")) + elif "fabric.mod.json" in jar.namelist(): + # This jar file uses Fabric + logging.debug("identified mod as fabric mod") + mod_json = json.loads(jar.read("fabric.mod.json").decode("utf-8")) + + # Update form with data + self.mod_title_edit.setText(mod_json.get("name", "")) + self.mod_slug_edit.setText(slugify(mod_json.get("name", ""), + replacements=[["&", "and"], ["$", "s"], ["'", ""]])) + self.mod_version_edit.setText(mod_json.get("version", "")) + self.mod_loader_edit.setText("fabric") + + # Some spaghetti code for dealing with the possibility of authors being a []string or a dict. + authors = mod_json.get("authors", []) + author_list = [] + if len(authors) > 0 and type(authors[0]) == dict: + for author in authors: + author_list.append(author.get("name")) + elif len(authors) > 0 and type(authors[0]) == str: + author_list = authors + self.mod_author_edit.setText(", ".join(author_list)) + self.mod_url_edit.setText(mod_json.get("contact", {}).get("homepage", "")) + self.mod_desc_edit.setPlainText(mod_json.get("description", "")) + elif "quilt.mod.json" in jar.namelist(): + # This jar file uses Quilt + logging.debug("identified mod as quilt mod") + mod_json = json.loads(jar.read("quilt.mod.json").decode("utf-8")) + + # Update form with data + self.mod_title_edit.setText(mod_json.get("quilt_loader", {}).get("metadata", {}).get("name", "")) + self.mod_slug_edit.setText(slugify(mod_json.get("quilt_loader", {}).get("metadata", {}).get("name", ""), + replacements=[["&", "and"], ["$", "s"], ["'", ""]])) + self.mod_version_edit.setText(mod_json.get("quilt_loader", {}).get("version", "")) + self.mod_loader_edit.setText("quilt") + self.mod_author_edit.setText( + ", ".join(mod_json.get("quilt_loader", {}).get("metadata", {}).get("contributors", {}).keys())) + self.mod_url_edit.setText( + mod_json.get("quilt_loader", {}).get("metadata", {}).get("contact", {}).get("homepage", "")) + self.mod_desc_edit.setPlainText( + mod_json.get("quilt_loader", {}).get("metadata", {}).get("description", "")) + else: + logging.debug("failed to identify mod loader") + jar.close() + + def process_click(self): + self.btn_process.setEnabled(False) + self.btn_process.setText("Processing...") + + worker = Worker(self.process, self.mod_slug_edit.text(), self.mod_title_edit.text(), + self.mod_version_edit.text(), self.mod_mc_version_edit.text(), self.mod_loader_edit.text(), + self.mod_author_edit.text(), self.mod_url_edit.text(), self.mod_desc_edit.toPlainText(), + self.jar_path) + + worker.signals.finished.connect(self.finished_processing) + worker.signals.error.connect(self.failed_processing) + self.thread_pool.start(worker) + + def process(self, mod_slug: str, mod_title: str, mod_version: str, mod_mc_version: str, mod_loader: str, + mod_author: str, mod_url: str, mod_desc: str, jar_path: str): + logging.debug("attempting jar compression") + self.solder_packer.compress_jar(mod_slug, mod_mc_version, mod_loader, mod_version, jar_path) + logging.debug("jar compression done") + if config["upload"]: + logging.debug("attempting to register mod with solder") + self.solder_packer.register_mod(mod_slug, mod_title, mod_author, mod_desc, mod_url) + logging.debug("registration complete") + logging.debug("attempting to register mod version with solder") + self.solder_packer.register_mod_version(mod_slug, mod_mc_version, mod_loader, mod_version) + logging.debug("registration complete") + + def finished_processing(self): + self.btn_process.setEnabled(True) + self.btn_process.setText("Process") + + def failed_processing(self): + dialog = QtW.QMessageBox(self) + dialog.setWindowTitle("Error") + dialog.setText("There was an error processing this JAR.") + dialog.exec() + + self.finished_processing() + + +class FileDropArea(QtW.QLabel): + dropped = QtC.Signal(str) + + def __init__(self, title, parent): + super(FileDropArea, self).__init__(title, parent) + self.parent = parent + self.setAcceptDrops(True) + + def dragEnterEvent(self, e): + e.accept() + + @QtC.Slot() + def dropEvent(self, e): + file_path = e.mimeData().urls()[0].toLocalFile() + self.dropped.emit(file_path) + self.setText(os.path.basename(file_path)) + + +class OptionsDialog(QtW.QDialog): + closed = QtC.Signal() + + def __init__(self, parent=None): + QtW.QDialog.__init__(self, parent) + + self.setMinimumSize(300, 200) + + self.setWindowIcon(QtG.QIcon(f"{base_path}{os.sep}wrenchIcon.ico")) + self.setWindowTitle("Options") + + layout = QtW.QGridLayout() + self.setLayout(layout) + + layout.addWidget(QtW.QLabel("Export Folder"), 1, 1) + layout.addWidget(QtW.QLabel("Technic URL"), 2, 1) + layout.addWidget(QtW.QLabel("Technic Cookie"), 3, 1) + + self.input_export_folder = QtW.QLineEdit(config["path"]) + layout.addWidget(self.input_export_folder, 1, 2) + self.input_technic_url = QtW.QLineEdit(config["url"]) + layout.addWidget(self.input_technic_url, 2, 2) + self.input_technic_cookie = QtW.QLineEdit(config["token"]) + layout.addWidget(self.input_technic_cookie, 3, 2) + + self.checkbox_extract_data = QtW.QCheckBox("Extract Mod Info") + self.checkbox_extract_data.setChecked(config["extract"]) + layout.addWidget(self.checkbox_extract_data, 4, 1) + + self.checkbox_upload_data = QtW.QCheckBox("Upload Data") + self.checkbox_upload_data.setChecked(config["upload"]) + layout.addWidget(self.checkbox_upload_data, 4, 2) + + button_save = QtW.QPushButton("Save") + layout.addWidget(button_save, 5, 1, 1, 2) + button_save.clicked.connect(self.save_options) + + def save_options(self): + config["path"] = self.input_export_folder.text() + config["url"] = self.input_technic_url.text() + config["token"] = self.input_technic_cookie.text() + config["extract"] = (self.checkbox_extract_data.checkState() == QtC.Qt.CheckState.Checked) + config["upload"] = (self.checkbox_upload_data.checkState() == QtC.Qt.CheckState.Checked) + + with open(f"{base_path}{os.sep}config.toml", "w") as f: + toml.dump(config, f) + f.close() + self.close() + + @QtC.Slot() + def closeEvent(self, event): + self.closed.emit() + + +class WorkerSignals(QtC.QObject): + finished = QtC.Signal() + error = QtC.Signal(tuple) + + +class Worker(QtC.QRunnable): + def __init__(self, fn, *args, **kwargs): + super().__init__() + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + + @QtC.Slot() + def run(self): + try: + self.fn(*self.args, **self.kwargs) + except: + exec_type, value = sys.exc_info()[:2] + self.signals.error.emit((exec_type, value, traceback.format_exc())) + finally: + self.signals.finished.emit() + + +def get_default(struct: dict, key: str, default: any): + return default if key not in struct else struct[key] + + +if __name__ == '__main__': + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) + + try: + config = toml.load(f"{base_path}{os.sep}config.toml") + except FileNotFoundError: + config = {} + + config["path"] = get_default(config, "path", os.getcwd()) + config["url"] = get_default(config, "url", os.environ.get("SOLDER_URL") or "http://localhost") + config["token"] = get_default(config, "token", os.environ.get("SOLDER_KEY") or "") + config["extract"] = get_default(config, "extract", True) + config["upload"] = get_default(config, "upload", False) + with open(f"{base_path}{os.sep}config.toml", "w") as f: + toml.dump(config, f) + f.close() + + app = QtW.QApplication(sys.argv) + form = Form() + form.show() + sys.exit(app.exec()) diff --git a/wrenchIcon.ico b/wrenchIcon.ico new file mode 100644 index 0000000000000000000000000000000000000000..58f56f06158d66d337fce717788fb0ace83cfb50 GIT binary patch literal 19106 zcmW(+1yJ2w6U}=Mcc-{Rao6Hj+}+*XrNG0ByHlXJwrHVH3KVyDcb69TpYKm5lbcCq z?`CK3&fYzH008uM{&#=?av)X?0Px<%Q7TFwQIQCd-iD~MGLmZlz5VY(fP1?cxRqP| z_vGxRqyZW)^v(tVIcZr*F%6%kQ$zS%1I^iv2AXzwtf}S^d(O5TX>c$Ugo{~0IVDD) zUqC)(pFDatHsbGWIn|!K&c#^2lR6QqT zlFK7tMf9(%6}?{-keU-kTe4D_^_2r#|0>pf77w#9IP(i0iFTk&w#8g@`E0ge@sQkP zu|6D#3eO{kGaa$s-R*#l8>oev^FlIX%ZO#MRxjzQS?*g-_~Tz(U!&7(6wZhG8O?^$_QFLOr+D%KP$el3h`c-=*?TW;Z|;q zc%4TuFcKnh$IKVD4gp$l&`vq-F4GFULxsT)C17P#?nLZjQ3?QHyV#(iXDTWMa+36V{5>|$B1prF$K;rXojhU|UJuMNDjtaYMF|MKqNjSpaUp8NH z5Sbjkd0D0V?DhXz5y8L*{<0H@?6OLLIswl@2O|FtI?tK=pnqfVMafbX_swQqLTV$5 zc5HsBz*ZryAYdWrYrM(H)9}WsFUEda!fF2-irIz$m{gkSnd~je>lQx;SRutobXc?9 z@g5k1fV`0FM^LqKTH%N7((eu>^s=bcApu%miBhx2y}z-OeL0Qwnh?=`ONCfrClyvq zZj6&55#0#@&?Y8^*Pna~&qypUpes@!>{4tWZsd-9(Bm_b{WpYpQ3U0`-&d5Zf*!%y zU4�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 Eg7rVT9RL6T literal 0 HcmV?d00001