34 Commits

Author SHA1 Message Date
9cb11f2f68 Style update for After Dark theme 2025-05-25 01:12:46 -04:00
dee71e03d0 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	modules/download_wii.py
#	qt/py/ui_AboutDialog.py
2025-05-25 01:09:32 -04:00
e8d6a19d03 Disable debug line that was setting progress bar to busy on launch 2025-05-25 01:02:57 -04:00
811e2ef01f Added progress bar to show download progress 2025-05-25 00:58:55 -04:00
72b7ae5789 merge upstream 2025-05-18 23:51:04 -04:00
db3947a100 Fix some remaining blue 2025-05-12 19:26:08 -04:00
c2e17bece7 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	resources/style.qss
2025-05-12 19:20:20 -04:00
47431c8834 merge upstream 2025-05-09 13:03:18 -04:00
ce099365cd merge upstream 2025-05-09 11:16:16 -04:00
0b6551d50d Merge remote-tracking branch 'origin/main' 2025-05-08 22:32:57 -04:00
9a9775348b After Dark color scheme B) 2025-05-08 22:32:20 -04:00
e1f8a23919 Merge remote-tracking branch 'refs/remotes/upstream/main' 2025-05-08 22:31:12 -04:00
ee5012383f Merge remote-tracking branch 'upstream/main' 2025-05-08 22:26:27 -04:00
45616f7f57 Merge remote-tracking branch 'upstream/main' 2025-05-07 08:55:41 -04:00
7feeeefc87 Merge remote-tracking branch 'upstream/main' 2025-05-04 19:23:08 -04:00
6689eaae70 Merge remote-tracking branch 'upstream/main' 2025-04-22 21:53:52 -04:00
d63acba656 Merge remote-tracking branch 'upstream/main' 2025-04-21 23:34:34 -04:00
0f96dc75a2 Merge remote-tracking branch 'upstream/main' 2025-02-23 19:33:48 -05:00
220fcc5e91 Merge remote-tracking branch 'upstream/main' 2025-02-18 21:31:54 -05:00
ab28a7bf1a Merge remote-tracking branch 'origin/main' 2025-02-18 21:31:06 -05:00
0bb87bf75f Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	README.md
#	modules/download_wii.py
2025-02-18 21:27:16 -05:00
92bfeb2374 Merge remote-tracking branch 'upstream/main' 2025-01-13 14:24:38 -05:00
86da2d62b0 Merge remote-tracking branch 'upstream/main' 2025-01-05 01:31:20 -05:00
1e88c22f7c Merge remote-tracking branch 'upstream/main' 2024-12-23 18:04:18 -05:00
c4ed6e6ee6 Merge remote-tracking branch 'origin/main' 2024-12-23 12:56:45 -05:00
b337be8c08 Merge remote-tracking branch 'upstream/main' 2024-12-23 12:56:27 -05:00
76911db12d Merge remote-tracking branch 'upstream/main' 2024-12-22 21:53:12 -05:00
a361a45314 Merge remote-tracking branch 'upstream/main' 2024-12-22 17:16:37 -05:00
724c7e554b Merge remote-tracking branch 'upstream/main' 2024-12-21 20:08:03 -05:00
469cd96392 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	README.md
2024-12-19 20:34:19 -05:00
398654609b Merge changes from upstream 2024-12-18 16:43:35 -05:00
78f98b2c73 Fix minor issue with Ticket forging 2024-12-13 23:17:20 -05:00
5872ca4676 Merge major improvements from upstream 2024-12-13 23:09:23 -05:00
147e72c8c9 Added Title Key generation code 2024-12-13 16:56:15 -05:00
14 changed files with 274 additions and 96 deletions

View File

