Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5f4b66f
initial Pyside rework
AtiDev17 Feb 2, 2025
1a16c30
fix progressbar
AtiDev17 Feb 2, 2025
5e6d461
fix thread crashing and UI blocking when installing assets
AtiDev17 Feb 2, 2025
c49a709
fix checkboxes
AtiDev17 Feb 2, 2025
26117df
remove debug prints
AtiDev17 Feb 2, 2025
5fe758e
break up GUI and remove patches.py
AtiDev17 Feb 2, 2025
f245f07
fix messagebox freezing the GUI
AtiDev17 Feb 2, 2025
73d129d
fix install signal bug
AtiDev17 Feb 2, 2025
3560698
improve existing asset detection
AtiDev17 Feb 2, 2025
cd4e2ec
format code
AtiDev17 Feb 2, 2025
5638486
fix delete archive checkbox
AtiDev17 Feb 2, 2025
bd3f4b7
patch a patoolib function to create the no window flag
AtiDev17 Feb 2, 2025
22fcada
update version
AtiDev17 Feb 2, 2025
421be9a
improve build workflow
AtiDev17 Feb 2, 2025
43bf3ac
Update build.yml
AtiDev17 Feb 2, 2025
369b934
Update build.yml
AtiDev17 Feb 2, 2025
41de317
Update build.yml
AtiDev17 Feb 2, 2025
0357cfd
Update build.yml
AtiDev17 Feb 2, 2025
2cbf55d
increase required python version to 3.12
AtiDev17 Feb 2, 2025
eeb7965
fix window scaling
AtiDev17 Feb 2, 2025
5a7c1ed
log and ignore request exceptions to github
AtiDev17 Feb 2, 2025
f7eabc4
keep the last three logs instead of two
AtiDev17 Feb 3, 2025
2892f80
remove unused imports in main
AtiDev17 Feb 4, 2025
219ece2
add icons and update workflow
AtiDev17 Feb 4, 2025
e6ad19f
typo
AtiDev17 Feb 4, 2025
2bba06c
change one image to ico
AtiDev17 Feb 4, 2025
933e2fc
forgot to push images
AtiDev17 Feb 4, 2025
a155bc8
fix path to gui image
AtiDev17 Feb 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 24 additions & 26 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,48 @@ name: Build

on:
push:
branches:
- "*"
branches: ["*"]
pull_request:
branches:
- main
branches: [main]

jobs:
build:
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@v4.2.2
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v5.3.0
- name: Set up Python with caching
uses: actions/setup-python@v5.4.0
with:
python-version: '>=3.10'
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller -r requirements.txt

- name: Build with PyInstaller
run: >
pyinstaller --noconfirm --clean --onefile
--add-binary "7z\7z.exe:7z"
--add-data="icons\gui_icon.ico:icons"
--icon="icons\app_icon.ico"
--name DazContentInstaller
main.pyw

- name: Prepare distribution package
shell: pwsh
run: |
pyinstaller --add-binary "7z/7z.exe;7z" --onefile main.pyw --name DazContentInstaller

- name: Create package directory
run: mkdir -p dist/DazContentInstaller

- name: Create logger directory
run: mkdir -p dist/DazContentInstaller/logs

- name: Copy config.ini file
run: cp config.ini dist/DazContentInstaller/

- name: Move the built binary
run: mv dist/DazContentInstaller.exe dist/DazContentInstaller/

$targetDir = "dist/DazContentInstaller"
New-Item -Path $targetDir -ItemType Directory -Force
Copy-Item config.ini -Destination $targetDir/
Move-Item dist/DazContentInstaller.exe -Destination $targetDir/

- name: Upload Artifacts
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.3.3
with:
name: DazContentInstaller
path: dist/
path: dist
132 changes: 132 additions & 0 deletions GUI/asset_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from PySide6.QtWidgets import (
QFrame,
QCheckBox,
QHBoxLayout,
QProgressBar,
QLabel,
QPushButton,
QMessageBox,
QStyle,
)
from PySide6.QtCore import QThread, Signal

