mirror of
https://github.com/NinjaCheetah/NUSGet.git
synced 2026-03-06 10:35:31 -05:00
Compare commits
38 Commits
v1.4.0
...
f674c8904d
| Author | SHA1 | Date | |
|---|---|---|---|
| f674c8904d | |||
|
cffa8f79ff
|
|||
|
109e3dc25a
|
|||
|
db6dc65791
|
|||
|
9cb11f2f68
|
|||
|
dee71e03d0
|
|||
|
e8d6a19d03
|
|||
|
811e2ef01f
|
|||
| 72b7ae5789 | |||
|
db3947a100
|
|||
|
c2e17bece7
|
|||
| 47431c8834 | |||
| ce099365cd | |||
|
0b6551d50d
|
|||
|
9a9775348b
|
|||
|
e1f8a23919
|
|||
|
ee5012383f
|
|||
|
45616f7f57
|
|||
|
7feeeefc87
|
|||
|
6689eaae70
|
|||
|
d63acba656
|
|||
|
0f96dc75a2
|
|||
|
220fcc5e91
|
|||
|
ab28a7bf1a
|
|||
|
0bb87bf75f
|
|||
|
92bfeb2374
|
|||
|
86da2d62b0
|
|||
|
1e88c22f7c
|
|||
|
c4ed6e6ee6
|
|||
|
b337be8c08
|
|||
|
76911db12d
|
|||
|
a361a45314
|
|||
|
724c7e554b
|
|||
|
469cd96392
|
|||
|
398654609b
|
|||
|
78f98b2c73
|
|||
|
5872ca4676
|
|||
|
147e72c8c9
|
137
NUSGet.py
137
NUSGet.py
@@ -22,21 +22,23 @@ import sys
|
||||
import webbrowser
|
||||
from importlib.metadata import version
|
||||
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtGui import QActionGroup, QIcon
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox, QFileDialog, QListView
|
||||
from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject, QLibraryInfo, QTranslator, QLocale
|
||||
from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject, QLibraryInfo
|
||||
|
||||
from qt.py.ui_AboutDialog import AboutNUSGet
|
||||
from qt.py.ui_MainMenu import Ui_MainWindow
|
||||
|
||||
from modules.config import *
|
||||
from modules.core import *
|
||||
from modules.theme import is_dark_theme
|
||||
from modules.language import *
|
||||
from modules.theme import *
|
||||
from modules.tree import NUSGetTreeModel, TIDFilterProxyModel
|
||||
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 +47,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,9 +83,6 @@ 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)
|
||||
# About and About Qt Buttons
|
||||
self.ui.actionAbout.triggered.connect(self.about_nusget)
|
||||
self.ui.actionAbout_Qt.triggered.connect(lambda: QMessageBox.aboutQt(self))
|
||||
self.ui.pack_archive_checkbox.toggled.connect(
|
||||
lambda: connect_is_enabled_to_checkbox([self.ui.archive_file_entry], self.ui.pack_archive_checkbox))
|
||||
self.ui.custom_out_dir_checkbox.toggled.connect(
|
||||
@@ -126,19 +125,65 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
dropdown_delegate = ComboBoxItemDelegate()
|
||||
self.ui.console_select_dropdown.setItemDelegate(dropdown_delegate)
|
||||
self.ui.console_select_dropdown.currentIndexChanged.connect(self.selected_console_changed)
|
||||
# Fix the annoying background on the help menu items.
|
||||
self.ui.menuHelp.setWindowFlags(self.ui.menuHelp.windowFlags() | Qt.FramelessWindowHint)
|
||||
self.ui.menuHelp.setWindowFlags(self.ui.menuHelp.windowFlags() | Qt.NoDropShadowWindowHint)
|
||||
self.ui.menuHelp.setAttribute(Qt.WA_TranslucentBackground)
|
||||
# ------------
|
||||
# Options Menu
|
||||
# ------------
|
||||
# Fix the annoying background on the option menu items and submenus.
|
||||
fixup_qmenu_background(self.ui.menu_options)
|
||||
fixup_qmenu_background(self.ui.menu_options_language)
|
||||
fixup_qmenu_background(self.ui.menu_options_theme)
|
||||
# Build a QActionGroup so that the language options function like radio buttons, because selecting multiple
|
||||
# languages at once makes no sense.
|
||||
language_group = QActionGroup(self)
|
||||
language_group.setExclusive(True)
|
||||
current_language = ""
|
||||
try:
|
||||
current_language = config_data["language"]
|
||||
except KeyError:
|
||||
pass
|
||||
for action in self.ui.menu_options_language.actions():
|
||||
language_group.addAction(action)
|
||||
if current_language != "":
|
||||
if LANGS[current_language] == action.text():
|
||||
action.setChecked(True)
|
||||
# There is no language set, so check the system language option.
|
||||
if current_language == "":
|
||||
self.ui.action_language_system.setChecked(True)
|
||||
language_group.triggered.connect(lambda lang=action: self.change_language(lang.text()))
|
||||
# Another QActionGroup used for the theme selector.
|
||||
theme_group = QActionGroup(self)
|
||||
theme_group.setExclusive(True)
|
||||
for action in self.ui.menu_options_theme.actions():
|
||||
theme_group.addAction(action)
|
||||
self.ui.action_theme_system.triggered.connect(lambda: self.change_theme("system"))
|
||||
self.ui.action_theme_light.triggered.connect(lambda: self.change_theme("light"))
|
||||
self.ui.action_theme_dark.triggered.connect(lambda: self.change_theme("dark"))
|
||||
try:
|
||||
match config_data["theme"]:
|
||||
case "light":
|
||||
self.ui.action_theme_light.setChecked(True)
|
||||
case "dark":
|
||||
self.ui.action_theme_dark.setChecked(True)
|
||||
case _:
|
||||
self.ui.action_theme_system.setChecked(True)
|
||||
except KeyError:
|
||||
self.ui.action_theme_system.setChecked(True)
|
||||
# ---------
|
||||
# Help Menu
|
||||
# ---------
|
||||
# Same fix for help menu items.
|
||||
fixup_qmenu_background(self.ui.menu_help)
|
||||
self.ui.action_about.triggered.connect(self.about_nusget)
|
||||
self.ui.action_about_qt.triggered.connect(lambda: QMessageBox.aboutQt(self))
|
||||
# Save some light/dark theme values for later, including the appropriately colored info icon.
|
||||
if is_dark_theme():
|
||||
if is_dark_theme(config_data):
|
||||
bg_color = "#2b2b2b"
|
||||
icon = QIcon(os.path.join(os.path.dirname(__file__), "resources", "information_white.svg"))
|
||||
else:
|
||||
bg_color = "#e3e3e3"
|
||||
icon = QIcon(os.path.join(os.path.dirname(__file__), "resources", "information_black.svg"))
|
||||
self.ui.actionAbout.setIcon(icon)
|
||||
self.ui.actionAbout_Qt.setIcon(icon)
|
||||
self.ui.action_about.setIcon(icon)
|
||||
self.ui.action_about_qt.setIcon(icon)
|
||||
# Title tree loading code. Now powered by Models:tm:
|
||||
wii_model = NUSGetTreeModel(wii_database, root_name="Wii Titles")
|
||||
vwii_model = NUSGetTreeModel(vwii_database, root_name="vWii Titles")
|
||||
@@ -230,6 +275,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 +442,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 +609,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):
|
||||
@@ -595,6 +652,26 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
about_box = AboutNUSGet([nusget_version, version("libWiiPy"), version("libTWLPy")])
|
||||
about_box.exec()
|
||||
|
||||
def change_language(self, new_lang):
|
||||
set_language(config_data, new_lang)
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setIcon(QMessageBox.Icon.Warning)
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
|
||||
msg_box.setWindowTitle(app.translate("MainWindow", "Restart Required"))
|
||||
msg_box.setText(app.translate("MainWindow", "NUSGet must be restarted for the selected language to take effect."))
|
||||
msg_box.exec()
|
||||
|
||||
def change_theme(self, new_theme):
|
||||
set_theme(config_data, new_theme)
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setIcon(QMessageBox.Icon.Warning)
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
|
||||
msg_box.setWindowTitle(app.translate("MainWindow", "Restart Required"))
|
||||
msg_box.setText(app.translate("MainWindow", "NUSGet must be restarted for the selected theme to take effect."))
|
||||
msg_box.exec()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
@@ -657,16 +734,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(config_data):
|
||||
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)
|
||||
@@ -677,16 +748,10 @@ if __name__ == "__main__":
|
||||
translator = QTranslator(app)
|
||||
if translator.load(QLocale.system(), 'qtbase', '_', path):
|
||||
app.installTranslator(translator)
|
||||
translator = QTranslator(app)
|
||||
# Get the translation path, and call get_language() to find the appropriate translations to load based on the
|
||||
# settings and system language.
|
||||
path = os.path.join(os.path.dirname(__file__), "resources", "translations")
|
||||
# Unix-likes and Windows handle this differently, apparently. Unix-likes will try `nusget_xx_XX.qm` and then fall
|
||||
# back on just `nusget_xx.qm` if the region-specific translation for the language can't be found. On Windows, no
|
||||
# such fallback exists, and so this code manually implements that fallback, since for languages like Spanish NUSGet
|
||||
# doesn't use region-specific translations.
|
||||
locale = QLocale.system()
|
||||
if not translator.load(QLocale.system(), 'nusget', '_', path):
|
||||
base_locale = QLocale(locale.language())
|
||||
translator.load(base_locale, 'nusget', '_', path)
|
||||
translator = get_language(QTranslator(app), config_data, path)
|
||||
app.installTranslator(translator)
|
||||
|
||||
window = MainWindow()
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
@@ -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:
|
||||
- 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.)
|
||||
|
||||
**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:**
|
||||
- "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!**
|
||||
|
||||
27
modules/config.py
Normal file
27
modules/config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# "modules/config.py", licensed under the MIT license
|
||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
||||
|
||||
import os
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
def get_config_file() -> pathlib.Path:
|
||||
config_dir = pathlib.Path(os.path.join(
|
||||
os.environ.get('APPDATA') or
|
||||
os.environ.get('XDG_CONFIG_HOME') or
|
||||
os.path.join(os.environ['HOME'], '.config'),
|
||||
"NUSGet"
|
||||
))
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
return config_dir.joinpath("config.json")
|
||||
|
||||
|
||||
def save_config(config_data: dict) -> None:
|
||||
config_file = get_config_file()
|
||||
print(f"writing data: {config_data}")
|
||||
open(config_file, "w").write(json.dumps(config_data, indent=4))
|
||||
|
||||
|
||||
def update_setting(config_data: dict, setting: str, value: any) -> None:
|
||||
config_data[setting] = value
|
||||
save_config(config_data)
|
||||
@@ -1,15 +1,12 @@
|
||||
# "modules/core.py", licensed under the MIT license
|
||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
||||
|
||||
import os
|
||||
import json
|
||||
import pathlib
|
||||
import requests
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from PySide6.QtCore import Qt, QSize
|
||||
from PySide6.QtWidgets import QStyledItemDelegate, QSizePolicy
|
||||
from PySide6.QtWidgets import QStyledItemDelegate
|
||||
|
||||
|
||||
# This is required to make the dropdown look correct with the custom styling. A little fuzzy on the why, but it has to
|
||||
@@ -63,6 +60,13 @@ def connect_is_enabled_to_checkbox(items, chkbox):
|
||||
item.setEnabled(False)
|
||||
|
||||
|
||||
def fixup_qmenu_background(menu):
|
||||
# These fixes are required to not have a square background poking out from behind the rounded corners of a QMenu.
|
||||
menu.setWindowFlags(menu.windowFlags() | Qt.FramelessWindowHint)
|
||||
menu.setWindowFlags(menu.windowFlags() | Qt.NoDropShadowWindowHint)
|
||||
menu.setAttribute(Qt.WA_TranslucentBackground)
|
||||
|
||||
|
||||
def check_nusget_updates(app, current_version: str, progress_callback=None) -> str | None:
|
||||
# Simple function to make a request to the GitHub API and then check if the latest available version is newer.
|
||||
gh_api_request = requests.get(url="https://api.github.com/repos/NinjaCheetah/NUSGet/releases/latest", stream=True)
|
||||
@@ -81,25 +85,3 @@ def check_nusget_updates(app, current_version: str, progress_callback=None) -> s
|
||||
return new_version
|
||||
progress_callback.emit(app.translate("MainWindow", "\n\nYou're running the latest release of NUSGet."))
|
||||
return None
|
||||
|
||||
|
||||
def get_config_file() -> pathlib.Path:
|
||||
config_dir = pathlib.Path(os.path.join(
|
||||
os.environ.get('APPDATA') or
|
||||
os.environ.get('XDG_CONFIG_HOME') or
|
||||
os.path.join(os.environ['HOME'], '.config'),
|
||||
"NUSGet"
|
||||
))
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
return config_dir.joinpath("config.json")
|
||||
|
||||
|
||||
def save_config(config_data: dict) -> None:
|
||||
config_file = get_config_file()
|
||||
print(f"writing data: {config_data}")
|
||||
open(config_file, "w").write(json.dumps(config_data))
|
||||
|
||||
|
||||
def update_setting(config_data: dict, setting: str, value: any) -> None:
|
||||
config_data[setting] = value
|
||||
save_config(config_data)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
||||
|
||||
import pathlib
|
||||
|
||||
from typing import List, Tuple
|
||||
from .tkey import find_tkey
|
||||
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,
|
||||
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 +35,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:
|
||||
@@ -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.
|
||||
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.
|
||||
forge_ticket = False
|
||||
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!")
|
||||
pack_wad_enabled = False
|
||||
decrypt_contents_enabled = False
|
||||
# If libWiiPy returns an error, then no ticket is available. Try to forge a ticket after we download the
|
||||
# content.
|
||||
progress_callback.emit(0, 0, " - No Ticket is available! Will try forging a Ticket.")
|
||||
forge_ticket = True
|
||||
# Load the content records from the TMD, and begin iterating over the records.
|
||||
title.load_content_records()
|
||||
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}"
|
||||
# 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])
|
||||
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_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 +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
|
||||
# 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 +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"
|
||||
# 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 +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 '/\\:*"?<>|'})
|
||||
# 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.
|
||||
|
||||
79
modules/language.py
Normal file
79
modules/language.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# "modules/language.py", licensed under the MIT license
|
||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
||||
|
||||
from modules.config import update_setting
|
||||
|
||||
from PySide6.QtCore import QLocale, QTranslator
|
||||
|
||||
|
||||
LANGS = {
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"it": "Italiano",
|
||||
"no": "Norsk",
|
||||
"ro": "Românǎ",
|
||||
"ko": "한국어",
|
||||
}
|
||||
|
||||
|
||||
def set_language(config_data: dict, lang: str) -> None:
|
||||
# Match the selected language. These names will NOT be translated since they represent each language in that
|
||||
# language, but the "System (Default)" option will, so that will match the default case.
|
||||
match lang:
|
||||
case "English":
|
||||
print("setting language to English")
|
||||
update_setting(config_data, "language", "en")
|
||||
case "Español":
|
||||
print("setting language to Spanish")
|
||||
update_setting(config_data, "language", "es")
|
||||
case "Deutsch":
|
||||
print("setting language to German")
|
||||
update_setting(config_data, "language", "de")
|
||||
case "Français":
|
||||
print("setting language to French")
|
||||
update_setting(config_data, "language", "fr")
|
||||
case "Italiano":
|
||||
print("setting language to Italian")
|
||||
update_setting(config_data, "language", "it")
|
||||
case "Norsk":
|
||||
print("setting language to Norwegian")
|
||||
update_setting(config_data, "language", "no")
|
||||
case "Română":
|
||||
print("setting language to Romanian")
|
||||
update_setting(config_data, "language", "ro")
|
||||
case "한국어":
|
||||
print("setting language to Korean")
|
||||
update_setting(config_data, "language", "ko")
|
||||
case _:
|
||||
print("setting language to system (default)")
|
||||
update_setting(config_data, "language", "")
|
||||
|
||||
|
||||
def get_language(translator: QTranslator, config_data: dict, path: str) -> QTranslator:
|
||||
try:
|
||||
lang = config_data["language"]
|
||||
except KeyError:
|
||||
lang = ""
|
||||
# A specific language was set in the app's settings.
|
||||
if lang != "":
|
||||
# If the target language is English, then return an empty translator because that's the default.
|
||||
if lang == "en":
|
||||
return translator
|
||||
if translator.load(QLocale(lang), 'nusget', '_', path):
|
||||
return translator
|
||||
else:
|
||||
# If we get here, then the saved language is invalid, so clear it and run again to use the system language.
|
||||
update_setting(config_data, "language", "")
|
||||
return get_language(translator, config_data, path)
|
||||
else:
|
||||
# Unix-likes and Windows handle this differently, apparently. Unix-likes will try `nusget_xx_XX.qm` and then
|
||||
# fall back on just `nusget_xx.qm` if the region-specific translation for the language can't be found. On
|
||||
# Windows, no such fallback exists, and so this code manually implements that fallback, since for languages like
|
||||
# Spanish NUSGet doesn't use region-specific translations.
|
||||
locale = QLocale.system()
|
||||
if not translator.load(QLocale.system(), 'nusget', '_', path):
|
||||
base_locale = QLocale(locale.language())
|
||||
translator.load(base_locale, 'nusget', '_', path)
|
||||
return translator
|
||||
@@ -1,9 +1,13 @@
|
||||
# "modules/theme.py", licensed under the MIT license
|
||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
||||
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
from modules.config import update_setting
|
||||
|
||||
|
||||
def is_dark_theme_windows():
|
||||
# This has to be here so that Python doesn't try to import it on non-Windows.
|
||||
import winreg
|
||||
@@ -16,6 +20,7 @@ def is_dark_theme_windows():
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_dark_theme_macos():
|
||||
# macOS is weird. If the dark theme is on, then `defaults read -g AppleInterfaceStyle` returns "Dark". If the light
|
||||
# theme is on, then trying to read this key fails and returns an error instead.
|
||||
@@ -28,6 +33,7 @@ def is_dark_theme_macos():
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_dark_theme_linux():
|
||||
try:
|
||||
import subprocess
|
||||
@@ -42,7 +48,36 @@ def is_dark_theme_linux():
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def is_dark_theme():
|
||||
|
||||
def is_dark_theme(config_data: dict):
|
||||
# Theming priority order:
|
||||
# 1. `THEME` environment variable
|
||||
# 2. Theme saved in config.json
|
||||
# 3. The system theme
|
||||
# 4. Light, if all else fails (though personally I'd prefer dark)
|
||||
#
|
||||
# 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, read the user's preference in config.json.
|
||||
try:
|
||||
match config_data["theme"]:
|
||||
case "light":
|
||||
return False
|
||||
case "dark":
|
||||
return True
|
||||
case _:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
# If a theme wasn't set (or the theme is "system"), then check for the system theme.
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
return is_dark_theme_windows()
|
||||
@@ -50,3 +85,16 @@ def is_dark_theme():
|
||||
return is_dark_theme_macos()
|
||||
else:
|
||||
return is_dark_theme_linux()
|
||||
|
||||
|
||||
def set_theme(config_data: dict, theme: str) -> None:
|
||||
match theme:
|
||||
case "light":
|
||||
print("setting theme to light")
|
||||
update_setting(config_data, "theme", "light")
|
||||
case "dark":
|
||||
print("setting theme to dark")
|
||||
update_setting(config_data, "theme", "dark")
|
||||
case _:
|
||||
print("setting theme to system (default)")
|
||||
update_setting(config_data, "theme", "")
|
||||
|
||||
53
modules/tkey.py
Normal file
53
modules/tkey.py
Normal 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 |
@@ -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
|
||||
|
||||
@@ -31,15 +31,52 @@ class Ui_MainWindow(object):
|
||||
MainWindow.resize(1010, 675)
|
||||
MainWindow.setMinimumSize(QSize(1010, 675))
|
||||
MainWindow.setMaximumSize(QSize(1010, 675))
|
||||
self.actionAbout = QAction(MainWindow)
|
||||
self.actionAbout.setObjectName(u"actionAbout")
|
||||
self.action_about = QAction(MainWindow)
|
||||
self.action_about.setObjectName(u"action_about")
|
||||
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.HelpAbout))
|
||||
self.actionAbout.setIcon(icon)
|
||||
self.actionAbout.setMenuRole(QAction.MenuRole.ApplicationSpecificRole)
|
||||
self.actionAbout_Qt = QAction(MainWindow)
|
||||
self.actionAbout_Qt.setObjectName(u"actionAbout_Qt")
|
||||
self.actionAbout_Qt.setIcon(icon)
|
||||
self.actionAbout_Qt.setMenuRole(QAction.MenuRole.ApplicationSpecificRole)
|
||||
self.action_about.setIcon(icon)
|
||||
self.action_about.setMenuRole(QAction.MenuRole.ApplicationSpecificRole)
|
||||
self.action_about_qt = QAction(MainWindow)
|
||||
self.action_about_qt.setObjectName(u"action_about_qt")
|
||||
self.action_about_qt.setIcon(icon)
|
||||
self.action_about_qt.setMenuRole(QAction.MenuRole.ApplicationSpecificRole)
|
||||
self.action_language_system = QAction(MainWindow)
|
||||
self.action_language_system.setObjectName(u"action_language_system")
|
||||
self.action_language_system.setCheckable(True)
|
||||
self.action_language_system.setChecked(False)
|
||||
self.action_language_english = QAction(MainWindow)
|
||||
self.action_language_english.setObjectName(u"action_language_english")
|
||||
self.action_language_english.setCheckable(True)
|
||||
self.action_language_spanish = QAction(MainWindow)
|
||||
self.action_language_spanish.setObjectName(u"action_language_spanish")
|
||||
self.action_language_spanish.setCheckable(True)
|
||||
self.action_language_german = QAction(MainWindow)
|
||||
self.action_language_german.setObjectName(u"action_language_german")
|
||||
self.action_language_german.setCheckable(True)
|
||||
self.action_language_french = QAction(MainWindow)
|
||||
self.action_language_french.setObjectName(u"action_language_french")
|
||||
self.action_language_french.setCheckable(True)
|
||||
self.action_language_italian = QAction(MainWindow)
|
||||
self.action_language_italian.setObjectName(u"action_language_italian")
|
||||
self.action_language_italian.setCheckable(True)
|
||||
self.action_language_norwegian = QAction(MainWindow)
|
||||
self.action_language_norwegian.setObjectName(u"action_language_norwegian")
|
||||
self.action_language_norwegian.setCheckable(True)
|
||||
self.action_language_romanian = QAction(MainWindow)
|
||||
self.action_language_romanian.setObjectName(u"action_language_romanian")
|
||||
self.action_language_romanian.setCheckable(True)
|
||||
self.action_language_korean = QAction(MainWindow)
|
||||
self.action_language_korean.setObjectName(u"action_language_korean")
|
||||
self.action_language_korean.setCheckable(True)
|
||||
self.action_theme_system = QAction(MainWindow)
|
||||
self.action_theme_system.setObjectName(u"action_theme_system")
|
||||
self.action_theme_system.setCheckable(True)
|
||||
self.action_theme_light = QAction(MainWindow)
|
||||
self.action_theme_light.setObjectName(u"action_theme_light")
|
||||
self.action_theme_light.setCheckable(True)
|
||||
self.action_theme_dark = QAction(MainWindow)
|
||||
self.action_theme_dark.setObjectName(u"action_theme_dark")
|
||||
self.action_theme_dark.setCheckable(True)
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.horizontalLayout_3 = QHBoxLayout(self.centralwidget)
|
||||
@@ -308,10 +345,18 @@ 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)
|
||||
|
||||
@@ -319,14 +364,35 @@ class Ui_MainWindow(object):
|
||||
self.menubar = QMenuBar(MainWindow)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 1010, 21))
|
||||
self.menuHelp = QMenu(self.menubar)
|
||||
self.menuHelp.setObjectName(u"menuHelp")
|
||||
self.menu_help = QMenu(self.menubar)
|
||||
self.menu_help.setObjectName(u"menu_help")
|
||||
self.menu_options = QMenu(self.menubar)
|
||||
self.menu_options.setObjectName(u"menu_options")
|
||||
self.menu_options_language = QMenu(self.menu_options)
|
||||
self.menu_options_language.setObjectName(u"menu_options_language")
|
||||
self.menu_options_theme = QMenu(self.menu_options)
|
||||
self.menu_options_theme.setObjectName(u"menu_options_theme")
|
||||
MainWindow.setMenuBar(self.menubar)
|
||||
|
||||
self.menubar.addAction(self.menuHelp.menuAction())
|
||||
self.menuHelp.addAction(self.actionAbout)
|
||||
self.menuHelp.addAction(self.actionAbout_Qt)
|
||||
self.menuHelp.addSeparator()
|
||||
self.menubar.addAction(self.menu_options.menuAction())
|
||||
self.menubar.addAction(self.menu_help.menuAction())
|
||||
self.menu_help.addAction(self.action_about)
|
||||
self.menu_help.addAction(self.action_about_qt)
|
||||
self.menu_help.addSeparator()
|
||||
self.menu_options.addAction(self.menu_options_language.menuAction())
|
||||
self.menu_options.addAction(self.menu_options_theme.menuAction())
|
||||
self.menu_options_language.addAction(self.action_language_system)
|
||||
self.menu_options_language.addAction(self.action_language_english)
|
||||
self.menu_options_language.addAction(self.action_language_spanish)
|
||||
self.menu_options_language.addAction(self.action_language_german)
|
||||
self.menu_options_language.addAction(self.action_language_french)
|
||||
self.menu_options_language.addAction(self.action_language_italian)
|
||||
self.menu_options_language.addAction(self.action_language_norwegian)
|
||||
self.menu_options_language.addAction(self.action_language_romanian)
|
||||
self.menu_options_language.addAction(self.action_language_korean)
|
||||
self.menu_options_theme.addAction(self.action_theme_system)
|
||||
self.menu_options_theme.addAction(self.action_theme_light)
|
||||
self.menu_options_theme.addAction(self.action_theme_dark)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
|
||||
@@ -339,8 +405,20 @@ class Ui_MainWindow(object):
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
|
||||
self.actionAbout.setText(QCoreApplication.translate("MainWindow", u"About NUSGet", None))
|
||||
self.actionAbout_Qt.setText(QCoreApplication.translate("MainWindow", u"About Qt", None))
|
||||
self.action_about.setText(QCoreApplication.translate("MainWindow", u"About NUSGet", None))
|
||||
self.action_about_qt.setText(QCoreApplication.translate("MainWindow", u"About Qt", None))
|
||||
self.action_language_system.setText(QCoreApplication.translate("MainWindow", u"System (Default)", None))
|
||||
self.action_language_english.setText(QCoreApplication.translate("MainWindow", u"English", None))
|
||||
self.action_language_spanish.setText(QCoreApplication.translate("MainWindow", u"Espa\u00f1ol", None))
|
||||
self.action_language_german.setText(QCoreApplication.translate("MainWindow", u"Deutsch", None))
|
||||
self.action_language_french.setText(QCoreApplication.translate("MainWindow", u"Fran\u00e7ais", None))
|
||||
self.action_language_italian.setText(QCoreApplication.translate("MainWindow", u"Italiano", None))
|
||||
self.action_language_norwegian.setText(QCoreApplication.translate("MainWindow", u"Norsk", None))
|
||||
self.action_language_romanian.setText(QCoreApplication.translate("MainWindow", u"Rom\u00e2n\u0103", None))
|
||||
self.action_language_korean.setText(QCoreApplication.translate("MainWindow", u"\ud55c\uad6d\uc5b4", None))
|
||||
self.action_theme_system.setText(QCoreApplication.translate("MainWindow", u"System (Default)", None))
|
||||
self.action_theme_light.setText(QCoreApplication.translate("MainWindow", u"Light", None))
|
||||
self.action_theme_dark.setText(QCoreApplication.translate("MainWindow", u"Dark", None))
|
||||
self.tree_filter_input.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Search", None))
|
||||
self.tree_filter_reset_btn.setText(QCoreApplication.translate("MainWindow", u"Clear", None))
|
||||
self.platform_tabs.setTabText(self.platform_tabs.indexOf(self.wii_tab), QCoreApplication.translate("MainWindow", u"Wii", None))
|
||||
@@ -369,6 +447,9 @@ class Ui_MainWindow(object):
|
||||
"li.checked::marker { content: \"\\2612\"; }\n"
|
||||
"</style></head><body style=\" font-family:'.AppleSystemUIFont'; font-size:13pt; 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))
|
||||
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None))
|
||||
self.menu_help.setTitle(QCoreApplication.translate("MainWindow", u"Help", None))
|
||||
self.menu_options.setTitle(QCoreApplication.translate("MainWindow", u"Options", None))
|
||||
self.menu_options_language.setTitle(QCoreApplication.translate("MainWindow", u"Language", None))
|
||||
self.menu_options_theme.setTitle(QCoreApplication.translate("MainWindow", u"Theme", None))
|
||||
# retranslateUi
|
||||
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>247</height>
|
||||
<height>222</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="markdown">
|
||||
@@ -440,6 +440,25 @@ li.checked::marker { content: "\2612"; }
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</item>
|
||||
</layout>
|
||||
@@ -453,17 +472,47 @@ li.checked::marker { content: "\2612"; }
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
<widget class="QMenu" name="menu_help">
|
||||
<property name="title">
|
||||
<string>Help</string>
|
||||
</property>
|
||||
<addaction name="actionAbout"/>
|
||||
<addaction name="actionAbout_Qt"/>
|
||||
<addaction name="action_about"/>
|
||||
<addaction name="action_about_qt"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<addaction name="menuHelp"/>
|
||||
<widget class="QMenu" name="menu_options">
|
||||
<property name="title">
|
||||
<string>Options</string>
|
||||
</property>
|
||||
<widget class="QMenu" name="menu_options_language">
|
||||
<property name="title">
|
||||
<string>Language</string>
|
||||
</property>
|
||||
<addaction name="action_language_system"/>
|
||||
<addaction name="action_language_english"/>
|
||||
<addaction name="action_language_spanish"/>
|
||||
<addaction name="action_language_german"/>
|
||||
<addaction name="action_language_french"/>
|
||||
<addaction name="action_language_italian"/>
|
||||
<addaction name="action_language_norwegian"/>
|
||||
<addaction name="action_language_romanian"/>
|
||||
<addaction name="action_language_korean"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_options_theme">
|
||||
<property name="title">
|
||||
<string>Theme</string>
|
||||
</property>
|
||||
<addaction name="action_theme_system"/>
|
||||
<addaction name="action_theme_light"/>
|
||||
<addaction name="action_theme_dark"/>
|
||||
</widget>
|
||||
<addaction name="menu_options_language"/>
|
||||
<addaction name="menu_options_theme"/>
|
||||
</widget>
|
||||
<addaction name="menu_options"/>
|
||||
<addaction name="menu_help"/>
|
||||
</widget>
|
||||
<action name="actionAbout">
|
||||
<action name="action_about">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::HelpAbout"/>
|
||||
</property>
|
||||
@@ -474,7 +523,7 @@ li.checked::marker { content: "\2612"; }
|
||||
<enum>QAction::MenuRole::ApplicationSpecificRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAbout_Qt">
|
||||
<action name="action_about_qt">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::HelpAbout"/>
|
||||
</property>
|
||||
@@ -485,6 +534,105 @@ li.checked::marker { content: "\2612"; }
|
||||
<enum>QAction::MenuRole::ApplicationSpecificRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_system">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>System (Default)</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_english">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>English</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_spanish">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Español</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_german">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Deutsch</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_french">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Français</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_italian">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Italiano</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_norwegian">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Norsk</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_romanian">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Română</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_language_korean">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>한국어</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_theme_system">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>System (Default)</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_theme_light">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Light</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_theme_dark">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Dark</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pyside6
|
||||
nuitka~=2.6.0
|
||||
libWiiPy
|
||||
git+https://github.com/NinjaCheetah/libWiiPy
|
||||
libTWLPy
|
||||
zstandard
|
||||
requests
|
||||
imageio
|
||||
imageio
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
@@ -47,7 +47,7 @@ QMenuBar::item:selected {
|
||||
}
|
||||
|
||||
QMenuBar::item:pressed {
|
||||
background-color: #1a73e8;
|
||||
background-color: #6c1ae8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -61,14 +61,14 @@ QMenu {
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
padding: 6px 2px;
|
||||
padding: 6px 16px 6px 4px;
|
||||
margin: 2px;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: #1a73e8;
|
||||
background-color: #6c1ae8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -88,13 +88,13 @@ QRadioButton {
|
||||
|
||||
QRadioButton:hover {
|
||||
background-color: rgba(60, 60, 60, 1);
|
||||
border-color: #4a86e8;
|
||||
border-color: #9c4ae8;
|
||||
}
|
||||
|
||||
QRadioButton:checked {
|
||||
background-color: rgba(26, 115, 232, 0.08);
|
||||
border: 1px solid #1a73e8;
|
||||
color: #1a73e8;
|
||||
border: 1px solid #6c1ae8;
|
||||
color: #6c1ae8;
|
||||
}
|
||||
|
||||
QRadioButton::indicator {
|
||||
@@ -107,13 +107,13 @@ QRadioButton::indicator {
|
||||
}
|
||||
|
||||
QRadioButton::indicator:checked {
|
||||
background-color: #1a73e8;
|
||||
border: 1px solid #1a73e8;
|
||||
background-color: #6c1ae8;
|
||||
border: 1px solid #6c1ae8;
|
||||
image: url("{IMAGE_PREFIX}/rounded_square.svg");
|
||||
}
|
||||
|
||||
QRadioButton::indicator:hover {
|
||||
border-color: #1a73e8;
|
||||
border-color: #6c1ae8;
|
||||
}
|
||||
|
||||
QLineEdit {
|
||||
@@ -124,11 +124,11 @@ QLineEdit {
|
||||
margin: 4px 0px;
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
selection-background-color: #1a73e8;
|
||||
selection-background-color: #6c1ae8;
|
||||
}
|
||||
|
||||
QLineEdit:focus {
|
||||
border-color: #1a73e8;
|
||||
border-color: #6c1ae8;
|
||||
}
|
||||
|
||||
QLineEdit:disabled {
|
||||
@@ -187,11 +187,11 @@ QTreeView::item:hover {
|
||||
}
|
||||
|
||||
QTreeView::item:focus {
|
||||
background-color: rgba(26, 115, 232, 0.08);
|
||||
background-color: rgba(64, 26, 232, 0.15);
|
||||
}
|
||||
|
||||
QTreeView::item:selected {
|
||||
background-color: #1a73e8;
|
||||
background-color: #6c1ae8;
|
||||
}
|
||||
|
||||
QTreeView QScrollBar:vertical {
|
||||
@@ -211,7 +211,7 @@ QTreeView::branch:open:has-children:has-siblings {
|
||||
QTextBrowser {
|
||||
color: white;
|
||||
background-color: #1a1a1a;
|
||||
selection-background-color: #1a73e8;
|
||||
selection-background-color: #6c1ae8;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
@@ -229,17 +229,17 @@ QPushButton {
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: rgba(60, 60, 60, 1);
|
||||
border-color: #4a86e8;
|
||||
border-color: #9c4ae8;
|
||||
}
|
||||
|
||||
QPushButton:focus {
|
||||
background-color: rgba(60, 60, 60, 1);
|
||||
border-color: #4a86e8;
|
||||
border-color: #9c4ae8;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: rgba(26, 115, 232, 0.15);
|
||||
border: 1px solid #1a73e8;
|
||||
background-color: rgba(64, 26, 232, 0.15);
|
||||
border: 1px solid #6c1ae8;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
@@ -261,18 +261,18 @@ QComboBox {
|
||||
}
|
||||
|
||||
QComboBox:on {
|
||||
background-color: rgba(26, 115, 232, 0.15);
|
||||
border: 1px solid #1a73e8;
|
||||
background-color: rgba(64, 26, 232, 0.15);
|
||||
border: 1px solid #6c1ae8;
|
||||
}
|
||||
|
||||
QComboBox:hover {
|
||||
background-color: rgba(60, 60, 60, 1);
|
||||
border-color: #4a86e8;
|
||||
border-color: #9c4ae8;
|
||||
}
|
||||
|
||||
QComboBox:focus {
|
||||
background-color: rgba(60, 60, 60, 1);
|
||||
border-color: #4a86e8;
|
||||
border-color: #9c4ae8;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
@@ -301,7 +301,7 @@ QComboBox QAbstractItemView::item {
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView::item:hover {
|
||||
background-color: #1a73e8;
|
||||
background-color: #6c1ae8;
|
||||
}
|
||||
|
||||
QScrollBar:vertical {
|
||||
@@ -320,7 +320,7 @@ QScrollBar::handle:vertical {
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background-color: rgba(26, 115, 232, 0.4);
|
||||
background-color: rgba(71, 26, 232, 0.4);
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical {
|
||||
@@ -350,7 +350,7 @@ QScrollBar::handle:horizontal {
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background-color: rgba(26, 115, 232, 0.4);
|
||||
background-color: rgba(71, 26, 232, 0.4);
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal {
|
||||
@@ -369,6 +369,24 @@ QMessageBox QLabel {
|
||||
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 {
|
||||
show-decoration-selected: 1;
|
||||
outline: 0;
|
||||
@@ -383,7 +401,7 @@ WrapCheckboxWidget {
|
||||
|
||||
WrapCheckboxWidget:hover {
|
||||
background-color: rgba(60, 60, 60, 1);
|
||||
border-color: #4a86e8;
|
||||
border-color: #9c4ae8;
|
||||
}
|
||||
|
||||
WrapCheckboxWidget:disabled {
|
||||
@@ -403,16 +421,20 @@ WrapCheckboxWidget QCheckBox::indicator {
|
||||
border: 1px solid #5f6368;
|
||||
}
|
||||
|
||||
WrapCheckboxWidget QCheckBox::indicator::focus {
|
||||
background-color: rgba(64, 26, 232, 0.15);
|
||||
}
|
||||
|
||||
WrapCheckboxWidget QCheckBox::indicator:checked {
|
||||
background-color: #1a73e8;
|
||||
border: 1px solid #1a73e8;
|
||||
background-color: #6c1ae8;
|
||||
border: 1px solid #6c1ae8;
|
||||
image: url("{IMAGE_PREFIX}/check.svg");
|
||||
}
|
||||
|
||||
WrapCheckboxWidget QCheckBox::indicator:hover {
|
||||
border-color: #1a73e8;
|
||||
border-color: #6c1ae8;
|
||||
}
|
||||
|
||||
WrapCheckboxWidget QCheckBox:checked {
|
||||
color: #1a73e8;
|
||||
color: #6c1ae8;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ QMenu {
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
padding: 6px 2px;
|
||||
padding: 6px 16px 6px 4px;
|
||||
margin: 2px;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
@@ -377,6 +377,24 @@ QMessageBox QLabel {
|
||||
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 {
|
||||
show-decoration-selected: 1;
|
||||
outline: 0;
|
||||
|
||||
Reference in New Issue
Block a user