@@ -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_wii import run_nus_download_wii
from modules.download_dsi import run_nus_download_dsi 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"], regions = {"World": ["41"], "USA/NTSC": ["45"], "Europe/PAL": ["50"], "Japan": ["4A"], "Korea": ["4B"], "China": ["43"],
"Australia/NZ": ["55"]} "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. # Signals needed for the worker used for threading the downloads.
class WorkerSignals(QObject): class WorkerSignals(QObject):
result = Signal(object) result = Signal(object)
progress = Signal(str) progress = Signal(int, int, str)
# Worker class used to thread the downloads. # Worker class used to thread the downloads.
@@ -230,6 +230,18 @@ class MainWindow(QMainWindow, Ui_MainWindow):
return return
self.ui.patch_ios_checkbox.setEnabled(False) 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): def update_log_text(self, new_text):
# This method primarily exists to be the handler for the progress signal emitted by the worker thread. # This method primarily exists to be the handler for the progress signal emitted by the worker thread.
self.log_text += new_text + "\n" self.log_text += new_text + "\n"
@@ -385,7 +397,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked(), self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked(),
self.ui.archive_file_entry.text()) self.ui.archive_file_entry.text())
worker.signals.result.connect(self.check_download_result) 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) self.threadpool.start(worker)
def check_download_result(self, result): def check_download_result(self, result):
@@ -552,7 +564,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui.use_wiiu_nus_checkbox.isChecked(), self.ui.use_local_checkbox.isChecked(), 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()) self.ui.pack_vwii_mode_checkbox.isChecked(), self.ui.patch_ios_checkbox.isChecked())
worker.signals.result.connect(self.check_batch_result) 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) self.threadpool.start(worker)
def choose_output_dir(self): def choose_output_dir(self):
@@ -657,12 +669,6 @@ if __name__ == "__main__":
# NUSGet look nice and pretty. # NUSGet look nice and pretty.
app.setStyle("fusion") app.setStyle("fusion")
theme_sheet = "style_dark.qss" 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(): if is_dark_theme():
theme_sheet = "style_dark.qss" theme_sheet = "style_dark.qss"
else: else:

View File

@@ -1,3 +1,5 @@
# NUSGet After Dark
A modern and supercharged NUS downloader built with Python and Qt6. Powered by libWiiPy and libTWLPy. Fork with features not acceptable for prod.
<div align="center"> <div align="center">
<img src="https://github.com/user-attachments/assets/156eb949-93aa-4453-b7a0-99b784ec0c8c" alt="The icon for NUSGet" width=256 height=256> <img src="https://github.com/user-attachments/assets/156eb949-93aa-4453-b7a0-99b784ec0c8c" alt="The icon for NUSGet" width=256 height=256>
<h1>NUSGet</h1> <h1>NUSGet</h1>
@@ -17,11 +19,12 @@ NUSGet also offers the ability to create vWii WADs that can be installed from wi
The following features are available for all supported consoles: The following features are available for all supported consoles:
- Downloading encrypted contents (files like `00000000`, `00000001`, etc.) directly from the update servers for any title. - Downloading encrypted contents (files like `00000000`, `00000001`, etc.) directly from the update servers for any title.
- Creating decrypted contents (*.app files) from the encrypted contents on the servers. Only supported for free titles. - Creating decrypted contents (*.app files) from the encrypted contents on the servers.
- Scripting support, allowing you to perform batch downloads of any titles for the Wii, vWii, or DSi in one script. (See `example-script.json` for an example of the scripting format.) - Scripting support, allowing you to perform batch downloads of any titles for the Wii, vWii, or DSi in one script. (See `example-script.json` for an example of the scripting format.)
**For Wii and vWii titles only:** **For Wii and vWii titles only:**
- "Pack installable archive (WAD/TAD)": Pack the encrypted contents, TMD, and Ticket into a WAD file that can be installed on a Wii or in Dolphin Emulator. Only supported for free titles. - "Pack installable archive (WAD/TAD)": Pack the encrypted contents, TMD, and Ticket into a WAD file that can be installed on a Wii or in Dolphin Emulator.
- Forging Tickets for titles without a common Ticket available on the NUS by using the Title Key algorithm to derive the key needed to decrypt the title.
**For vWii titles only:** **For vWii titles only:**
- "Re-encrypt title using the Wii Common Key": Re-encrypt the Title Key in a vWii title's Ticket before packing the WAD, so that the WAD can be installed via a normal WAD manager on the vWii, and can be extracted with legacy tools. **This also creates WADs that can be installed directly in Dolphin, allowing for running the vWii System Menu in Dolphin without a vWii NAND dump!** - "Re-encrypt title using the Wii Common Key": Re-encrypt the Title Key in a vWii title's Ticket before packing the WAD, so that the WAD can be installed via a normal WAD manager on the vWii, and can be extracted with legacy tools. **This also creates WADs that can be installed directly in Dolphin, allowing for running the vWii System Menu in Dolphin without a vWii NAND dump!**