from GUI.gui_utilities import truncate_string
from GUI.worker import Worker
from GUI.shared_data import install_asset_list, remove_asset_list
from helper import file_operations
from content_database import delete_archive
from installer import start_installer_gui


class AssetWidget(QFrame):
"""Custom widget to represent an asset in the UI."""

installation_finished = Signal(int)
warning_signal = Signal(str, str)

def __init__(
self, parent, tab_name: str, asset_name: str = "", file_path: str = ""
):
super().__init__(parent)
self.asset_name = asset_name
self.file_path = file_path
self.file_size = file_operations.get_file_size(self.file_path)
self.tab_name = tab_name
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setLineWidth(1)
self.setStyleSheet("AssetWidget { border: 1px solid #4A90E2; }")

layout = QHBoxLayout(self)
self.checkbox = QCheckBox(truncate_string(self.asset_name))
self.checkbox.setToolTip(self.asset_name)
layout.addWidget(self.checkbox)

if tab_name == "Install":
self._create_install_widgets(layout)
else:
self._create_uninstall_widgets(layout)

self.warning_signal.connect(self.show_warning_message)

def show_warning_message(self, title, message):
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle(title)
warning_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_MessageBoxWarning
)
msg_box.setWindowIcon(warning_icon)
msg_box.setText(message)
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
msg_box.setModal(True) # Block interaction with parent widget
msg_box.finished.connect(
self.installation_finished.emit
) # Cleanup after dismissal
msg_box.show()

def _create_install_widgets(self, layout):
self.progressbar = QProgressBar()
self.progressbar.setValue(0)
layout.addWidget(self.progressbar)

self.label = QLabel(self.file_size)
layout.addWidget(self.label)

self.button = QPushButton("Install")
self.button.clicked.connect(self.install_asset)
layout.addWidget(self.button)

def _create_uninstall_widgets(self, layout):
self.button = QPushButton("Remove")
self.button.clicked.connect(self.remove_asset)
layout.addWidget(self.button)

def remove_from_view(self):
"""Removes the asset widget from the UI."""
if self in install_asset_list:
install_asset_list.remove(self)
self.setParent(None)
self.deleteLater()

def install_asset(self):
self.button.setEnabled(False)
self.thread = QThread()
self.worker = Worker(self._perform_installation)
self.worker.progress.connect(self.progressbar.setValue)
self.worker.moveToThread(self.thread)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.started.connect(self.worker.run)
self.installation_finished.connect(
lambda status: self.remove_from_view()
) # Single connection
self.thread.start()

def _perform_installation(self, progress_callback):
try:
imported, exists = start_installer_gui(
self.file_path,
progress_callback=progress_callback,
is_delete_archive=self.window().tab_view.is_delete_archive,
)
if exists:
self.warning_signal.emit(
"Asset Exists",
f"'{self.asset_name}' is already installed!",
)
elif not imported:
self.warning_signal.emit(
"Install Failed",
f"Failed to install '{self.asset_name}'. Check logs.",
)
else:
self.installation_finished.emit(0)
except Exception as e:
self.warning_signal.emit("Error", f"Installation failed: {str(e)}")

def remove_asset(self):
"""Removes the asset from the database and the uninstall list."""
delete_archive(self.asset_name)
if self in remove_asset_list:
remove_asset_list.remove(self)
self.remove_from_view()
23 changes: 23 additions & 0 deletions GUI/gui_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from PySide6.QtWidgets import QWidget, QApplication


def center_window(window: QWidget, width: int, height: int) -> None:
"""Centers and resizes the window to the center of the screen, adjusted for OS scaling."""
screen = QApplication.primaryScreen()
# Get the OS scaling factor
scaling_factor = screen.devicePixelRatio()
# Scale the width and height
scaled_width = int(width / scaling_factor)
scaled_height = int(height / scaling_factor)
# Resize the window with scaled dimensions
window.resize(scaled_width, scaled_height)
# Center the window
available_geometry = screen.availableGeometry()
frame = window.frameGeometry()
frame.moveCenter(available_geometry.center())
window.move(frame.topLeft())


