diff --git a/README.md b/README.md index 34faefa..3915311 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -# Bitcoin Improvement Proposal - 0038 / BIP38 +

+ BIP38
Bitcoin Improvement Proposal - 0038 +

+ +

+ Releases · Manual · API Docs · Donation +

+ +
[![Build Status](https://img.shields.io/github/actions/workflow/status/meherett/python-bip38/build.yml)](https://github.com/meherett/python-bip38/actions/workflows/build.yml) [![PyPI Version](https://img.shields.io/pypi/v/bip38.svg?color=blue)](https://pypi.org/project/bip38) @@ -7,10 +15,14 @@ [![PyPI Python Version](https://img.shields.io/pypi/pyversions/bip38.svg)](https://pypi.org/project/bip38) [![Coverage Status](https://coveralls.io/repos/github/meherett/python-bip38/badge.svg?branch=master)](https://coveralls.io/github/meherett/python-bip38) +
+ A Python library for the implementation of Bitcoin Improvement Proposal - 0038 / (BIP38) protocol. This library supports both [No EC-multiply](https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki#encryption-when-ec-multiply-flag-is-not-used) and [EC-multiply](https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki#encryption-when-ec-multiply-mode-is-used) modes and is compatible with over 150+ cryptocurrencies. It's specifically tailored for Pay-to-PubKey-Hash (P2PKH) address types. +![Desktop Application](docs/static/gif/bip38.gif) + For more info see the [Passphrase-protected private key - BIP38](https://en.bitcoin.it/wiki/BIP_0038) spec. ## Installation @@ -417,7 +429,7 @@ BIP38 Decrypted: { To get started, just fork this repo, clone it locally, and run: ``` -pip install -e .[tests,docs] +pip install -e .[desktop,tests,docs] ``` ## Testing @@ -450,9 +462,8 @@ This module supports more than 150+ cryptocurrencies, including the following: Buy me a coffee if You found this tool helpful: -- **Bitcoin** - 12uaGVdX1t86FXLQ4yYPrRQDCK7xGGu82r -- **Ethereum / Tether** - 0xCCAad7A87fd81553d0F93F743Fb4Fc6B213b228B -- **Bitcoin / Ethereum / Tether** - With Unstoppable [hd.wallet](https://ud.me/hd.wallet) +- **Bitcoin** - 16c7ajUwHEMaafrceuYSrd35SDjmfVdjoS +- **Ethereum / ERC20** - 0xD3cbCB0B6F82A03C715D665b72dC44CEf54e6D9B Thank you very much for your support. diff --git a/build.py b/build.py new file mode 100644 index 0000000..b646cb0 --- /dev/null +++ b/build.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +# Copyright © 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +from cx_Freeze import setup, Executable + +import platform + +from desktop.info import __version__ as version +from bip38.info import __author__ as author + +app_name = "BIP38" + +if platform.system() == "Windows": + icon_path = "desktop/ui/images/icon/icon.ico" +else: + icon_path = "desktop/ui/images/svg/logo.svg" + +msi_shortcut_table = [ + ( + "DesktopShortcut", # Shortcut + "DesktopFolder", # Directory_ + app_name, # Name that will be show on the link + "TARGETDIR", # Component_ + f"[TARGETDIR]{app_name}.exe", # Target exe to exexute + None, # Arguments + None, # Description + None, # Hotkey + None, # Icon + None, # IconIndex + None, # ShowCmd + 'TARGETDIR' # WkDir + ) +] + +msi_directory_table = [ + ("ProgramMenuFolder", "TARGETDIR", "."), + ("BIP38Menu", "ProgramMenuFolder", "BIP38~1|BIP38") +] + +msi_data = { + "Shortcut": msi_shortcut_table, + "Directory": msi_directory_table +} + +bdist_msi_opt = { + "add_to_path": False, + "data": msi_data, + "initial_target_dir": f"[ProgramFiles64Folder]\\{app_name}", + "install_icon": icon_path, + "upgrade_code": "{E4A369F6-FC76-3013-A420-3BB9B370711C}" +} + +build_exe_opt = { + "packages": ["_scrypt"], + "excludes": ["tkinter", "PySide6.QtNetwork", "PySide6.translations"], + "bin_excludes": [ + "Qt6Network.dll", "Qt6OpenGL.dll", "Qt6Pdf.dll", "Qt6Qml.dll", "Qt6QmlMeta.dll", + "Qt6QmlModels.dll", "Qt6QmlWorkerScript.dll", "Qt6Quick.dll", "Qt6VirtualKeyboard.dll", + "qgif.dll", "qicns.dll", "qjpeg.dll", "qpdf.dll", "qtga.dll", "qtiff.dll", "qwbmp.dll", "qwebp.dll", + "qtvirtualkeyboardplugin.dll", "qtuiotouchplugin.dll", "qdirect2d.dll", "qoffscreen.dll", "qminimal.dll" + ], + "include_msvcr": True +} + +executables = [ + Executable( + "launch.py", + base="gui", + icon=icon_path, + target_name=app_name, + shortcut_name=app_name, + shortcut_dir="BIP38Menu", + copyright=f"Copyright (C) 2024 {app_name}" + ) +] + +setup( + name=app_name, + author=author, + version=version, + executables=executables, + options={ + "build_exe": build_exe_opt, + "bdist_msi": bdist_msi_opt + } +) \ No newline at end of file diff --git a/desktop/__init__.py b/desktop/__init__.py new file mode 100644 index 0000000..b6e3e62 --- /dev/null +++ b/desktop/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +# Copyright © 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit \ No newline at end of file diff --git a/desktop/core.py b/desktop/core.py new file mode 100644 index 0000000..a79ab2f --- /dev/null +++ b/desktop/core.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +# Copyright © 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +from typing import Optional + +import json + +from PySide6.QtWidgets import ( + QMainWindow, QWidget, QStackedWidget, + QFrame, QComboBox +) +from PySide6.QtCore import ( + Qt, QFileSystemWatcher, Signal, QSize +) +from PySide6.QtGui import ( + QFontDatabase, QIcon +) + +from desktop.utils import ( + resolve_path, put_svg +) +from desktop.ui.ui_bip38 import Ui_MainWindow +from desktop.info import __version__ as desktop_ver +from bip38.info import __version__ as library_ver + +class Application(QMainWindow): + _instance: Optional['Application'] = None + ui: Ui_MainWindow = None + theme_watcher: QFileSystemWatcher = None + resized: Signal = Signal(object) + + def __new__(cls, *args, **kwargs) -> 'Application': + """ + Create a new instance if not already created, implementing the Singleton pattern. + """ + if not cls._instance: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self) -> None: + """ + Initialize the Application instance if not already initialized. + """ + if not hasattr(self, 'initialized'): + super().__init__() + self.initialize() + self.initialized = True + + @classmethod + def instance(cls) -> 'Application': + """ + Get the singleton instance of the Application. + + :return: The singleton instance of Application. + """ + if not cls._instance: + cls._instance = cls() + return cls._instance + + def initialize(self) -> None: + """ + Perform initialization tasks for the application, such as setting up the UI and loading resources. + """ + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + self.detached_window = None + + self.setWindowTitle("Bitcoin Improvement Proposal - 0038") + self.bip38_icon = QIcon(resolve_path("desktop/ui/images/icon/icon.ico")) + self.setWindowIcon(self.bip38_icon) + + put_svg( + self.ui.bip38LogoHLayout, + resolve_path("desktop/ui/images/svg/full-logo.svg"), + 84, + 44 + ) + self.ui.bip38LogoHLayout.setContentsMargins(0, 0, 5, 0) + + css_path = resolve_path("desktop/ui/css/theme.css") + self.theme_watcher = QFileSystemWatcher([css_path]) + self.theme_watcher.fileChanged.connect(lambda: self.load_stylesheet(css_path)) + QFontDatabase.addApplicationFont(resolve_path("desktop/ui/font/HDWallet.ttf")) + self.load_stylesheet(css_path) + + info = { + "library": library_ver, + "desktop": desktop_ver + } + self.ui.outputQTextEdit.setPlaceholderText(json.dumps(info, indent=4)) + + # remove combo box shadow + for qComboBox in self.findChildren(QComboBox): + qComboBox.findChild(QFrame).setWindowFlags(Qt.Popup | Qt.NoDropShadowWindowHint) + + def load_stylesheet(self, path: str) -> None: + """ + Load and apply a stylesheet from the specified path. + + :param path: The path to the stylesheet file. + """ + try: + with open(path, 'r', encoding='utf-8') as style_file: + stylesheet = style_file.read() + self.setStyleSheet(stylesheet) + except Exception as e: + print(f"Failed to load stylesheet: {e}") + + def resizeEvent(self, event) -> None: + super().resizeEvent(event) + self.resized.emit(event) \ No newline at end of file diff --git a/desktop/info.py b/desktop/info.py new file mode 100644 index 0000000..1b3339d --- /dev/null +++ b/desktop/info.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +# Copyright © 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +__version__: str = "v0.1.0" \ No newline at end of file diff --git a/desktop/main.py b/desktop/main.py new file mode 100644 index 0000000..db54c26 --- /dev/null +++ b/desktop/main.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 + +# Copyright © 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +import re +import os +import json +import inspect +import functools +from typing import ( + Optional, Union +) + +from PySide6.QtCore import QRegularExpression +from PySide6.QtWidgets import ( + QWidget, QPushButton +) +from PySide6.QtGui import ( + QTextCharFormat, QTextCursor, QColor, + QTextFormat, QTextOption, QRegularExpressionValidator +) + +from bip38 import ( + cryptocurrencies, BIP38 +) +from bip38.exceptions import ( + Error, PassphraseError, WIFError +) +from bip38.cryptocurrencies import ICryptocurrency +from bip38.wif import ( + get_wif_type, private_key_to_wif +) + +from desktop.core import Application + + +str_to_int = lambda s: int(s) if s.isdigit() else None + +class BIP38Application: + """ + Main application class for managing the UI and core functionalities. + """ + TEXT_COLOR: QColor = QColor(255, 255, 255) + ERROR_COLOR: QColor = QColor(255, 96, 96) + JSON_PATTERNS = [ + (re.compile(r'\".*?\"(?=\s*:)'), QColor(101, 97, 156)), # keys + (re.compile(r'\".*?\"'), QColor(255, 255, 255)), # string + (re.compile(r'\b\d+\b'), QColor(125, 211, 252)), # numbers + (re.compile(r'\btrue\b|\bfalse\b|\bnull\b'), QColor(21, 128, 61)), # boolean/null + (re.compile(r'[{}[\],:]'), QColor(255, 255, 255)) # punctuation + ] + + def __init__(self) -> None: + """ + Initialize the MainApplication class. + """ + super().__init__() + + self.app: Application = Application.instance() + self.ui = self.app.ui + + # bip38 cryptocurrencies {name: class} dict + self.cryptocurrencies: dict = { + name: cls for name, cls in inspect.getmembers(cryptocurrencies, inspect.isclass) + if issubclass(cls, ICryptocurrency) + } + del self.cryptocurrencies["ICryptocurrency"] # Remove Interface class from the dict + + self.modes: dict = { + "No EC-Multiply": self.ui.noECQWidget, + "EC-Multiply": self.ui.ecQWidget, + } + + self.wif_types = ["wif", "wif-compressed"] + + self.inputs = { + "Passphrase": { + "input": self.ui.passphraseQLineEdit, + "optional": False, # cant be empty + "min_length": 1 + }, + "Private Key": { + "input": self.ui.noECPrivateKeyQLineEdit, + "validator": self.regex_validator("^[0-9A-Fa-f]{1,64}$"), # hex only + "optional": False, + "min_length": 64 + }, + "WIF": { + "input": self.ui.noECWIFQLineEdit, + "validator": self.regex_validator("^[0-9A-Za-z]{1,52}$"), # alpha num only + "optional": False, + "min_length": 51 + }, + "Owner Salt": { + "input": self.ui.ecOwnerSaltQLineEdit, + "validator": self.regex_validator("^[0-9A-Fa-f]{1,16}$"), # hex only + "optional": False, + "min_length": 16 + }, + "Seed": { + "input": self.ui.ecSeedQLineEdit, + "validator": self.regex_validator("^[0-9A-Fa-f]{1,48}$"), # hex only + "optional": False, + "min_length": 48 + }, + "Lot": { + "input": self.ui.ecLotQLineEdit, + "optional": True, + "min_length": 6 + }, + "Sequence": { + "input": self.ui.ecSequenceQLineEdit, + "optional": True, + "min_length": 1 + }, + "Intermediate Passphrase": { + "input": self.ui.ecIPassphraseQLineEdit, + "validator": self.regex_validator("^passphrase[0-9A-Za-z]{1,62}$"), # alpha num only with passphrase prefix + "optional": False, + "min_length": 72 + }, + "Confirmation Code": { + "input": self.ui.ecConfirmCodeQLineEdit, + "validator": self.regex_validator("^cfrm[0-9A-Za-z]{1,71}$"), # alpha num only with passphrase cfrm + "optional": False, + "min_length": 75 + }, + "Encrypted WIF": { + "input": self.ui.decryptWIFQLineEdit, + "validator": self.regex_validator("^[0-9A-Za-z]{1,58}$"), # alpha num only + "optional": False, + "min_length": 58 + } + } + + self.__init_ui() + + def __init_ui(self) -> None: + """ + Initialize the UI components and their connections. + """ + + self.ui.outputQTextEdit.setWordWrapMode(QTextOption.NoWrap) + + ## populate and bindings + self.ui.cryptocurrencyQComboBox.addItems( + sorted(self.cryptocurrencies.keys(), key=str.casefold) + ) + self.ui.cryptocurrencyQComboBox.currentTextChanged.connect(self.change_cryptocurrency) + + self.ui.modeQComboBox.addItems( + self.modes.keys() + ) + self.ui.modeQComboBox.currentTextChanged.connect(self.change_mode) + + self.ui.noECWIFTypeQComboBox.addItems(self.wif_types) + self.ui.noECWIFTypeQComboBox.setCurrentIndex(0) + + self.ui.createEncryptedWIFTypeQComboBox.addItems(self.wif_types) + self.ui.createEncryptedWIFTypeQComboBox.setCurrentIndex(0) + + # validation stuff + + for key, item in self.inputs.items(): + qt_input = item["input"] + + if "validator" in item: + qt_input.setValidator(item["validator"]) + qt_input.textChanged.connect(functools.partial(self.validate_input, key)) + + self.ui.ecLotQLineEdit.textEdited.connect( + lambda: self.validate_int(self.ui.ecLotQLineEdit) + ) + self.ui.ecLotQLineEdit.editingFinished.connect( + lambda: self.enforce_int_range(self.ui.ecLotQLineEdit, 100000, 999999) + ) + + self.ui.ecSequenceQLineEdit.textEdited.connect( + lambda: self.validate_int(self.ui.ecSequenceQLineEdit) + ) + self.ui.ecSequenceQLineEdit.editingFinished.connect( + lambda: self.enforce_int_range(self.ui.ecSequenceQLineEdit, 0, 4095) + ) + + # button bindings + self.ui.noECPrivateKeyConvertQPushButton.clicked.connect(self.noec_convert_private_key) + self.ui.noECEncryptQPushButton.clicked.connect(self.noec_encrypt) + + self.ui.ecOwnerSaltGenerateQPushButton.clicked.connect( + lambda: self.ui.ecOwnerSaltQLineEdit.setText( + os.urandom(8).hex() + ) + ) + self.ui.ecSeedGenerateQPushButton.clicked.connect( + lambda: self.ui.ecSeedQLineEdit.setText( + os.urandom(24).hex() + ) + ) + + self.ui.ecIPassphraseGenerateQPushButton.clicked.connect(self.ec_generate_ipassphrase) + self.ui.ecConfirmCodeVerifyQPushButton.clicked.connect(self.ec_confirm_code) + self.ui.createEncryptedWIFQPushButton.clicked.connect(self.create_encrypted_wif) + + self.ui.decryptWIFQPushButton.clicked.connect(self.decrypt) + + ## setting default values + + # will update network combo too + self.ui.cryptocurrencyQComboBox.setCurrentText("Bitcoin") + self.ui.modeQComboBox.setCurrentIndex(0) + + # Custom components + + self.clear_button = QPushButton("Clear", self.app) + self.clear_button.setFixedWidth(60) + self.clear_button.clicked.connect(lambda: self.ui.outputQTextEdit.setText(None)) + self.clear_button.clicked.connect(self.clean_all_required) + + self.app.resized.connect(self.update_clear_button_position) + self.ui.outputQTextEdit.verticalScrollBar().rangeChanged.connect(self.update_clear_button_position) + + def update_clear_button_position(self) -> None: + button_width = self.clear_button.sizeHint().width() + button_height = self.clear_button.sizeHint().height() + window_width = self.app.width() + window_height = self.app.height() + + right_margin = 15 + bottom_margin = 10 + right_margin += 5 if self.ui.outputQTextEdit.verticalScrollBar().maximum() > 0 else 0 + + self.clear_button.move(window_width - button_width - right_margin, window_height - button_height - bottom_margin) + + def change_cryptocurrency(self, cryptocurrency: str) -> None: + cryptocurrency_class: ICryptocurrency = self.cryptocurrencies[cryptocurrency] + + self.ui.networkQComboBox.clear() + self.ui.networkQComboBox.addItems( + [network.title() for network in cryptocurrency_class.NETWORKS.keys()] + ) + self.ui.networkQComboBox.setCurrentIndex(0) + + def change_mode(self, mode: str) -> None: + is_on_ec = "EC-Multiply" == mode + self.ui.decryptWIFTypeContainerQFrame.setVisible(is_on_ec) + self.ui.createEncryptedWIFQPushButton.setVisible(is_on_ec) + self.ui.modeQStackedWidget.setCurrentWidget(self.modes[mode]) + self.clean_all_required() + + def cryptocurrency(self): + cryptocurrency_name: str = self.ui.cryptocurrencyQComboBox.currentText() + cryptocurrency: ICryptocurrency = self.cryptocurrencies[cryptocurrency_name] + return cryptocurrency + + def network(self): + return self.ui.networkQComboBox.currentText().lower() + + + def bip38(self): + return BIP38( + cryptocurrency=self.cryptocurrency(), + network=self.network() + ) + + def noec_convert_private_key(self): + try: + private_key, = self.validate_and_get("Private Key") + + wif: str = private_key_to_wif( + private_key=private_key, + cryptocurrency=self.cryptocurrency(), + network=self.network(), + wif_type=self.ui.noECWIFTypeQComboBox.currentText() + ) + self.ui.noECWIFQLineEdit.setText(wif) + + output = { + "cryptocurrency": self.ui.cryptocurrencyQComboBox.currentText(), + "network": self.network(), + "wif_type": self.ui.noECWIFTypeQComboBox.currentText(), + "private_key": private_key, + "wif": wif + } + self.log(output) + + except (Error, BIP38Application.ValidationError) as e: + self.logerr(e) + + def noec_encrypt(self): + try: + wif, passphrase = self.validate_and_get("WIF", "Passphrase") + + encrypted_wif: str = self.bip38().encrypt( + wif=wif, + passphrase=passphrase + ) + self.ui.decryptWIFQLineEdit.setText(encrypted_wif) + + wif_type = get_wif_type(wif=wif, cryptocurrency=self.cryptocurrency(), network=self.network()) + + output = { + "cryptocurrency": self.ui.cryptocurrencyQComboBox.currentText(), + "network": self.network(), + "passphrase": passphrase, + "wif": wif, + "wif_type": wif_type, + "encrypted_wif": encrypted_wif + } + self.log(output) + except WIFError as we: + self.set_required(self.ui.noECWIFQLineEdit, True) + self.logerr(we) + except (Error, BIP38Application.ValidationError) as e: + self.logerr(e) + + def ec_generate_ipassphrase(self): + try: + + passphrase, owner_salt, lot, sequence = self.validate_and_get( + "Passphrase", "Owner Salt", "Lot", "Sequence" + ) + + lot: Optional[int] = str_to_int(lot) + sequence: Optional[int] = str_to_int(sequence) + + if (lot and sequence is None) or (lot is None and sequence): + self.set_required(self.ui.ecLotQLineEdit, True) + self.set_required(self.ui.ecSequenceQLineEdit, True) + raise BIP38Application.ValidationError("Lot and Sequence must both be set or both left blank.") + + intermediate_passphrase: str = self.bip38().intermediate_code( + passphrase=passphrase, + owner_salt=owner_salt, + lot=lot, + sequence=sequence + ) + self.ui.ecIPassphraseQLineEdit.setText(intermediate_passphrase) + + output = { + "cryptocurrency": self.ui.cryptocurrencyQComboBox.currentText(), + "network": self.network(), + "passphrase": passphrase, + "owner_salt": owner_salt, + "lot": lot, + "sequence": sequence, + "intermediate_passphrase": intermediate_passphrase + } + self.log(output) + except (Error, BIP38Application.ValidationError) as e: + self.logerr(e) + + def ec_confirm_code(self): + try: + passphrase, confirmation_code = self.validate_and_get( + "Passphrase", "Confirmation Code" + ) + + confirmation_code = self.bip38().confirm_code( + passphrase=passphrase, + confirmation_code=confirmation_code, + detail=True + ) + self.log(confirmation_code) + + except PassphraseError as pe: + self.set_required(self.ui.passphraseQLineEdit, True) + self.logerr(pe) + except Error as e: + self.set_required(self.ui.ecConfirmCodeQLineEdit, True) + self.logerr(e) + except BIP38Application.ValidationError as ve: + self.logerr(ve) + + def create_encrypted_wif(self): + try: + seed, intermediate_passphrase = self.validate_and_get( + "Seed", "Intermediate Passphrase" + ) + + wif_type: str = self.ui.createEncryptedWIFTypeQComboBox.currentText() + + encrypted_wif: str = self.bip38().create_new_encrypted_wif( + intermediate_passphrase=intermediate_passphrase, + wif_type=wif_type, + seed=seed, + ) + self.ui.decryptWIFQLineEdit.setText(encrypted_wif["encrypted_wif"]) + self.log(encrypted_wif) + + except PassphraseError as pe: + self.set_required(self.ui.ecIPassphraseQLineEdit, True) + self.logerr(pe) + except (Error, BIP38Application.ValidationError) as e: + self.logerr(e) + + def decrypt(self): + try: + encrypted_wif, passphrase = self.validate_and_get("Encrypted WIF", "Passphrase") + decrypted_wif: dict = self.bip38().decrypt( + encrypted_wif=encrypted_wif, + passphrase=passphrase, + detail=True + ) + self.log(decrypted_wif) + except WIFError as we: + self.set_required(self.ui.decryptWIFQLineEdit, True) + self.logerr(we) + except PassphraseError as pe: + self.set_required(self.ui.passphraseQLineEdit, True) + self.logerr(pe) + except (Error, BIP38Application.ValidationError) as e: + self.logerr(e) + + def log(self, data: Optional[Union[str, dict]], end="\n") -> None: + if isinstance(data, dict): + data = json.dumps(data, indent=4) + + cursor = self.ui.outputQTextEdit.textCursor() + cursor.movePosition(QTextCursor.End) + + default_format = QTextCharFormat() + default_format.setForeground(BIP38Application.TEXT_COLOR) + + # Apply highlighting + for line in data.splitlines(): + pos = 0 + while pos < len(line): + match_found = False + for pattern, color in BIP38Application.JSON_PATTERNS: + match = pattern.match(line, pos) + if match: + cformat = QTextCharFormat() + cformat.setForeground(color) + cursor.insertText(match.group(), cformat) + pos = match.end() + match_found = True + break + if not match_found: + cursor.insertText(line[pos], default_format) + pos += 1 + cursor.insertText(end, default_format) + + self.ui.outputQTextEdit.setTextCursor(cursor) + self.ui.outputQTextEdit.ensureCursorVisible() + + def logerr(self, err: str , end: str="\n") -> None: + cformat = QTextCharFormat() + cformat.setForeground(BIP38Application.ERROR_COLOR) + + default_format = QTextCharFormat() + default_format.setForeground(BIP38Application.TEXT_COLOR) + + cursor = self.ui.outputQTextEdit.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.insertText("ERROR", cformat) + cursor.insertText(f": {err}{end}", default_format) + + self.ui.outputQTextEdit.setTextCursor(cursor) + self.ui.outputQTextEdit.ensureCursorVisible() + + ## validation related functions + + class ValidationError(Exception): + def __init__(self, message): + super().__init__(message) + + def format_required(self, invalid_inputs): + if len(invalid_inputs) == 1: + return f"{invalid_inputs[0]} is required" + if len(invalid_inputs) == 2: + return f"{invalid_inputs[0]} and {invalid_inputs[1]} are required" + + return f"{', '.join(invalid_inputs[:-1])}, and {invalid_inputs[-1]} are required" + + def validate_and_get(self, *args): + self.clean_all_required() # forget old validations + + all_data = () + invalid_inputs = [] + + for input_key in args: + input_data = self.inputs[input_key] + text = input_data["input"].text() + + all_data += (text,) + if not self.validate_input(input_key, text, validate_optional=True): + invalid_inputs.append(input_key) + + if invalid_inputs: + raise BIP38Application.ValidationError(self.format_required(invalid_inputs)) + + return all_data + + def validate_input(self, input_key, text, validate_optional=False): + input_data = self.inputs[input_key] + + qt_input = input_data["input"] + optional = input_data["optional"] if validate_optional else True # Handle all fields as optional if validate_optional is true. + min_length = input_data["min_length"] + + is_valid = len(text) >= min_length or (optional and len(text) == 0) + self.set_required(qt_input, not is_valid) + + return is_valid + + def set_required(self, widget, value=True): + widget.setProperty("required", value) + widget.style().unpolish(widget) + widget.style().polish(widget) + widget.update() + + def clean_all_required(self): + for key, item in self.inputs.items(): + self.set_required(item["input"], False) + + def validate_int(self, line_edit): + text = line_edit.text() + if not text.isdigit(): + line_edit.setText(''.join(filter(str.isdigit, text))) + + def enforce_int_range(self, line_edit, min_value, max_value): + text = line_edit.text() + validator = line_edit.validator() + + if text.isdigit(): + value = int(text) + value = max(min_value, min(value, max_value)) # clamp + line_edit.setText(str(value)) + + def regex_validator(self, regex): + return QRegularExpressionValidator(QRegularExpression(regex)) diff --git a/desktop/ui/__init__.py b/desktop/ui/__init__.py new file mode 100644 index 0000000..b6e3e62 --- /dev/null +++ b/desktop/ui/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +# Copyright © 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit \ No newline at end of file diff --git a/desktop/ui/bip38.ui b/desktop/ui/bip38.ui new file mode 100644 index 0000000..09dbfbb --- /dev/null +++ b/desktop/ui/bip38.ui @@ -0,0 +1,1832 @@ + + + MainWindow + + + + 0 + 0 + 900 + 491 + + + + + 0 + 0 + + + + + 900 + 0 + + + + + 900 + 16777215 + + + + MainWindow + + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 280 + + + + + 16777215 + 280 + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 7 + + + 15 + + + 15 + + + 15 + + + 5 + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + + + 150 + 16777215 + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Cryptocurrency + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + (Select) + + + + + + + + + + + 115 + 16777215 + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Network + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + (Select) + + + + + + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Passphrase + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + 145 + 16777215 + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Mode + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + (Select) + + + + + + + + + + + + + 0 + + + + + 7 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Private Key + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + 145 + 16777215 + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + WIF Type + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + (Select) + + + + + + + + + + + 115 + 16777215 + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::LayoutDirection::LeftToRight + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + PointingHandCursor + + + Convert + + + + + + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Wallet Important Format (WIF) + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Encrypt + + + + + + + + + + + + + + + + + + 7 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Owner Salt + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Generate + + + + + + + + + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Seed + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Generate + + + + + + + + + + + + + + 100 + 16777215 + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Lot + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 100000 + + + + + + + + + + + + + + 100 + 16777215 + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Sequence + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + 0 + + + + + + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Intermediate Passphrase + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Generate + + + + + + + + + + + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Confirm Code + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Verify + + + + + + + + + + + + + + + + + + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Encrypted WIF + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + 145 + 16777215 + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 15 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + WIF Type + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 145 + 0 + + + + (Select) + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Orientation::Vertical + + + + 20 + 49 + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + PointingHandCursor + + + Create + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Decrypt + + + + + + + + + + + + + + + + + + + + 0 + 200 + + + + true + + + + + + + + + + + + + + diff --git a/desktop/ui/css/theme.css b/desktop/ui/css/theme.css new file mode 100644 index 0000000..20c9d92 --- /dev/null +++ b/desktop/ui/css/theme.css @@ -0,0 +1,231 @@ +QMainWindow { + background-color: #11111b; +} + +QWidget { + outline: none; + color: white; + font: 10pt "Segoe UI"; +} + +QFrame{ + background-color: transparent; + border: none; +} + +QLabel { + font: 10pt "HDWallet"; + letter-spacing: 1px; + color: rgb(255, 255, 255); + text-transform: uppercase; +} + + +/* Scroll bar */ +QScrollBar:vertical { + border: none; + background: rgb(16, 21, 27); + width: 8px; + border-radius: 4px; +} + + QScrollBar::handle:vertical { + background: #65619c; + min-height: 55px; + border-radius: 4px; + } + + QScrollBar::add-line:vertical { + border: none; + height: 20px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + subcontrol-position: bottom; + subcontrol-origin: margin; + } + + QScrollBar::sub-line:vertical { + border: none; + height: 20px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + subcontrol-position: top; + subcontrol-origin: margin; + } + + QScrollBar::up-arrow:vertical, + QScrollBar::down-arrow:vertical { + background: transparent; + } + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical{ + background: rgba(0, 0, 0, 0.3); +} + + +QScrollBar:horizontal { + border: none; + background: rgb(16, 21, 27); + width: 8px; + border-radius: 4px; +} + +/* Inputs */ +QComboBox { + background-color: rgba(28, 28, 37, 1); + border-radius: 5px; + color: rgb(255, 255, 255); + padding: 5px 10px; + height: 25px; +} + + +QComboBox:disabled{ + background-color: rgba(32, 36, 43, 0.5); +} + +QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 30px; + border-left-width: 3px; + border-left-style: solid; + background-image: url(:/png/images/png/cil-arrow-bottom.png); + background-position: center; + background-repeat: no-repeat; +} + +QComboBox::drop-down:checked { + background-image: url(:/png/images/png/cil-arrow-top.png); +} + +QComboBox::drop-down:hover { + background-color: transparent; +} + + +QComboBox QAbstractItemView { + color: white; + border: none; + padding: 10px; + background-color: rgba(28, 28, 37, 1); + outline: none; + border-bottom: 2px solid rgba(0, 0, 0, 0.5); +} + +QComboBox QAbstractItemView::item { + padding: 5px; + width: fit-content; +} + +QComboBox QAbstractItemView::item:hover { + outline: none; +} + +QComboBox QAbstractItemView::item:selected { + background-color: #11111b; + color: white; + selection-background-color: white; + outline: none; +} + +QLineEdit { + font: 9pt "Segoe UI"; + letter-spacing: 1px; + background-color: rgba(28, 28, 37, 1); + border: 1px solid rgba(0, 0, 0, 0.0); + border-radius: 5px; + color: rgb(255, 255, 255); + padding: 4px 7px; + height: 25px; + selection-color: #DDD; + selection-background-color: #65619c; +} + +QLineEdit:hover { + background-color: rgba(28, 28, 37, 0.7); +} + +QLineEdit::disabled { + background-color: rgba(25, 30, 36, 0.5); +} + +QLineEdit[required="true"] { + border: 1px solid rgba(255, 96, 96, 0.5); +} + +QTextEdit { + background-color: black; + border-color: rgb(25, 30, 36); + color: rgb(255, 255, 255); + padding: 15px 5px; + padding-top: 10px; + selection-color: #DDD; + selection-background-color: #65619c; +} + +QPushButton { + font: 10pt "HDWallet"; + letter-spacing: 1px; + font-weight: lighter; + letter-spacing: 1px; + background-color: rgba(28, 28, 37, 1); + border-radius: 3px; + color: rgb(255, 255, 255); + padding: 5px 10px; + height: 25px; +} + +QPushButton:hover{ + background-color: rgba(28, 28, 37, 0.7); +} + +/* QMENU */ + +QMenu { + background-color: #11111b; + color: white; + border: 1px solid rgba(128, 128, 128, 0.25); + padding: 5px; + border-radius: 4px; +} + +QMenu::item { + background-color: transparent; + padding: 5px 15px; + color: white; + qproperty-icon: none; +} + +QMenu::item:selected { + background: rgb(25, 30, 36); + color: #65619c; + border-radius: 3px; +} + +QMenu::item:hover { + background-color: rgb(25, 30, 36); + color: #65619c; + border-radius: 3px; +} + +QMenu::item:disabled { + background-color: transparent; + color: rgba(255, 255, 255, 0.5); +} + +QMenu::item:disabled:selected { + color: rgba(255, 255, 255, 0.5); + background-color: transparent; +} + +QMenu::separator { + height: 1px; + background-color: rgba(128, 128, 128, 0.25); + margin: 5px 0; +} + +QMenu:disabled { + color: rgba(255, 255, 255, 0.5); +} diff --git a/desktop/ui/font/HDWallet.ttf b/desktop/ui/font/HDWallet.ttf new file mode 100644 index 0000000..ea686d7 Binary files /dev/null and b/desktop/ui/font/HDWallet.ttf differ diff --git a/desktop/ui/images/icon/icon.ico b/desktop/ui/images/icon/icon.ico new file mode 100644 index 0000000..6470bed Binary files /dev/null and b/desktop/ui/images/icon/icon.ico differ diff --git a/desktop/ui/images/svg/full-logo.svg b/desktop/ui/images/svg/full-logo.svg new file mode 100644 index 0000000..fed49cb --- /dev/null +++ b/desktop/ui/images/svg/full-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/desktop/ui/images/svg/logo.svg b/desktop/ui/images/svg/logo.svg new file mode 100644 index 0000000..7e9c7a5 --- /dev/null +++ b/desktop/ui/images/svg/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/desktop/ui/resources.qrc b/desktop/ui/resources.qrc new file mode 100644 index 0000000..a733b9f --- /dev/null +++ b/desktop/ui/resources.qrc @@ -0,0 +1,6 @@ + + + images/png/cil-arrow-top.png + images/png/cil-arrow-bottom.png + + diff --git a/desktop/ui/resources_rc.py b/desktop/ui/resources_rc.py new file mode 100644 index 0000000..374b2d9 --- /dev/null +++ b/desktop/ui/resources_rc.py @@ -0,0 +1,405 @@ +# Resource object code (Python 3) +# Created by: object code +# Created by: The Resource Compiler for Qt version 6.7.2 +# WARNING! All changes made in this file will be lost! + +from PySide6 import QtCore + +qt_resource_data = b"\ +\x00\x00\x06\xed\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\ +\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x05\xf1iTXtXML\ +:com.adobe.xmp\x00\x00\ +\x00\x00\x00 \ + \ + <\ +/xmpMM:History> \ + \x8d\x0b\ +\x9e5\x00\x00\x00\xa2IDAT8\x11c\xf8\xff\xff\ +?\x03%\x98d\x0d\xcf\x9e=c\x04a\xb2\x0c\x00j\ +d\x22\xdb\x05@\xcd\xccP\xda\x04\x88\x8bH2\x00I\ +\xb3&\x10?\x05\xe2\x0a\x98w0\xfcG\x84\xe6bd\ +\xb5\x18\x1a\x91\xfd\x89\xa6\xf9\x09\xcc\xe90qd\x03\xa4\ +\x80x'\x10\xebB\xf9,Xl\xc6\xd0\x8c\x12\x06@\ +\x892 ~\x0e\xc4\x86Hb\xe8\xcef\xc6\x1b\x0b@\ +\x05i@\xfc\x16\x88-\x81X\xe5\x19\x04\xe0\xd4\x8c\xee\ +\x02&(\x1d\x01\xb5\x15\xa7\xb3\xf1\xb9\x00fH\x00\x10\ +\xe7\xe2J \ + \ + <\ +/xmpMM:History> \ + \xacr\ +\xe7\xb6\x00\x00\x00\xd2IDAT8\x8d\xc5\xd31J\ +\x041\x14\x87\xf1_f#.\x82\xde\xc4R\x0f\xb0\xae\ +'\xf0\x06\xb66\xdeD\xef`c\xad\x9d`+X\x8f\ +\xe0^\xc0\xca\xc6~\xdd\x89\x85\x09\x86a\x06e\xa70\ +M\xf8\x93|_\xde{\x90\x90R2e5\x93h\xc4\ +\xb6m\x1btS*\xe80\xdb\x82\x0dE\x00\x9b-\x04\ +\xa9\x16\xec\xe3\xf8\x8f\xe0A}\xb7\x08\xe6\xb8\xc1\xe5/\ +p\xc0-.J\x8eY\xf2\x8e3<\xe4\x83\xeb\x0a\xda\ +\xc1:\xc3\xf79\x9f\x976\xa2\x9f!\xbe\xe0\x04\x8f=\ +\xc9:\xefw\xd8\xc5iUM\x8a9l\x10\xf1ZI\ +\x02\xae\xaa\x97\xe7XV\xadw2T\xd6\xe7\x80\xa4\xc1\ +\x11\xf6\xb0\xe8\xc3}A_\xb2\xc0\x13\x9e\xc7\xe0!A\ +\x91\xcc\xb0\xc2!>\xc6\xe01\x01\xdf3i\xf0\x96s\ +\x18\x82!\xfc\xfbo\xfc\x02d\x060\xdc\x02*\xb0\x83\ +\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x07 \ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\ +\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x05\xf1iTXtXML\ +:com.adobe.xmp\x00\x00\ +\x00\x00\x00 \ + \ + <\ +/xmpMM:History> \ + \xaa~\ +\x0dL\x00\x00\x00\xd5IDAT8\x8d\xc5\xd3=N\ +\x031\x14\x04\xe0\xcf\xbbKEM\xc5\x05@\xe2(@\ +\x0d\x05\xc7\xa1\x01!8\x01'\x80\x13\xf0s\x80\xb4{\ +\x12@\x22\x89\x021\x05o\x91\x15\x9c\x88(\x05\xd3x\ +<~3z~\x96S\xce\xd9&h6r\xa3\xeb\xfb\ +~U\xf8 +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +import os +from PySide6.QtWidgets import QLayout +from PySide6.QtSvgWidgets import QSvgWidget +from PySide6.QtCore import QSize + +def clear_layout(layout: QLayout, delete: bool = True) -> None: + """ + Clear all widgets from a layout. + + :param layout: The layout to clear. + :param delete: Whether to delete the widgets after removing them from the layout. + """ + if layout is not None: + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None and delete: + widget.deleteLater() + else: + clear_layout(item.layout()) + + +def resolve_path(path: str) -> str: + """ + Resolve the absolute path of a given relative path. + + :param path: The relative path to resolve. + :type path: str + :return: The absolute path of the given relative path. + :rtype: str + """ + return os.path.abspath(os.path.join(os.path.dirname(__file__), "../", path)) + + +def put_svg(layout: QLayout, path: str, width: int, height: int) -> QSvgWidget: + """ + Add an SVG widget to a layout. + + :param layout: The layout to add the SVG widget to. + :param path: The path to the SVG file. + :param width: The width of the SVG widget. + :param height: The height of the SVG widget. + :return: The SVG widget. + """ + clear_layout(layout) + svg = QSvgWidget(path) + svg.setMinimumSize(QSize(width, height)) + svg.setMaximumSize(QSize(width, height)) + svg.setStyleSheet("background: transparent") + layout.addWidget(svg) + return svg diff --git a/docs/index.rst b/docs/index.rst index b7d66be..280ee99 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -455,7 +455,7 @@ We welcome pull requests. To get started, just fork this `github repository \ No newline at end of file diff --git a/docs/static/svg/bip38.svg b/docs/static/svg/bip38.svg new file mode 100644 index 0000000..7e9c7a5 --- /dev/null +++ b/docs/static/svg/bip38.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launch.py b/launch.py new file mode 100644 index 0000000..6b3a25e --- /dev/null +++ b/launch.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +# Copyright © 2024, Eyoel Tadesse +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/license/mit + +from PySide6.QtWidgets import QApplication +from PySide6.QtGui import QPalette, QColor +from PySide6.QtCore import Qt + +import sys + +from desktop.main import BIP38Application + + +def main() -> None: + qapp: QApplication = QApplication(sys.argv) + + palette = QPalette() + palette.setColor(QPalette.Active, QPalette.Text, QColor(255, 255, 255)) + palette.setColor(QPalette.Disabled, QPalette.Text, QColor(170, 170, 170)) + qapp.setPalette(palette) + qapp.setStyle("Windows") + + main_application: BIP38Application = BIP38Application() + main_application.app.show() + sys.exit(qapp.exec()) + + +if __name__ == '__main__': + main() diff --git a/requirements/desktop.txt b/requirements/desktop.txt new file mode 100644 index 0000000..01a3fdc --- /dev/null +++ b/requirements/desktop.txt @@ -0,0 +1,2 @@ +PySide6==6.7.2 +cx-Freeze==7.2.0 \ No newline at end of file diff --git a/setup.py b/setup.py index f1875fd..d83a261 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,8 @@ def get_requirements(name: str) -> List[str]: include_package_data=True, extras_require=dict( docs=get_requirements(name="requirements/docs"), - tests=get_requirements(name="requirements/tests") + tests=get_requirements(name="requirements/tests"), + desktop=get_requirements(name="requirements/desktop") ), classifiers=[ "Development Status :: 5 - Production/Stable",