View File

@@ -53,5 +53,5 @@ def run_nus_download_batch(out_folder: pathlib.Path, titles: List[BatchTitleData
# failed title. # failed title.
result = 1 result = 1
failed_titles.append(title.tid) 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) return BatchResults(result, warning_titles, failed_titles)

View File

@@ -29,10 +29,10 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
title_dir.mkdir(exist_ok=True) title_dir.mkdir(exist_ok=True)
# Announce the title being downloaded, and the version if applicable. # Announce the title being downloaded, and the version if applicable.
if title_version is not None: 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: else:
progress_callback.emit(f"Downloading title {tid} vLatest, please wait...") progress_callback.emit(0, 0, f"Downloading title {tid} vLatest, please wait...")
progress_callback.emit(" - Downloading and parsing TMD...") 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. # Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
try: try:
if title_version is not None: 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()) 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. # Use a local ticket, if one exists and "use local files" is enabled.
if use_local_chkbox and version_dir.joinpath("tik").exists(): 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()) title.load_ticket(version_dir.joinpath("tik").read_bytes())
else: else:
progress_callback.emit(" - Downloading and parsing Ticket...") progress_callback.emit(-1, -1, " - Downloading and parsing Ticket...")
try: try:
title.load_ticket(libTWLPy.download_ticket(tid)) title.load_ticket(libTWLPy.download_ticket(tid))
version_dir.joinpath("tik").write_bytes(title.ticket.dump()) version_dir.joinpath("tik").write_bytes(title.ticket.dump())
except ValueError: except ValueError:
# If libTWLPy returns an error, then no ticket is available. Log this, and disable options requiring a # 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. # 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 pack_tad_enabled = False
decrypt_contents_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. # 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}" 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. # 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(): 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() content = version_dir.joinpath(content_file_name).read_bytes()
else: 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)...") f"{title.tmd.content_record.content_size} bytes)...")
content = libTWLPy.download_content(tid, title.tmd.content_record.content_id) 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 encrypted contents is on, write out the content after its downloaded.
if keep_enc_chkbox is True: if keep_enc_chkbox is True:
version_dir.joinpath(content_file_name).write_bytes(content) 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 local contents is still true, decrypt the content and write out the decrypted file.
if decrypt_contents_enabled is True: if decrypt_contents_enabled is True:
try: 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() dec_content = title.get_content()
content_file_name = f"{title.tmd.content_record.content_id:08X}.app" content_file_name = f"{title.tmd.content_record.content_id:08X}.app"
version_dir.joinpath(content_file_name).write_bytes(dec_content) 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 is still true, pack the TMD, ticket, and content into a TAD.
if pack_tad_enabled is True: if pack_tad_enabled is True:
# Get the TAD certificate chain, courtesy of libTWLPy. # 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()) 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. # 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: 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. # 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}") 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 '/\\:*"?<>|'}) tad_file_name = tad_file_name.translate({ord(c): None for c in '/\\:*"?<>|'})
# Have libTWLPy dump the TAD, and write that data out. # Have libTWLPy dump the TAD, and write that data out.
version_dir.joinpath(tad_file_name).write_bytes(title.dump_tad()) 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 # 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 # 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. # code 1 so that a warning popup is shown informing them of this.

View File

