Version 0.1
Created a small PyQt6-based window to manage Lethal Company mods by storing their zip files and extracting them correctly into the BepInEx folder and remove them again. Creates a small config file which is currently set nearby the main.py as a yamldev
parent
7023135c33
commit
4705da0157
@ -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)
|
@ -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'))
|
@ -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()
|
@ -0,0 +1,2 @@
|
||||
PyQt6>=6.6.1
|
||||
pyyaml>=6.0.1
|
@ -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
|
@ -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())
|
@ -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
|
@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>594</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="ModListLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="InstalledModLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="InstalledModsLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>14</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
<underline>false</underline>
|
||||
<kerning>true</kerning>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Installed Mods:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="InstalledModsListView"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="ModListButtonsLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="ApplyChangesButton">
|
||||
<property name="text">
|
||||
<string>Apply Changes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="DisacrdChangesButton">
|
||||
<property name="text">
|
||||
<string>Discard Changes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="ActionListLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="AvailableModsLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>14</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
<underline>false</underline>
|
||||
<kerning>true</kerning>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Available Mods:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListView" name="AvailableModsList"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="AvailableModsButtonLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="DeleteModFilesButton">
|
||||
<property name="text">
|
||||
<string>Delete Mod files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="TODOButton">
|
||||
<property name="text">
|
||||
<string>TODO</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>30</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuSettings">
|
||||
<property name="title">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<addaction name="actionSet_game_folder"/>
|
||||
<addaction name="actionRemove_ALL_manager_files"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAdd">
|
||||
<property name="title">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
<addaction name="actionAdd_new_mod"/>
|
||||
<addaction name="actionrefresh_mods"/>
|
||||
<addaction name="actionCheck_for_Updates"/>
|
||||
</widget>
|
||||
<addaction name="menuAdd"/>
|
||||
<addaction name="menuSettings"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<action name="actionSet_game_folder">
|
||||
<property name="text">
|
||||
<string>Set game folder</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionrefresh_detected_mods">
|
||||
<property name="text">
|
||||
<string>Refresh Mods</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAdd_new_Mod">
|
||||
<property name="text">
|
||||
<string>Add new Mod</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAdd_new_mod">
|
||||
<property name="text">
|
||||
<string>Add new mod</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionrefresh_mods">
|
||||
<property name="text">
|
||||
<string>Refresh mods</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionRemove_ALL_manager_files">
|
||||
<property name="text">
|
||||
<string>Remove ALL manager files</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCheck_for_Updates">
|
||||
<property name="text">
|
||||
<string>Check for Updates</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
Loading…
Reference in New Issue