diff --git a/ModManager.py b/ModManager.py new file mode 100644 index 0000000..6b4cedc --- /dev/null +++ b/ModManager.py @@ -0,0 +1,359 @@ +import glob +import hashlib +import json +import logging +import os +import shutil +import sys +import tempfile +from typing import Tuple, List +from zipfile import ZipFile + +from PyQt6.QtWidgets import QApplication + +from files.settings import LCSettings +from window.main_window import MainWindow + + +class ModManager: + + VERSION = "0.1" + + def __init__(self, log_level: int = logging.INFO): + self.__logger = logging.getLogger("ModManager") + sh = logging.StreamHandler(sys.stdout) + sh.setFormatter(logging.Formatter( + fmt="%(asctime)s.%(msecs)03d [%(name)-12.12s] [%(levelname)-5.5s] %(message)s")) + self.__logger.addHandler(sh) + self.__logger.setLevel(log_level) + + self.__app = QApplication([]) + self.__window = MainWindow(self, self.__logger, version=ModManager.VERSION) + self.__settings = LCSettings() + + self.available_mods = dict() + self.installed_mods = dict() + + while self.__settings.get_game_folder() is None: + self.__window.on_action_set_game_folder() + + self.create_manager_folder() + self.index_stored_mods() + self.index_installed_mods() + + def run(self): + self.__logger.debug(f"Starting LCMod Manager {ModManager.VERSION} ...") + self.__window.show() + + status = self.__app.exec() + sys.exit(status) + + def __adjust_folder_paths(self, path: str) -> str: + """ + Adjusts a given path containing common issues to one compatible with BepInEx + + :param input: + :return: + """ + if path.endswith(".dll") and "BepInEx" not in path: + path = os.path.join("BepInEx", "plugins", path) + if path.startswith("config" + os.path.sep) or path.startswith("plugins" + os.path.sep): + path = os.path.join("BepInEx", path) + if path.lower().startswith("bepinex") and not path.startswith("BepInEx"): + path = "BepInEx" + path[len("BepinEx"):] + + return path + + def create_manager_folder(self): + game_folder = self.__settings.get_game_folder() + if game_folder is not None: + manager_folder = os.path.join(game_folder, LCSettings.FOLDER_NAME) + if not os.path.isdir(manager_folder): + self.__logger.debug(f"Creating folder: {manager_folder}") + os.mkdir(manager_folder) + + def is_folder_valid_game_folder(self, dir_path: str) -> bool: + req_files = ["Lethal Company.exe", "doorstop_config.ini"] + req_folders = ["Lethal Company_Data", "MonoBleedingEdge", "BepInEx"] + for entry in os.listdir(dir_path): + fullpath_entry = os.path.join(dir_path, entry) + if os.path.isfile(fullpath_entry): + if entry in req_files: + req_files.remove(entry) + elif os.path.isdir(fullpath_entry): + if entry in req_folders: + req_folders.remove(entry) + + if len(req_folders) == len(req_files) == 0: + return True + else: + if len(req_files) == 1 and req_files[0] == "doorstop_config.ini" or \ + len(req_folders) == 1 and req_folders[0] == "BepInEx": + self.__logger.error(f"new game path rejected! Following files and folders were " + f"expected but not present: {req_files + req_folders}\nBepInEx not installed?") + return False + self.__logger.error(f"new game path rejected! Following files and folders were " + f"expected but not present: {req_files + req_folders}") + return False + + def set_game_folder(self, dir_path: str) -> bool: + """ + Sets the game_path to dir_path in the settings + :param dir_path: + :return: indicates if the new game folder path was accepted + """ + if len(dir_path) == 0: + self.__logger.debug("new game path selection got probably cancelled.") + return True + + is_valid = self.is_folder_valid_game_folder(dir_path) + if is_valid: + s = self.__settings.get_settings() + s["game_path"] = dir_path + self.__settings.apply_changes() + self.create_manager_folder() + self.index_stored_mods() + self.index_installed_mods() + return is_valid + + def is_mod_installed(self, mod_name: str) -> bool: + """ + Checks if a given mod has been installed to the game already + :param mod_name: + :return: + """ + r = self.available_mods[mod_name] + mod_files = r["mod_files"] + for i in range(len(mod_files)): + file = mod_files[i] + orig_file = r["orig_mod_files"][i] + if not os.path.exists(os.path.join(self.__settings.get_game_folder(), file)): + return False + else: + if os.path.isfile(os.path.join(self.__settings.get_game_folder(), file)): + hash_installed = self.get_file_hash(open(os.path.join(self.__settings.get_game_folder(), file), 'rb')) + modzip = os.path.join(self.__settings.get_mod_folder(), self.available_mods[mod_name]["path"]) + hash_in_storage = self.get_file_hash(ZipFile(modzip).open(orig_file, 'r')) + if hash_installed != hash_in_storage: + return False + return True + + def is_valid_mod_file(self, file_path: str): + """ + Checks if the given file is a valid mod zip file + :param file_path: + :return: + """ + if not os.path.isfile(file_path): + return False + zip = ZipFile(file_path) + contents = zip.namelist() + + if "manifest.json" in contents: + return True + return False + + def get_file_hash(self, file_buffer) -> str: + md5_obj = hashlib.md5() + while True: + buffer = file_buffer.read(8096) + if not buffer: + break + md5_obj.update(buffer) + return md5_obj.hexdigest() + + def get_mod_hash(self, file_path: str) -> str: + md5_obj = hashlib.md5() + with open(file_path, 'rb') as file: + while True: + buffer = file.read(8096) + if not buffer: + break + md5_obj.update(buffer) + return md5_obj.hexdigest() + + def get_mod_info(self, file_path: str) -> Tuple[str, str, List[str]]: + """ + Returns the name and version string of a given mod file + :param file_path: + :return: + """ + if not self.is_valid_mod_file(file_path): + self.__logger.error(f"Tried to get mod info of an invalid file: {file_path}") + raise ValueError(f"Tried to get mod info of an invalid file: {file_path}") + + zip = ZipFile(file_path) + f = zip.open('manifest.json') + contents = zip.namelist() + + orig_files = [] + files = [] + for file in contents: + if "icon.png" in file or "manifest.json" in file or file.endswith(".md"): + continue + orig_files.append(file) + file = self.__adjust_folder_paths(file) + + files.append(file) + + manifest = json.load(f) + return str(manifest["name"]), str(manifest["version_number"]), files, orig_files + + def index_installed_mods(self): + """ + Checks all installed mods against the known mod files. + + For unknown mods a placeholder is created + :return: + """ + self.installed_mods = dict() + if self.__settings.get_game_folder() is None: + return + + files = [] + to_search = [self.__settings.get_plugin_folder()] + while len(to_search) > 0: + curr = to_search.pop() + if os.path.isdir(curr): + r = os.listdir(curr) + for i in r: + to_search.append(os.path.join(curr, i)) + elif os.path.isfile(curr) and curr.endswith('.dll'): + files.append(curr) + + for mod_name in self.available_mods.keys(): + if self.is_mod_installed(mod_name): + self.installed_mods[mod_name] = self.available_mods[mod_name] + + unknown_mod = dict() + unresolved_files = files.copy() + for file in files: + for mod in self.installed_mods.keys(): + if file[len(self.__settings.get_game_folder())+1:] in self.installed_mods[mod]["mod_files"]: + unresolved_files.remove(file) + if file in unresolved_files: + unknown_mod[os.path.basename(file)] = {"mod_files": [file]} + for key in unknown_mod.keys(): + self.installed_mods[key] = unknown_mod[key] + + self.__window.set_installed_mods(self.installed_mods) + self.__window.set_available_mods(self.available_mods) + + def index_stored_mods(self): + """ + Goes through all mods in ModManager.FOLDER_NAME and tries to add them to the available mod list. + + Ignores mods that are already on the list + :return: + """ + self.available_mods = dict() + if self.__settings.get_game_folder() is None: + return + for file in os.listdir(os.path.join(self.__settings.get_game_folder(), LCSettings.FOLDER_NAME)): + full_path = os.path.join(self.__settings.get_mod_folder(), str(file)) + if not self.is_valid_mod_file(full_path): + self.__logger.warning(f"File {file} is not a valid mod file but inside the mod storage folder!") + mod_name, mod_version, mod_files, orig_mod_files = self.get_mod_info(full_path) + if mod_name is None: + self.__logger.warning(f"Mod \"{full_path}\" did not have the expected path. Ignoring it ...") + continue + + self.available_mods[mod_name] = {"path": file, "version": mod_version, "mod_files": mod_files, + "orig_mod_files": orig_mod_files} + self.__window.set_available_mods(self.available_mods) + + def add_mod_file(self, file_path: str): + """ + Adds the mod to the mod list + :param file_path: + :return: + """ + if self.__settings.get_settings()["game_path"] is None: + self.__logger.error("Can't add a mod without a valid game path!") + + if not self.is_valid_mod_file(file_path): + self.__logger.warning(f"File {file_path} was not a mod file!") + return + + dst = os.path.join(self.__settings.get_mod_folder(), + os.path.basename(file_path)) + if os.path.isfile(dst): + hash1 = self.get_mod_hash(file_path) + hash2 = self.get_mod_hash(dst) + + if hash1 != hash2: + self.__logger.info("Given file is different than the one stored. Overwriting it!") + else: + self.__logger.info("Given file is the same as the one stored. Ignoring it!") + return + self.create_manager_folder() + self.__logger.info(f"File {file_path} added as a mod file ...") + shutil.copy(file_path, + os.path.join(self.__settings.get_mod_folder(), os.path.basename(file_path))) + self.index_stored_mods() + self.index_installed_mods() + + def install_mod(self, mod_name: str): + """ + Installs the given mod by extracting all files into the game directory. + + This assumes that the .zip is structured like the game folder as all mods should be + :param mod_name: + :return: + """ + if mod_name not in self.available_mods.keys(): + self.__logger.critical(f"Tried to install a mod that doesn't exist: {mod_name}") + return + self.__logger.info(f"Installing mod \"{mod_name}\" ...") + mod_zip = self.available_mods[mod_name]["path"] + + with ZipFile(os.path.join(self.__settings.get_mod_folder(), mod_zip), 'r') as zip_ref: + #zip_ref.extractall(self.__settings.get_game_folder()) + contents = zip_ref.namelist() + with tempfile.TemporaryDirectory() as tmp_dir: + for file in contents: + content = file + if "icon.png" in file or "manifest.json" in file or file.endswith(".md"): + continue + file = self.__adjust_folder_paths(file) + + #print("Extracting", content, "to", tmp_dir) + zip_ref.extract(content, tmp_dir) + if content.endswith(os.path.sep): + #print("Skipped moving", os.path.join(tmp_dir, content)) + continue + #print("Moving", os.path.join(tmp_dir, content), "to", + # os.path.join(self.__settings.get_game_folder(), file)) + parent_dir = os.path.join(self.__settings.get_game_folder(), file).split(os.path.basename(file))[0] + if not os.path.exists(parent_dir): + os.mkdir(parent_dir) + shutil.move(os.path.join(tmp_dir, content), os.path.join(self.__settings.get_game_folder(), file)) + self.index_installed_mods() + + def uninstall_mod(self, mod_name: str): + """ + Uninstalls the given mod by removing all files (not folders) that the manager is aware of + + Note: For untracked mods this will only be the .dll in the plugins folder + :param mod_name: + :return: + """ + self.__logger.info(f"Uninstalling mod \"{mod_name}\" ...") + for file in self.installed_mods[mod_name]["mod_files"]: + full_path = os.path.join(self.__settings.get_game_folder(), file) + if os.path.isfile(full_path): + self.__logger.debug(f"Deleting file \"{full_path}\" ...") + os.remove(full_path) + self.index_installed_mods() + + def nuke_manager_files(self): + self.__logger.info("Deleting all manager related files ...") + self.__logger.debug(f"Deleting folder \"{self.__settings.get_mod_folder()}\"") + files = glob.glob(os.path.join(self.__settings.get_mod_folder(), '*.zip')) + for f in files: + self.__logger.debug(f"Deleting file \"{f}\"") + os.remove(f) + os.removedirs(self.__settings.get_mod_folder()) + self.__logger.debug(f"Deleting configuration file \"{self.__settings.file_path}\"") + os.remove(self.__settings.file_path) + self.__app.exit(0) diff --git a/files/__init__.py b/files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/files/settings.py b/files/settings.py new file mode 100644 index 0000000..1756029 --- /dev/null +++ b/files/settings.py @@ -0,0 +1,46 @@ +import logging +import os.path + +import yaml + + +class LCSettings: + + FOLDER_NAME = "LCModManager" + FILE_NAME = "lc_mod_manager_settings.yaml" + DEFAULT_SETTINGS = { + "game_path": None, + } + + def __init__(self): + self.__logger = logging.getLogger("ModManager") + + self.file_path = "./"+LCSettings.FILE_NAME + if not os.path.isfile(self.file_path): + self.__logger.warning(f"Couldn't find settings file \"{self.file_path}\" during LCSettings.init() ...") + self.__create_default_settings_file() + self.settings = yaml.safe_load(open(self.file_path, 'r')) + + def __create_default_settings_file(self): + self.__logger.warning(f"Writing new default settings file at {self.file_path}") + yaml.safe_dump(LCSettings.DEFAULT_SETTINGS, open(self.file_path, 'w')) + + def get_settings(self) -> dict: + return self.settings + + def get_game_folder(self) -> str: + return self.settings["game_path"] + + def get_mod_folder(self) -> str: + return os.path.join(self.settings["game_path"], LCSettings.FOLDER_NAME) + + def get_plugin_folder(self) -> str: + return os.path.join(self.settings["game_path"], "BepInEx", "plugins") + + def apply_changes(self): + """ + Writes the current settings to file in case there were changes + :return: + """ + self.__logger.info("Writing settings down to file!") + yaml.safe_dump(self.settings, open(self.file_path, 'w')) diff --git a/main.py b/main.py new file mode 100644 index 0000000..43820b4 --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +import logging + +from ModManager import ModManager + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + m = ModManager(log_level=logging.INFO) + m.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..724d2de --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyQt6>=6.6.1 +pyyaml>=6.0.1 \ No newline at end of file diff --git a/window/__init__.py b/window/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/window/main_window.py b/window/main_window.py new file mode 100644 index 0000000..98f5341 --- /dev/null +++ b/window/main_window.py @@ -0,0 +1,178 @@ +import logging +import os.path +import sys +from typing import Dict, List + +from PyQt6 import QtWidgets +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QStandardItemModel, QStandardItem + +from window.mod_manager_window_export import Ui_MainWindow + + +class MainWindow(QtWidgets.QMainWindow): + + def __init__(self, parent, parent_logger: logging.Logger, version: str = ""): + super(MainWindow, self).__init__() + self.parent = parent + self.__logger = logging.getLogger("MainWindow") + for handler in parent_logger.handlers: + self.__logger.addHandler(handler) + self.__logger.setLevel(parent_logger.level) + + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self.setWindowTitle(f"LC Mod Manager {version}") + + self.ui.actionAdd_new_mod.triggered.connect(self.on_action_add_new_mod) + self.ui.actionrefresh_mods.triggered.connect(self.on_action_refresh_mods) + self.ui.actionCheck_for_Updates.triggered.connect(self.on_action_check_for_updates) + self.ui.actionSet_game_folder.triggered.connect(self.on_action_set_game_folder) + self.ui.actionRemove_ALL_manager_files.triggered.connect(self.on_action_remove_all_files) + + self.ui.DeleteModFilesButton.pressed.connect(self.on_pressed_delete_mod_files_button) + self.ui.ApplyChangesButton.pressed.connect(self.on_pressed_apply_changes_button) + self.ui.DisacrdChangesButton.pressed.connect(self.on_pressed_discard_changes_button) + + self.ui.TODOButton.hide() + self.ui.ApplyChangesButton.hide() + self.ui.DisacrdChangesButton.hide() + self.ui.DeleteModFilesButton.hide() + + self.ui.actionCheck_for_Updates.setDisabled(True) + + def set_available_mods(self, available_mods: Dict[str, str]): + """ + Sets the given mods as the list of available mods + :param available_mods: + :return: + """ + item_model = QStandardItemModel(self.ui.AvailableModsList) + for mod_name in available_mods.keys(): + item = QStandardItem(mod_name + f" ({available_mods[mod_name]['version']})") + item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled) + item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) + item.setData(mod_name, Qt.ItemDataRole.UserRole) + + item_model.appendRow(item) + if self.parent.is_mod_installed(mod_name): + item.setCheckState(Qt.CheckState.Checked) + + item_model.itemChanged.connect(self.on_available_mod_item_changed) + self.ui.AvailableModsList.setModel(item_model) + + def set_installed_mods(self, installed_mods: Dict[str, str]): + """ + Sets the given mods as the list of installed mods + :param installed_mods: string list of all the mods as to be written to the list + :return: + """ + item_model = QStandardItemModel(self.ui.InstalledModsListView) + for mod_name in installed_mods.keys(): + if mod_name in self.parent.available_mods.keys(): + mod_version = self.parent.available_mods[mod_name]['version'] + else: + mod_version = "Not Tracked" + item = QStandardItem(mod_name + f" ({mod_version})") + item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled) + item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) + item.setData(mod_name, Qt.ItemDataRole.UserRole) + + item_model.appendRow(item) + item.setCheckState(Qt.CheckState.Checked) + + item_model.itemChanged.connect(self.on_installed_mod_item_changed) + self.ui.InstalledModsListView.setModel(item_model) + + # UI Callback functions + ## Actions + def on_action_add_new_mod(self): + self.__logger.debug("Action: \"add new mod\" triggered!") + dialog = QtWidgets.QFileDialog(self, "Select Lethal Company mod") + dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles) + + result = dialog.getOpenFileName(filter='ZIP (*.zip)') + self.__logger.debug(f"user selected \"{result[0]}\"") + if not os.path.isfile(result[0]): + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Not a file") + dialog.setInformativeText(f"The given file \"{result}\" did not look like a file!") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) + dialog.exec() + if not self.parent.is_valid_mod_file(result[0]): + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Not a valid mod file") + dialog.setInformativeText( + f"The given file \"{result}\" did not look like a mod file. Is the manifest.json present?") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) + dialog.exec() + + self.parent.add_mod_file(result[0]) + + def on_action_refresh_mods(self): + self.__logger.debug("Action: \"refresh mods\" triggered!") + self.parent.index_stored_mods() + self.parent.index_installed_mods() + + def on_action_check_for_updates(self): + self.__logger.debug("Action: \"check for updates\" triggered!") + + def on_action_set_game_folder(self): + self.__logger.debug("Action: \"set game folder\" triggered!") + dialog = QtWidgets.QFileDialog(self, "Select Lethal Company folder") + dialog.setFileMode(QtWidgets.QFileDialog.FileMode.Directory) + dialog.setOptions(QtWidgets.QFileDialog.Option.ShowDirsOnly) + + result = dialog.getExistingDirectory() + dir_accepted = self.parent.set_game_folder(result) + + while not dir_accepted: + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Invalid game path") + dialog.setInformativeText(f"The given path \"{result}\" did not look like the Lethal Company game folder!\n" + f"If you can't find it. Try using steam and select \"browse local files\"!") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) + dialog.exec() + + dialog = QtWidgets.QFileDialog(self, "Select Lethal Company folder") + dialog.setFileMode(QtWidgets.QFileDialog.FileMode.Directory) + dialog.setOptions(QtWidgets.QFileDialog.Option.ShowDirsOnly) + + result = dialog.getExistingDirectory() + dir_accepted = self.parent.set_game_folder(result) + + def on_action_remove_all_files(self): + self.__logger.debug("Action: \"remove all manager files\" triggered!") + self.parent.nuke_manager_files() + + ## Buttons + + def on_pressed_delete_mod_files_button(self): + self.__logger.debug("Pressed button: \"Delete Mod Files\"") + raise NotImplementedError + + def on_pressed_apply_changes_button(self): + self.__logger.debug("Pressed button: \"Apply Changes\"") + raise NotImplementedError + + def on_pressed_discard_changes_button(self): + self.__logger.debug("Pressed button: \"Discard Changes\"") + raise NotImplementedError + + def on_available_mod_item_changed(self, item: QStandardItem): + self.__logger.debug(f"Available Mod list item \"{item.text()}\" changed to {item.checkState()}") + mod_name = item.data(Qt.ItemDataRole.UserRole) + self.parent.install_mod(mod_name) + + def on_installed_mod_item_changed(self, item: QStandardItem): + self.__logger.debug(f"Installed Mod list item \"{item.text()}\" changed to {item.checkState()}") + mod_name = item.data(Qt.ItemDataRole.UserRole) + if item.checkState() == Qt.CheckState.Unchecked: # mod should be uninstalled + accepted = QtWidgets.QMessageBox.question(self, "Really uninstall mod?", + f"Do you really want to uninstall the mod \"{mod_name}\"?\n" + "This could lead to permanent data loss if it wasn't tracked!") + if accepted: + self.parent.uninstall_mod(mod_name) + else: + return diff --git a/window/mod_manager_window_export.py b/window/mod_manager_window_export.py new file mode 100644 index 0000000..02d1857 --- /dev/null +++ b/window/mod_manager_window_export.py @@ -0,0 +1,141 @@ +# Form implementation generated from reading ui file 'mod_manager_window.ui' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(800, 594) + self.centralwidget = QtWidgets.QWidget(parent=MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.ModListLayout = QtWidgets.QVBoxLayout() + self.ModListLayout.setObjectName("ModListLayout") + self.InstalledModLayout = QtWidgets.QVBoxLayout() + self.InstalledModLayout.setObjectName("InstalledModLayout") + self.InstalledModsLabel = QtWidgets.QLabel(parent=self.centralwidget) + font = QtGui.QFont() + font.setPointSize(14) + font.setBold(True) + font.setUnderline(False) + font.setWeight(75) + font.setKerning(True) + self.InstalledModsLabel.setFont(font) + self.InstalledModsLabel.setObjectName("InstalledModsLabel") + self.InstalledModLayout.addWidget(self.InstalledModsLabel) + self.InstalledModsListView = QtWidgets.QListView(parent=self.centralwidget) + self.InstalledModsListView.setObjectName("InstalledModsListView") + self.InstalledModLayout.addWidget(self.InstalledModsListView) + self.ModListButtonsLayout = QtWidgets.QHBoxLayout() + self.ModListButtonsLayout.setObjectName("ModListButtonsLayout") + self.ApplyChangesButton = QtWidgets.QPushButton(parent=self.centralwidget) + self.ApplyChangesButton.setObjectName("ApplyChangesButton") + self.ModListButtonsLayout.addWidget(self.ApplyChangesButton) + self.DisacrdChangesButton = QtWidgets.QPushButton(parent=self.centralwidget) + self.DisacrdChangesButton.setObjectName("DisacrdChangesButton") + self.ModListButtonsLayout.addWidget(self.DisacrdChangesButton) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.ModListButtonsLayout.addItem(spacerItem) + self.InstalledModLayout.addLayout(self.ModListButtonsLayout) + self.ModListLayout.addLayout(self.InstalledModLayout) + self.horizontalLayout.addLayout(self.ModListLayout) + self.ActionListLayout = QtWidgets.QVBoxLayout() + self.ActionListLayout.setObjectName("ActionListLayout") + self.AvailableModsLabel = QtWidgets.QLabel(parent=self.centralwidget) + font = QtGui.QFont() + font.setPointSize(14) + font.setBold(True) + font.setUnderline(False) + font.setWeight(75) + font.setKerning(True) + self.AvailableModsLabel.setFont(font) + self.AvailableModsLabel.setObjectName("AvailableModsLabel") + self.ActionListLayout.addWidget(self.AvailableModsLabel) + self.AvailableModsList = QtWidgets.QListView(parent=self.centralwidget) + self.AvailableModsList.setObjectName("AvailableModsList") + self.ActionListLayout.addWidget(self.AvailableModsList) + self.AvailableModsButtonLayout = QtWidgets.QHBoxLayout() + self.AvailableModsButtonLayout.setObjectName("AvailableModsButtonLayout") + self.DeleteModFilesButton = QtWidgets.QPushButton(parent=self.centralwidget) + self.DeleteModFilesButton.setObjectName("DeleteModFilesButton") + self.AvailableModsButtonLayout.addWidget(self.DeleteModFilesButton) + self.TODOButton = QtWidgets.QPushButton(parent=self.centralwidget) + self.TODOButton.setObjectName("TODOButton") + self.AvailableModsButtonLayout.addWidget(self.TODOButton) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.AvailableModsButtonLayout.addItem(spacerItem1) + self.ActionListLayout.addLayout(self.AvailableModsButtonLayout) + self.horizontalLayout.addLayout(self.ActionListLayout) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(parent=MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 30)) + self.menubar.setObjectName("menubar") + self.menuSettings = QtWidgets.QMenu(parent=self.menubar) + self.menuSettings.setObjectName("menuSettings") + self.menuAdd = QtWidgets.QMenu(parent=self.menubar) + self.menuAdd.setObjectName("menuAdd") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + self.actionSet_game_folder = QtGui.QAction(parent=MainWindow) + self.actionSet_game_folder.setObjectName("actionSet_game_folder") + self.actionrefresh_detected_mods = QtGui.QAction(parent=MainWindow) + self.actionrefresh_detected_mods.setObjectName("actionrefresh_detected_mods") + self.actionAdd_new_Mod = QtGui.QAction(parent=MainWindow) + self.actionAdd_new_Mod.setObjectName("actionAdd_new_Mod") + self.actionAdd_new_mod = QtGui.QAction(parent=MainWindow) + self.actionAdd_new_mod.setObjectName("actionAdd_new_mod") + self.actionrefresh_mods = QtGui.QAction(parent=MainWindow) + self.actionrefresh_mods.setObjectName("actionrefresh_mods") + self.actionRemove_ALL_manager_files = QtGui.QAction(parent=MainWindow) + self.actionRemove_ALL_manager_files.setObjectName("actionRemove_ALL_manager_files") + self.actionCheck_for_Updates = QtGui.QAction(parent=MainWindow) + self.actionCheck_for_Updates.setObjectName("actionCheck_for_Updates") + self.menuSettings.addAction(self.actionSet_game_folder) + self.menuSettings.addAction(self.actionRemove_ALL_manager_files) + self.menuAdd.addAction(self.actionAdd_new_mod) + self.menuAdd.addAction(self.actionrefresh_mods) + self.menuAdd.addAction(self.actionCheck_for_Updates) + self.menubar.addAction(self.menuAdd.menuAction()) + self.menubar.addAction(self.menuSettings.menuAction()) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.InstalledModsLabel.setText(_translate("MainWindow", "Installed Mods:")) + self.ApplyChangesButton.setText(_translate("MainWindow", "Apply Changes")) + self.DisacrdChangesButton.setText(_translate("MainWindow", "Discard Changes")) + self.AvailableModsLabel.setText(_translate("MainWindow", "Available Mods:")) + self.DeleteModFilesButton.setText(_translate("MainWindow", "Delete Mod files")) + self.TODOButton.setText(_translate("MainWindow", "TODO")) + self.menuSettings.setTitle(_translate("MainWindow", "Settings")) + self.menuAdd.setTitle(_translate("MainWindow", "Add")) + self.actionSet_game_folder.setText(_translate("MainWindow", "Set game folder")) + self.actionrefresh_detected_mods.setText(_translate("MainWindow", "Refresh Mods")) + self.actionAdd_new_Mod.setText(_translate("MainWindow", "Add new Mod")) + self.actionAdd_new_mod.setText(_translate("MainWindow", "Add new mod")) + self.actionrefresh_mods.setText(_translate("MainWindow", "Refresh mods")) + self.actionRemove_ALL_manager_files.setText(_translate("MainWindow", "Remove ALL manager files")) + self.actionCheck_for_Updates.setText(_translate("MainWindow", "Check for Updates")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + sys.exit(app.exec()) diff --git a/window/pyqt_files/convert_ui_files.sh b/window/pyqt_files/convert_ui_files.sh new file mode 100755 index 0000000..b6ea1e7 --- /dev/null +++ b/window/pyqt_files/convert_ui_files.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +clear && python -m PyQt6.uic.pyuic mod_manager_window.ui -o ../mod_manager_window_export.py -x diff --git a/window/pyqt_files/mod_manager_window.ui b/window/pyqt_files/mod_manager_window.ui new file mode 100644 index 0000000..55dfd4a --- /dev/null +++ b/window/pyqt_files/mod_manager_window.ui @@ -0,0 +1,198 @@ + + + MainWindow + + + + 0 + 0 + 800 + 594 + + + + MainWindow + + + + + + + + + + + + 14 + 75 + true + false + true + + + + Installed Mods: + + + + + + + + + + + + Apply Changes + + + + + + + Discard Changes + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + 14 + 75 + true + false + true + + + + Available Mods: + + + + + + + + + + + + Delete Mod files + + + + + + + TODO + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + 0 + 0 + 800 + 30 + + + + + Settings + + + + + + + Add + + + + + + + + + + + + Set game folder + + + + + Refresh Mods + + + + + Add new Mod + + + + + Add new mod + + + + + Refresh mods + + + + + Remove ALL manager files + + + + + Check for Updates + + + + + +