@@ -2,13 +2,17 @@
# Copyright 2024-2025 NinjaCheetah & Contributors # Copyright 2024-2025 NinjaCheetah & Contributors
import pathlib import pathlib
from typing import List, Tuple
from .tkey import find_tkey
import libWiiPy import libWiiPy
from libWiiPy.title.ticket import _TitleLimit
def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool, 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, 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): 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. # Actual NUS download function that runs in a separate thread.
# Immediately knock out any invalidly formatted Title IDs. # Immediately knock out any invalidly formatted Title IDs.
if len(tid) != 16: if len(tid) != 16:
@@ -31,16 +35,16 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
title_dir.mkdir(exist_ok=True) title_dir.mkdir(exist_ok=True)
# Announce the title being downloaded, and the version if applicable. # Announce the title being downloaded, and the version if applicable.
if title_version is not None: 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: else:
progress_callback.emit(f"Downloading title {tid} vLatest, please wait...") progress_callback.emit(-1, -1, f"Downloading title {tid} vLatest, please wait...")
progress_callback.emit(" - Downloading and parsing TMD...") 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. # Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
try: try:
if title_version is not None: 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: 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 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. # If libWiiPy returns an error, that means that either the TID or version doesn't exist, so return code -2.
except ValueError: except ValueError:
@@ -51,20 +55,20 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
# Write out the TMD to a file. # Write out the TMD to a file.
version_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump()) 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. # Use a local ticket, if one exists and "use local files" is enabled.
forge_ticket = False
if use_local_chkbox and version_dir.joinpath("tik").exists(): 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()) title.load_ticket(version_dir.joinpath("tik").read_bytes())
else: else:
progress_callback.emit(" - Downloading and parsing Ticket...") progress_callback.emit(-1, -1, " - Downloading and parsing Ticket...")
try: 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()) version_dir.joinpath("tik").write_bytes(title.ticket.dump())
except ValueError: except ValueError:
# If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a # If libWiiPy returns an error, then no ticket is available. Try to forge a ticket after we download the
# ticket so that they aren't attempted later. # content.
progress_callback.emit(" - No Ticket is available!") progress_callback.emit(0, 0, " - No Ticket is available! Will try forging a Ticket.")
pack_wad_enabled = False forge_ticket = True
decrypt_contents_enabled = False
# Load the content records from the TMD, and begin iterating over the records. # Load the content records from the TMD, and begin iterating over the records.
title.load_content_records() title.load_content_records()
content_list = [] content_list = []
@@ -73,24 +77,57 @@ 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}" 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. # 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(): 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()) content_list.append(version_dir.joinpath(content_file_name).read_bytes())
else: 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"(Content ID: {title.tmd.content_records[content].content_id}, Size: "
f"{title.tmd.content_records[content].content_size} bytes)...") f"{title.tmd.content_records[content].content_size} bytes)...")
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id, content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
wiiu_endpoint=wiiu_nus_enabled)) wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
progress_callback.emit(" - Done!") progress_callback.emit(-1, -1, " - Done!")
# If keep encrypted contents is on, write out each content after its downloaded. # If keep encrypted contents is on, write out each content after its downloaded.
if keep_enc_chkbox is True: if keep_enc_chkbox is True:
version_dir.joinpath(content_file_name).write_bytes(content_list[content]) version_dir.joinpath(content_file_name).write_bytes(content_list[content])
title.content.content_list = content_list title.content.content_list = content_list
# Try to forge a Ticket, if a common one wasn't available.
if forge_ticket is True:
progress_callback.emit(0, 0, " - Attempting to forge Ticket...")
try:
title_key = find_tkey(tid, title.content.content_list[0], title.tmd.content_records[0])
title_key_enc = libWiiPy.title.encrypt_title_key(title_key, 0, tid)
ticket = libWiiPy.title.Ticket()
ticket.common_key_index = 0
ticket.console_id = 0
ticket.content_access_permissions = b'\xff' * 64
ticket.ecdh_data = b'\x00' * 60
ticket.permit_mask = b'\x00' * 4
ticket.permitted_titles = b'\x00' * 4
ticket.signature = b'\x00' * 256
ticket.signature_issuer = "Root-CA00000001-XS00000003" + ("\x00" * 38)
ticket.signature_type = b'\x00\x01' * 2
ticket.ticket_id = b'\x00' * 8
ticket.ticket_version = 0
ticket.title_export_allowed = 0
ticket.title_id = tid.encode()
ticket.title_key_enc = title_key_enc
ticket.title_limits_list = [_TitleLimit(0, 0) for _ in range(0, 8)]
ticket.title_version = 0
ticket.unknown1 = b'\xff' * 2
ticket.unknown2 = b'\x00' * 48
ticket.fakesign()
title.ticket = ticket
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
progress_callback.emit(-1, -1, " - Successfully forged Ticket!")
except Exception:
progress_callback.emit(-1, -1, " - Ticket could not be forged!")
pack_wad_enabled = False
decrypt_contents_enabled = False
# If decrypt local contents is still true, decrypt each content and write out the decrypted file. # If decrypt local contents is still true, decrypt each content and write out the decrypted file.
if decrypt_contents_enabled is True: if decrypt_contents_enabled is True:
try: try:
for content in range(len(title.tmd.content_records)): 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})...") f"(Content ID: {title.tmd.content_records[content].content_id})...")
dec_content = title.get_content_by_index(content) dec_content = title.get_content_by_index(content)
content_file_name = f"{title.tmd.content_records[content].content_id:08X}.app" content_file_name = f"{title.tmd.content_records[content].content_id:08X}.app"
@@ -105,15 +142,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 # 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.) # 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"): 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_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.common_key_index = 0
title.ticket.title_key_enc = title_key_common title.ticket.title_key_enc = title_key_common
# Get the WAD certificate chain, courtesy of libWiiPy. # 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)) 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. # 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: 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. # 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}") wad_file_name = wad_file_name.replace("-vLatest", f"-v{title_version}")
@@ -123,14 +160,14 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
wad_file_name = f"{tid}-v{title_version}.wad" 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 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): 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 = libWiiPy.title.IOSPatcher()
ios_patcher.load(title) ios_patcher.load(title)
patch_count = ios_patcher.patch_all() patch_count = ios_patcher.patch_all()
if patch_count > 0: if patch_count > 0:
progress_callback.emit(f" - Applied {patch_count} patches!") progress_callback.emit(-1, -1, f" - Applied {patch_count} patches!")
else: 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() title = ios_patcher.dump()
# Append "-PATCHED" to the end of the WAD file name to make it clear that it was modified. # 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:] wad_file_name = wad_file_name[:-4] + "-PATCHED" + wad_file_name[-4:]
@@ -140,7 +177,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 '/\\:*"?<>|'}) wad_file_name = wad_file_name.translate({ord(c): None for c in '/\\:*"?<>|'})
# Have libWiiPy dump the WAD, and write that data out. # Have libWiiPy dump the WAD, and write that data out.
version_dir.joinpath(wad_file_name).write_bytes(title.dump_wad()) 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 # 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 # 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. # code 1 so that a warning popup is shown informing them of this.

