collections-app/MainGui.py
2025-09-06 19:07:26 -05:00

552 lines
22 KiB
Python

from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QMainWindow, QWidget, QToolBar, QAction,
QSizePolicy, QToolButton, QTreeWidget,
QListWidget, QListWidgetItem, QTreeWidgetItem, QGroupBox, QLineEdit,
QLabel, QTextEdit, QPushButton, QFileDialog, QSlider, QHBoxLayout, QVBoxLayout, QFrame, QGridLayout, QComboBox, QFontDialog, QTextBrowser)
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtGui import QIcon, QFont
from qtwidgets import Toggle
from striprtf.striprtf import rtf_to_text
from cloud_backend import LoginDialog, login, upload_file
import tempfile
import export_process, import_process, upload_process
import settings, pathlib, os, sys, icons, easySQL, tables, json, zipfile
global status_bar
global tool_bar
class ModificationItem(QTreeWidgetItem):
def __init__(self):
super().__init__()
self.id = 0
class ColectionItem(QListWidgetItem):
def __init__(self):
super().__init__()
self.id = 0
class MainGui(QMainWindow):
def __init__(self) -> None:
self.app_settings = settings.app_settings
super().__init__()
self.setWindowTitle(self.app_settings.get_setting('window_title'))
self.setMaximumHeight(720)
self.setMaximumWidth(1280)
self.resize(self.app_settings.get_setting('window_width'), self.app_settings.get_setting('window_height'))
self.setWindowIcon(QIcon(icons.window_icon))
self.widget = QWidget()
self.tool_bar = QToolBar()
self.addToolBar(self.tool_bar)
self.tool_bar.setMovable(False)
self.tool_bar.setFixedHeight(35)
self.exit_app = QAction("Save and exit", self)
self.exit_app.setIcon(QIcon(icons.exit_icon))
self.exit_app.triggered.connect(self.exit_application)
self.tool_bar.addAction(self.exit_app)
self.tool_bar.addSeparator()
# need an import button that imports a selected pcmp from a file dialog
self.import_tool = QAction("Import PCMP File", self)
self.import_tool.setToolTip("Imports a PCMP file from system into your Penumbra installation.")
self.import_tool.setIcon(QIcon(icons.import_icon))
self.import_tool.triggered.connect(self.import_collection)
self.tool_bar.addAction(self.import_tool)
# need an export button that exports a selected collection in the list
self.export_tool = QAction("Export Collection to Archive", self)
self.export_tool.setToolTip("Export collection from Penumbra installation into a share-able PCMP archive")
self.export_tool.setIcon(QIcon(icons.export_icon))
self.export_tool.setEnabled(False)
self.export_tool.triggered.connect(self.export_collection)
self.tool_bar.addAction(self.export_tool)
# need an upload dummy button for future features
self.upload_tool = QAction("Upload Collection to the set up server.", self)
self.upload_tool.setToolTip("If a server has been configured alongside a profile on that server, you may upload the collection straight there for others to download without needing to share it.")
self.upload_tool.setIcon(QIcon(icons.upload_icon))
self.upload_tool.setEnabled(False)
self.upload_tool.triggered.connect(self.upload_collection)
self.tool_bar.addAction(self.upload_tool)
self.refresh_tool = QAction("Refresh", self)
self.refresh_tool.setToolTip("Refreshes the loaded data and selected collection data!")
self.refresh_tool.setIcon(QIcon(icons.refresh))
self.refresh_tool.setEnabled(True)
self.tool_bar.addAction(self.refresh_tool)
#add a spacer
self.spacer = QWidget()
self.spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.tool_bar.addWidget(self.spacer)
self.mode_label = QLabel()
self.mode_label.setText('Mode')
self.tool_bar.addWidget(self.mode_label)
self.mode_slider = Toggle()
self.mode_slider.setToolTip('Sets the mode between basic or advanced details allowing for more manipulation of imports and exports. Only use this if you know what you are doing!')
self.mode_slider.toggled.connect(self.mode_change)
self.tool_bar.addWidget(self.mode_slider)
self.settings_button = QAction("Settings", self)
self.settings_button.setIcon(QIcon(icons.configure))
self.tool_bar.addAction(self.settings_button)
self.settings_button.triggered.connect(self.change_font_size)
self.help_button = QAction("Help", self)
self.help_button.setIcon(QIcon(icons.help_icon))
self.tool_bar.addAction(self.help_button)
# need a database connection indicator
self.database_indicator = QAction("Database Status", self)
self.database_indicator.setIcon(QIcon(icons.database_bad))
self.tool_bar.addAction(self.database_indicator)
# need a sign-in/profile placard that acts as a button to change login info
self.profile_indicator = QToolButton()
self.profile_indicator.setText("Profile/Login")
self.profile_indicator.setToolTip("If a server is configured use this to attempt so sign in to a profile on that server. This profile will be used for when uploading a collection.")
self.profile_indicator.setIcon(QIcon(icons.profile))
self.profile_indicator.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
self.profile_indicator.clicked.connect(self.login_user)
self.tool_bar.addWidget(self.profile_indicator)
# main vertical layout object
self.horizontal_layout = QHBoxLayout(self)
# collection layout V
self.collection_vertical_layout = QVBoxLayout()
# self.inner_canvas.setStyleSheet("background-color: blue")
self.collection_list = QListWidget()
self.col_frame = QFrame()
self.col_frame.setFrameStyle(QFrame.Panel)
self.collection_list.setFixedWidth(200)
self.collection_list.setFixedHeight(300)
self.collection_vertical_layout.addWidget(self.collection_list)
self.collection_vertical_layout.addWidget(self.col_frame)
# Close out collection Layout V
self.details_vertical_layout = QVBoxLayout()
self.details_group = QGroupBox()
self.details_group.setTitle('Collection Details')
self.details_horizontal_layout = QHBoxLayout()
self.horizontal_form_layout = QGridLayout()
# ---- Form Control Start ---->
self.name_label = QLabel()
self.name_label.setText("Collection Name")
self.name_edit = QLineEdit()
self.name_edit.setMaximumWidth(200)
self.version_label = QLabel()
self.version_label.setText("Version")
self.version_edit = QLineEdit()
self.version_edit.setMaximumWidth(200)
self.comment_label = QLabel()
self.comment_label.setText("Comments")
self.comment_edit = QTextEdit()
self.comment_edit.setMaximumHeight(150)
self.comment_edit.setMaximumHeight(250)
self.horizontal_form_layout.addWidget(self.name_label, 0, 0)
self.horizontal_form_layout.addWidget(self.name_edit, 0, 1)
self.horizontal_form_layout.addWidget(self.version_label, 1, 0)
self.horizontal_form_layout.addWidget(self.version_edit, 1, 1)
self.horizontal_form_layout.addWidget(self.comment_label, 2, 0)
self.horizontal_form_layout.addWidget(self.comment_edit, 2, 1)
# ---- Form Control End ---->
self.details_group.setLayout(self.horizontal_form_layout)
self.details_group.setFixedHeight(300)
self.details_group.setMaximumWidth(500)
self.character_links = QGroupBox()
self.character_links.setTitle("Character Links")
self.character_links.setFixedHeight(300)
self.character_links.setFixedWidth(200)
self.character_grid_layout = QGridLayout()
self.links_list = QListWidget()
self.character_grid_layout.addWidget(self.links_list)
self.character_links.setLayout(self.character_grid_layout)
self.search_horizontal_layout = QHBoxLayout()
self.clear_filter_mods = QPushButton()
self.clear_filter_mods.setIcon(QIcon(icons.clear_filter))
self.clear_filter_mods.setMaximumWidth(30)
self.clear_filter_mods.setMaximumHeight(30)
self.mod_list_search = QLineEdit()
self.mod_list_search.textEdited.connect(self.filter_mods)
self.mod_list_search.setFixedHeight(30)
self.mod_list_search.setFixedWidth(200)
self.search_horizontal_layout.addWidget(self.clear_filter_mods)
self.search_horizontal_layout.addWidget(self.mod_list_search)
self.search_horizontal_layout.setSpacing(5)
self.search_horizontal_layout.setAlignment(Qt.AlignLeft)
self.mod_list = QTreeWidget()
self.details_horizontal_layout.addWidget(self.details_group)
self.details_horizontal_layout.addWidget(self.character_links)
self.details_vertical_layout.addLayout(self.details_horizontal_layout)
self.details_vertical_layout.addLayout(self.search_horizontal_layout)
self.details_vertical_layout.addWidget(self.mod_list)
# ---- Details/Modlist Layout V ---- >
self.advanced_widget_canvas = QWidget()
self.advanced_widget_canvas.setLayout(self.details_vertical_layout)
self.horizontal_layout.addLayout(self.collection_vertical_layout)
self.basic_vertical_layout = QVBoxLayout()
self.basic_horizontal_layout = QHBoxLayout()
self.basic_vertical_layout.addLayout(self.basic_horizontal_layout)
self.basic_group = QGroupBox()
self.basic_group.setTitle("Basic Mode Instructions")
self.basic_horizontal_layout.addWidget(self.basic_group)
self.group_layout = QGridLayout()
self.basic_group.setLayout(self.group_layout)
self.label1 = QLabel()
#self.label1.setAcceptRichText(True)
self.label1.setText("hello World")
self.label1.setWordWrap(True)
self.label1.setMinimumWidth(400)
self.group_layout.addWidget(self.label1, 0, 0)
self.group_layout.setAlignment(Qt.AlignLeft)
self.group_layout.addWidget(self.name_edit, 1, 0)
self.basic_widget_canvas = QWidget()
self.basic_widget_canvas.setLayout(self.basic_vertical_layout)
# this is where the if split needs to happen
basic_mode = True
advanced_mode = False
self.horizontal_layout.addWidget(self.advanced_widget_canvas)
self.horizontal_layout.addWidget(self.basic_widget_canvas)
if advanced_mode:
self.basic_widget_canvas.setHidden(True)
self.advanced_widget_canvas.setHidden(False)
if basic_mode:
self.basic_widget_canvas.setHidden(False)
self.advanced_widget_canvas.setHidden(True)
self.widget.setLayout(self.horizontal_layout)
self.setCentralWidget(self.widget)
self.selected_item = None
self.selected_item_meta = {}
self.selected_item_id = 0
self.selected_item_data = {}
def change_font_size(self, a):
(font, ok) = QFontDialog.getFont(QFont("Helvetica [Cronyx]", 10), self)
if ok:
# the user clicked OK and font is set to the font the user selected
self.setFont(font)
def __post__init__(self):
self.collection_list.itemClicked.connect(self.populate_details)
if not settings.app_settings.get_setting("update_opt_in") or settings.app_settings.lock_client:
self.profile_indicator.setEnabled(False)
self.database_indicator.setEnabled(False)
self.repopulate_collections()
self.repopulate_modslist()
def exit_application(self):
self.app_settings.save_settings()
sys.exit()
def mode_change(self, a):
if a:
self.setWindowTitle(f"{self.app_settings.get_setting('window_title')} (Advanced Mode)")
self.app_settings.set_setting('advanced_mode', True)
self.basic_widget_canvas.setHidden(True)
self.advanced_widget_canvas.setHidden(False)
else:
self.setWindowTitle(f"{self.app_settings.get_setting('window_title')} (Basic Mode)")
self.app_settings.set_setting('advanced_mode', False)
self.advanced_widget_canvas.setHidden(True)
self.basic_widget_canvas.setHidden(False)
if not self.app_settings.get_setting('advanced_mode'):
self.version_edit.setDisabled(True)
self.comment_edit.setDisabled(True)
self.links_list.setDisabled(True)
self.mod_list_search.setDisabled(True)
self.clear_filter_mods.setDisabled(True)
else:
self.version_edit.setDisabled(False)
self.comment_edit.setDisabled(False)
self.links_list.setDisabled(False)
self.mod_list_search.setDisabled(False)
self.clear_filter_mods.setDisabled(False)
self.repopulate_modslist()
def get_export_upload_payload(self) -> dict:
collection_data = easySQL.fetchone_from_table(tables.collections, filter=('collection_id', self.selected_item_id))
collection_settings = json.loads(collection_data.settings)
# The below process handles the repackaging payload for a collection allowing for advanced changes such
# as enabling or putting a modification on ignore.
# TODO: enabled_mods and mods_to_copy could be consolidated into a single dictionary
enabled_mods = {}
mods_to_copy = []
count = self.mod_list.topLevelItemCount()
for index in range(count):
item = self.mod_list.topLevelItem(index)
if item.checkState(0) == 2:
modification_row = easySQL.fetchone_from_table(tables.modifications, filter=('name', item.text(0)))
mods_to_copy.append((modification_row.name, modification_row.mod_path, modification_row.total_files))
try:
mod_settings = collection_settings[item.text(0)]
mod_settings['Enabled'] = True
enabled_mods[item.text(0)] = mod_settings
except:
pass
payload: {str, any} = {
'collection_json':{
'Version': collection_data.version,
'Id': collection_data.uuid,
'Name': self.name_edit.text(),
'Settings': enabled_mods,
'Inheritance': []
},
'character_links': json.loads(collection_data.character_links),
'meta_data': {
"collection_name": self.name_edit.text(),
"collection_uuid": collection_data.uuid,
"version": self.version_edit.text(),
"comments": self.comment_edit.toPlainText()
},
'mods_to_copy': mods_to_copy
}
return payload
def upload_collection(self):
payload = self.get_export_upload_payload()
progress_dialog = upload_process.UploadProgressDialog(self, payload)
progress_dialog.start_upload()
def export_collection(self):
payload = self.get_export_upload_payload()
dialog = QFileDialog(self)
dialog.setFileMode(QFileDialog.DirectoryOnly)
if dialog.exec_():
save_directory = dialog.selectedFiles()[0]
payload['save_directory'] = save_directory
# todo: remove this
with open('test.json', 'w+') as file:
json.dump(payload, file, indent=2)
progress_dialog = export_process.ExportProgressDialog(self, payload)
progress_dialog.start_exportation()
def import_collection(self):
dialog = QFileDialog(self)
dialog.setFileMode(QFileDialog.AnyFile)
dialog.setNameFilter("Archive (*.pcmp)")
if dialog.exec_():
path_to_zip = pathlib.Path(dialog.selectedFiles()[0])
print(path_to_zip) # do some validation on the path
with zipfile.ZipFile(path_to_zip) as collection_pcmp:
collection_uuid = ''
with collection_pcmp.open("meta.json") as _file:
meta = json.load(_file)
collection_uuid = meta["collection_uuid"]
with collection_pcmp.open(f"{collection_uuid}.json") as _file:
collection_data = json.load(_file)
if collection_uuid in [row.uuid for row in easySQL.fetchall_from_table(tables.collections)]:
warning_dialog = QMessageBox(self)
warning_dialog.setWindowTitle('Collection already exists...')
warning_dialog.setText(f"It appears that this collection already exists, would you like to overwrite it?")
warning_dialog.setIcon(QMessageBox.Question)
warning_dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
button = warning_dialog.exec()
if button == QMessageBox.No:
return
payload = {
'collection_json': collection_data,
'meta_data': meta,
'path_to_zip': path_to_zip,
'mods_to_copy': [key for key in collection_data['Settings'].keys()]
}
print(payload)
self.progress_dialog = import_process.ImportProgressDialog(self, payload)
self.progress_dialog.import_collection()
def login_user(self):
dialog = LoginDialog(self)
if dialog.exec_():
self.app_settings.user_data = dialog.user_data
self.app_settings.connected = True
self.profile_indicator.setIcon(QIcon(settings.app_settings.user_data['avatar']))
self.profile_indicator.setText(settings.app_settings.username)
self.database_indicator.setIcon(QIcon(icons.database_good))
def filter_mods(self):
filter = self.mod_list_search.text()
count = self.mod_list.topLevelItemCount()
for index in range(count):
item = self.mod_list.topLevelItem(index)
present_filter = [mod_name.strip() for mod_name in json.loads(self.selected_item_data.settings).keys()]
if filter in item.text(0) and item.text(0) in present_filter:
item.setHidden(False)
else:
item.setHidden(True)
def repopulate_modslist(self, enabled: list = [], present: list = []):
self.mod_list.clear()
self.mod_list.setHeaderLabel('Modification Name')
self.mod_list.setAlternatingRowColors(True)
list = easySQL.fetchall_from_table(tables.modifications)
for d, row in enumerate(list):
item = ModificationItem()
item.setText(0, row.name)
if row.name in enabled:
item.setCheckState(0, 2)
else:
item.setCheckState(0, 0)
item.id = row.mod_id
self.mod_list.addTopLevelItem(item)
if row.name in present or len(present)==0:
item.setHidden(False)
item.setDisabled(False)
else:
item.setHidden(True)
item.setDisabled(True)
if not self.app_settings.get_setting('advanced_mode'):
item.setDisabled(True)
def repopulate_collections(self):
self.collection_list.clear()
self.collection_list.setAlternatingRowColors(True)
list = [(row.collection_id, row.name) for row in easySQL.fetchall_from_table(tables.collections)]
for id, name in list:
item = ColectionItem()
item.setText(name)
item.id = id
item.setToolTip(f"{id}")
self.collection_list.addItem(item)
def repopulate_character_links(self):
self.links_list.clear()
links = json.loads(self.selected_item_data.character_links)
if links['Default']:
item = QListWidgetItem()
item.setText("Default")
item.setCheckState(2)
self.links_list.addItem(item)
if links["Yourself"]:
item = QListWidgetItem()
item.setText("Yourself")
item.setCheckState(2)
self.links_list.addItem(item)
if len(links["Individuals"]) > 0:
for link in links["Individuals"]:
item = QListWidgetItem()
item.setText(link["Display"])
item.setCheckState(2)
self.links_list.addItem(item)
def populate_details(self, item):
self.export_tool.setEnabled(True)
if self.app_settings.connected:
self.upload_tool.setEnabled(True)
else:
self.upload_tool.setEnabled(False)
self.selected_item_id = item.id
self.selected_item = item
self.selected_item_data = easySQL.fetchone_from_table(tables.collections, filter=("collection_id", item.id))
self.selected_item_meta = easySQL.fetchone_from_table(tables.metadata, filter=("collection_name", self.selected_item_data.name))
settings_filter = [mod_name.strip() for mod_name, settings in json.loads(self.selected_item_data.settings).items() if settings['Enabled']]
present_filter = [mod_name.strip() for mod_name in json.loads(self.selected_item_data.settings).keys()]
self.repopulate_modslist(enabled=settings_filter, present=present_filter)
self.repopulate_character_links()
self.name_edit.setText(self.selected_item_data.name)
self.version_edit.setText(self.selected_item_meta.version)
self.comment_edit.setPlainText(self.selected_item_meta.comments)
if not self.app_settings.get_setting('advanced_mode'):
self.version_edit.setDisabled(True)
self.comment_edit.setDisabled(True)
self.links_list.setDisabled(True)
self.mod_list_search.setDisabled(True)
self.clear_filter_mods.setDisabled(True)
self.mod_list.setDisabled(True)
else:
self.version_edit.setDisabled(False)
self.comment_edit.setDisabled(False)
self.links_list.setDisabled(False)
self.mod_list_search.setDisabled(False)
self.clear_filter_mods.setDisabled(False)
self.mod_list.setDisabled(False)