def truncate_string(text: str, max_length: int = 50) -> str:
"""Truncates strings with ellipsis"""
return text[: max_length - 3] + "..." if len(text) > max_length else text
133 changes: 133 additions & 0 deletions GUI/install_tab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from pathlib import Path

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QCheckBox,
QScrollArea,
QHBoxLayout,
QPushButton,
QFileDialog,
QMessageBox,
)
from GUI.asset_widget import AssetWidget
from helper import file_operations
from helper.file_operations import is_file_archive
from GUI.shared_data import install_asset_list


class InstallTab(QWidget):
"""Custom widget for the Install tab with drag-and-drop support."""

def __init__(self, parent):
super().__init__(parent)
self.setup_ui()
self.setAcceptDrops(True) # Enable drops for this widget
self.is_delete_archive = False

def setup_ui(self):
layout = QVBoxLayout(self)

# Check All checkbox
self.check_install = QCheckBox("Check all")
self.check_install.stateChanged.connect(self.toggle_install_checkboxes)
layout.addWidget(self.check_install)

# Scroll area
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
self.scroll_content = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_content)
self.scroll_layout.addStretch()
scroll_area.setWidget(self.scroll_content)
layout.addWidget(scroll_area)

# Bottom buttons
bottom_frame = QWidget()
bottom_layout = QHBoxLayout(bottom_frame)

self.del_archive_checkbox = QCheckBox("Delete Archive after Installation")
self.del_archive_checkbox.stateChanged.connect(
lambda state: setattr(
self.parent().parent(),
"is_delete_archive",
state == Qt.CheckState.Checked.value,
)
)
bottom_layout.addWidget(self.del_archive_checkbox)

self.remove_button = QPushButton("Remove selected")
self.remove_button.clicked.connect(self.remove_selected)
bottom_layout.addWidget(self.remove_button)

self.add_asset_button = QPushButton("Add Asset")
self.add_asset_button.clicked.connect(self.select_file)
bottom_layout.addWidget(self.add_asset_button)

self.install_button = QPushButton("Install selected")
self.install_button.clicked.connect(self.install_assets)
bottom_layout.addWidget(self.install_button)

layout.addWidget(bottom_frame)

@staticmethod
def toggle_install_checkboxes(state):
checked = state == 2 # 2 corresponds to Qt.Checked
for asset in install_asset_list:
asset.checkbox.setChecked(checked)

def add_asset_widget(self, asset_name: str, asset_path: str):
"""Adds a new asset widget to the install scroll area."""
asset = AssetWidget(self.scroll_content, "Install", asset_name, asset_path)
self.scroll_layout.insertWidget(self.scroll_layout.count() - 1, asset)
install_asset_list.append(asset)

def select_file(self):
"""Prompts user to select a file and adds an asset widget."""
file_path, _ = QFileDialog.getOpenFileName(self, "Select Asset File")
if file_path:
file_name = Path(file_path).name
if is_file_archive(file_name):
asset_name = file_operations.get_file_name_without_extension(file_name)
self.add_asset_widget(asset_name, file_path)
else:
QMessageBox.information(self, "Info", "The File is not an archive")

@staticmethod
def remove_selected():
for asset in install_asset_list.copy():
if asset.checkbox.isChecked():
asset.remove_from_view()

def install_assets(self):
msg = QMessageBox.question(
self,
"Install?",
"Do you want to install the selected assets?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if msg == QMessageBox.StandardButton.Yes:
self.install_button.setEnabled(False)
for asset in install_asset_list.copy():
if asset.checkbox.isChecked():
asset.install_asset()
self.install_button.setEnabled(True)
self.check_install.setChecked(False)

def dragEnterEvent(self, event):
"""Accept drag events if the dragged content contains files."""
if event.mimeData().hasUrls():
event.acceptProposedAction()

def dropEvent(self, event):
"""Handle dropped files."""
files = [url.toLocalFile() for url in event.mimeData().urls()]
for file_path in files:
if is_file_archive(file_path):
file_name = Path(file_path).name
asset_name = file_operations.get_file_name_without_extension(file_name)
self.add_asset_widget(asset_name, file_path)
else:
QMessageBox.information(self, "Info", "The File is not an archive")
event.acceptProposedAction()
Loading