View File

@@ -1,6 +1,7 @@
# "modules/theme.py", licensed under the MIT license # "modules/theme.py", licensed under the MIT license
# Copyright 2024-2025 NinjaCheetah & Contributors # Copyright 2024-2025 NinjaCheetah & Contributors
import os
import platform import platform
import subprocess import subprocess
@@ -43,6 +44,17 @@ def is_dark_theme_linux():
return False return False
def is_dark_theme(): 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() system = platform.system()
if system == "Windows": if system == "Windows":
return is_dark_theme_windows() return is_dark_theme_windows()

53
modules/tkey.py Normal file
View File

@@ -0,0 +1,53 @@
# "tkey-gen.py", licensed under the MIT license
# Copyright 2024 NinjaCheetah
import binascii
import hashlib
import libWiiPy
from libWiiPy.types import _ContentRecord
def _secret(start, length):
ret = b''
add = start + length
for _ in range(length):
unsigned_start = start & 0xFF # Compensates for how Python handles negative values vs PHP.
ret += bytes.fromhex(f"{unsigned_start:02x}"[-2:])
nxt = start + add
add = start
start = nxt
return ret
def _mungetid(tid):
# Remove leading zeroes from the TID.
while tid.startswith("00"):
tid = tid[2:]
if tid == "":
tid = "00"
# In PHP, the last character just gets dropped if you make a hex string from an odd-length input, so this
# replicates that functionality.
if len(tid) % 2 != 0:
tid = tid[:-1]
return bytes.fromhex(tid)
def _derive_key(tid, passwd):
key_secret = _secret(-3, 10)
salt = hashlib.md5(key_secret + _mungetid(tid)).digest()
# Had to reduce the length here from 32 to 16 when converting to get the same length keys.
return hashlib.pbkdf2_hmac("sha1", passwd.encode(), salt, 20, 16).hex()
def find_tkey(tid: str, banner_enc: bytes, content_record: _ContentRecord) -> bytes:
# Find a working Title Key by generating a key with a password, then decrypting content 0 and comparing it to the
# expected hash. If the hash matches, then we generated the correct key.
passwds = ["nintendo", "mypass"]
for passwd in passwds:
key = binascii.unhexlify(_derive_key(tid, passwd).encode())
banner_dec = libWiiPy.title.decrypt_content(banner_enc, key, content_record.index, content_record.content_size)
banner_dec_hash = hashlib.sha1(banner_dec).hexdigest()
content_record_hash = content_record.content_hash.decode()
if banner_dec_hash == content_record_hash:
return key
raise Exception("Valid Title Key could not be generated")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -18,9 +18,9 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QTransform) QTransform)
from PySide6.QtWidgets import (QApplication, QComboBox, QHBoxLayout, QHeaderView, from PySide6.QtWidgets import (QApplication, QComboBox, QHBoxLayout, QHeaderView,
QLabel, QLayout, QLineEdit, QMainWindow, QLabel, QLayout, QLineEdit, QMainWindow,
QMenu, QMenuBar, QPushButton, QSizePolicy, QMenu, QMenuBar, QProgressBar, QPushButton,
QSpacerItem, QTabWidget, QTextBrowser, QTreeView, QSizePolicy, QSpacerItem, QTabWidget, QTextBrowser,
QVBoxLayout, QWidget) QTreeView, QVBoxLayout, QWidget)
from qt.py.ui_WrapCheckboxWidget import WrapCheckboxWidget 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 = QTextBrowser(self.centralwidget)
self.log_text_browser.setObjectName(u"log_text_browser") 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.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) self.horizontalLayout_3.addLayout(self.vertical_layout_controls)
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow) self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar") 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 = QMenu(self.menubar)
self.menuHelp.setObjectName(u"menuHelp") self.menuHelp.setObjectName(u"menuHelp")
MainWindow.setMenuBar(self.menubar) MainWindow.setMenuBar(self.menubar)
@@ -367,7 +375,7 @@ class Ui_MainWindow(object):
"hr { height: 1px; border-width: 0; }\n" "hr { height: 1px; border-width: 0; }\n"
"li.unchecked::marker { content: \"\\2610\"; }\n" "li.unchecked::marker { content: \"\\2610\"; }\n"
"li.checked::marker { content: \"\\2612\"; }\n" "li.checked::marker { content: \"\\2612\"; }\n"
"</style></head><body style=\" font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;\">\n" "</style></head><body style=\" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;\">\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;\"><br /></p></body></html>", None)) "<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;\"><br /></p></body></html>", None))
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None))
# retranslateUi # retranslateUi

