Files
solderPacker/solderPackerGUI.py
T
big-duckie 1783201372 Slug now updates when use changes mod name.
Added folder selection dialog to options menu.
Fixed bug where options were not being saved to the correct path.
2024-11-27 18:47:17 -07:00

359 lines
15 KiB
Python

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(__file__)
class Form(QtW.QMainWindow):
def __init__(self):
QtW.QMainWindow.__init__(self)
self.setMinimumSize(500, 600)
self.setWindowIcon(QtG.QIcon(os.path.join(base_path, "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("")
self.mod_title_edit.textChanged.connect(self.update_slug)
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 update_slug(self):
self.mod_slug_edit.setText(slugify(self.mod_title_edit.text(),
replacements=[["&", "and"], ["$", "s"], ["'", ""]]))
def file_dropped(self, file_path: str):
self.jar_path = file_path
self.btn_process.setText("Process")
if config["extract"]:
jar = zipfile.ZipFile(file_path, "r")
loaders = []
if "META-INF/mods.toml" in jar.namelist():
# This jar file uses FML
logging.debug("identified mod as forge mod")
loaders.append("forge")
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.get("mods", [{}])[0].get("version") == "${file.jarVersion}":
try:
mod_toml["mods"][0]["version"] = re.findall(
r"Implementation-Version: (.*)", jar.read("META-INF/MANIFEST.MF").decode("utf-8"))[0]
except IndexError:
mod_toml["mods"][0]["version"] = "0.0.0"
# Update form with data
self.mod_title_edit.setText(mod_toml.get("mods", [{}])[0].get("displayName", "").strip())
self.mod_slug_edit.setText(slugify(self.mod_title_edit.text(),
replacements=[["&", "and"], ["$", "s"], ["'", ""]]).strip())
self.mod_version_edit.setText(mod_toml.get("mods", [{}])[0].get("version", "").strip())
self.mod_author_edit.setText(mod_toml.get("mods", [{}])[0].get("authors", "").strip())
self.mod_url_edit.setText(mod_toml.get("mods", [{}])[0].get("displayURL", "").strip())
self.mod_desc_edit.setPlainText(mod_toml.get("mods", [{}])[0].get("description", "").strip())
if "fabric.mod.json" in jar.namelist():
# This jar file uses Fabric
logging.debug("identified mod as fabric mod")
loaders.append("fabric")
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", "").strip())
self.mod_slug_edit.setText(slugify(mod_json.get("name", ""),
replacements=[["&", "and"], ["$", "s"], ["'", ""]]))
self.mod_version_edit.setText(mod_json.get("version", "").strip())
# 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).strip())
self.mod_url_edit.setText(mod_json.get("contact", {}).get("homepage", "").strip())
self.mod_desc_edit.setPlainText(mod_json.get("description", "").strip())
if "quilt.mod.json" in jar.namelist():
# This jar file uses Quilt
logging.debug("identified mod as quilt mod")
loaders.append("quilt")
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", "").strip())
self.mod_slug_edit.setText(slugify(mod_json.get("quilt_loader", {}).get("metadata", {}).get("name", ""),
replacements=[["&", "and"], ["$", "s"], ["'", ""]]).strip())
self.mod_version_edit.setText(mod_json.get("quilt_loader", {}).get("version", "").strip())
self.mod_author_edit.setText(", ".join(
mod_json.get("quilt_loader", {}).get("metadata", {}).get("contributors", {}).keys()).strip())
self.mod_url_edit.setText(
mod_json.get("quilt_loader", {}).get("metadata", {}).get("contact", {}).get("homepage", "").strip())
self.mod_desc_edit.setPlainText(
mod_json.get("quilt_loader", {}).get("metadata", {}).get("description", "").strip())
self.mod_loader_edit.setText("+".join(loaders))
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("Finished")
def failed_processing(self, err):
dialog = QtW.QMessageBox(self)
dialog.setWindowTitle("Error")
dialog.setText("There was an error processing this JAR.")
dialog.exec()
logging.debug(err)
self.btn_process.setEnabled(True)
self.btn_process.setText("Failed")
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(400, 200)
self.setWindowIcon(QtG.QIcon(os.path.join(base_path, "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.btn_export_folder = QtW.QPushButton("Browse")
self.btn_export_folder.clicked.connect(self.select_folder)
layout.addWidget(self.btn_export_folder, 1, 3)
self.input_technic_url = QtW.QLineEdit(config["url"])
layout.addWidget(self.input_technic_url, 2, 2, 1, 2)
self.input_technic_cookie = QtW.QLineEdit(config["token"])
layout.addWidget(self.input_technic_cookie, 3, 2, 1, 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, 1, 2)
button_save = QtW.QPushButton("Save")
layout.addWidget(button_save, 5, 1, 1, 3)
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(os.path.join(os.getcwd(), "config.toml"), "w") as configFile:
toml.dump(config, configFile)
configFile.close()
self.close()
def select_folder(self):
self.input_export_folder.setText(str(QtW.QFileDialog.getExistingDirectory(self, "Select Folder")))
@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(os.path.join(os.getcwd(), "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(os.path.join(os.getcwd(), "config.toml"), "w") as f:
toml.dump(config, f)
f.close()
app = QtW.QApplication(sys.argv)
form = Form()
form.show()
sys.exit(app.exec())