diff --git a/NUSGet.py b/NUSGet.py index c3f5208..a1ab2cf 100644 --- a/NUSGet.py +++ b/NUSGet.py @@ -36,7 +36,7 @@ from modules.download_batch import run_nus_download_batch from modules.download_wii import run_nus_download_wii from modules.download_dsi import run_nus_download_dsi -nusget_version = "1.4.0" +nusget_version = "1.4.1" regions = {"World": ["41"], "USA/NTSC": ["45"], "Europe/PAL": ["50"], "Japan": ["4A"], "Korea": ["4B"], "China": ["43"], "Australia/NZ": ["55"]} @@ -45,7 +45,7 @@ regions = {"World": ["41"], "USA/NTSC": ["45"], "Europe/PAL": ["50"], "Japan": [ # Signals needed for the worker used for threading the downloads. class WorkerSignals(QObject): result = Signal(object) - progress = Signal(str) + progress = Signal(int, int, str) # Worker class used to thread the downloads. @@ -81,6 +81,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.ui.download_btn.clicked.connect(self.download_btn_pressed) self.ui.script_btn.clicked.connect(self.script_btn_pressed) self.ui.custom_out_dir_btn.clicked.connect(self.choose_output_dir) + self.ui.progress_bar.setRange(0, 0) # About and About Qt Buttons self.ui.actionAbout.triggered.connect(self.about_nusget) self.ui.actionAbout_Qt.triggered.connect(lambda: QMessageBox.aboutQt(self)) @@ -230,6 +231,18 @@ class MainWindow(QMainWindow, Ui_MainWindow): return self.ui.patch_ios_checkbox.setEnabled(False) + def download_progress_update(self, done, total, log_text): + if done == 0 and total == 0: + self.ui.progress_bar.setRange(0, 0) + elif done == -1 and total == -1: + pass + else: + self.ui.progress_bar.setRange(0, total) + self.ui.progress_bar.setValue(done) + # Pass the text on to the log text updater, if it was provided. + if log_text: + self.update_log_text(log_text) + def update_log_text(self, new_text): # This method primarily exists to be the handler for the progress signal emitted by the worker thread. self.log_text += new_text + "\n" @@ -385,7 +398,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked(), self.ui.archive_file_entry.text()) worker.signals.result.connect(self.check_download_result) - worker.signals.progress.connect(self.update_log_text) + worker.signals.progress.connect(self.download_progress_update) self.threadpool.start(worker) def check_download_result(self, result): @@ -552,7 +565,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.ui.use_wiiu_nus_checkbox.isChecked(), self.ui.use_local_checkbox.isChecked(), self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked()) worker.signals.result.connect(self.check_batch_result) - worker.signals.progress.connect(self.update_log_text) + worker.signals.progress.connect(self.download_progress_update) self.threadpool.start(worker) def choose_output_dir(self): @@ -657,16 +670,10 @@ if __name__ == "__main__": # NUSGet look nice and pretty. app.setStyle("fusion") theme_sheet = "style_dark.qss" - try: - # Check for an environment variable overriding the theme. This is mostly for theme testing but would also allow - # you to force a theme. - if os.environ["THEME"].lower() == "light": - theme_sheet = "style_light.qss" - except KeyError: - if is_dark_theme(): - theme_sheet = "style_dark.qss" - else: - theme_sheet = "style_light.qss" + if is_dark_theme(): + theme_sheet = "style_dark.qss" + else: + theme_sheet = "style_light.qss" stylesheet = open(os.path.join(os.path.dirname(__file__), "resources", theme_sheet)).read() image_path_prefix = pathlib.Path(os.path.join(os.path.dirname(__file__), "resources")).resolve().as_posix() stylesheet = stylesheet.replace("{IMAGE_PREFIX}", image_path_prefix) diff --git a/modules/download_batch.py b/modules/download_batch.py index bc86208..e76ace4 100644 --- a/modules/download_batch.py +++ b/modules/download_batch.py @@ -53,5 +53,5 @@ def run_nus_download_batch(out_folder: pathlib.Path, titles: List[BatchTitleData # failed title. result = 1 failed_titles.append(title.tid) - progress_callback.emit(f"Batch download finished.") + progress_callback.emit(0, 1, f"Batch download finished.") return BatchResults(result, warning_titles, failed_titles) diff --git a/modules/download_dsi.py b/modules/download_dsi.py index 91054de..8c2bd66 100644 --- a/modules/download_dsi.py +++ b/modules/download_dsi.py @@ -29,10 +29,10 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_ title_dir.mkdir(exist_ok=True) # Announce the title being downloaded, and the version if applicable. if title_version is not None: - progress_callback.emit(f"Downloading title {tid} v{title_version}, please wait...") + progress_callback.emit(0, 0, f"Downloading title {tid} v{title_version}, please wait...") else: - progress_callback.emit(f"Downloading title {tid} vLatest, please wait...") - progress_callback.emit(" - Downloading and parsing TMD...") + progress_callback.emit(0, 0, f"Downloading title {tid} vLatest, please wait...") + progress_callback.emit(-1, -1, " - Downloading and parsing TMD...") # Download a specific TMD version if a version was specified, otherwise just download the latest TMD. try: if title_version is not None: @@ -50,17 +50,17 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_ version_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump()) # Use a local ticket, if one exists and "use local files" is enabled. if use_local_chkbox and version_dir.joinpath("tik").exists(): - progress_callback.emit(" - Parsing local copy of Ticket...") + progress_callback.emit(-1, -1, " - Parsing local copy of Ticket...") title.load_ticket(version_dir.joinpath("tik").read_bytes()) else: - progress_callback.emit(" - Downloading and parsing Ticket...") + progress_callback.emit(-1, -1, " - Downloading and parsing Ticket...") try: title.load_ticket(libTWLPy.download_ticket(tid)) version_dir.joinpath("tik").write_bytes(title.ticket.dump()) except ValueError: # If libTWLPy returns an error, then no ticket is available. Log this, and disable options requiring a # ticket so that they aren't attempted later. - progress_callback.emit(" - No Ticket is available!") + progress_callback.emit(-1, -1, " - No Ticket is available!") pack_tad_enabled = False decrypt_contents_enabled = False # Load the content record from the TMD, and download the content it lists. DSi titles only have one content. @@ -68,13 +68,13 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_ content_file_name = f"{title.tmd.content_record.content_id:08X}" # Check for a local copy of the current content if "use local files" is enabled, and use it. if use_local_chkbox and version_dir.joinpath(content_file_name).exists(): - progress_callback.emit(" - Using local copy of content") + progress_callback.emit(-1, -1, " - Using local copy of content") content = version_dir.joinpath(content_file_name).read_bytes() else: - progress_callback.emit(f" - Downloading content (Content ID: {title.tmd.content_record.content_id}, Size: " + progress_callback.emit(-1, -1, f" - Downloading content (Content ID: {title.tmd.content_record.content_id}, Size: " f"{title.tmd.content_record.content_size} bytes)...") content = libTWLPy.download_content(tid, title.tmd.content_record.content_id) - progress_callback.emit(" - Done!") + progress_callback.emit(-1, -1, " - Done!") # If keep encrypted contents is on, write out the content after its downloaded. if keep_enc_chkbox is True: version_dir.joinpath(content_file_name).write_bytes(content) @@ -82,7 +82,7 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_ # If decrypt local contents is still true, decrypt the content and write out the decrypted file. if decrypt_contents_enabled is True: try: - progress_callback.emit(f" - Decrypting content (Content ID: {title.tmd.content_record.content_id})...") + progress_callback.emit(-1, -1, f" - Decrypting content (Content ID: {title.tmd.content_record.content_id})...") dec_content = title.get_content() content_file_name = f"{title.tmd.content_record.content_id:08X}.app" version_dir.joinpath(content_file_name).write_bytes(dec_content) @@ -93,10 +93,10 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_ # If pack TAD is still true, pack the TMD, ticket, and content into a TAD. if pack_tad_enabled is True: # Get the TAD certificate chain, courtesy of libTWLPy. - progress_callback.emit(" - Building certificate...") + progress_callback.emit(-1, -1, " - Building certificate...") title.tad.set_cert_data(libTWLPy.download_cert()) # Use a typed TAD name if there is one, and auto generate one based on the TID and version if there isn't. - progress_callback.emit("Packing TAD...") + progress_callback.emit(-1, -1, "Packing TAD...") if tad_file_name != "" and tad_file_name is not None: # Batch downloads may insert -vLatest, so if it did we can fill in the real number now. tad_file_name = tad_file_name.replace("-vLatest", f"-v{title_version}") @@ -110,7 +110,7 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_ tad_file_name = tad_file_name.translate({ord(c): None for c in '/\\:*"?<>|'}) # Have libTWLPy dump the TAD, and write that data out. version_dir.joinpath(tad_file_name).write_bytes(title.dump_tad()) - progress_callback.emit("Download complete!") + progress_callback.emit(0, 1, "Download complete!") # This is where the variables come in. If the state of these variables doesn't match the user's choice by this # point, it means that they enabled decryption or TAD packing for a title that doesn't have a ticket. Return # code 1 so that a warning popup is shown informing them of this. diff --git a/modules/download_wii.py b/modules/download_wii.py index cf94ed5..e2d1be0 100644 --- a/modules/download_wii.py +++ b/modules/download_wii.py @@ -9,6 +9,8 @@ import libWiiPy def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool, decrypt_contents_chkbox: bool, wiiu_nus_chkbox: bool, use_local_chkbox: bool, repack_vwii_chkbox: bool, patch_ios: bool, wad_file_name: str, progress_callback=None): + def progress_update(done, total): + progress_callback.emit(done, total, None) # Actual NUS download function that runs in a separate thread. # Immediately knock out any invalidly formatted Title IDs. if len(tid) != 16: @@ -31,16 +33,16 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_ title_dir.mkdir(exist_ok=True) # Announce the title being downloaded, and the version if applicable. if title_version is not None: - progress_callback.emit(f"Downloading title {tid} v{title_version}, please wait...") + progress_callback.emit(0, 0, f"Downloading title {tid} v{title_version}, please wait...") else: - progress_callback.emit(f"Downloading title {tid} vLatest, please wait...") - progress_callback.emit(" - Downloading and parsing TMD...") + progress_callback.emit(-1, -1, f"Downloading title {tid} vLatest, please wait...") + progress_callback.emit(-1, -1, " - Downloading and parsing TMD...") # Download a specific TMD version if a version was specified, otherwise just download the latest TMD. try: if title_version is not None: - title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled)) + title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update)) else: - title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled)) + title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update)) title_version = title.tmd.title_version # If libWiiPy returns an error, that means that either the TID or version doesn't exist, so return code -2. except ValueError: @@ -52,17 +54,17 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_ version_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump()) # Use a local ticket, if one exists and "use local files" is enabled. if use_local_chkbox and version_dir.joinpath("tik").exists(): - progress_callback.emit(" - Parsing local copy of Ticket...") + progress_callback.emit(-1, -1, " - Parsing local copy of Ticket...") title.load_ticket(version_dir.joinpath("tik").read_bytes()) else: - progress_callback.emit(" - Downloading and parsing Ticket...") + progress_callback.emit(-1, -1, " - Downloading and parsing Ticket...") try: - title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled)) + title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update)) version_dir.joinpath("tik").write_bytes(title.ticket.dump()) except ValueError: # If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a # ticket so that they aren't attempted later. - progress_callback.emit(" - No Ticket is available!") + progress_callback.emit(0, 0, " - No Ticket is available!") pack_wad_enabled = False decrypt_contents_enabled = False # Load the content records from the TMD, and begin iterating over the records. @@ -73,15 +75,15 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_ content_file_name = f"{title.tmd.content_records[content].content_id:08X}" # Check for a local copy of the current content if "use local files" is enabled, and use it. if use_local_chkbox is True and version_dir.joinpath(content_file_name).exists(): - progress_callback.emit(f" - Using local copy of content {content + 1} of {len(title.tmd.content_records)}") + progress_callback.emit(-1, -1, f" - Using local copy of content {content + 1} of {len(title.tmd.content_records)}") content_list.append(version_dir.joinpath(content_file_name).read_bytes()) else: - progress_callback.emit(f" - Downloading content {content + 1} of {len(title.tmd.content_records)} " + progress_callback.emit(0, 0, f" - Downloading content {content + 1} of {len(title.tmd.content_records)} " f"(Content ID: {title.tmd.content_records[content].content_id}, Size: " f"{title.tmd.content_records[content].content_size} bytes)...") content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id, - wiiu_endpoint=wiiu_nus_enabled)) - progress_callback.emit(" - Done!") + wiiu_endpoint=wiiu_nus_enabled, progress=progress_update)) + progress_callback.emit(-1, -1, " - Done!") # If keep encrypted contents is on, write out each content after its downloaded. if keep_enc_chkbox is True: version_dir.joinpath(content_file_name).write_bytes(content_list[content]) @@ -90,7 +92,7 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_ if decrypt_contents_enabled is True: try: for content in range(len(title.tmd.content_records)): - progress_callback.emit(f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} " + progress_callback.emit(-1, -1, f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} " f"(Content ID: {title.tmd.content_records[content].content_id})...") dec_content = title.get_content_by_index(content) content_file_name = f"{title.tmd.content_records[content].content_id:08X}.app" @@ -105,15 +107,15 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_ # re-encrypted with the common key instead of the vWii key, so that the title can be installed from within # vWii mode. (vWii mode does not have access to the vWii key, only Wii U mode has that.) if repack_vwii_chkbox is True and (tid[3] == "7" or tid[7] == "7"): - progress_callback.emit(" - Re-encrypting Title Key with the common key...") + progress_callback.emit(-1, -1, " - Re-encrypting Title Key with the common key...") title_key_common = libWiiPy.title.encrypt_title_key(title.ticket.get_title_key(), 0, title.tmd.title_id) title.ticket.common_key_index = 0 title.ticket.title_key_enc = title_key_common # Get the WAD certificate chain, courtesy of libWiiPy. - progress_callback.emit(" - Building certificate...") + progress_callback.emit(-1, -1, " - Building certificate...") title.load_cert_chain(libWiiPy.title.download_cert_chain(wiiu_endpoint=wiiu_nus_enabled)) # Use a typed WAD name if there is one, and auto generate one based on the TID and version if there isn't. - progress_callback.emit(" - Packing WAD...") + progress_callback.emit(-1, -1, " - Packing WAD...") if wad_file_name != "" and wad_file_name is not None: # Batch downloads may insert -vLatest, so if it did we can fill in the real number now. wad_file_name = wad_file_name.replace("-vLatest", f"-v{title_version}") @@ -123,14 +125,14 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_ wad_file_name = f"{tid}-v{title_version}.wad" # If enabled (after we make sure it's an IOS), apply all main IOS patches. if patch_ios and (tid[:8] == "00000001" and int(tid[-2:], 16) > 2): - progress_callback.emit(" - Patching IOS...") + progress_callback.emit(-1, -1, " - Patching IOS...") ios_patcher = libWiiPy.title.IOSPatcher() ios_patcher.load(title) patch_count = ios_patcher.patch_all() if patch_count > 0: - progress_callback.emit(f" - Applied {patch_count} patches!") + progress_callback.emit(-1, -1, f" - Applied {patch_count} patches!") else: - progress_callback.emit(" - No patches could be applied! Is this a stub IOS?") + progress_callback.emit(-1, -1, " - No patches could be applied! Is this a stub IOS?") title = ios_patcher.dump() # Append "-PATCHED" to the end of the WAD file name to make it clear that it was modified. wad_file_name = wad_file_name[:-4] + "-PATCHED" + wad_file_name[-4:] @@ -140,7 +142,7 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_ wad_file_name = wad_file_name.translate({ord(c): None for c in '/\\:*"?<>|'}) # Have libWiiPy dump the WAD, and write that data out. version_dir.joinpath(wad_file_name).write_bytes(title.dump_wad()) - progress_callback.emit("Download complete!") + progress_callback.emit(0, 1, "Download complete!") # This is where the variables come in. If the state of these variables doesn't match the user's choice by this # point, it means that they enabled decryption or WAD packing for a title that doesn't have a ticket. Return # code 1 so that a warning popup is shown informing them of this. diff --git a/modules/theme.py b/modules/theme.py index 1906b9a..b210fac 100644 --- a/modules/theme.py +++ b/modules/theme.py @@ -1,6 +1,7 @@ # "modules/theme.py", licensed under the MIT license # Copyright 2024-2025 NinjaCheetah & Contributors +import os import platform import subprocess @@ -43,6 +44,17 @@ def is_dark_theme_linux(): return False def is_dark_theme(): + # First, check for an environment variable overriding the theme, and use that if it exists. + try: + if os.environ["THEME"].lower() == "light": + return False + elif os.environ["THEME"].lower() == "dark": + return True + else: + print(f"Unknown theme specified: \"{os.environ['THEME']}\"") + except KeyError: + pass + # If the theme wasn't overridden, then check the current system theme. system = platform.system() if system == "Windows": return is_dark_theme_windows() diff --git a/qt/py/ui_MainMenu.py b/qt/py/ui_MainMenu.py index e2a4915..09a3bc8 100644 --- a/qt/py/ui_MainMenu.py +++ b/qt/py/ui_MainMenu.py @@ -18,9 +18,9 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, QTransform) from PySide6.QtWidgets import (QApplication, QComboBox, QHBoxLayout, QHeaderView, QLabel, QLayout, QLineEdit, QMainWindow, - QMenu, QMenuBar, QPushButton, QSizePolicy, - QSpacerItem, QTabWidget, QTextBrowser, QTreeView, - QVBoxLayout, QWidget) + QMenu, QMenuBar, QProgressBar, QPushButton, + QSizePolicy, QSpacerItem, QTabWidget, QTextBrowser, + QTreeView, QVBoxLayout, QWidget) from qt.py.ui_WrapCheckboxWidget import WrapCheckboxWidget @@ -308,17 +308,25 @@ class Ui_MainWindow(object): self.log_text_browser = QTextBrowser(self.centralwidget) self.log_text_browser.setObjectName(u"log_text_browser") - self.log_text_browser.setMinimumSize(QSize(0, 247)) + self.log_text_browser.setMinimumSize(QSize(0, 222)) self.vertical_layout_controls.addWidget(self.log_text_browser) + self.progress_bar = QProgressBar(self.centralwidget) + self.progress_bar.setObjectName(u"progress_bar") + self.progress_bar.setMinimumSize(QSize(0, 25)) + self.progress_bar.setMaximumSize(QSize(16777215, 30)) + self.progress_bar.setValue(0) + + self.vertical_layout_controls.addWidget(self.progress_bar) + self.horizontalLayout_3.addLayout(self.vertical_layout_controls) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 1010, 21)) + self.menubar.setGeometry(QRect(0, 0, 1010, 30)) self.menuHelp = QMenu(self.menubar) self.menuHelp.setObjectName(u"menuHelp") MainWindow.setMenuBar(self.menubar) @@ -367,7 +375,7 @@ class Ui_MainWindow(object): "hr { height: 1px; border-width: 0; }\n" "li.unchecked::marker { content: \"\\2610\"; }\n" "li.checked::marker { content: \"\\2612\"; }\n" -"
\n" +"\n" "