View File

@@ -422,7 +422,7 @@
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
<height>247</height> <height>222</height>
</size> </size>
</property> </property>
<property name="markdown"> <property name="markdown">
@@ -435,11 +435,30 @@ p, li { white-space: pre-wrap; }
hr { height: 1px; border-width: 0; } hr { height: 1px; border-width: 0; }
li.unchecked::marker { content: &quot;\2610&quot;; } li.unchecked::marker { content: &quot;\2610&quot;; }
li.checked::marker { content: &quot;\2612&quot;; } li.checked::marker { content: &quot;\2612&quot;; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QProgressBar" name="progress_bar">
<property name="minimumSize">
<size>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>30</height>
</size>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>
@@ -450,7 +469,7 @@ li.checked::marker { content: &quot;\2612&quot;; }
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1010</width> <width>1010</width>
<height>21</height> <height>30</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuHelp"> <widget class="QMenu" name="menuHelp">

View File

@@ -1,6 +1,6 @@
pyside6 pyside6
nuitka~=2.6.0 nuitka~=2.6.0
libWiiPy git+https://github.com/NinjaCheetah/libWiiPy
libTWLPy libTWLPy
zstandard zstandard
requests requests

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -47,7 +47,7 @@ QMenuBar::item:selected {
} }
QMenuBar::item:pressed { QMenuBar::item:pressed {
background-color: #1a73e8; background-color: #6c1ae8;
color: white; color: white;
} }
@@ -68,7 +68,7 @@ QMenu::item {
} }
QMenu::item:selected { QMenu::item:selected {
background-color: #1a73e8; background-color: #6c1ae8;
color: white; color: white;
} }
@@ -88,13 +88,13 @@ QRadioButton {
QRadioButton:hover { QRadioButton:hover {
background-color: rgba(60, 60, 60, 1); background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8; border-color: #9c4ae8;
} }
QRadioButton:checked { QRadioButton:checked {
background-color: rgba(26, 115, 232, 0.08); background-color: rgba(26, 115, 232, 0.08);
border: 1px solid #1a73e8; border: 1px solid #6c1ae8;
color: #1a73e8; color: #6c1ae8;
} }
QRadioButton::indicator { QRadioButton::indicator {
@@ -107,13 +107,13 @@ QRadioButton::indicator {
} }
QRadioButton::indicator:checked { QRadioButton::indicator:checked {
background-color: #1a73e8; background-color: #6c1ae8;
border: 1px solid #1a73e8; border: 1px solid #6c1ae8;
image: url("{IMAGE_PREFIX}/rounded_square.svg"); image: url("{IMAGE_PREFIX}/rounded_square.svg");
} }
QRadioButton::indicator:hover { QRadioButton::indicator:hover {
border-color: #1a73e8; border-color: #6c1ae8;
} }
QLineEdit { QLineEdit {
@@ -124,11 +124,11 @@ QLineEdit {
margin: 4px 0px; margin: 4px 0px;
font-size: 13px; font-size: 13px;
color: #ffffff; color: #ffffff;
selection-background-color: #1a73e8; selection-background-color: #6c1ae8;
} }
QLineEdit:focus { QLineEdit:focus {
border-color: #1a73e8; border-color: #6c1ae8;
} }
QLineEdit:disabled { QLineEdit:disabled {
@@ -187,11 +187,11 @@ QTreeView::item:hover {
} }
QTreeView::item:focus { QTreeView::item:focus {
background-color: rgba(26, 115, 232, 0.08); background-color: rgba(64, 26, 232, 0.15);
} }
QTreeView::item:selected { QTreeView::item:selected {
background-color: #1a73e8; background-color: #6c1ae8;
} }
QTreeView QScrollBar:vertical { QTreeView QScrollBar:vertical {
@@ -211,7 +211,7 @@ QTreeView::branch:open:has-children:has-siblings {
QTextBrowser { QTextBrowser {
color: white; color: white;
background-color: #1a1a1a; background-color: #1a1a1a;
selection-background-color: #1a73e8; selection-background-color: #6c1ae8;
} }
QPushButton { QPushButton {
@@ -229,17 +229,17 @@ QPushButton {
QPushButton:hover { QPushButton:hover {
background-color: rgba(60, 60, 60, 1); background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8; border-color: #9c4ae8;
} }
QPushButton:focus { QPushButton:focus {
background-color: rgba(60, 60, 60, 1); background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8; border-color: #9c4ae8;
} }
QPushButton:pressed { QPushButton:pressed {
background-color: rgba(26, 115, 232, 0.15); background-color: rgba(64, 26, 232, 0.15);
border: 1px solid #1a73e8; border: 1px solid #6c1ae8;
} }
QPushButton:disabled { QPushButton:disabled {
@@ -261,18 +261,18 @@ QComboBox {
} }
QComboBox:on { QComboBox:on {
background-color: rgba(26, 115, 232, 0.15); background-color: rgba(64, 26, 232, 0.15);
border: 1px solid #1a73e8; border: 1px solid #6c1ae8;
} }
QComboBox:hover { QComboBox:hover {
background-color: rgba(60, 60, 60, 1); background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8; border-color: #9c4ae8;
} }
QComboBox:focus { QComboBox:focus {
background-color: rgba(60, 60, 60, 1); background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8; border-color: #9c4ae8;
} }
QComboBox::drop-down { QComboBox::drop-down {
@@ -301,7 +301,7 @@ QComboBox QAbstractItemView::item {
} }
QComboBox QAbstractItemView::item:hover { QComboBox QAbstractItemView::item:hover {
background-color: #1a73e8; background-color: #6c1ae8;
} }
QScrollBar:vertical { QScrollBar:vertical {
@@ -320,7 +320,7 @@ QScrollBar::handle:vertical {
} }
QScrollBar::handle:vertical:hover { QScrollBar::handle:vertical:hover {
background-color: rgba(26, 115, 232, 0.4); background-color: rgba(71, 26, 232, 0.4);
} }
QScrollBar::add-line:vertical { QScrollBar::add-line:vertical {
@@ -350,7 +350,7 @@ QScrollBar::handle:horizontal {
} }
QScrollBar::handle:horizontal:hover { QScrollBar::handle:horizontal:hover {
background-color: rgba(26, 115, 232, 0.4); background-color: rgba(71, 26, 232, 0.4);
} }
QScrollBar::add-line:horizontal { QScrollBar::add-line:horizontal {
@@ -369,6 +369,24 @@ QMessageBox QLabel {
color: white; color: white;
} }
QProgressBar {
border: 1px solid rgba(70, 70, 70, 1);
border-radius: 8px;
background-color: #1a1a1a;
text-align: center;
color: white;
padding-left: 1px;
}
QProgressBar::chunk {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #6c1ae8, stop: 1 #8941ec
);
border-radius: 5px;
margin: 0.5px;
}
WrapCheckboxWidget { WrapCheckboxWidget {
show-decoration-selected: 1; show-decoration-selected: 1;
outline: 0; outline: 0;
@@ -383,7 +401,7 @@ WrapCheckboxWidget {
WrapCheckboxWidget:hover { WrapCheckboxWidget:hover {
background-color: rgba(60, 60, 60, 1); background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8; border-color: #9c4ae8;
} }
WrapCheckboxWidget:disabled { WrapCheckboxWidget:disabled {
@@ -403,16 +421,20 @@ WrapCheckboxWidget QCheckBox::indicator {
border: 1px solid #5f6368; border: 1px solid #5f6368;
} }
WrapCheckboxWidget QCheckBox::indicator::focus {
background-color: rgba(64, 26, 232, 0.15);
}
WrapCheckboxWidget QCheckBox::indicator:checked { WrapCheckboxWidget QCheckBox::indicator:checked {
background-color: #1a73e8; background-color: #6c1ae8;
border: 1px solid #1a73e8; border: 1px solid #6c1ae8;
image: url("{IMAGE_PREFIX}/check.svg"); image: url("{IMAGE_PREFIX}/check.svg");
} }
WrapCheckboxWidget QCheckBox::indicator:hover { WrapCheckboxWidget QCheckBox::indicator:hover {
border-color: #1a73e8; border-color: #6c1ae8;
} }
WrapCheckboxWidget QCheckBox:checked { WrapCheckboxWidget QCheckBox:checked {
color: #1a73e8; color: #6c1ae8;
} }

View File

@@ -377,6 +377,24 @@ QMessageBox QLabel {
color: #000000; color: #000000;
} }
QProgressBar {
border: 1px solid rgb(163, 163, 163);
border-radius: 8px;
background-color: #ececec;
text-align: center;
padding: 1px;
color: black;
}
QProgressBar::chunk {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #1a73e8, stop: 1 #5596f4
);
border-radius: 5px;
margin: 0.5px;
}
WrapCheckboxWidget { WrapCheckboxWidget {
show-decoration-selected: 1; show-decoration-selected: 1;
outline: 0; outline: 0;